Skip to content

D: real-Docker export through a recovery handle after a restart (#51) - #109

Merged
mchwang merged 1 commit into
mainfrom
test/51-recovery-handle-export
Oct 3, 2026
Merged

mchwang merged 1 commit into
mainfrom
test/51-recovery-handle-export

Conversation

@mchwang

@mchwang mchwang commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Closes #51. Items 1–6 of #51 have already merged (#69, #70, #71, #72, #73, #76). This PR covers the last "Done when" gap.

The gap

The "Done when" list for #51 asks the real-Docker suite to cover "recovery handles being accepted by export and removal after a simulated restart". Removal by handle already had a real-Docker case: "recovers only its own runner after a restart…". Export by handle ran only against the fake Docker CLI, in test/agent-storage-async.test.ts.

What this adds

One test in test/agent-container.test.ts, which runs in the real-docker job: "exports through a recovery handle after a restart, then removes the storage by that handle".

  • A real restart. A separate Node process builds the image (from cache), clones the repository and calls prepareTaskFilesystems, then exits without releasing anything. This process never held the allocator value, its trusted clone or its live allocation.
  • The test makes the agent's changes, then calls recoverLeftovers(runnerOwner). Recovery removes nothing and returns exactly one storage handle.
  • Export through the handle:
    • Without F's metadataBaseline, the export is refused before anything runs.
    • With a wrong baseline, the export fails because the metadata changed.
    • With the right baseline, it returns the agent's committed, edited, staged, new and binary changes, and leaves both volumes unchanged.
  • No export container left behind. After every export, whether refused or not, only the keeper still carries the allocation label.
  • Removal by handle. After the export, removeTaskFilesystems(handle) removes the keeper and both volumes, and the handle is spent.
  • Cleanup. A cleanup sweep by allocation label runs on every path and never throws, so it cannot hide the original failure.

Verification

  • Ran locally against Docker 27.4. The new test and the existing recovery test both pass, and tsc is clean.
  • Mutation check. I changed resolveMetadataBaseline to ignore the baseline that F passes for a recovery handle. The new test then failed with "the metadata changed since the storage was seeded". I reverted the change afterwards.

Review

An independent review subagent found no blocking defects.

Applied:

  • Assert that no export container is left.
  • Assert that report.removed is empty.
  • Parse only the child's last stdout line, which survives the legacy builder.
  • Make the finally sweep unable to throw.
  • Rename the module helper to specifier.

Declined:

  • An allowlisted child environment. This is a test-only subprocess, and it needs PATH and the Docker settings.
  • A Docker client leaking if the child hits its 120 s timeout. The risk is low, and the sibling test uses the same sweep.

🤖 Generated with Claude Code

The last "Done when" item of #51: removal by recovery handle had a
real-Docker case, but export by handle only ran against the fake Docker
CLI. An earlier Node process now allocates real task storage and exits
without releasing it; this process recovers it, exports through the
handle (refused without F's baseline, failed with a wrong one, correct
with the right one, storage unchanged, no export container left), then
removes the storage by that handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang merged commit adde4fc into main Oct 3, 2026
2 checks passed
@mchwang
mchwang deleted the test/51-recovery-handle-export branch October 3, 2026 22:10
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.

D follow-ups required by the F1 runner lifecycle contract

1 participant