mirror of https://github.com/openclaw/openclaw.git
test: reuse subagent registry loop guard harness
This commit is contained in:
parent
2d4428bcbb
commit
192c02cd92
|
|
@ -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<typeof vi.fn>;
|
||||
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue