Problem
Main CI failed in the composability clean-checkout battery after merging #374:
Every other job passed. The clean-checkout test run finished with 397 passing suites and one failure:
CW3: a daemon starts in the contextual directory and is stopped afterwards
Received function did not throw
expect(() => process.kill(Number(pid), 0)).toThrow();
CW3 writes the daemon shell PID to a marker and samples kill(pid, 0) immediately after the document run returns:
| it("CW3: a daemon starts in the contextual directory and is stopped afterwards",function*(){ |
| constfixture=yield*useFixture(); |
| constmarker=join(fixture.root,"daemon.txt"); |
| yield*writeDocument( |
| fixture, |
| [ |
| `<InDirectory path="${fixture.target}">`, |
| "```bash daemon exec", |
| `pwd > ${marker}; echo $$ >> ${marker}; sleep 30`, |
| "```", |
| "```sh exec", |
| "sleep 0.5", |
| "```", |
| "</InDirectory>", |
| ].join("\n"), |
| ); |
| |
| yield*run(fixture); |
| |
| const[reported,pid]=(yield*readTextFile(marker)).trim().split("\n"); |
| expect(reported).toBe(fixture.target); |
| // Structured teardown stopped it: nothing survives the execution. |
| expect(()=>process.kill(Number(pid),0)).toThrow(); |
Root cause
kill(pid, 0) tests whether a PID is still addressable. It continues to succeed for a terminated child in the short interval before its parent reaps it. The assertion therefore confuses OS process-table disappearance with the product contract that structured teardown stopped the daemon.
This is the same confirmed reap-window mechanism documented in:
The final #338 evidence established that @effectionx/process teardown can resume after pipe EOF before the child is reaped, and that the observed PID is a zombie in this window. A zombie cannot run, hold resources, or perform work. CW3 retained the obsolete probe after the sibling tests were corrected.
The #374 workflow-storage changes are not implicated: their tests passed in the failing battery.
Recommended solution
Change the test, not production daemon teardown.
Retain a real daemon command that records its own working directory, so CW3 still proves that the subprocess receives the contextual Env.cwd. Replace the PID-existence assertion with the process-API teardown observation established by #339:
- Install middleware around the real
ProcessApi.daemon. - Record
daemon:start before delegation. - Register an
ensure before delegation so LIFO teardown records daemon:stop only after the real daemon resource finishes teardown. - Place or identify a probe after the relevant component boundary when useful, and assert the exact ordering.
- Continue delegating to the real implementation so a real subprocess starts and stops.
- Remove the PID marker and direct
process.kill(pid, 0) assertion.
Prefer extracting or sharing the existing #339 timeline helper if that produces a clean test boundary. Do not duplicate subtly different lifecycle semantics merely to keep the files independent.
A bounded retry around kill(pid, 0), like the earlier TD8 repair, would stop the flake but remains second choice: it waits for OS reaping, which #338 established is not the product contract. Changing @effectionx/process to wait for reaping is also out of scope unless a separate product requirement demonstrates that PID disappearance is required.
Acceptance
- CW3 still proves the real daemon runs in the contextual directory.
- CW3 proves structured teardown completes at the intended scope boundary.
- The test does not inspect PID existence or OS zombie state.
- A missing daemon launch fails closed.
- Missing or incorrectly ordered teardown fails.
- The existing daemon hang-based smoke continues to prove a genuinely leaked long-running process prevents completion.
- The focused test passes repeatedly under load.
- The repository verification gates pass.
Problem
Main CI failed in the
composabilityclean-checkout battery after merging #374:mainis red: CI failure at a38993a #376Every other job passed. The clean-checkout test run finished with 397 passing suites and one failure:
CW3 writes the daemon shell PID to a marker and samples
kill(pid, 0)immediately after the document run returns:executable.md/packages/core/tests/contextual-cwd.test.ts
Lines 146 to 168 in a38993a
Root cause
kill(pid, 0)tests whether a PID is still addressable. It continues to succeed for a terminated child in the short interval before its parent reaps it. The assertion therefore confuses OS process-table disappearance with the product contract that structured teardown stopped the daemon.This is the same confirmed reap-window mechanism documented in:
The final #338 evidence established that
@effectionx/processteardown can resume after pipe EOF before the child is reaped, and that the observed PID is a zombie in this window. A zombie cannot run, hold resources, or perform work. CW3 retained the obsolete probe after the sibling tests were corrected.The #374 workflow-storage changes are not implicated: their tests passed in the failing battery.
Recommended solution
Change the test, not production daemon teardown.
Retain a real daemon command that records its own working directory, so CW3 still proves that the subprocess receives the contextual
Env.cwd. Replace the PID-existence assertion with the process-API teardown observation established by #339:ProcessApi.daemon.daemon:startbefore delegation.ensurebefore delegation so LIFO teardown recordsdaemon:stoponly after the real daemon resource finishes teardown.process.kill(pid, 0)assertion.Prefer extracting or sharing the existing #339 timeline helper if that produces a clean test boundary. Do not duplicate subtly different lifecycle semantics merely to keep the files independent.
A bounded retry around
kill(pid, 0), like the earlier TD8 repair, would stop the flake but remains second choice: it waits for OS reaping, which #338 established is not the product contract. Changing@effectionx/processto wait for reaping is also out of scope unless a separate product requirement demonstrates that PID disappearance is required.Acceptance