Skip to content

Stabilize SQLite entrypoint parking test - #826

Merged
ryan-winkler merged 2 commits into
dannyvfilms:latestfrom
srow90:codex/issue-825-entrypoint-parking-test
Aug 17, 2026
Merged

Stabilize SQLite entrypoint parking test#826
ryan-winkler merged 2 commits into
dannyvfilms:latestfrom
srow90:codex/issue-825-entrypoint-parking-test

Conversation

@srow90

@srow90 srow90 commented Aug 16, 2026

Copy link
Copy Markdown

Summary

Problem

test_entrypoint_parks_once_instead_of_polling_exited_checker inherits VIRTUAL_ENV from the test runner. The entrypoint correctly prepends that environment to PATH, which bypasses the test's command wrappers. The test then starts the real recovery server and only reaches its asserted parking sleep when that server exits, making the result depend on platform, port state, and test order.

This was the only failure after 3,981 tests in PR #792's application suite and reproduced as a targeted failure locally on the unchanged latest entrypoint/test code.

Solution

  • Clear VIRTUAL_ENV in this test subprocess so its explicit wrapper directory stays first.
  • Add a portable timeout wrapper.
  • Match the complete Python argument list and exit the recovery-server stub cleanly before asserting the fallback parking loop.

Production entrypoint behavior is unchanged.

AI Assistance

Generated with OpenAI Codex (gpt-5.6-sol, Codex 5.6 SOL).

Validation

  • Targeted test before the patch — failed locally with parking_child_before_term == False.
  • Targeted test after the patch — passed once, then passed five consecutive repeat runs.
  • uv run --no-sync ruff check src/config/tests/test_sqlite_integrity.py — passed.
  • git diff --check — passed.
  • Full SQLite integrity module was attempted on macOS, but its backup-path cases require Linux /proc/self/fd; those platform failures are unrelated to this test-only patch. The hosted Linux application suite is the authoritative module/full-suite gate.

Contract Handoff

  • Domain guide regeneration/check outcome: Not applicable; no domain vocabulary changes.
  • Verified OpenAPI regeneration outcome: Not applicable; no API changes.
  • Contract-test outcome: Not applicable; no contract changes.

Human Review

  • Pending human review.
  • Completed — reviewer/evidence:

Gstack QA

  • Pending /gstack-qa.
  • Completed — report/outcome:

Migration Sync Gate (Required for upstream -> latest sync PRs)

Not applicable; this is not an upstream sync PR and contains no migrations.

Notes

- Keep the test wrappers ahead of inherited virtual environments
- Stub timeout and recovery-server transitions deterministically

Fixes dannyvfilms#825

@ryan-winkler ryan-winkler left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review outcome

One small test-isolation gap remains. The proposed repair is otherwise focused, easy to review, and correctly leaves production startup behavior unchanged.

Required follow-up

Please apply the same VIRTUAL_ENV isolation to test_entrypoint_term_during_integrity_check_stops_before_migrations.

That adjacent test creates the same python and timeout wrappers and starts the same entrypoint. It still inherits the caller's virtual environment. When that environment is active, entrypoint.sh can prepend its bin directory and bypass the test wrappers. The test can then miss checker waiting and exercise the real recovery path instead of the intended TERM path.

This is the same root cause as #825. I expanded #825 instead of creating a second issue.

Security and failure-path review

  • Reviewed the complete one-file diff and the entrypoint process flow that it controls.
  • No production source, API, database schema, dependency, secret, or user-data path changes.
  • The generated wrappers stay inside a private temporary directory and execute fixed local commands.
  • No exploitable security issue was found.
  • The remaining risk is false test confidence when the caller environment changes command resolution.

QA evidence

  • Lint: passed.
  • CodeQL: passed.
  • Docker Image: passed.
  • Application test job: still running when this review was submitted.
  • The PR records a failing targeted test before the repair, a passing targeted test after it, five repeat passes, Ruff success, and git diff --check success.

After the follow-up change, please repeat both entrypoint subprocess tests with an active virtual environment. This tests the real boundary that caused #825.

