From 192c02cd929f5fd6d4e0dbbc669a1284079377db Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 1 Apr 2026 21:26:04 +0100 Subject: [PATCH] test: reuse subagent registry loop guard harness --- ...agent-registry.announce-loop-guard.test.ts | 194 ++++++++---------- 1 file changed, 86 insertions(+), 108 deletions(-) diff --git a/src/agents/subagent-registry.announce-loop-guard.test.ts b/src/agents/subagent-registry.announce-loop-guard.test.ts index 3367c984a84..90f30686f9d 100644 --- a/src/agents/subagent-registry.announce-loop-guard.test.ts +++ b/src/agents/subagent-registry.announce-loop-guard.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import * as registry from "./subagent-registry.js"; /** * Regression test for #18264: Gateway announcement delivery loop. @@ -8,114 +9,91 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; * forever via the max-retry and expiration guards. */ -function createLoopGuardConfigModuleMock() { - return { - loadConfig: () => ({ - session: { store: "/tmp/test-store", mainKey: "main" }, - agents: {}, - }), - }; -} - -function createLoopGuardSessionsModuleMock() { - return { - loadSessionStore: () => ({ - "agent:main:subagent:child-1": { sessionId: "sess-child-1", updatedAt: 1 }, - "agent:main:subagent:expired-child": { sessionId: "sess-expired", updatedAt: 1 }, - "agent:main:subagent:retry-budget": { sessionId: "sess-retry", updatedAt: 1 }, - }), - resolveAgentIdFromSessionKey: (key: string) => { - const match = key.match(/^agent:([^:]+)/); - return match?.[1] ?? "main"; - }, - resolveMainSessionKey: () => "agent:main:main", - resolveStorePath: () => "/tmp/test-store", - updateSessionStore: vi.fn(), - }; -} - -function createLoopGuardGatewayCallModuleMock() { - return { - callGateway: vi.fn().mockResolvedValue({ status: "ok" }), - }; -} - -function createLoopGuardAgentEventsModuleMock() { - return { - onAgentEvent: vi.fn().mockReturnValue(() => {}), - }; -} - -function createLoopGuardSubagentAnnounceModuleMock() { - return { - runSubagentAnnounceFlow: vi.fn().mockResolvedValue(false), - }; -} - -function createLoopGuardAnnounceQueueModuleMock() { - return { - resetAnnounceQueuesForTests: vi.fn(), - }; -} - -function createLoopGuardTimeoutModuleMock() { - return { - resolveAgentTimeoutMs: () => 60_000, - }; -} - -vi.mock("../config/config.js", createLoopGuardConfigModuleMock); - -vi.mock("../config/sessions.js", createLoopGuardSessionsModuleMock); - -vi.mock("../gateway/call.js", createLoopGuardGatewayCallModuleMock); - -vi.mock("../infra/agent-events.js", createLoopGuardAgentEventsModuleMock); - -vi.mock("./subagent-announce.js", createLoopGuardSubagentAnnounceModuleMock); - -const loadSubagentRegistryFromDisk = vi.fn(() => new Map()); -const saveSubagentRegistryToDisk = vi.fn(); - -vi.mock("./subagent-registry.store.js", () => ({ - loadSubagentRegistryFromDisk, - saveSubagentRegistryToDisk, +const mocks = vi.hoisted(() => ({ + loadConfig: vi.fn(() => ({ + session: { store: "/tmp/test-store", mainKey: "main" }, + agents: {}, + })), + updateSessionStore: vi.fn(), + callGateway: vi.fn().mockResolvedValue({ status: "ok" }), + onAgentEventStop: vi.fn(), + onAgentEvent: vi.fn(), + runSubagentAnnounceFlow: vi.fn().mockResolvedValue(false), + captureSubagentCompletionReply: vi.fn(), + loadSubagentRegistryFromDisk: vi.fn(() => new Map()), + saveSubagentRegistryToDisk: vi.fn(), + resetAnnounceQueuesForTests: vi.fn(), + resolveAgentTimeoutMs: vi.fn(() => 60_000), })); -vi.mock("./subagent-announce-queue.js", createLoopGuardAnnounceQueueModuleMock); +vi.mock("../config/config.js", () => ({ + loadConfig: mocks.loadConfig, +})); -vi.mock("./timeout.js", createLoopGuardTimeoutModuleMock); +vi.mock("../config/sessions.js", () => ({ + loadSessionStore: () => ({ + "agent:main:subagent:child-1": { sessionId: "sess-child-1", updatedAt: 1 }, + "agent:main:subagent:expired-child": { sessionId: "sess-expired", updatedAt: 1 }, + "agent:main:subagent:retry-budget": { sessionId: "sess-retry", updatedAt: 1 }, + }), + resolveAgentIdFromSessionKey: (key: string) => { + const match = key.match(/^agent:([^:]+)/); + return match?.[1] ?? "main"; + }, + resolveMainSessionKey: () => "agent:main:main", + resolveStorePath: () => "/tmp/test-store", + updateSessionStore: mocks.updateSessionStore, +})); + +vi.mock("../gateway/call.js", () => ({ + callGateway: mocks.callGateway, +})); + +vi.mock("../infra/agent-events.js", () => ({ + onAgentEvent: mocks.onAgentEvent, +})); + +vi.mock("./subagent-announce.js", () => ({ + runSubagentAnnounceFlow: mocks.runSubagentAnnounceFlow, + captureSubagentCompletionReply: mocks.captureSubagentCompletionReply, +})); + +vi.mock("./subagent-registry.store.js", () => ({ + loadSubagentRegistryFromDisk: mocks.loadSubagentRegistryFromDisk, + saveSubagentRegistryToDisk: mocks.saveSubagentRegistryToDisk, +})); + +vi.mock("./subagent-announce-queue.js", () => ({ + resetAnnounceQueuesForTests: mocks.resetAnnounceQueuesForTests, +})); + +vi.mock("./timeout.js", () => ({ + resolveAgentTimeoutMs: mocks.resolveAgentTimeoutMs, +})); describe("announce loop guard (#18264)", () => { - let registry: typeof import("./subagent-registry.js"); - let announceFn: ReturnType; - - async function loadFreshSubagentRegistryLoopGuardModulesForTest() { - vi.resetModules(); - vi.doMock("../config/config.js", createLoopGuardConfigModuleMock); - vi.doMock("../config/sessions.js", createLoopGuardSessionsModuleMock); - vi.doMock("../gateway/call.js", createLoopGuardGatewayCallModuleMock); - vi.doMock("../infra/agent-events.js", createLoopGuardAgentEventsModuleMock); - vi.doMock("./subagent-announce.js", createLoopGuardSubagentAnnounceModuleMock); - vi.doMock("./subagent-registry.store.js", () => ({ - loadSubagentRegistryFromDisk, - saveSubagentRegistryToDisk, - })); - vi.doMock("./subagent-announce-queue.js", createLoopGuardAnnounceQueueModuleMock); - vi.doMock("./timeout.js", createLoopGuardTimeoutModuleMock); - registry = await import("./subagent-registry.js"); - const subagentAnnounce = await import("./subagent-announce.js"); - announceFn = vi.mocked(subagentAnnounce.runSubagentAnnounceFlow); - } - - beforeEach(async () => { + beforeEach(() => { vi.useFakeTimers(); - await loadFreshSubagentRegistryLoopGuardModulesForTest(); + mocks.callGateway.mockClear(); + mocks.captureSubagentCompletionReply.mockClear(); + mocks.loadConfig.mockClear(); + mocks.loadSubagentRegistryFromDisk.mockReset(); + mocks.loadSubagentRegistryFromDisk.mockReturnValue(new Map()); + mocks.onAgentEventStop.mockClear(); + mocks.onAgentEvent.mockReset(); + mocks.onAgentEvent.mockReturnValue(mocks.onAgentEventStop); + mocks.resetAnnounceQueuesForTests.mockClear(); + mocks.resolveAgentTimeoutMs.mockClear(); + mocks.runSubagentAnnounceFlow.mockReset(); + mocks.runSubagentAnnounceFlow.mockResolvedValue(false); + mocks.saveSubagentRegistryToDisk.mockClear(); + mocks.updateSessionStore.mockClear(); + registry.resetSubagentRegistryForTests({ persist: false }); }); afterEach(() => { + registry.resetSubagentRegistryForTests({ persist: false }); vi.useRealTimers(); - loadSubagentRegistryFromDisk.mockReturnValue(new Map()); vi.clearAllMocks(); }); @@ -182,29 +160,29 @@ describe("announce loop guard (#18264)", () => { }), }, ])("$name", async ({ createEntry }) => { - announceFn.mockClear(); + mocks.runSubagentAnnounceFlow.mockClear(); registry.resetSubagentRegistryForTests(); const entry = createEntry(Date.now()); - loadSubagentRegistryFromDisk.mockReturnValue(new Map([[entry.runId, entry]])); + mocks.loadSubagentRegistryFromDisk.mockReturnValue(new Map([[entry.runId, entry]])); // Initialization attempts resume once, then gives up for exhausted entries. registry.initSubagentRegistry(); await Promise.resolve(); await Promise.resolve(); - expect(announceFn).not.toHaveBeenCalled(); + expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled(); expect(entry.cleanupCompletedAt).toBeDefined(); }); test("expired completion-message entries are still resumed for announce", async () => { - announceFn.mockReset(); - announceFn.mockResolvedValueOnce(true); + mocks.runSubagentAnnounceFlow.mockReset(); + mocks.runSubagentAnnounceFlow.mockResolvedValueOnce(true); registry.resetSubagentRegistryForTests(); const now = Date.now(); const runId = "test-expired-completion-message"; - loadSubagentRegistryFromDisk.mockReturnValue( + mocks.loadSubagentRegistryFromDisk.mockReturnValue( new Map([ [ runId, @@ -229,17 +207,17 @@ describe("announce loop guard (#18264)", () => { await Promise.resolve(); await Promise.resolve(); - expect(announceFn).toHaveBeenCalledTimes(1); + expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1); }); test("announce rejection resets cleanupHandled so retries can resume", async () => { - announceFn.mockReset(); - announceFn.mockRejectedValueOnce(new Error("announce failed")); + mocks.runSubagentAnnounceFlow.mockReset(); + mocks.runSubagentAnnounceFlow.mockRejectedValueOnce(new Error("announce failed")); registry.resetSubagentRegistryForTests(); const now = Date.now(); const runId = "test-announce-rejection"; - loadSubagentRegistryFromDisk.mockReturnValue( + mocks.loadSubagentRegistryFromDisk.mockReturnValue( new Map([ [ runId,