Repository navigation
fix: hermetic test fixtures, load-sized budgets and a coded env lock refusal - #504
Merged
Merged
Conversation
… docs
A dangling or non-directory `.hack/.internal` beside an env layer made
`hack env set`/`unset` refuse with one line that had no stable code (and
`--json` reported E_UNEXPECTED). EnvLayerLockRefusedError is now a coded
E_PATH_CONFLICT refusal, the code `hack env hydrate` already gives the same
entry.
docs/env.md said hydrate reports E_PATH_CONFLICT for a `.hack/.internal`
that is a file; it reports E_CONFIG_INVALID ("Cannot inspect Hack project
inputs ...") from its earlier adoption-state check, which refuses ENOTDIR
rather than treating it as absent. The doc now says so, and a CLI test pins
it. The storage carrier reference and the probe-budget test now state the
decided reason for a hung helper start since #437: storage-budget, never
probe-timeout.
Refs HACK-1459
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixture `git` inherited the runner's environment: a caller-exported GIT_DIR made tests/native-env-metadata.test.ts commit, branch and register a worktree in another repository, and a contributor's global core.hooksPath (read through XDG_CONFIG_HOME even under a private HOME) refused fixture commits. tests/helpers/fixture-git.ts now builds every fixture git environment from PATH/TMPDIR plus the caller's values, removes every GIT_* variable, DOCKER_HOST/DOCKER_CONTEXT and the runner's HOME/XDG_CONFIG_HOME, and sets GIT_CONFIG_GLOBAL=/dev/null, GIT_CONFIG_NOSYSTEM=1 and a fixed identity. All fixture git spawns in tests/ go through it; the e2e harness gains an exact-env option for them and the checkout write guard drops caller repository variables. tests/fixture-hermeticity.test.ts proves an exported GIT_DIR, work tree, index and hostile global hooks leave a scratch repository byte-for-byte unchanged, and refuses any new unrouted fixture git spawn. Budgets sized for the work under load, assertions unchanged: - native-env-metadata: the template beforeAll gets a named 60 s budget (24/36 runs exceeded 5 s under I/O load) and afterAll waits for a timed-out build, so template directories no longer leak. - native-compose-file-owner (HACK-1460): one named 60 s per-case budget replaces the 5 s/15 s/30 s mix, and afterEach waits for a timed-out body before teardown so it cannot change the next case's store. - native-compose-storage-witness-command: `down --recover` ran under invoke's 1 s HACK_COMPOSE_STARTUP_TIMEOUT_MS and reported private-uncertain once load stretched the stop; recovery now gets a 30 s deadline, CLI processes a 45 s kill bound, the carrier the product default, and the case runs again with an ambient DOCKER_HOST at a missing socket. Refs HACK-1459 Refs HACK-1460 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 30-run soak caught the GIT_DIR hermeticity case failing 2/30: `commit` starts a detached `git maintenance run --auto` that takes objects/maintenance.lock after the command returns, so the fixture repository changed under the comparison. Fixture git now sets maintenance.auto=false and gc.auto=0 through GIT_CONFIG_COUNT, which also keeps an unowned background git from outliving any fixture; a test pins both settings. native-env-metadata cases and the generation linked-worktree case each run the product's Git worktree verification and took more than 5 s at load average above 100; they get named 30 s budgets. Assertions unchanged. Refs HACK-1459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 30-run soak at load average 120 still failed the recovery cases: the 30 s recovery deadline and the 45 s kill bound both expired mid-stop (exit 137). Recovery now runs with DEFAULT_COMPOSE_STARTUP_TIMEOUT_MS, as on a host that sets no budget, CLI processes get a kill bound a minute above it, and cases an 8 minute budget. Assertions unchanged; in CI the file still takes about 5 s. Refs HACK-1459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second 30-run soak passed 29/30 at load average 200-300; the one failure was the unknown-enrolled case, whose `ps` and `down` still ran under invoke's 1 s HACK_COMPOSE_STARTUP_TIMEOUT_MS. Every source CLI process in the file now runs with the product default budget, the same one recovery already uses. Assertions unchanged. Refs HACK-1459 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean text merge; packages/cli typecheck passed on the merge result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ownership query in native down kept a fixed 15 s aggregate bound and ignored HACK_COMPOSE_STARTUP_TIMEOUT_MS, so a slow engine was refused as probe-timeout at effect-ownership while most of the stop budget remained. The query now follows that budget, at least 15 s and at most the probe's 60 s ceiling. Other operations keep the 15 s default. Correct docs/env.md: a .hack/.internal that is a regular file gives E_CONFIG_INVALID, only a symlink gives E_PATH_CONFLICT. Name a budget for the dependent route finalizer cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ronment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in tests/checkout-write-guard.test.ts: run() keeps this branch's complete child environment, and the slow-status case from next builds its PATH override on the private CLI environment explicitly, so it keeps the private HOME it had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alone In a native down, the last ownership check ran after the stop deadline was fixed with a window of its own. An engine that answered just before the deadline started compose down with about a second left, and the stop was killed partway. That check is now bounded by the smaller of the probe timeout and the time left, less a reserve for the child: a fifth of the budget above 1 s, at most 5 s. A slow engine is refused as probe-timeout before any child runs, and down --recover completes the stop. The hermeticity scanner checked a whole surrounding block for each call, so an unrouted spawn beside a helper call passed. It now scans the call's own parentheses, ignoring brackets in string text, with inline negative controls. It also recognizes run from src/lib/shell.ts and flags Bun shell templates starting with git; an argv held in a variable stays a documented gap. The stricter scan exposed one real unrouted spawn from next, in tests/env-set-local-contract.test.ts, which now uses the shared environment. The hermeticity file gets a longer case budget because a case outlived Bun's 5 s default on a loaded host. Refs HACK-1459, HACK-1460. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps both import additions in the e2e fixture: this PR's fixture git environment and next's gitignore markers and Docker ownership record. Three git spawns that arrived on next (#545 and the HACK-1709/1711 follow-ups) built their own git environment, which this PR's scan refuses. They now use fixtureGitEnv. The legacy-scope test keeps its private HOME; its commits now carry the shared fixture identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
commented
Nov 3, 2026
Brings in #499, #557-#564, #566, #568-#570. #563 added a file-owner case with its own timeout, which this PR's two-argument test wrapper rejected. The wrapper now forwards a case timeout and keeps the file's 60 s budget as its floor. The fixture git spawn scan still passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
November 4, 2026 03:47
roodboi
added a commit
that referenced
this pull request
Nov 4, 2026
Refreshes the base after #499, #504, #554 and the other merges of Nov 3. This PR and #570 made the same import repair to tests/native-compose-adoption-generation.test.ts, so the merge listed mutationCommands twice; this takes next's copy of that file. Typecheck passes, and so does that file's run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
added a commit
that referenced
this pull request
Nov 4, 2026
Refreshes the base for HACK-1810 and HACK-1801 after #554, #504 and the other merges of Nov 3. The text merge is clean. Next changed five files this PR also touches: the model coverage doc, adoption binding and generation, and the binding and routing generation tests. Typecheck passes, and so do this PR's adoption test files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Update at 9fe1186: this merges
nextat 1f5a905, bringing in #499, #557–#564, #566 and #568–#570.The text merge was clean, but typecheck found one semantic conflict. #563 added a file-owner case that passes its own timeout, and this PR's two-argument
testwrapper intests/native-compose-file-owner.test.tsrejected the third argument. The wrapper now forwards a case timeout, and the file's 60 s budget stays the floor.Checked on Bun 1.4.4, with the config compiler built with Rust 1.97.1:
tscpasses, and Biome passes.Update at 72661c0: merged
nextat 1df527b (#545, #419, #337). The e2e fixture conflict was two import additions; both are kept. This PR's git spawn scan then caught three spawns that arrived onnextwith their own git environment:tests/project-env-worktree-local.test.ts,tests/env-legacy-service-scope.test.ts(which keeps its privateHOMEand now commits with the shared fixture identity) andtests/e2e/scenarios/worktree-parallel-up.ts. All three now usefixtureGitEnv. Focused tests on Bun 1.4.4, with the scan and the env, doctor and lifecycle files: 478 pass, 0 fail. Typecheck, Biome and gitleaks pass. The e2e scenario change is covered only by hosted docker-e2e.Current state (head ba522c9, 2 Nov)
Fixes the three findings of the independent review of a16f361.
origin/next(8621d90) is merged in. The older notes below describe earlier heads.runfromsrc/lib/shell.tsand flags Bun shell templates that start with git. An argv held in a variable is a documented gap. The stricter scan found one real unrouted spawn fromnext, intests/env-set-local-contract.test.ts, now routed.compose downis bounded by the smaller of the probe timeout and the time left, less a reserve for the child: a fifth of the budget above 1 s, at most 5 s. A slow engine is refused asprobe-timeoutbefore any child runs, andhack down --recovercompletes the stop.tests/fixture-hermeticity.test.tshas a 120 s case budget, because a case outlived the 5 s default on a loaded host. No assertion changed.Verified locally with Bun 1.4.2 and a private HOME, HACK_HOME and DOCKER_CONFIG, HACK_DAEMON_AUTOSTART_DISABLE=1 and DOCKER_HOST at a missing socket, at load about 8-15:
fixture-hermeticity,env-set-local-contract,checkout-write-guard,native-compose-stop-admission. One run each, passing:native-compose-storage-witness-command,native-compose-file-owner.packages/clitypecheck, Biome on the changed files and gitleaks over a16f361..ba522c9 pass. Hosted CI on this head is not yet confirmed here.Open:
tests/native-compose-down-command.test.tsstill fails under this load (HACK-1768). A paired run gave 9 failures with this change and 9 without. Owner decision from the review: with the timeout unset, a hung engine now takes up to 60 s, not 15 s, to refuse each ownership query in a nativedown.Current state (head f3619d8, 2 Nov)
Rework after the failed verification of 0f51025.
origin/next(ae032e7) is merged in.downkept a fixed 15 s bound and ignoredHACK_COMPOSE_STARTUP_TIMEOUT_MS, and no option or env input could change it. It now follows that budget, at least 15 s and at most the probe's 60 s ceiling. Other operations keep 15 s. On an unset host, a hung engine now takes up to 60 s, not 15 s, to refuse a nativedown.docs/env.mdnow says a regular-file.hack/.internalgivesE_CONFIG_INVALIDforenv setandenv unset, which matches their real output.docs/cli.mddescribes the new bound.Verified locally with Bun 1.4.2, a private HOME, HACK_HOME and DOCKER_CONFIG, and no
dockeron PATH:tests/native-compose-storage-witness-command.test.tspassed 5 of 5 runs at load about 10 and 5 of 5 at load 33-98.effect-ownership,probe-timeout) when the wiring is removed.packages/clitypecheck, Biome on the changed files and gitleaks pass.Open:
tests/native-compose-down-command.test.tsfailed 10-11 of 14 cases at load 30-60 with this change. Many hitinvoke's 15 s kill (exit 137), includingupcalls this change does not touch; the exit 1 failures were not diagnosed. Without the wiring it failed 11 of 14 at load 30-54 and 13 of 14 at load 62-162. At load 13-20 this head failed 4 of 14: two exit 137 and twodown --recoverexit 1. This file usesinvoke's default 1 s budget, so the query bound stays at the old 15 s and the fix cannot change its results. The file has not been run at idle. Hosted CI at this head is still to be read.Summary
Follow-ups from HACK-1459 (items 1-6 plus its comment) and the HACK-1460 flake. Test fixtures no longer read the caller's Git or Docker environment, load-sensitive cases get named budgets sized for their work, a timed-out case can no longer break the next one, and one env refusal gets a stable code.
Root causes
gitcalls in about 50 test files inherited the runner's environment.GIT_DIR/GIT_WORK_TREEsent the fixture'sinit/commit/worktree addinto that repository.tests/native-env-metadata.test.tscleared those variables inbeforeEach, but its fixture runs inbeforeAll, before that.$XDG_CONFIG_HOME/git/config, whosecore.hooksPathguard refused fixture commits. That is the actual cause of the local-only failure ofnative-compose-generation"real linked worktree ...". The fixture already set its identity with-c user.*; it fails on the hook, not the identity.down --recoverstorage-witness cases fail locally.DOCKER_HOSTdid not cause this. The cases fail the same way with and without it, and with a private or a real HOME.invoke()defaultsHACK_COMPOSE_STARTUP_TIMEOUT_MSto 1 s, anddown --recoverbounds its owned stop by that budget. On a loaded host (load average 60-150 here) the stop's synthetic Docker processes take longer than 1 s, so the CLI reportsnative_compose_effect_refusal {effect-execution, private-uncertain}instead ofE_COMPOSE_FAILED. With a 60 s budget the same run gaveE_COMPOSE_FAILEDat load average 108. Two more wall-clock bounds expired under load: the in-process carrier deadline (Date.now() + 30_000, which gave "witness missing ... uncertain") andinvoke's 15 s kill (exit 137).afterAllas soon asbeforeAlltimes out, while the build keeps creatingnative-env-metadata-template-*directories. Probe: a 200 ms hook budget with a 300 ms body showed afterAll running while the body was still in progress.upcancellation case had an explicit 5 s budget. Other cases had 15 or 30 s. When a case times out, Bun runsafterEachwhile the body keeps running, and that body reads the module-levelstore/root, which the nextbeforeEachreplaces. A late body can therefore act on the next case's store. That is how one timeout becomes "state unsafe" in the next case.storage-budget. That is the right reason, because the remedy is the budget variable that the summary names. The code, the probe-budget test and the reference already agreed; the doc and the test now state it explicitly.hack env hydratewith a.hack/.internalfile is refused by the earlier legacy adoption-state check (ENOTDIR counts as an input access failure, never as absence), so it reportsE_CONFIG_INVALID. docs/env.md saidE_PATH_CONFLICT.EnvLayerLockRefusedErrorwas an uncodedClosedCliError, so the CLI printed it without anE_*prefix and--jsonreportedE_UNEXPECTED.Fix
tests/helpers/fixture-git.ts:fixtureGitEnv(),fixtureGitShellEnv()(forsrc/lib/shell.ts),runFixtureGit[Sync]().PATH/TMPDIRplus the caller's values.GIT_*variable,DOCKER_HOST/DOCKER_CONTEXTand the runner'sHOME/XDG_CONFIG_HOME.GIT_CONFIG_GLOBAL=/dev/null,GIT_CONFIG_NOSYSTEM=1, a fixed author and committer identity, andGIT_TERMINAL_PROMPT=0.gitEnvlets a test set a Git variable on purpose (used forGIT_INDEX_FILEandGIT_CEILING_DIRECTORIES).tests/goes through it (about 50 files). The e2erunCommandgets anexactEnvoption. The checkout write guard drops a caller's repository variables but keeps the contributor's config.tests/fixture-hermeticity.test.ts:GIT_DIR, the work tree, the index, the object directory, the common dir, a hostileGIT_CONFIG_GLOBALand a hostile identity, then runs every fixture route. The scratch repository stays byte-for-byte unchanged, and no hostile hook runs..gitconfigcannot add hooks, signing or identity.DOCKER_HOST/DOCKER_CONTEXTnever reaches fixture git or the private CLI env.native-compose-storage-witness-command: namedRECOVERY_DEADLINE_MS(30 s),CLI_COMMAND_TIMEOUT_MS(45 s) and a 3 min case budget. The carrier deadline uses the product default. The source-unavailable recovery case also runs with an ambientDOCKER_HOSTat a missing socket plus aDOCKER_CONTEXT.native-env-metadata: namedTEMPLATE_BUDGET_MS(60 s) on both hooks.afterAllwaits for the build to settle, and a build that outlives that wait removes its own directory.native-compose-file-owner: one namedFILE_OWNER_CASE_BUDGET_MS(60 s) viasetDefaultTimeoutreplaces the per-case 5/15/30 s budgets.afterEachwaits for the running body before teardown.EnvLayerLockRefusedErroris now a codedE_PATH_CONFLICTHackCliError. docs/env.md is aligned on hydrate's.internalfile and the lock refusal. The storage-carrier reference namesstorage-budgetas the reason for a hung start.Assertions in the flaky cases are unchanged. Only budgets, deadlines and teardown order changed.
Evidence
In progress: local 30-run soaks under load and hosted CI at this head; this section is updated when they finish.
Gaps
Listed when the evidence above is complete.
Refs HACK-1459
Refs HACK-1460
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.