Skip to content

Keep each Docker test run's containers and counts to itself - #185

Merged
mchwang merged 5 commits into
mainfrom
fix/docker-test-run-isolation
Oct 20, 2026
Merged

mchwang merged 5 commits into
mainfrom
fix/docker-test-run-isolation

Conversation

@mchwang

@mchwang mchwang commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Keeps each run of the Docker-backed supervisor and container tests to its own containers and counts. Ported from the local branch fix/supervisor-test-cleanup (84dd662, never opened as a PR) onto current main. It was not rebased, because the tests have changed a lot since.

Problem, checked on main 42abf4a

Problem Still on main? Evidence (macOS, Docker Desktop 27.4.0)
A container that a failed run leaves behind takes the next run's name Yes Agent names are a hash of the attempt ID (agents/container/profile.ts:318), and the supervisor tests use fixed IDs. With codeboost-agent-finite-d4f568b800c6a61b left running, "captures finite output" fails: Conflict. The container name ... is already in use.
Concurrent runs count each other's objects Yes Another run's volumes were created during "fails closed without leaving storage when a repository exceeds its allocation". The test failed with expected [ …(8) ] to deeply equal []. Under the same load, the keeper-orphan test listed 17 of the other run's keepers as orphans, and its code force-removes every orphan before it asserts.
macOS /var vs /private/var in the unreleased-inventory tests No Both tests pass on main, because profile.ts:298-299 now records the resolved directory. The old branch's resolved() helper is not ported.

Change

  • Supervisor test runs: each run has its own random runner owner and attempt-ID suffix, so agent container names are unique to the run.
  • Container test runs: each run has its own random runner owner. Its attempt IDs were already random.
  • New fixture test/fixtures/docker-sweep.ts (removeRunnerObjects): both files call it in a finally block after their ordinary afterAll cleanup. It removes everything labelled with the run's owner: containers and volumes by force, networks without force.
    • It reports what it finds and any Docker call that fails.
    • It never throws, so an error from the ordinary cleanup still fails the hook.
    • It may not run before a hook timeout.
  • Inventory counts limited to this run:
    • The three refused-storage tests count their own allocation's objects (byAllocation, now at module level; fixture() takes a caller-made owner).
    • The vendor-network test counts both allocations' networks and proxies.
    • The keeper-orphan test and the supervisor's egress count use the runner owner.
  • First supervisor test timeout: it gets a 60 s timeout. Its fixture creates task storage in Docker, and that hit Vitest's 5 s default on the main baseline run.

Validation

Each gate is reported separately. The machine was heavily loaded (load average 60–90).

  • Typecheck (npx tsc --noEmit) on a0c38a3: passed.
  • Real Docker, both files serially (npx vitest run --project docker test/agent-supervisor.test.ts test/agent-container.test.ts) on validated head a0c38a3: 198 passed, 0 failed, 1 skipped (the Linux-only non-UTF-8 test). Neither sweep found anything to remove.
  • Intermediate checkpoints, real Docker:
    • 5a1f3a0, both files: 197 passed, 1 failed. The failure was "does not wedge when an injected decoder ignores abort": 16.9 s against a 15 s bound that includes creating fixture storage. Machine load caused it, not the change.
    • 5a1f3a0, supervisor file rerun: 47/47 passed.
  • Regressions:
    • Leftover name: with the same leftover finite container running, "captures finite output" passes on 523a4da (it fails on main).
    • Concurrent objects: another run kept creating volumes and keepers while the 13 targeted storage, egress and keeper tests ran. All 13 passed on 523a4da. The storage test fails on main.
    • Sweep: a throwaway test (not committed) leaves an agent that ignores SIGTERM running, then fails (checked on 7b4a9c3). The ordinary cleanup throws (Vendor network cleanup did not settle) and still fails the suite. The sweep then removes 2 containers, 1 network and 2 volumes, and nothing with that owner is left.
    • Sweep in agent-container: a similar probe (checked on 47e8e09) leaves task storage untracked. The sweep removes the keeper and both volumes.
  • Parallel suite and CI: not run locally for this change, because only Docker-backed test files and a fixture changed. CI test and real-docker will report.

Reviews

Base for every round: 42abf4a.

Round Reviewer Head Findings What changed / declined
Self author 5a1f3a0 none —
1 independent agent 5a1f3a0 (1) Runner-scoped storage counts miss an object whose runner label is wrong or missing. (2) The sweep comment overclaims for hook timeouts. (3) The sweep removes leaks silently. (4) A profile() parameter shadows attempt(). All fixed in 523a4da: count by allocation, correct the comment, report removals, rename the parameter.
2 independent agent 523a4da (1) The vendor-network test ignores the unlabelled proxy, and its comment is half true. (2) Allocation filters are narrower than the old "any allocation" filter. (3) Failed sweep calls are not reported. (4) Comment wording on timeouts. (1), (3) and (4) fixed in 7b4a9c3. (2) declined: daemon-wide filters are the bug being fixed, and storage always applies owner labels.
3 independent agent 7b4a9c3 (1) The sweep can use the whole hook budget (informational). (2) With a random owner, nothing would find agent-container leftovers. (3) The adapter-cancel egress check cannot fail, because its shim never calls Docker. (2) fixed in 47e8e09 (shared fixture used by both files). (1) is already stated in the comment. (3) is pre-existing and outside this change.
4 independent agent 47e8e09 The doc comment says networks are force-removed. Fixed in a0c38a3.
5 independent agent a0c38a3 none. One style nit: the container file writes io.codeboost.runner literally. Declined: that file writes every label literally.
Copilot copilot-pull-request-reviewer a0c38a3 none ("No open issues", approval recommended) —