Scope and contract check

  • Documentation and Markdown changes: not needed; this is a test-only repair.
  • OpenAPI and contract artifacts: not applicable.
  • UI screenshots, interaction review, accessibility review, and cognitive-load review: not applicable because no user-facing surface changes.
  • No new helper is needed for two tests. If a third entrypoint subprocess test is added later, move the repeated wrapper and environment setup into one test helper so all tests use the same boundary.

Related work

  • Fix scope: #825
  • Failure source and related application suite: #792
  • Upstream comparison: FuzzyGrim/Yamtrack has no equivalent SQLite recovery-server or parking flow, so this repair is specific to Floppy.

Post-mortem

Trigger: the test inherited a caller-controlled environment that could reorder command lookup.

Why it escaped: the first parking test received an explicit environment fix, but the adjacent TERM test uses the same subprocess pattern and kept the old environment behavior.

Prevention: define the command-resolution boundary explicitly in every entrypoint subprocess test, and keep each wrapper limited to the command that the test owns.

The PR body was not changed.

@@ -959,6 +963,7 @@ def test_entrypoint_parks_once_instead_of_polling_exited_checker(self):
"FLOPPY_DB_PATH": db_path,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for this test. Please apply the same isolation to the adjacent test_entrypoint_term_during_integrity_check_stops_before_migrations environment. It creates the same wrapper-first PATH, but it still inherits VIRTUAL_ENV, so an active caller environment can bypass both wrappers.

Minimal follow-up in that test's env mapping:

                 "PATH": f"{bin_path}:{os.environ['PATH']}",
                 "PYTHONPATH": str(ENTRYPOINT.parent / "src"),
+                "VIRTUAL_ENV": "",

This is the same root cause as #825, not a separate issue. Please run both entrypoint subprocess tests repeatedly with an active virtual environment after this change.

@ryan-winkler

Copy link
Copy Markdown

This is the correct fix. Verified under the condition that actually breaks it.

I reached the same root cause independently while chasing this failure on #830, then found #825 had it first. Adding the executable evidence, since as far as I can tell nobody had yet run either candidate fix with port 8000 free:

# unshare -rn so port 8000 is free, which is the CI condition
#826 (this PR):            Ran 1 test in 0.105s   OK
#834 (raise bound to 60s): Ran 1 test in 60.018s  FAILED

The mechanism, for the record: sqlite_recovery_server.serve()
(src/config/sqlite_recovery_server.py:548) binds port 8000, and on success
calls serve_forever(). The entrypoint then never reaches the wrapped parking
sleep that writes PARKING_PID_FILE.

port 8000 occupied -> serve() returns (exit 0)          -> parking loop reached, test passes
port 8000 free     -> serve() blocks in serve_forever() -> pidfile never written, test fails

Directly demonstrated, same command both ways:

occupied: [entrypoint] Could not use port 8000 for the recovery page: [Errno 98] ... exit 0
free:     (no output, killed by timeout)                                          exit 124

That is why it passes locally, where we all run Floppy on 8000, and fails in CI.

Two suggestions:

  1. Take the assertion messages from test(config): stop the parking test depending on whether port 8000 is free #834. It attaches the entrypoint log to both
    assertTrue calls. The bare False is not true is what made this costly to
    diagnose from a CI log, and this PR does not have them.
  2. Consider a one-line comment in the wrapper saying why the recovery server is
    stubbed. Without it, the next person may read the stub as incidental and
    remove it, and the test will go back to depending on the machine's port state.

case "$*" instead of case "$2" is also the right call: python -m mod puts
the module in $2 while python -c '...' puts the code there, so matching the
whole line is what makes both cases reachable.

@ryan-winkler

Copy link
Copy Markdown

Verified this fix works, and found the sibling gap #825 predicted is still open. Evidence for both.

This fix is correct

# unshare -rn, so port 8000 is free (the CI condition)
#826 (this PR):            Ran 1 test in 0.105s   OK   (3/3 repeats)
#834 (raise bound to 60s): Ran 1 test in 60.018s  FAILED

The mechanism, confirmed directly: sqlite_recovery_server.serve()
(src/config/sqlite_recovery_server.py:548) binds port 8000 and calls
serve_forever() on success, so the entrypoint never reaches the wrapped parking
sleep. Same command both ways:

