From 04655e4a91770f40918c17ca6b56f4f34a1b11c2 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 09:52:53 +0800 Subject: [PATCH] test(workflow): wait for the reaped state instead of a fixed 20 ms sleep (#1425) The plugin-load reap test slept 20 ms after apply() and then asserted. Loading starts reap without awaiting it, so the sleep is a race: it passes on a fast box and fails on a loaded runner, and the failure looks exactly like the bug the test exists to catch. Wait for what reap writes to the lane store (status failed, ephemeral released), bounded by a 2 s deadline that names the last state it saw. A slow release no longer flakes; a missing reap still fails. Replayed on main 92a6aabd (Written-by: Rework): main moved three commits on and rewrote this test file for the node-Bot tool grant, so untilRun is reapplied on top of main's version. Only this test file differs from main. Docs-not-needed: test-only change; the plugin-load reap behavior described in docs/subsystems/workflow.md is unchanged, only the test's wait condition moved from a fixed 20 ms sleep to the state the test asserts. --- plugins/workflow/test/ephemeral.test.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/workflow/test/ephemeral.test.js b/plugins/workflow/test/ephemeral.test.js index 5a056bef7..6c6819ea8 100644 --- a/plugins/workflow/test/ephemeral.test.js +++ b/plugins/workflow/test/ephemeral.test.js @@ -442,6 +442,21 @@ function persistedRun(dir, workflowId) { return active ?? lane.last ?? null } +// #1425: the plugin starts reap without awaiting it (index.js:92), so this test waits for the +// state reap writes, never for a fixed wall-clock delay: a 20 ms sleep passes on a fast box and +// flakes on a loaded runner, and the failure it produces looks like the bug the test hunts. The +// deadline only bounds failure, and the message names the state the run was last in. +async function untilRun(dir, workflowId, settled, { timeout = 2000, interval = 10 } = {}) { + const deadline = Date.now() + timeout + let last + for (;;) { + last = persistedRun(dir, workflowId) + if (settled(last)) return last + if (Date.now() >= deadline) throw new Error(`the run was not reaped within ${timeout} ms; last state: ${JSON.stringify(last)}`) + await new Promise((resolve) => setTimeout(resolve, interval)) + } +} + test('a load reaps the run a restart left running: its Bots go and the run ends failed (#1388)', async () => { const dir = mkdtempSync(join(tmpdir(), 'wf-reap-')) const released = [] @@ -576,7 +591,9 @@ test('the plugin load itself reaps a run the restart left running (#1388)', asyn warn() {}, } const loaded = apply({ flybot: { scope: () => fb }, effect(fn) { fn() } }, { dir }) - await new Promise((resolve) => setTimeout(resolve, 20)) + // Loading calls reap without awaiting it; wait for what it writes, with a deadline. + const settled = await untilRun(dir, made.id, (run) => run?.status === 'failed' && run.ephemeral === undefined) + assert.equal('handed' in settled, false) assert.deepEqual(released, ['eph-plan']) const run = loaded.service.state().workflows.find(item => item.id === made.id).run assert.equal(run.status, 'failed')