diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index 03027a1509b..477c4451cf8 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -66,18 +66,9 @@ async function withAcpManagerTaskStateDir(run: (root: string) => Promise): }); } -async function waitForAssertion(assertion: () => void, timeoutMs = 2_000, stepMs = 5) { - const startedAt = Date.now(); - for (;;) { - try { - assertion(); - return; - } catch (error) { - if (Date.now() - startedAt >= timeoutMs) { - throw error; - } - await new Promise((resolve) => setTimeout(resolve, stepMs)); - } +async function flushMicrotasks(rounds = 3): Promise { + for (let index = 0; index < rounds; index += 1) { + await Promise.resolve(); } } @@ -338,20 +329,19 @@ describe("AcpSessionManager", () => { requestId: "direct-parented-run", }); - await waitForAssertion(() => - expect(findTaskByRunId("direct-parented-run")).toMatchObject({ - runtime: "acp", - requesterSessionKey: "agent:quant:telegram:quant:direct:822430204", - childSessionKey: "agent:codex:acp:child-1", - label: "Quant patch", - task: "Implement the feature and report back", - status: "succeeded", - progressSummary: - "Write failed: permission denied for /root/oc-acp-write-should-fail.txt.", - terminalOutcome: "blocked", - terminalSummary: "Permission denied for /root/oc-acp-write-should-fail.txt.", - }), - ); + await flushMicrotasks(); + + expect(findTaskByRunId("direct-parented-run")).toMatchObject({ + runtime: "acp", + requesterSessionKey: "agent:quant:telegram:quant:direct:822430204", + childSessionKey: "agent:codex:acp:child-1", + label: "Quant patch", + task: "Implement the feature and report back", + status: "succeeded", + progressSummary: "Write failed: permission denied for /root/oc-acp-write-should-fail.txt.", + terminalOutcome: "blocked", + terminalSummary: "Permission denied for /root/oc-acp-write-should-fail.txt.", + }); }); }); diff --git a/src/mcp/channel-server.test.ts b/src/mcp/channel-server.test.ts index 7f7a0054799..76c565f2bd3 100644 --- a/src/mcp/channel-server.test.ts +++ b/src/mcp/channel-server.test.ts @@ -113,7 +113,7 @@ async function connectMcp(params: { describe("openclaw channel mcp server", () => { describe("gateway-backed flows", () => { - installGatewayTestHooks(); + installGatewayTestHooks({ scope: "suite" }); test("lists conversations, reads messages, and waits for events", async () => { const storePath = await createSessionStoreFile(); diff --git a/src/plugin-sdk/channel-lifecycle.test.ts b/src/plugin-sdk/channel-lifecycle.test.ts index 8534fbe9bbb..98e15f0f8b2 100644 --- a/src/plugin-sdk/channel-lifecycle.test.ts +++ b/src/plugin-sdk/channel-lifecycle.test.ts @@ -23,11 +23,13 @@ function createFakeServer(): FakeServer { } async function expectTaskPending(task: Promise) { - const early = await Promise.race([ - task.then(() => "resolved"), - new Promise<"pending">((resolve) => setTimeout(() => resolve("pending"), 25)), - ]); - expect(early).toBe("pending"); + let settled = false; + void task.finally(() => { + settled = true; + }); + await Promise.resolve(); + await Promise.resolve(); + expect(settled).toBe(false); } describe("plugin-sdk channel lifecycle helpers", () => { diff --git a/src/test-utils/session-state-cleanup.test.ts b/src/test-utils/session-state-cleanup.test.ts index 2449dd0e80f..8f5759e0fa3 100644 --- a/src/test-utils/session-state-cleanup.test.ts +++ b/src/test-utils/session-state-cleanup.test.ts @@ -11,11 +11,17 @@ import { withSessionStoreLockForTest, } from "../config/sessions/store.js"; import { resetFileLockStateForTest } from "../infra/file-lock.js"; -import { cleanupSessionStateForTest } from "./session-state-cleanup.js"; +import { + cleanupSessionStateForTest, + resetSessionStateCleanupRuntimeForTests, + setSessionStateCleanupRuntimeForTests, +} from "./session-state-cleanup.js"; const acquireSessionWriteLockMock = vi.hoisted(() => vi.fn(async () => ({ release: vi.fn(async () => {}) })), ); +const drainFileLockStateMock = vi.hoisted(() => vi.fn(async () => undefined)); +const drainSessionWriteLockStateMock = vi.hoisted(() => vi.fn(async () => undefined)); function createDeferred() { let resolve!: (value: T | PromiseLike) => void; @@ -27,6 +33,12 @@ function createDeferred() { return { promise, resolve, reject }; } +async function flushMicrotasks(rounds = 3): Promise { + for (let index = 0; index < rounds; index += 1) { + await Promise.resolve(); + } +} + describe("cleanupSessionStateForTest", () => { beforeEach(() => { vi.useRealTimers(); @@ -34,7 +46,13 @@ describe("cleanupSessionStateForTest", () => { resetFileLockStateForTest(); resetSessionWriteLockStateForTest(); acquireSessionWriteLockMock.mockClear(); + drainFileLockStateMock.mockClear(); + drainSessionWriteLockStateMock.mockClear(); setSessionWriteLockAcquirerForTests(acquireSessionWriteLockMock); + setSessionStateCleanupRuntimeForTests({ + drainFileLockStateForTest: drainFileLockStateMock, + drainSessionWriteLockStateForTest: drainSessionWriteLockStateMock, + }); }); afterEach(() => { @@ -43,6 +61,7 @@ describe("cleanupSessionStateForTest", () => { resetFileLockStateForTest(); resetSessionWriteLockStateForTest(); resetSessionStoreLockRuntimeForTests(); + resetSessionStateCleanupRuntimeForTests(); vi.restoreAllMocks(); }); @@ -66,14 +85,18 @@ describe("cleanupSessionStateForTest", () => { settled = true; }); - await new Promise((resolve) => setTimeout(resolve, 25)); + await flushMicrotasks(); expect(settled).toBe(false); + expect(drainFileLockStateMock).not.toHaveBeenCalled(); + expect(drainSessionWriteLockStateMock).not.toHaveBeenCalled(); release.resolve(); await running; await cleanupPromise; expect(getSessionStoreLockQueueSizeForTest()).toBe(0); + expect(drainFileLockStateMock).toHaveBeenCalledTimes(1); + expect(drainSessionWriteLockStateMock).toHaveBeenCalledTimes(1); } finally { release.resolve(); await running?.catch(() => undefined); diff --git a/src/test-utils/session-state-cleanup.ts b/src/test-utils/session-state-cleanup.ts index 7487fc5b802..fbb7b3869aa 100644 --- a/src/test-utils/session-state-cleanup.ts +++ b/src/test-utils/session-state-cleanup.ts @@ -5,9 +5,29 @@ import { } from "../config/sessions/store.js"; import { drainFileLockStateForTest } from "../infra/file-lock.js"; +let fileLockDrainerForTests: typeof drainFileLockStateForTest | null = null; +let sessionWriteLockDrainerForTests: typeof drainSessionWriteLockStateForTest | null = null; + +export function setSessionStateCleanupRuntimeForTests(params: { + drainFileLockStateForTest?: typeof drainFileLockStateForTest | null; + drainSessionWriteLockStateForTest?: typeof drainSessionWriteLockStateForTest | null; +}): void { + if ("drainFileLockStateForTest" in params) { + fileLockDrainerForTests = params.drainFileLockStateForTest ?? null; + } + if ("drainSessionWriteLockStateForTest" in params) { + sessionWriteLockDrainerForTests = params.drainSessionWriteLockStateForTest ?? null; + } +} + +export function resetSessionStateCleanupRuntimeForTests(): void { + fileLockDrainerForTests = null; + sessionWriteLockDrainerForTests = null; +} + export async function cleanupSessionStateForTest(): Promise { await drainSessionStoreLockQueuesForTest(); clearSessionStoreCacheForTest(); - await drainFileLockStateForTest(); - await drainSessionWriteLockStateForTest(); + await (fileLockDrainerForTests ?? drainFileLockStateForTest)(); + await (sessionWriteLockDrainerForTests ?? drainSessionWriteLockStateForTest)(); }