From 9590e2ccaed780a361710c2cfeeccef64831cc8a Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 30 Mar 2026 19:09:31 +0100 Subject: [PATCH] test: stabilize windows task registry and exec timeouts --- src/process/exec.test.ts | 28 ++-- src/tasks/task-registry.test.ts | 249 +++++++++++++++++--------------- 2 files changed, 151 insertions(+), 126 deletions(-) diff --git a/src/process/exec.test.ts b/src/process/exec.test.ts index 792231cffe6..c94efc61d34 100644 --- a/src/process/exec.test.ts +++ b/src/process/exec.test.ts @@ -45,7 +45,7 @@ describe("runCommandWithTimeout", () => { expect(resolved.npm_config_fund).toBe("false"); }); - it("kills command when no output timeout elapses", async () => { + it("kills command when no output timeout elapses", { timeout: 15_000 }, async () => { const result = await runCommandWithTimeout( [process.execPath, "-e", "setTimeout(() => {}, 5_000)"], { @@ -59,18 +59,22 @@ describe("runCommandWithTimeout", () => { expect(result.code).not.toBe(0); }); - it("reports global timeout termination when overall timeout elapses", async () => { - const result = await runCommandWithTimeout( - [process.execPath, "-e", "setTimeout(() => {}, 5_000)"], - { - timeoutMs: 200, - }, - ); + it( + "reports global timeout termination when overall timeout elapses", + { timeout: 15_000 }, + async () => { + const result = await runCommandWithTimeout( + [process.execPath, "-e", "setTimeout(() => {}, 5_000)"], + { + timeoutMs: 200, + }, + ); - expect(result.termination).toBe("timeout"); - expect(result.noOutputTimedOut).toBe(false); - expect(result.code).not.toBe(0); - }); + expect(result.termination).toBe("timeout"); + expect(result.noOutputTimedOut).toBe(false); + expect(result.code).not.toBe(0); + }, + ); it.runIf(process.platform === "win32")( "on Windows spawns node + npm-cli.js for npm argv to avoid spawn EINVAL", diff --git a/src/tasks/task-registry.test.ts b/src/tasks/task-registry.test.ts index 64111206abb..76c7ee12006 100644 --- a/src/tasks/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -98,6 +98,19 @@ async function flushAsyncWork(times = 4) { } } +async function withTaskRegistryTempDir(run: (root: string) => Promise): Promise { + return await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + process.env.OPENCLAW_STATE_DIR = root; + resetTaskRegistryForTests(); + try { + return await run(root); + } finally { + // Close the sqlite-backed registry before Windows temp-dir cleanup tries to remove it. + resetTaskRegistryForTests(); + } + }); +} + describe("task-registry", () => { afterEach(() => { vi.useRealTimers(); @@ -116,7 +129,7 @@ describe("task-registry", () => { }); it("updates task status from lifecycle events", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -156,7 +169,7 @@ describe("task-registry", () => { }); it("summarizes task pressure by status and runtime", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -210,7 +223,7 @@ describe("task-registry", () => { }); it("delivers ACP completion to the requester channel when a delivery origin exists", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -268,7 +281,7 @@ describe("task-registry", () => { }); it("records delivery failure and queues a session fallback when direct delivery misses", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockRejectedValueOnce(new Error("telegram unavailable")); @@ -314,7 +327,7 @@ describe("task-registry", () => { }); it("still wakes the parent when blocked delivery misses the outward channel", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockRejectedValueOnce(new Error("telegram unavailable")); @@ -351,7 +364,7 @@ describe("task-registry", () => { }); it("marks internal fallback delivery as session queued instead of delivered", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -389,7 +402,7 @@ describe("task-registry", () => { }); it("wakes the parent for blocked tasks even when delivery falls back to the session", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -421,7 +434,7 @@ describe("task-registry", () => { }); it("does not include internal progress detail in the terminal channel message", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -472,7 +485,7 @@ describe("task-registry", () => { }); it("surfaces blocked outcomes separately from completed tasks", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -513,7 +526,7 @@ describe("task-registry", () => { }); it("does not queue an unblock follow-up for ordinary completed tasks", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -552,7 +565,7 @@ describe("task-registry", () => { }); it("keeps distinct task records when different producers share a runId", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -585,7 +598,7 @@ describe("task-registry", () => { }); it("suppresses duplicate ACP delivery when a preferred spawned task shares the runId", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -638,7 +651,7 @@ describe("task-registry", () => { }); it("adopts preferred ACP spawn metadata when collapsing onto an earlier direct record", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -724,7 +737,7 @@ describe("task-registry", () => { }); it("collapses ACP run-owned task creation onto the existing spawned task", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -764,7 +777,7 @@ describe("task-registry", () => { }); it("delivers a terminal ACP update only once when multiple notifiers race", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -809,7 +822,7 @@ describe("task-registry", () => { }); it("restores persisted tasks from disk on the next lookup", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -838,7 +851,7 @@ describe("task-registry", () => { }); it("indexes tasks by session key for latest and list lookups", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests({ persist: false }); const nowSpy = vi.spyOn(Date, "now"); @@ -870,7 +883,7 @@ describe("task-registry", () => { }); it("projects inspection-time orphaned tasks as lost without mutating the registry", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -902,7 +915,7 @@ describe("task-registry", () => { }); it("marks orphaned tasks lost with cleanupAfter in a single maintenance pass", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); const now = Date.now(); @@ -935,7 +948,7 @@ describe("task-registry", () => { }); it("prunes old terminal tasks during maintenance sweeps", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); @@ -965,7 +978,7 @@ describe("task-registry", () => { }); it("previews and repairs missing cleanup timestamps during maintenance", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); const now = Date.now(); @@ -1012,7 +1025,7 @@ describe("task-registry", () => { }); it("summarizes inspectable task audit findings", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); const now = Date.now(); @@ -1060,7 +1073,7 @@ describe("task-registry", () => { }); it("delivers concise state-change updates only when notify policy requests them", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); hoisted.sendMessageMock.mockResolvedValue({ @@ -1179,7 +1192,7 @@ describe("task-registry", () => { }); it("keeps background ACP progress off the foreground lane and only sends a terminal notify", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); resetSystemEventsForTest(); @@ -1252,7 +1265,7 @@ describe("task-registry", () => { }); it("delivers a concise terminal failure message without internal ACP chatter", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); resetSystemEventsForTest(); @@ -1418,7 +1431,7 @@ describe("task-registry", () => { }); it("emits concise state-change updates without surfacing raw ACP chatter", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; resetTaskRegistryForTests(); resetSystemEventsForTest(); @@ -1480,112 +1493,120 @@ describe("task-registry", () => { }); it("cancels ACP-backed tasks through the ACP session manager", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { const registry = await loadFreshTaskRegistryModulesForControlTest(); process.env.OPENCLAW_STATE_DIR = root; registry.resetTaskRegistryForTests(); - hoisted.cancelSessionMock.mockResolvedValue(undefined); + try { + hoisted.cancelSessionMock.mockResolvedValue(undefined); - const task = registry.createTaskRecord({ - runtime: "acp", - requesterSessionKey: "agent:main:main", - requesterOrigin: { - channel: "telegram", - to: "telegram:123", - }, - childSessionKey: "agent:codex:acp:child", - runId: "run-cancel-acp", - task: "Investigate issue", - status: "running", - deliveryStatus: "pending", - }); - - const result = await registry.cancelTaskById({ - cfg: {} as never, - taskId: task.taskId, - }); - - expect(hoisted.cancelSessionMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: {}, - sessionKey: "agent:codex:acp:child", - reason: "task-cancel", - }), - ); - expect(result).toMatchObject({ - found: true, - cancelled: true, - task: expect.objectContaining({ - taskId: task.taskId, - status: "cancelled", - error: "Cancelled by operator.", - }), - }); - await waitForAssertion(() => - expect(hoisted.sendMessageMock).toHaveBeenCalledWith( - expect.objectContaining({ + const task = registry.createTaskRecord({ + runtime: "acp", + requesterSessionKey: "agent:main:main", + requesterOrigin: { channel: "telegram", to: "telegram:123", - content: "Background task cancelled: ACP background task (run run-canc).", + }, + childSessionKey: "agent:codex:acp:child", + runId: "run-cancel-acp", + task: "Investigate issue", + status: "running", + deliveryStatus: "pending", + }); + + const result = await registry.cancelTaskById({ + cfg: {} as never, + taskId: task.taskId, + }); + + expect(hoisted.cancelSessionMock).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + sessionKey: "agent:codex:acp:child", + reason: "task-cancel", }), - ), - ); + ); + expect(result).toMatchObject({ + found: true, + cancelled: true, + task: expect.objectContaining({ + taskId: task.taskId, + status: "cancelled", + error: "Cancelled by operator.", + }), + }); + await waitForAssertion(() => + expect(hoisted.sendMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + to: "telegram:123", + content: "Background task cancelled: ACP background task (run run-canc).", + }), + ), + ); + } finally { + registry.resetTaskRegistryForTests(); + } }); }); it("cancels subagent-backed tasks through subagent control", async () => { - await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => { + await withTaskRegistryTempDir(async (root) => { const registry = await loadFreshTaskRegistryModulesForControlTest(); process.env.OPENCLAW_STATE_DIR = root; registry.resetTaskRegistryForTests(); - hoisted.killSubagentRunAdminMock.mockResolvedValue({ - found: true, - killed: true, - }); + try { + hoisted.killSubagentRunAdminMock.mockResolvedValue({ + found: true, + killed: true, + }); - const task = registry.createTaskRecord({ - runtime: "subagent", - requesterSessionKey: "agent:main:main", - requesterOrigin: { - channel: "telegram", - to: "telegram:123", - }, - childSessionKey: "agent:worker:subagent:child", - runId: "run-cancel-subagent", - task: "Investigate issue", - status: "running", - deliveryStatus: "pending", - }); - - const result = await registry.cancelTaskById({ - cfg: {} as never, - taskId: task.taskId, - }); - - expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: {}, - sessionKey: "agent:worker:subagent:child", - }), - ); - expect(result).toMatchObject({ - found: true, - cancelled: true, - task: expect.objectContaining({ - taskId: task.taskId, - status: "cancelled", - error: "Cancelled by operator.", - }), - }); - await waitForAssertion(() => - expect(hoisted.sendMessageMock).toHaveBeenCalledWith( - expect.objectContaining({ + const task = registry.createTaskRecord({ + runtime: "subagent", + requesterSessionKey: "agent:main:main", + requesterOrigin: { channel: "telegram", to: "telegram:123", - content: "Background task cancelled: Subagent task (run run-canc).", + }, + childSessionKey: "agent:worker:subagent:child", + runId: "run-cancel-subagent", + task: "Investigate issue", + status: "running", + deliveryStatus: "pending", + }); + + const result = await registry.cancelTaskById({ + cfg: {} as never, + taskId: task.taskId, + }); + + expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + sessionKey: "agent:worker:subagent:child", }), - ), - ); + ); + expect(result).toMatchObject({ + found: true, + cancelled: true, + task: expect.objectContaining({ + taskId: task.taskId, + status: "cancelled", + error: "Cancelled by operator.", + }), + }); + await waitForAssertion(() => + expect(hoisted.sendMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "telegram", + to: "telegram:123", + content: "Background task cancelled: Subagent task (run run-canc).", + }), + ), + ); + } finally { + registry.resetTaskRegistryForTests(); + } }); }); });