diff --git a/server/src/task-lifecycle-watcher.test.ts b/server/src/task-lifecycle-watcher.test.ts index a0365747d..ac4ae201e 100644 --- a/server/src/task-lifecycle-watcher.test.ts +++ b/server/src/task-lifecycle-watcher.test.ts @@ -45,6 +45,26 @@ afterAll(() => { try { server?.stop(true); } catch {} }); +/** + * 轮询到 `ready()` 为真,或到期抛错。 + * + * 定长 sleep 的问题不是「慢」,是**报错报在错的层**:超时之后测试红在 + * 「事件没写」这条断言上,读的人会去查 watcher,而真实原因可能是子进程 + * 还没起来。这里到期时把那句话直接说出来。 + */ +async function waitUntil(ready: () => boolean, timeoutMs: number, what: string | null): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + if (ready()) return; + if (Date.now() >= deadline) { + // what === null:到期本身就是期望结果(调用方随后自己断言),不抛。 + if (what === null) return; + throw new Error(`timed out after ${timeoutMs}ms: ${what}`); + } + await Bun.sleep(25); + } +} + describe("#167 Hub delivered-stale lifecycle watcher", () => { test("30s/60s thresholds are exact and non-delivered tasks stay silent", () => { const first = recordDeliveredStaleEvents(NOW); @@ -132,7 +152,14 @@ describe("#167 Hub delivered-stale lifecycle watcher", () => { stdout: "pipe", stderr: "pipe", }); - await Bun.sleep(800); + // 这里**不**等「hub 就绪」——因为没有一个只有 hub 起来了才成立的廉价判据: + // db 文件在上面那步 init 里就已经存在了,拿它当条件的话这个等待恒真,等于没等。 + // 真正需要「hub 起来了」的是下面那条断言,而它已经改成轮询到目标状态, + // hub 起得慢只是让它多等几轮。 + // + // 这一步保留的是原来那条断言的原意:**子进程没有立刻崩**。所以只给它一个 + // 短窗口,并且如果它在窗口内退出就立刻停下来报错,不用把 800ms 睡满。 + await waitUntil(() => child.exitCode !== null, 800, null); expect(child.exitCode).toBeNull(); const taskId = "stale-live-wiring"; @@ -147,14 +174,23 @@ describe("#167 Hub delivered-stale lifecycle watcher", () => { expect(childDb.query<{ count: number }, [string]>( "SELECT COUNT(*) AS count FROM task_events WHERE task_id = ?1", ).get(taskId)!.count).toBe(0); - await Bun.sleep(3_200); - expect(childDb.query<{ count: number }, [string]>( + // 巡检周期是 COMMHUB_DELIVERED_STALE_PATROL_MS=25ms —— 事件在插入后 + // 几十毫秒内就该出现。原来这里是定长 sleep(3_200),纯粹是余量: + // 4.0s 的 sleep 装在 bun 默认的 5.0s 单测预算里,只剩 1s 给两次进程启动。 + // 实测在 CI 上被这一条打红过(#798 让这个文件第一次进 CI 才暴露)。 + // 改成「轮询到目标状态,或到期报错」:常态下快 ~60 倍,慢的时候等得起。 + const countStale = () => childDb.query<{ count: number }, [string]>( "SELECT COUNT(*) AS count FROM task_events WHERE task_id = ?1 AND event_type = 'task.warning.delivered_stale_30s'", - ).get(taskId)!.count).toBe(1); + ).get(taskId)!.count; + await waitUntil(() => countStale() === 1, 20_000, + "watcher did not write the delivered_stale_30s event"); + expect(countStale()).toBe(1); } finally { childDb.close(); try { child.kill("SIGTERM"); } catch {} await child.exited; } - }); + // 🔴 显式超时:这一条要起两个真 bun 进程,bun 默认的 5s 对它不成立。 + // 上面两处已改成轮询,常态用不到这个上限;它只保证「慢」不会被报成「坏」。 + }, 30_000); });