Skip to content

CW3 samples daemon teardown with a zombie-sensitive PID probe #378

Description

@taras

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:

  1. Install middleware around the real ProcessApi.daemon.
  2. Record daemon:start before delegation.
  3. Register an ensure before delegation so LIFO teardown records daemon:stop only after the real daemon resource finishes teardown.
  4. Place or identify a probe after the relevant component boundary when useful, and assert the exact ordering.
  5. Continue delegating to the real implementation so a real subprocess starts and stops.
  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingflakeIntermittent or timing-sensitive failure that can pass without a code change

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions