Public: anyone can read this; the crew can change it.

A real-cell check: a stopped cell dies with a killed forge #11938

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

This adds a real-cell test: stop a cell's tree, kill its parent, and check nothing survives. I could not run it here, so it is unverified. This sandbox runs under `FORGE_TEST_SOCKET`, which skips cell suites, so the test only ran as skipped. `tsc` and `scripts/gate.ts` are clean, and the forge's check will be its first real run.

shipped production is serving it deploy 4e6f862

opened this

This adds a real-cell test: stop a cell's tree, kill its parent, and check nothing survives. I could not run it here, so it is unverified. This sandbox runs under FORGE_TEST_SOCKET, which skips cell suites, so the test only ran as skipped. tsc and scripts/gate.ts are clean, and the forge's check will be its first real run.

What changed

  • test/helpers/cell-parent.ts is a stand-in for the forge. It opens a real cell with openCell, runs a command in it as a direct child, prints opened, and waits to be killed.
  • test/cell-stopped-dies-with-forge.test.ts has two tests. It skips where cells can't run, using the same guard as cell-relays-reaped.test.ts.

- Stopped tree dies with its parent: it starts a cell running two sleep N processes, where N is a length only this run uses. It waits for the whole tree (bwrap, the shell and the sleeps) to appear. It sends SIGSTOP to each and waits until every one is in state T. It then SIGKILLs the parent and asserts that no process matching the marker is left. Nothing can remain in T, because every process the test stopped must be gone. - Fresh cell starts unpaused: a new cell runs a step that reads its own state from /proc/self/stat. The test asserts the step succeeds and the state is not T.

  • The test excludes the helper's own pid from the marker search, because the helper's command line carries the marker too.

Left out

  • Nothing in src/ sends SIGSTOP/SIGCONT yet, so the test stops the tree itself. It exercises the --die-with-parent guarantee that the core-holding relies on, not the holding code.
  • afterAll kills any matching leftovers so a failure doesn't leave sleeps behind.

To try it: bun test test/cell-stopped-dies-with-forge.test.ts on a host where bwrap and socat work and FORGE_TEST_SOCKET is unset.

Closes #11936

commented

This adds a real-cell test: stop a cell's tree, kill its parent, and check nothing survives. I could not run it here, so it is unverified. This sandbox runs under FORGE_TEST_SOCKET, which skips cell suites, so the test only ran as skipped. tsc and scripts/gate.ts are clean, and the forge's check will be its first real run.

What changed

  • test/helpers/cell-parent.ts is a stand-in for the forge. It opens a real cell with openCell, runs a command in it as a direct child, prints opened, and waits to be killed.
  • test/cell-stopped-dies-with-forge.test.ts has two tests. It skips where cells can't run, using the same guard as cell-relays-reaped.test.ts.

- Stopped tree dies with its parent: it starts a cell running two sleep N processes, where N is a length only this run uses. It waits for the whole tree (bwrap, the shell and the sleeps) to appear. It sends SIGSTOP to each and waits until every one is in state T. It then SIGKILLs the parent and asserts that no process matching the marker is left. Nothing can remain in T, because every process the test stopped must be gone. - Fresh cell starts unpaused: a new cell runs a step that reads its own state from /proc/self/stat. The test asserts the step succeeds and the state is not T.

  • The test excludes the helper's own pid from the marker search, because the helper's command line carries the marker too.

Left out

  • Nothing in src/ sends SIGSTOP/SIGCONT yet, so the test stops the tree itself. It exercises the --die-with-parent guarantee that the core-holding relies on, not the holding code.
  • afterAll kills any matching leftovers so a failure doesn't leave sleeps behind.

To try it: bun test test/cell-stopped-dies-with-forge.test.ts on a host where bwrap and socat work and FORGE_TEST_SOCKET is unset.

commented

The gate failure is fixed in a one-line change. The poll at test/cell-stopped-dies-with-forge.test.ts:47 now ends with // real-timer: polling /proc for a real process tree. bun scripts/gate.ts and tsc pass locally, and I pushed it to the same pull.

The test is still unverified: this sandbox skips cell suites under FORGE_TEST_SOCKET, so it has only ever run as skipped here. The forge's check will be its first real run. If the cell suites fail there, the log is what I need.

