test: stop every suite reading and writing the machine it runs on - #287
Merged
Conversation
…tate `identity.actor_id()` reads the real ~/.config/witan/config.toml and the real OIDC token cache, and nothing in the suite stopped it. So on any machine that had run `witan login` the branch views got namespaced under the resolved actor and `check_writable` took its logged-in branch, while four tests asserted the un-namespaced names and the logged-out refusal prose. They were green in CI the whole time: a GitHub runner has no witan identity, so CI only ever exercised the half those tests describe. Four standing local failures is the number at which a contributor stops reading the summary line. #276, #277 and #278 each spent a stash-and-rerun establishing they were pre-existing. `_fresh_identity` now points WITAN_CONFIG and WITAN_TOKEN_CACHE at tmp_path and clears the env vars that short-circuit ahead of them, making logged-out deterministic rather than incidental. `logged_in_actor` opts back in through WITAN_ACTOR — the path a non-interactive writer actually takes — and covers the half a CI runner cannot reach: a logged-in writer's views carry their owner, on the local store too. `check_writable` loses its `actor` default, which is what made the guard's behaviour depend on the caller's environment in the first place. Every call site already resolves an actor, because it needs one to name the view it is about to write. The fallback also made `ingest` wrong: a request arriving with no actor was judged against the serving process's identity instead of being refused for having none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RH7kCwSp8TbKQLZ7kbVnY1
The class behind #282 and #285, swept across all five packages instead of fixed a sixth time when it next trips someone. A hostile-environment run — HOME seeded to look like a working developer's machine, a narrow terminal, a witan identity present — found considerably more than the two known cases: 19 of witan's 49 test files created a real graph in ~/.local/share/witan witan-code's test_ingest.py created a real code store in the same tree 8 agent-config-kit tests assert an unwrapped Rich message, so they fail on a narrow terminal; 2 of them already fail at an ordinary width The witan and witan-code leaks were silent. Every test passed. The evidence they had been running for a long time is sitting in the real store directory: https_github.com_test_cg.omni, the fixture repo from test_branches.py. testsupport/hermetic.py redirects HOME, the XDG dirs, the witan state files and both graph stores into a throwaway directory, clears the ambient WITAN_* selectors, and pins the terminal width. Each package gets a rootdir conftest.py that loads it. ★ It runs at IMPORT, not in a fixture, and that is the load-bearing detail. Importing witan.server IS a write — _ensure_graph creates the store at module scope, deliberately, because the CLI's local-dispatch guard depends on it. That import happens during collection, so an autouse fixture body runs long after the store exists. Only something imported by a rootdir conftest is early enough. PATH deliberately keeps its entry for the real ~/.local/bin: that is where CI installs the omnigraph binary and where _find_binary looks when PATH misses. A binary is a tool, not state, and relocating it would break every test that needs a graph to prevent a leak that cannot happen. Redirection fixes today's leaks and not tomorrow's, so pytest_sessionfinish reports anything that reaches the real home anyway — a warning locally, where another agent session may legitimately be writing, and a failure on CI. Strictness is inferred from CI rather than set in each workflow: ten env blocks would be ten chances to miss the tenth, and it covers the publish workflows and whatever gets added next for free. Verified by inducing an actual leak: detected in all four strictness paths, exit 1 where it should fail and 0 where it should warn. 2394 tests pass across the five suites, and the fake home comes back empty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RH7kCwSp8TbKQLZ7kbVnY1
blarghmatey
commented
Aug 25, 2026
blarghmatey
commented
Aug 25, 2026
blarghmatey
commented
Aug 25, 2026
blarghmatey
commented
Aug 25, 2026
blarghmatey
commented
Aug 25, 2026
blarghmatey
commented
Aug 25, 2026
From /code-review high on #287. All six verified against the code before acting; the first two were real regressions this PR introduced. WITAN_REQUIRE_OMNIGRAPH is no longer cleared. witan-core-tests.yml sets it to "1" on the test step so a missing omnigraph binary is a hard failure rather than a skip, and test_binary_contract.py reads it at MODULE scope — after this plugin ran. Popping it reverted the entire binary-contract suite to skipping green. That suite exists to stop itself being retired quietly, and this change retired it quietly, which is the same defect the PR is about. Proved by probe: with the var exported, the suite saw '<absent>' before, '1' after. WITAN_TEST_OMNIGRAPH_SERVER/_GRAPH likewise. They are the documented opt-in for the live-server tests, whose skipif is evaluated at import, so clearing them made those tests unreachable — you could not opt in at all. Both cases are now an explicit _EXEMPT tuple with the reasoning, asserted disjoint from _CLEARED, because the distinction is subtle: clear what the suite must not INHERIT, keep what a human or a workflow set to make the suite STRICTER. Added the selectors the list missed: WITAN_OUTPUT_FORMAT (a cyclopts env_var on both CLIs), the seven WITAN_SCAN_* vars (an ambient ENABLED=false would run the suite with the write-path scanner off), WITAN_OMNIGRAPH_HTTP, OMNIGRAPH_BEARER_TOKEN, WITAN_OPTIMIZE_INTERVAL, CLAUDE_SESSION_ID. The leak detector was blind to the leak it was written for. It compared only immediate children, and the motivating defect lands at ~/.local/share/witan/code/<slug>.omni — inside a `code` directory that already exists on any machine that has run the indexer. It fired on CI, where the tree is absent, and nowhere else. My own verification missed this because the sentinel I wrote was a top-level child: a leak of a shape the real one does not have. Now a bounded-depth snapshot (3 levels) keyed on absolute path, with size+mtime markers so the #282 shape — an APPEND to an existing merge-watermarks.json — is visible at all, which no name-based check can see. Strictness no longer reads CI=false or CI=0 as true, matching the parsing test_binary_contract.py already uses. Dropped pytest_plugins for a direct import. That setting is only honoured in whichever conftest is top-level for the chosen rootdir, so running pytest from the repo root turned all five into non-top-level and aborted collection outright. Importing the module has the same effect with no such rule. Bare root-level pytest still fails on 46 pre-existing collection errors — one package's venv cannot satisfy five — identically to main at deedb68, so this restores parity rather than fixing something that worked. Deepening the watch surfaced ambient churn: four of five suites warned, every line of it the machine rather than the suite. Measured with a 95s idle probe and a parallel test-all. Modifications of those paths are now filtered; creations never are, so on CI — where none of them exist — a real write still reports. 2399 tests pass, no warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RH7kCwSp8TbKQLZ7kbVnY1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What are the relevant tickets?
Sweeps the defect class behind #282 and #285, and subsumes
tk-two-agent-config-kit-cli-tests-assert-a-terminal-2bec4f.Stacked on #285 (base is
witan-code-identity-tests) — that PR fixed this class in one package; this generalises it. Review #285 first, or read this one's diff alone, which is self-contained.Description (What does it do?)
Three separate discoveries of the same defect in four days — a test reading or writing the machine it runs on — is a pattern, not a coincidence. It survives because a GitHub runner is the empty case of every ambient input there is: no
witan login, no agent configs, no~/.gitconfig, and a terminal wide enough that nothing wraps. A test asserting any of those absences passes in CI and only in CI.Rather than fix it a sixth time when it next trips someone, I measured it. A hostile-environment run — HOME seeded to look like a working developer's machine, narrow terminal, witan identity present — found considerably more than the two known cases:
~/.local/share/witantest_ingest.pycreates a real code store in the same treeThe witan and witan-code leaks are silent — every test passes. They have been running for a long time, and the evidence is sitting in my real store directory right now:
That is
test_branches.py's fixture repo (REPO = "https://github.com/test/cg"), materialised as a real per-repo store next to genuine ones.The fix
testsupport/hermetic.pyredirectsHOME, the XDG dirs, the witan state files and both graph stores into a throwaway directory, clears the ambientWITAN_*selectors, and pins the terminal width. Each package gets a rootdirconftest.pythat loads it.It runs at import, not in a fixture, and that is the load-bearing detail. Importing
witan.serverIS a write —_ensure_graphcreates the store at module scope, deliberately, because the CLI's local-dispatch guard depends on it (#261, and #286 documents why it stays). That import happens during collection, so an autouse fixture body runs long after the store exists. Only something imported by a rootdir conftest is early enough — which is also the only place pytest acceptspytest_pluginsat all ("Defining 'pytest_plugins' in a non-top-level conftest is no longer supported",_pytest/config/__init__.py).PATHdeliberately keeps its entry for the real~/.local/bin. That is where CI installs the omnigraph binary and where_find_binarylooks whenPATHmisses. A binary is a tool, not state; relocating it would break every test that needs a graph, to prevent a leak that cannot happen.The guard
Redirection fixes today's leaks, not tomorrow's — a new package without the conftest, or anything resolving a real path itself, walks straight past it, silently. So
pytest_sessionfinishreports anything that reached the real home anyway.Strictness is inferred from
CIrather than set per-workflow: every runner exports it, so this lands in all four test workflows, all five publish workflows, and whatever gets added next, with no list to keep in sync. Tenenv:blocks would have been ten chances to forget the tenth. Warn-only locally, because your machine may legitimately be writing to those directories from another agent session, and a false failure people learn to re-run past is worth less than no check.AGENT_KIT_STRICT_HERMETICITY=1|0overrides.How can this be tested?
2394 tests pass across the five suites (1 / 308 / 572 / 529 / 984). Under the hostile harness every suite is green and the seeded HOME comes back byte-identical.
The guard was verified by inducing real leaks — throwaway tests writing a store inside the pre-existing
~/.local/share/witan/code/and appending to an existingmerge-watermarks.json, i.e. the two shapes the actual defects had:CI=true(inferred strict)AGENT_KIT_STRICT_HERMETICITY=1AGENT_KIT_STRICT_HERMETICITY=0The sentinel was removed afterwards. The leak path is deliberately not a permanent test — proving the detector fires means writing to the real home, which is the thing the mechanism exists to prevent — so
test_hermetic_guard.pyasserts the logic that decision rests on, plus that the redirection is actually in effect.The agent-config-kit failures were attributed by axis rather than guessed, at the base commit with the guard absent:
COLUMNS=40COLUMNS=200So all 8 are terminal width; none needed a populated HOME. (An earlier draft of the commit message said otherwise; corrected before this PR.)
just check-versions,just check-core-floor,./bin/gen_docs.py --checkand prek all pass. I also confirmed the newconftest.pyandtestsupport/do not land in a built wheel, by buildingwitan-codeand listing the archive — no version bump or CHANGELOG entry is needed because nothing published changes.Additional Context
Pinning the width is not a UX answer. It makes the suite assert message content, which is what those tests are about. Whether the CLI renders readably in a narrow terminal is a real question about the product that a green suite does not answer; the code says so where someone would otherwise be misled, and I can file it separately.
A new package needs its own rootdir
conftest.py— nothing supplies one automatically, and without it that suite runs against your real home. Documented in AGENTS.md next to the isolation guidance.testsupport/is deliberately not a workspace package: a sixth package would need a version, a CHANGELOG, a bumpversion block and a publish workflow for thirty lines of test support, and would land injust check-versions. It is imported off the repo root via asys.pathinsert in each rootdir conftest. (An earlier revision said this worked under a barepytestfrom the repo root too — it does not, and never did: that fails on 46 pre-existing collection errors because one package's venv cannot satisfy five, the same count asmainatdeedb68.)Not cleaned up: the historical debris already in my real
~/.local/share/witan/code(https_github.com_test_cg.omniand a number of other test-shaped stores). That is real user data on a real machine and not something a PR should delete.Review round 1 (
/code-review high, six findings — all addressed in 8b66899)Two were real regressions this PR introduced, and the first is worth reading:
WITAN_REQUIRE_OMNIGRAPHwas in_CLEARED.witan-core-tests.ymlsets it to"1"so a missing omnigraph binary is a hard failure rather than a skip, andtest_binary_contract.pyreads it at module scope — after this plugin runs. Clearing it forced_REQUIREDtoFalse. Confirmed with a probe reading the var at module scope:'<absent>'before the fix,'1'after.Correcting the impact, which both the finding and my first reply overstated. This did not make the suite skip. The mark is
skipif(_BINARY is None and not _REQUIRED)— both conditions — and CI installs the binary, so the 20 contract tests ran on the buggy commit too (run32857593245at67e460eshows them executing). What clearing the var destroyed was the tripwire: with_REQUIREDfalse, deleting the install step or a failed fetch would have left the suite skipping green instead of failing loudly, which is precisely the scenario that workflow comment exists to describe. A disabled safety net, not an already-retired suite. Still a real regression — the fix stands — but worth stating accurately, since "the suite was silently retired" is a stronger claim than the evidence supports.The leak detector was blind to the leak it was written for. It compared only immediate children, and the motivating defect lands at
~/.local/share/witan/code/<slug>.omni— inside acodedirectory that already exists on any machine that has run the indexer. It fired on CI, where the tree is absent, and nowhere else. My own verification missed this because the sentinel I induced was a top-level child — a leak of a shape the real one does not have. Now a depth-3 snapshot keyed on absolute path with size+mtime markers, so the #282 append-to-an-existing-file shape is detectable at all.The rest:
WITAN_TEST_OMNIGRAPH_SERVER/_GRAPHwere being cleared, making the live-server opt-in tests unreachable (both now in an_EXEMPTtuple asserted disjoint from_CLEARED); six more ambient selectors added;CI=false/CI=0no longer read as strict;pytest_pluginsdropped for a direct import.Deepening the watch surfaced a problem the shallow version hid: four of five suites then warned, every line of it ambient — the indexer touching
.omni.reposidecars, the OIDC client refreshing its token, Claude Code writing.claude.json. Measured with a 95-second idle probe (no tests running) and a paralleltest-all. Modifications of those paths are now filtered; creations never are, so on CI — where none of them exist — a real write still reports. Without that filter the check would have been re-run past, which this file's own docstring calls worse than no check.2399 tests pass, no warnings.