port occupied: [entrypoint] Could not use port 8000 ... [Errno 98]   exit 0
port free:     (blocks in serve_forever, killed by timeout)          exit 124

A time bound cannot fix a wait for something that never happens, which is why
#834 sits for 60s and still fails.

The sibling is still environment-dependent

test_entrypoint_term_during_integrity_check_stops_before_migrations does not get
the VIRTUAL_ENV: "" this PR adds to the parking test, and it fails:

VIRTUAL_ENV set to a venv outside the cwd:  FAILED
VIRTUAL_ENV unset:                          OK

The failure is the real integrity checker running instead of the stub: the log
shows real conflict output and Open http://localhost:8000/ where the test
expected checker waiting.

Why it is conditional rather than always broken. entrypoint.sh:14 reads

elif [ -n "$VIRTUAL_ENV" ] && [ -d "$VIRTUAL_ENV/bin" ] && [ "$VIRTUAL_ENV" != "$PWD/.venv" ]; then

so a venv at $PWD/.venv is deliberately ignored, and one anywhere else
gets prepended to PATH ahead of the test's wrapper directory. That is why
uv run --no-sync from the repo root passes (VIRTUAL_ENV == $PWD/.venv) while
the same test fails from a git worktree pointed at the main checkout's venv,
which is the workflow this repo needs whenever two sessions share the tree.

Latent in CI today because GitHub runners set no VIRTUAL_ENV. Live for anyone
testing from a worktree with one activated.

Suggest adding "VIRTUAL_ENV": "" to this test's env dict too, which is what
#825's review expansion asks for and keeps the work here as that issue directs.

Also worth taking from #834 before closing it: it attaches the entrypoint log to
both assertTrue calls. The bare False is not true is what made this expensive
to diagnose for three separate attempts.

@ryan-winkler

Copy link
Copy Markdown

Two more things a cross-model review surfaced on this, both arguments for merging it rather than against.

The test only ever covered the crash fallback, and this makes that honest.
There are two parking mechanisms here. In production the recovery server is the
parking process: it binds 8000 and serves until an operator chooses. The
sleep 86400 loop is reached only when that server exits without writing a
decision file. The test asserts a log line from the first mechanism
(SQLite startup is paused) and a pidfile from the second (the wrapped
sleep), so despite its name it has only ever exercised the fallback, and only
by accident of a bound port. Stubbing the server states that intent instead of
depending on it. Worth noting the consequence: the production parking path, where
the server stays up, still has no test. Not this PR's job, but it is a real gap.

It also removes a port-binding hazard from the suite. As things stand, in CI
the recovery server does bind 0.0.0.0:8000 during this test and is then killed
by the terminate(). With --parallel that is a shared global port and an
orphaned HTTP server if cleanup is ever incomplete. After this change nothing in
the suite binds 8000 at all.

Both point the same way. Happy to open a follow-up issue for the untested
production parking path if useful, though #825 says to keep this work here, so I
have not.

Applies the review note on dannyvfilms#826. The parking test got this isolation; its
neighbour did not, and it has the same dependency.