Clean independent review: round 5, base 42abf4a / head a0c38a3. After the PR opened, main moved to aded16e (#183). That commit touches none of this PR's files, and the branch was not rebased, so the reviewed diff is unchanged. Copilot review was requested on a0c38a3.

Review-lesson audit

  • Covered by an existing rule ("Treat every behavioural claim the change makes, in code comments … as something to verify", Review readiness): R1-2, R2-1 (comment), R2-3, R2-4, R4-1.
  • Covered by the rule that a test must fail when the feature it tests is broken: R1-1, R2-1 (proxy).
  • One-off:
    • R1-3 and R3-2: sweep design choices specific to this test helper.
    • R1-4: naming.
    • R3-1: limit already documented.
    • R3-3: pre-existing.
    • R2-2 and R5: declined, with reasons above.
  • New rules: none.

Follow-ups (not in this PR)

  • test/agent-network.test.ts, test/agent-gate.test.ts and test/runner-workspace.test.ts still use the fixed owner 0123456789abcdef…. agent-network.test.ts also uses the fixed attempt network-probe. runner-workspace.test.ts:155 lists volumes across the whole daemon, and states that as a precondition.
  • The adapter-cancel egress check in test/agent-supervisor.test.ts cannot fail (review round 3, finding 3).

The old branch fix/supervisor-test-cleanup stays until this merges.

🤖 Generated with Claude Code

mchwang and others added 5 commits October 20, 2026 13:07
The supervisor tests named agent containers from fixed attempt IDs under one
fixed runner owner, so a container that a failed run left behind (an agent that
ignores SIGTERM keeps running) took the next run's name and failed it with a
name conflict. Each run now has its own attempt suffix and runner owner, and
afterAll force-removes every container, network and volume labelled with that
owner after the ordinary cleanup, even when that cleanup throws.

The storage, egress-network and keeper counts in both files counted every
object on the daemon, so a concurrent run's objects failed them; the keeper
test would also force-remove another run's keepers. They now count only this
run's objects, by runner owner, or by allocation where the objects are made
without the runner label on purpose.

The first supervisor test had no timeout of its own and could exceed Vitest's
5 s default while its fixture created task storage.

Ported from fix/supervisor-test-cleanup (84dd662). Its macOS /var versus
/private/var fix for the unreleased-inventory tests is not needed: main records
the resolved directories already.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…moves

Independent review (round 1) found the runner-scoped storage counts would miss a
leaked object whose runner label was wrong or missing; the three tests now
count their own allocation's objects. The afterAll sweep now reports each
removal, its comment says a hook timeout skips it, and profile()'s parameter no
longer shadows attempt().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review (round 2): the vendor-network test counted only networks,
so a leaked unlabelled proxy would pass; it now counts every object of both
allocations. The afterAll sweep reports a Docker call that fails, and its
comment says it may not run before a hook timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review (round 3): with a random runner owner, nothing would find
what a failed agent-container run leaves behind. Both files' afterAll hooks now
call the same removeRunnerObjects fixture after their ordinary cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review (round 4): Docker cannot force-remove a network that a
container still uses, and the sweep does not try to.

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

Copilot AI 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.

🟢 Approval recommended

No blocking issues remain in these test-only changes, and the PR reports successful type checking and real-Docker validation.

No open issues 🎉

What changed in this PR

Isolates Docker-backed test runs so leftover or concurrent resources do not cause naming conflicts, incorrect counts, or cross-run cleanup.

Changes:

  • Adds per-run owners and unique supervisor attempt IDs.
  • Scopes resource checks to the relevant run or allocation.
  • Adds shared fallback cleanup and extends the first supervisor test’s timeout.
File Description
test/​fixtures/​docker-sweep.ts Adds owner-scoped cleanup with removal and failure reporting.
test/​agent-supervisor.test.ts Isolates names and counts, adds fallback cleanup, and adjusts a timeout.
test/​agent-container.test.ts Isolates inventory checks and adds fallback cleanup.

Review effort: Balanced

@mchwang
mchwang merged commit a56bbdc into main Oct 20, 2026
3 checks passed
mchwang added a commit that referenced this pull request Oct 21, 2026
…188)

* Give the network, gate and workspace Docker tests per-run isolation

Each file now has its own random runner owner and removes what that owner
still holds after its ordinary cleanup, as #185 did for the supervisor and
container tests.

- runner-workspace: the path identity probe test counted probe containers
  and probe-option volumes across the whole daemon, so a leftover probe
  container or another runner's probe failed it. It now follows only this
  probe's container, by the name a PATH shim records (the shim runs the
  real docker unchanged), and the volumes Docker's events show mounted in
  it.
- agent-gate: broken-isolation containers carry the runner label, and the
  client is killed with SIGKILL at its timeout. SIGTERM was passed to the
  container's shell, which ignores it, so the client waited for the
  container to exit.
- agent-network: network and proxy names are random, so the fixed attempt
  IDs reach only labels; the per-run owner and sweep are preventive.

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

* Fail a gate case whose probe client is killed at its timeout

A SIGKILLed client has no exit status, which the cases' not.toBe(0) checks
accepted, so a probe that hung would have passed. Found in independent
review.

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

---------

Co-authored-by: Claude Opus 5.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.

2 participants