Repository navigation
fix(e2e): tolerate pattern fixture containers removed before inspection - #386
Merged
Merged
Conversation
The P01 killed-installer step polls owned install containers while a concurrent `hack up` recreates the linked `install` service. Compose removed the listed container between `docker ps -aq` and `docker inspect`, so the helper failed with "No such object". ownedContainers now inspects with `--type container` and accepts only a listed ID that Docker reports missing and `docker ps` confirms absent; any other inspection failure still fails the scenario. Refs HACK-1296 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 22, 2026 08:11
roodboi
added a commit
that referenced
this pull request
Oct 30, 2026
…#528) Fixes HACK-1346 for the e2e harness on `main`. ## What I found - **The happy path doesn't leak.** I ran all five main Docker scenarios (`cache-prune`, `dependency-cache`, `up-down`, `lifecycle-host-process`, `worktree-parallel-up`). The host had 19 networks before the run and 19 after. - **Killed runs leak.** I sent SIGINT to the runner during `worktree-parallel-up`. It left 2 networks and 4 containers: `e2e-<hex>` and `e2e-<hex>--e2e-parallel`. Nothing runs a `finally` when the process is killed. - **The fallback sweep used the wrong branch name.** `downBestEffort` ran `docker compose -p <name>-<branch> down`, but the CLI names branch instances `<name>--<branch>` (`resolveLifecycleComposeProjectName`). So if `hack down --branch` failed, the fallback missed the branch instance. - **Nothing failed on a leak.** When a scenario skipped cleanup, the run still passed and the network stayed behind. - **Address pool.** This host's Docker allows 30 IPv4 `/24` pools. Non-e2e Compose projects already use 16 of them, so the 33–34 networks in the issue means the pool was full. P01 needs two networks: primary plus linked worktree. - **P01 isn't on `main`.** `pattern-p01-dependency-bootstrap` and `pattern-fixture.ts` exist only on `next` (#336, #386). Their fixtures get names from `randomProjectName()`, so this PR records ownership there too. Once `next` lands, those scenarios get the leak check and sweep with no further change. ## Change - New `tests/e2e/docker-ownership.ts`: - Every fixture name this process generates is recorded as owned, in memory and in a per-PID journal under `$TMPDIR/hack-e2e-ownership/` (override with `HACK_E2E_OWNERSHIP_DIR`). - Cleanup only touches Compose containers, networks and volumes whose `com.docker.compose.project` label is an owned name or `<owned>--<branch>`. It removes them by exact ID and never prunes. - It never touches other concurrent e2e runs or unrelated projects. - `runScenarios`: after each Docker scenario, any owned network still present **fails the scenario**. The harness then removes that scenario's owned resources and checks the networks are gone. - `run.ts`: - Prints `fixture networks: before=N after=M left by this run=K` and exits 1 if K > 0. - On SIGINT/SIGTERM/SIGHUP, removes the run's owned resources before exiting. - At start, reclaims journals left by dead runs (for example after SIGKILL). It skips journals whose process is still alive. - `downBestEffort` now uses `<name>--<branch>`. - README "Cleanup guarantees" is updated. ## Proof (real Docker on the shared m5 host, `./dist/hack`) | Case | main's harness | this PR | |---|---|---| | SIGINT during `worktree-parallel-up` | exit 130; **2 networks + 4 containers left** | exit 130; handler removed 4 containers + 2 networks; 0 left | | Mutation: `up-down` skips `hack down` and `downBestEffort` | **exit 0, PASS**, network `e2e-19c60e0e` left | **exit 1, FAIL** "scenario left 1 Docker network(s)…"; sweep removed it | | SIGKILL during `worktree-parallel-up` | n/a | 2 networks + 4 containers left (expected); the next run printed `reclaimed from a dead e2e run: …` and removed all of them | | Full suite (`HACK_E2E_DOCKER=1 HACK_E2E_REQUIRE_DOCKER=1`) | — | 14/14 pass, `fixture networks: before=0 after=0 left by this run=0` | Unit tests: `tests/e2e-docker-ownership.test.ts` (7 tests) use a fake Docker runner. They cover: - ownership boundaries (`e2e-abc1` does not own `e2e-abc12--x`); - that only owned resources are removed, containers then networks then volumes, with no prune; - the leak check, including through `runScenarios`; - journal recovery: dead PID swept, live PID left alone; - `branchComposeProject` matches the CLI's naming. Mutation check: with the leak check disabled, and with branch naming reverted to main's single dash, 2 of the 7 tests fail. All 7 pass with the fix. Gates: `bunx turbo run typecheck check test --force` passes (6/6 tasks, 968 tests, 0 fail). Without `--force`, turbo replays cached results because root `tests/` is outside the package inputs. `bunx ultracite check tests/e2e tests/e2e-docker-ownership.test.ts` is clean. ## P01 `runningConsumersComplete` race (on `next`): not confirmed It lists running `app` containers and then runs `docker exec` on each, so the code does check then act. The only overlapping operation is the first call, during the in-flight linked `hack up`. There, `app` depends on `install` (`service_completed_successfully`), and Compose waits on that dependency before it recreates `app`. The installer is killed, so `up` fails before any `app` container is touched. Every later call runs after `up` has settled. I didn't reproduce it, and I didn't run it here because P01 isn't on `main`. Leaving it with HACK-1296. ## Release signal Only test and harness code changes; no CLI behaviour changes, so no release. @codex review 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- codesmith:footer --> --- <a href="https://app.blacksmith.sh/hack-dance/codesmith/hack/pr/528?autoLogin=true&ref=codesmith_pr_footer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v3.svg"><img alt="View with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png" width="162" height="28"></picture></a> <a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1795988207&installation_model_id=17053&pr_number=528&ref=codesmith_pr_footer&repository=hack-dance%2Fhack&return_to=https%3A%2F%2Fgithub.com%2Fhack-dance%2Fhack%2Fpull%2F528&signature=ff90fcdabe6ff8b227747e4adee3f19aafed196418fc5c4e151c1affdb97fb40"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png" width="190" height="28"></picture></a> <sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you need.</sup> <!-- codesmith:autofix:disabled --> <!-- /codesmith:footer --> --------- Co-authored-by: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> 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.
Root cause
The P01 scenario failed on CI (PR #341, run 39435455774, job 118671149247, docker-e2e shard 3/4) at the start of the killed-installer step:
docker inspect 307b0e394010returned[]andError: No such object(exit 1).The timeline in the job log shows what happened. The lock-change step logged success at 14:03:28.54. The failure diagnostics then ran at 14:03:28.80. So the failure came from the first
waitForpoll after the step startedhack up --jsonin the background with a newinstallcommand and a newhack.dependencies.cache-generationlabel.The poll calls
ownedContainers, which runsdocker ps -aqand thendocker inspectas two separate calls. The ID it listed was the linked worktree's exitedinstallcontainer from the lock-change step. The concurrenthack upchanged that service's configuration, so Compose removed the container and created a new one between the two calls. The strictcheckedDockerinspect then failed the scenario. The diagnostics that followed show a fresh linkedinstallcontainer with exit 0 and no logs, which is the new container Compose had just created.The problem was in the test helper's list-then-inspect step. The dependency-cache behavior itself was not at fault.
Fix
tests/e2e/scenarios/pattern-fixture.ts:A new pure function,
partitionContainerInspection, splits adocker inspect --type container <listed ids>result into the inspected containers and the listed IDs that no longer exist. It accepts a missing ID only when all of these hold:No such objectorNo such containerline for an ID it listed (the wording differs between client versions);Any other result is an error: a timeout, output that is not JSON, a daemon or permission error, a missing ID it did not list, or an ID that is silently dropped.
ownedContainersinspects with--type container. For each missing ID, it runsdocker ps -aq --filter id=<id>to confirm the container is really gone, then leaves it out of the result and logs that it did so. Any other inspection failure still fails the scenario.What stays guaranteed
The scenario's assertions are unchanged. The killed attempt's volume must still be populated but have no ready marker. No running consumer may mount it. A retry of the same generation must still be refused and must address the same volume. A generation bump must still recover with a fresh payload.
only()still requires exactly one owned container, and thewaitForpoll retries until the running installer has started writing.Tests
tests/e2e/pattern-fixture.test.ts(new) covers:Each test was checked by mutating the code under test:
Evidence
mise x bun@1.4.2). Docker client and server 29.8.2 (OrbStack, arm64).dockershim on PATH removed one of two owned test containers just beforedocker inspect, then the realownedContainersran against the real daemon:docker inspect pattern fixture operation (expected exit 0, got 1). This is the CI error.owned container(s) <id> were removed between listing and inspection; confirmed absent.Error response from daemon: No such container: <id>with--type container, anderror: no such object: <id>without it. The tests cover both.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors (34 existing warnings). gitleaks 8.30.1 over the branch: no leaks.bun run teston a heavily loaded shared host (load average 40–145) had 795 failures across 119 files.--timeout 60000, 104 of those 119 files passed.tests/e2e/scenarios/pattern-fixture.ts.Remaining gaps
hack upfailed withall predefined address pools have been fully subnetted. The local daemon's networks belong to other projects and agents, and I did not prune any of them. Hosted docker-e2e shard 3 on this head is the end-to-end proof that is still needed.Refs HACK-1296
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.