entrypoint.sh keeps a virtual environment first on PATH unless it is the one at
$PWD/.venv (the guard from dannyvfilms#762). A test that stubs commands through a
wrapper-first PATH therefore loses its wrappers whenever VIRTUAL_ENV points
anywhere else: the real python runs, the integrity stub never fires, and the
assertion reports the real checker's output instead of "checker waiting".

That is why this passes under `uv run --no-sync` from the repo root, where
VIRTUAL_ENV equals $PWD/.venv and is ignored, and fails from a git worktree
pointed at another checkout's venv.

Measured in a fresh network namespace with port 8000 free, VIRTUAL_ENV set
outside the working directory:

  before: FAILED 3/3
  after:  OK 3/3

Full config suite in the same conditions: 121 tests, all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ryan-winkler

Copy link
Copy Markdown

Pushed the review note as 3ac1f3aa (maintainer edit).

Verified before pushing, all in a fresh network namespace with loopback up so port 8000 is genuinely free, and with VIRTUAL_ENV set outside the working directory:

Check Before After
test_entrypoint_term_during_integrity_check_stops_before_migrations FAILED 3/3 OK 3/3
test_entrypoint_parks_once_instead_of_polling_exited_checker OK 5/5 OK
config suite 121 tests OK
test_container_bootstrap + test_data_paths + config 143 tests OK
ruff check src clean

Two notes for whoever merges:

The earlier test (3.12) failure on this PR was unrelated. It was
app.tests.test_integration.IntegrationTest.test_season_progress_edit, a
Playwright test, and this PR touches only test_sqlite_integrity.py. That test
did not fail on two other recent runs of other branches. This push triggers a
fresh run, which will confirm it either way rather than leaving it assumed.

The timeout wrapper is safe as written, but narrowly. shift; exec "$@"
drops only $1, so it would break on a flag. All nine timeout call sites in
entrypoint.sh currently use the bare timeout <duration> <cmd> form, so it
holds today. Worth a one-line comment saying so, since the next person adding
timeout -s KILL would get a confusing failure.

@ryan-winkler
ryan-winkler merged commit 85e7377 into dannyvfilms:latest Aug 17, 2026
9 checks passed
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
Brings in the entrypoint parking test fix (#826), which was the only failure on
this branch's CI.
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
Found while verifying the rebase in a namespace where the test user is root.
Root bypasses file permission bits, so opening a 0444 database for append
succeeds, the writability check correctly finds nothing to report, and the test
fails on an assertion that cannot hold there.

CI runs as an unprivileged user, so this was green either way. Skipping it
explicitly states the precondition instead of leaving a test that passes or
fails on who runs it, which is the same fragility #825 and #826 just removed
from the entrypoint tests next door.

Asserts as a normal user, skips as root. Verified both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
Brings in the entrypoint parking test fix (#826), the only failure on this
branch's CI.
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
Picks up latest, including the entrypoint parking test fix (#826).
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
- Sleep for the interval the entrypoint asked for, not a shorter fixed one.
- Name which fault left no parking child.
- Assert the storage check ran once, which is what "parks once" means.

#826 made this test deterministic by stubbing the recovery page, so it no longer
depends on whether port 8000 is free. Three smaller problems are left.

The parking sleep stub runs "sleep 30" while the entrypoint asks for 86400. The
stub ends, the parking loop starts another one, and the pid file is overwritten
while the test reads it. The stub now passes the interval through. The other
"sleep 30" in this file stands in for a hanging checker and is left alone,
because its arguments are python's, not an interval.

The assertion reported only "False is not true", which does not say whether the
pid file never appeared or appeared naming a dead process. Those need opposite
fixes, and the distinction is what identified the port 8000 cause.

Verified with port 8000 taken and with it free, in a network namespace: 43
tests, 0.39s, green in both.

Refs #593

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ryan-winkler added a commit that referenced this pull request Aug 17, 2026
* 🔒 Remove credentials from the authority part of a URL

safe_url() removed the query string and the fragment but kept the netloc, so
a URL that carries "user:password@" passed through with the password intact.
Private podcast feeds authenticate that way, and Redis and database
connection strings do too, which means the credential could reach the log
file that Settings > Advanced offers users for download.

Rebuild the authority from its host and port instead of copying it. The port
is absent from most URLs, and urlsplit reports None for it then, so an
unconditional f-string would write the text "None" into the host name.

Parse the structure rather than match a pattern. A URL has a defined shape,
and reading that shape cannot miss a spelling the way a keyword list can.
This complements the redact_secrets() rule that already covers URL
credentials in free log text; this one keeps the reconstructed URL readable
instead of leaving "[REDACTED]:[REDACTED]@" in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🔍 Add a read-only SQLite inspection with a bounded scan

check_database_integrity() reaches a verdict and then acts on it: it writes
an incident report beside the database, creates a verified backup, can delete
orphaned rows, and ends the process with sys.exit. A diagnostic cannot call
it.

Add inspect_database(), which reports the same verdict from the same two
primitives and does none of the rest. The two callers share the primitives;
neither calls the other. That distinction is load-bearing. _check_foreign_keys
inspects inside BEGIN IMMEDIATE because the quarantine delete that follows
must operate on the rows the inspection found, and taking that write lock in
a diagnostic would block a running Floppy. Startup keeps its transaction
unchanged.

Bound the scan with a progress handler. sqlite3.connect(timeout=...) sets
busy_timeout, which is how long to wait for a lock, and does nothing to limit
how long a statement runs, so PRAGMA quick_check on a large file would run
past any value given there. Lock contention and scan duration are different
failures and get different bounds: five seconds for the lock, and a
caller-supplied deadline for the scan.

Measured on a 44.8 MB database: a full scan finishes in 0.06s, and a 0.05s
deadline ends it at 0.05s instead of letting it run on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ✨ Add floppy_preflight, a startup diagnostic for operators

An operator whose container will not start has no way to find out why. The
startup log says migrations and services did not run, and stops there. This
command answers the question directly: paths, settings, database, migrations
and Redis, each reported with what broke, why, and what to do next.

The checks live in app/preflight.py and the command renders them, the same
split redis_tuning and tune_redis already use, so the decisions return data
instead of printing it.

Design notes worth knowing:

Statuses are ok, warn, fail and skipped, and only fail changes the exit code.
Without a warn tier the checks would have to swallow a real hazard such as a
Redis server with no memory ceiling, or block a boot over it.

Every fix says where to run it. A chown pasted into a container shell fails,
and an environment variable edited there is gone on the next restart. The
wording follows the environment, because advice about compose files, PUID and
PGID is wrong for a source install or a packaged application.

Every detail is redacted before it is printed. Redis client errors quote the
connection string, password included, so the failure path is exactly where a
credential would otherwise escape.

The Redis check groups the five configured URLs by server, so the common
single-server stack is pinged once and a failure names the roles that server
carries. Brokers this command has no client for, such as RabbitMQ, are left
to their own services rather than reported as broken Redis.

The migration check does not run against a database that does not exist yet.
Connecting would create the file and reading the state would create a table
inside it, and looking at an installation must not build part of it.

Django's system checks are run explicitly rather than left on, because their
output would corrupt the JSON report, and silencing them would let a settings
error pass as a clean bill of health while the container dies.

Refs #597. Reuses the integrity decision from #593, the paths from #595 and
the Redis endpoints from #596.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🩹 Point a failed startup at the diagnostic, and check mounts first

Two changes to the startup script, both for the operator who is reading it
because something went wrong.

Name the diagnostic in each failure message. A command nobody can find does
not exist, and the moment they need it is the moment these lines are on their
screen. The two forms are not interchangeable: docker exec needs a running
container, so it works while startup is parked and fails with "Container is
restarting" once a path exits and a restart policy takes over. The parked
paths print the exec form and the exit path prints the one-off form.

Check the mounts before anything imports Django. A read-only or wrongly owned
mount is a common reason a container will not start, and it is the one
failure floppy_preflight cannot report: Django opens its log file while it
loads settings, so the process dies with a traceback before any command runs.
The shell can still name the directory at that point.

The data directory is fatal, because nothing works without it. The log
directory is a warning, which is how the ownership step further down already
treats it, and the warning is what tells the operator which directory caused
the traceback that follows. Both create the directory when it is absent,
matching what Django does with them a moment later, so a first start on a
fresh volume is not treated as a fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 📝 Document the startup check, and ask for it in bug reports

README gains a "Startup diagnostics" section next to SQLite startup recovery,
where an operator is already looking when a start fails. It gives both
container commands and says which to use for which symptom, the source form
with its SECRET prerequisite, the four result kinds, the options, the JSON
version policy, and a systemd ExecStartPre example.

The bug report template asked every reporter to download logs from Settings >
Advanced. That needs a running, logged-in Floppy, so the reports that most
need diagnostics could not carry any. Add the path for an installation that
will not start.

AGENTS.md lists the command as the way to tell a broken environment from a
broken change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 Keep the writability check from failing a start, and stop two tests depending on the machine

Four CI failures, all from this branch.

The startup writability check exited when a directory could not be created or
written. That broke two existing tests which drive entrypoint.sh with paths
that deliberately do not exist, because they cover path validation rather than
path usability, and /var/lib/floppy cannot be created without root.

The check should never have been a gate. The gates already exist: the ownership
step exits when it cannot chown the data directory, and Django raises when it
cannot create the generated secret. What was missing is a readable line for the
log directory, because Django opens its log file while it loads settings, so
the process dies with a logging traceback before any Floppy message appears.
Report that and continue. A pre-check that can stop a start which would
otherwise have worked is worse than no pre-check.

Two tests in this branch called check_migrations() without controlling
FLOPPY_DB_PATH, so they inherited whatever database the machine had. They
passed on a developer machine, where one exists, and failed in CI, where the
new short-circuit for a first start returned "skipped" instead of reaching the
mocked planner. Both now create a real database first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 Recognise Podman as a container runtime

Found by building the image and running it. Podman does not create
/.dockerenv; it writes /run/.containerenv. Checking only the Docker file meant
a containerised Floppy under Podman reported itself as a bare install and gave
advice written for one, telling the operator to check REDIS_URL rather than the
service in their stack. Podman is common in the self-hosted setups Floppy runs
in, so check both markers.

Verified in a real image: before this change the failure advice inside a Podman
container read "check that Redis is running and that REDIS_URL points at it",
and now it reads "check that the Redis service is running and reachable".

The same single-marker check exists in settings.py, where it decides whether to
generate a secret key. That behaves differently under Podman and is left alone
here: changing when Floppy generates its own secret is a separate decision.

README now states what each runtime does when exec cannot reach a restarting
container, measured rather than assumed: Docker reports "Container is
restarting", and Podman kills the attempt with exit code 137. Twelve exec
attempts against a looping container failed twelve times, and the one-off form
succeeded every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🧪 Cover the PostgreSQL branches, which had no tests at all

Comparing the QA scenarios against the committed suite showed check_database's
PostgreSQL path was never exercised, including the three branches that read the
SQLSTATE to tell a refused password from a refused connection.

Both read as "could not connect" and they send an operator to different places,
one to their credentials and one to the network, so the distinction is the point
of that code and it needed a test. Also asserts no database password reaches the
report, matching the Redis equivalent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 Six fixes from the final QA pass

A last review round against the finished code and a running container.

Never migrate on top of a broken foundation. When paths, settings or the
database failed, "skipped" migrations means unknown rather than pending, and
--auto-migrate ran migrate against an unreadable database, which can only make
an incident worse. Redis failing does not block it, and neither does the
migration check itself.

Do not call a mount mistake corruption. A directory at the database path was
reported twice: once correctly by the paths check, and again by the database
check as "the database file cannot be read", advising a backup restore. That
sends an operator to recover data that was never lost. The database check now
defers to the paths check for this.

Refresh the database result after migrating. On a first start the database check
says "no database file yet". Once migrate had created it, the report still
printed that sentence.

Check the database file itself, not only its directory. Every other check reads,
so a database file that was readable but not writable passed all of them and the
container then died on its first write. The file is now probed by opening it for
append, which adds and truncates nothing.

Redact the crash report. An unexpected exception went into the report verbatim,
so a settings or connection error carrying a password would have printed it.

Carry the migration output. The failure advised reading an error that had been
captured to keep the report parseable, and then discarded. The last twenty lines
now travel with the result.

Rejected one finding with evidence: redis-py decodes CONFIG replies regardless
of decode_responses, so config_get returns str keys and the memory-ceiling
warning fires correctly. Confirmed against a real unbounded Redis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🧪 Skip the read-only database assertion when running as root

Found while verifying the rebase in a namespace where the test user is root.
Root bypasses file permission bits, so opening a 0444 database for append
succeeds, the writability check correctly finds nothing to report, and the test
fails on an assertion that cannot hold there.

CI runs as an unprivileged user, so this was green either way. Skipping it
explicitly states the precondition instead of leaving a test that passes or
fails on who runs it, which is the same fragility #825 and #826 just removed
from the entrypoint tests next door.

Asserts as a normal user, skips as root. Verified both ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make SQLite entrypoint parking test deterministic

2 participants