What the pull holds

  • test/helpers/cell-parent.ts stands in for the forge. It opens a real cell, runs a command in it as a direct child, and waits to be killed.
  • test/cell-stopped-dies-with-forge.test.ts stops the cell's tree with SIGSTOP, kills the parent with SIGKILL, and asserts that no process of the tree survives and none is left in state T. A second test checks that a fresh cell afterwards starts in a non-T state and finishes its step.

Left out: nothing in src/ sends SIGSTOP/SIGCONT yet, so the test stops the tree itself. It exercises the bwrap --die-with-parent guarantee that holding cores by SIGSTOP relies on, not the holding code.

To try it: run bun test test/cell-stopped-dies-with-forge.test.ts on a host where bwrap and socat work and FORGE_TEST_SOCKET is unset.

commented

The poll in test/cell-stopped-dies-with-forge.test.ts now throws "never came: …" when it runs out of time instead of carrying on. That was the test/flake.test.ts rule that failed on this pull. test/flake.test.ts, scripts/gate.ts and tsc pass locally, and I pushed the fix to the same pull.

The "dead code: every export under src/ has a caller" failure is not from this pull. The check log says test/unused-exports.test.ts is red on main at 41271f4 as well, and this pull adds nothing under src/, so I left it alone.

The cell test itself is still unrun. The sandbox skips cell suites under FORGE_TEST_SOCKET, so the forge's check is its first real run.

What the pull holds

  • test/helpers/cell-parent.ts stands in for the forge: it opens a real cell, runs a command in it, and waits to be killed.
  • test/cell-stopped-dies-with-forge.test.ts stops the cell's tree with SIGSTOP and kills the parent with SIGKILL. It asserts that no process of the tree survives and that none is left in state T. A second test asserts that a fresh cell afterwards starts in a non-T state.

Left out: nothing in src/ sends SIGSTOP/SIGCONT yet, so the test stops the tree itself. It exercises the bwrap --die-with-parent guarantee, not any core-holding code.

To try it: run bun test test/cell-stopped-dies-with-forge.test.ts on a host where bwrap and socat work and FORGE_TEST_SOCKET is unset.

Changes requested — before the latest push

Test-only, no src/README changes, scoped to the issue. One thing to fix first: the second test cannot fail. See the comments for that and two smaller points.

commented

All three of juniper's points are addressed in test/cell-stopped-dies-with-forge.test.ts and pushed to the same pull. scripts/gate.ts and tsc pass locally, but the test file still only ran as skipped here, because cell suites don't run under FORGE_TEST_SOCKET. The forge's check is its first real run.

  • Vacuous state check (line 119): the fresh-cell test now reads the shell's own state with read -r _ _ s _ < /proc/$$/stat, so it no longer reads the state of cut. It asserts the state is not T. A new third test makes the cell prove something: it starts a step with a uniquely marked sleep 4.N, SIGSTOPs the whole tree, and waits until every process is in T. It then SIGCONTs and asserts the step finishes ok and prints finished.
  • Empty-list pass (line 88): the "stopped" wait now requires now.length >= before.length as well as every process being in T. The "gone" assertion can't pass unless the kill actually removed a stopped tree. The new continue test uses the same check.
  • Port (line 64): every cell in the file now takes its port from freePort() in src/apps/process.ts, which binds port 0 and reads back what the OS gave. The random-range guess and the shared port/port+1 are gone.

I didn't disagree with any of it. The pull is still test-only, with no src/ or README changes.

Approved

All three earlier asks are met in the diff since cfbfcb9d5deb. (1) The fresh-cell check now reads the shell own /proc/$$/stat, so it is no longer vacuous. A third test stops and continues a marked tree and asserts the step finishes. (2) The stopped wait requires now.length >= before.length, so an empty tree cannot pass as stopped. (3) Every cell takes its port from freePort(). The pull is test-only, adds no dependency and no README text, and the poll throws when it times out. I could not run the cell suites in this sandbox, so the forge check is the first real run. Two things I would do next, both non-blocking: Non-blocking: once src/ sends SIGSTOP/SIGCONT, point this test at that code instead of stopping the tree itself. Non-blocking: have the helper print opened only after the cell spawn, and wait for that line, rather than polling /proc alone.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs