From da3448cf3123e182f375d3e08d6d4dd5d520cc61 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 14:11:22 +0100 Subject: [PATCH] test: clear subagent registry broad matchers --- src/agents/subagent-registry.test.ts | 253 ++++++++++++++++++++------- 1 file changed, 192 insertions(+), 61 deletions(-) diff --git a/src/agents/subagent-registry.test.ts b/src/agents/subagent-registry.test.ts index cdea904f31d..d3f58de46cc 100644 --- a/src/agents/subagent-registry.test.ts +++ b/src/agents/subagent-registry.test.ts @@ -7,6 +7,67 @@ const noop = () => {}; const waitForFast = (callback: () => T | Promise) => vi.waitFor(callback, { timeout: 1_000, interval: 1 }); +function requireRecord(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`expected ${label} to be an object`); + } + return value as Record; +} + +function expectRecordFields( + value: unknown, + expected: Record, + label: string, +): Record { + const record = requireRecord(value, label); + for (const [key, expectedValue] of Object.entries(expected)) { + expect(record[key], `${label}.${key}`).toEqual(expectedValue); + } + return record; +} + +function getMockCallArg( + mock: ReturnType, + callIndex: number, + argIndex: number, + label: string, +): unknown { + const call = (mock.mock.calls as unknown[][])[callIndex]; + if (!call) { + throw new Error(`expected ${label} call ${callIndex}`); + } + return call[argIndex]; +} + +function findRecordCallArg( + mock: ReturnType, + argIndex: number, + label: string, + predicate: (record: Record) => boolean, +): Record { + for (const call of mock.mock.calls as unknown[][]) { + const value = call[argIndex]; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + continue; + } + const record = value as Record; + if (predicate(record)) { + return record; + } + } + throw new Error(`expected ${label}`); +} + +async function expectPathMissing(targetPath: string): Promise { + try { + await fs.access(targetPath); + } catch (error) { + expect((error as NodeJS.ErrnoException).code).toBe("ENOENT"); + return; + } + throw new Error(`expected ${targetPath} to be missing`); +} + const mocks = vi.hoisted(() => ({ callGateway: vi.fn(), onAgentEvent: vi.fn(() => noop), @@ -187,8 +248,10 @@ describe("subagent registry seam flow", () => { }); await waitForFast(() => { - expect(mocks.scheduleOrphanRecovery).toHaveBeenCalledWith( - expect.objectContaining({ delayMs: 1_000 }), + expectRecordFields( + getMockCallArg(mocks.scheduleOrphanRecovery, 0, 0, "orphan recovery"), + { delayMs: 1_000 }, + "orphan recovery params", ); }); expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled(); @@ -280,11 +343,21 @@ describe("subagent registry seam flow", () => { await mod.__testing.sweepOnceForTests(); await waitForFast(() => { - expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( - expect.objectContaining({ - childRunId: "run-stale-terminal", - outcome: expect.objectContaining({ status: "ok", endedAt: persistedEndedAt }), - }), + const announceParams = findRecordCallArg( + mocks.runSubagentAnnounceFlow, + 0, + "stale terminal announce", + (record) => record.childRunId === "run-stale-terminal", + ); + expectRecordFields( + announceParams, + { childRunId: "run-stale-terminal" }, + "stale terminal announce", + ); + expectRecordFields( + announceParams.outcome, + { status: "ok", endedAt: persistedEndedAt }, + "stale terminal announce outcome", ); }); @@ -292,10 +365,14 @@ describe("subagent registry seam flow", () => { .listSubagentRunsForRequester("agent:main:main") .find((entry) => entry.runId === "run-stale-terminal"); expect(run?.endedAt).toBe(persistedEndedAt); - expect(run?.outcome).toMatchObject({ - status: "ok", - endedAt: persistedEndedAt, - }); + expectRecordFields( + run?.outcome, + { + status: "ok", + endedAt: persistedEndedAt, + }, + "stale terminal run outcome", + ); expect(run?.cleanupCompletedAt).toBeTypeOf("number"); }); @@ -328,8 +405,10 @@ describe("subagent registry seam flow", () => { await mod.__testing.sweepOnceForTests(); await waitForFast(() => { - expect(mocks.scheduleOrphanRecovery).toHaveBeenCalledWith( - expect.objectContaining({ delayMs: 1_000 }), + expectRecordFields( + getMockCallArg(mocks.scheduleOrphanRecovery, 0, 0, "orphan recovery"), + { delayMs: 1_000 }, + "orphan recovery params", ); }); expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled(); @@ -362,8 +441,9 @@ describe("subagent registry seam flow", () => { label: undefined, }); - expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( - expect.objectContaining({ + expectRecordFields( + getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "completion announce"), + { childSessionKey: "agent:main:subagent:child", childRunId: "run-1", requesterSessionKey: "agent:main:main", @@ -377,13 +457,16 @@ describe("subagent registry seam flow", () => { endedAt: 222, elapsedMs: 111, }, - }), + }, + "completion announce params", ); expect(mocks.updateSessionStore).toHaveBeenCalledTimes(1); - expect(mocks.updateSessionStore).toHaveBeenCalledWith( + expect(getMockCallArg(mocks.updateSessionStore, 0, 0, "session store update")).toBe( "/tmp/test-session-store.json", - expect.any(Function), + ); + expect(getMockCallArg(mocks.updateSessionStore, 0, 1, "session store update")).toBeTypeOf( + "function", ); const updateStore = mocks.updateSessionStore.mock.calls[0]?.[1] as @@ -396,12 +479,16 @@ describe("subagent registry seam flow", () => { }, }; updateStore?.(store); - expect(store["agent:main:subagent:child"]).toMatchObject({ - startedAt: Date.parse("2026-03-24T12:00:00Z"), - endedAt: 222, - runtimeMs: 111, - status: "done", - }); + expectRecordFields( + store["agent:main:subagent:child"], + { + startedAt: Date.parse("2026-03-24T12:00:00Z"), + endedAt: 222, + runtimeMs: 111, + status: "done", + }, + "updated child session store entry", + ); expect(mocks.persistSubagentRunsToDisk).toHaveBeenCalled(); }); @@ -454,14 +541,18 @@ describe("subagent registry seam flow", () => { await waitForFast(() => { expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1); }); - expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( - expect.objectContaining({ - childRunId: "run-timeout-then-ok", - outcome: expect.objectContaining({ - status: "ok", - endedAt: 1_250, - }), - }), + const timeoutAnnounce = expectRecordFields( + getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "timeout retry announce"), + { childRunId: "run-timeout-then-ok" }, + "timeout retry announce params", + ); + expectRecordFields( + timeoutAnnounce.outcome, + { + status: "ok", + endedAt: 1_250, + }, + "timeout retry announce outcome", ); await vi.advanceTimersByTimeAsync(20_000); @@ -489,11 +580,13 @@ describe("subagent registry seam flow", () => { await vi.advanceTimersByTimeAsync(0); expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1); - expect( + expectRecordFields( mod .listSubagentRunsForRequester("agent:main:main") .find((entry) => entry.runId === "run-delete-give-up"), - ).toMatchObject({ runId: "run-delete-give-up", cleanup: "delete" }); + { runId: "run-delete-give-up", cleanup: "delete" }, + "delete give-up run", + ); await vi.advanceTimersByTimeAsync(1_000); expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(2); @@ -603,10 +696,10 @@ describe("subagent registry seam flow", () => { }); expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1); - expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( - expect.objectContaining({ - childRunId: "run-child-finished", - }), + expectRecordFields( + getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "child finished announce"), + { childRunId: "run-child-finished" }, + "child finished announce params", ); await waitForFast(() => { expect(mocks.onSubagentEnded).toHaveBeenCalledWith({ @@ -665,20 +758,26 @@ describe("subagent registry seam flow", () => { allowGatewaySubagentBinding: true, }); }); - expect(mocks.runSubagentEnded).toHaveBeenCalledWith( - expect.objectContaining({ + expectRecordFields( + getMockCallArg(mocks.runSubagentEnded, 0, 0, "subagent ended hook"), + { targetSessionKey: "agent:main:subagent:killed", reason: "subagent-killed", accountId: "acct-1", runId: "run-killed-init", outcome: "killed", error: "manual kill", - }), - expect.objectContaining({ + }, + "subagent ended hook params", + ); + expectRecordFields( + getMockCallArg(mocks.runSubagentEnded, 0, 1, "subagent ended hook context"), + { runId: "run-killed-init", childSessionKey: "agent:main:subagent:killed", requesterSessionKey: "agent:main:main", - }), + }, + "subagent ended hook context", ); }); @@ -739,7 +838,7 @@ describe("subagent registry seam flow", () => { expect(updated).toBe(1); await waitForFast(async () => { - await expect(fs.access(attachmentsDir)).rejects.toMatchObject({ code: "ENOENT" }); + await expectPathMissing(attachmentsDir); }); }); @@ -771,17 +870,27 @@ describe("subagent registry seam flow", () => { expect(updated).toBe(1); await waitForFast(() => { - expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith( - expect.objectContaining({ + const announceParams = findRecordCallArg( + mocks.runSubagentAnnounceFlow, + 0, + "interrupted announce", + (record) => record.childRunId === "run-interrupted", + ); + expectRecordFields( + announceParams, + { childRunId: "run-interrupted", requesterSessionKey: "agent:main:main", requesterOrigin: { channel: "quietchat", accountId: "acct-interrupted" }, - outcome: expect.objectContaining({ - status: "error", - error: expect.stringContaining("Automatic recovery failed after 2 attempts"), - }), - }), + }, + "interrupted announce params", ); + const outcome = expectRecordFields( + announceParams.outcome, + { status: "error" }, + "interrupted announce outcome", + ); + expect(String(outcome.error)).toContain("Automatic recovery failed after 2 attempts"); }); const run = mod .listSubagentRunsForRequester("agent:main:main") @@ -828,7 +937,7 @@ describe("subagent registry seam flow", () => { mod.releaseSubagentRun("run-release-delete"); await waitForFast(async () => { - await expect(fs.access(attachmentsDir)).rejects.toMatchObject({ code: "ENOENT" }); + await expectPathMissing(attachmentsDir); }); await waitForFast(() => { expect(mocks.onSubagentEnded).toHaveBeenCalledWith({ @@ -941,19 +1050,41 @@ describe("subagent registry seam flow", () => { await mod.__testing.sweepOnceForTests(); await waitForFast(() => { - expect(mocks.resolveContextEngine).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ - agentDir: "/tmp/agent-session", - workspaceDir: "/tmp/workspace-session", - }), + findRecordCallArg( + mocks.resolveContextEngine, + 1, + "session context engine cleanup", + (record) => + record.agentDir === "/tmp/agent-session" && + record.workspaceDir === "/tmp/workspace-session", + ); + findRecordCallArg( + mocks.resolveContextEngine, + 1, + "archive context engine cleanup", + (record) => + record.agentDir === "/tmp/agent-archive" && + record.workspaceDir === "/tmp/workspace-archive", ); expect(mocks.resolveContextEngine).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ + { + agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, + session: { mainKey: "main", scope: "per-sender" }, + }, + { + agentDir: "/tmp/agent-session", + workspaceDir: "/tmp/workspace-session", + }, + ); + expect(mocks.resolveContextEngine).toHaveBeenCalledWith( + { + agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, + session: { mainKey: "main", scope: "per-sender" }, + }, + { agentDir: "/tmp/agent-archive", workspaceDir: "/tmp/workspace-archive", - }), + }, ); }); });