test: clear subagent registry broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 14:11:22 +01:00
parent e71ef41c95
commit da3448cf31

View File

@@ -7,6 +7,67 @@ const noop = () => {};
const waitForFast = <T>(callback: () => T | Promise<T>) =>
vi.waitFor(callback, { timeout: 1_000, interval: 1 });
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`expected ${label} to be an object`);
}
return value as Record<string, unknown>;
}
function expectRecordFields(
value: unknown,
expected: Record<string, unknown>,
label: string,
): Record<string, unknown> {
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<typeof vi.fn>,
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<typeof vi.fn>,
argIndex: number,
label: string,
predicate: (record: Record<string, unknown>) => boolean,
): Record<string, unknown> {
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<string, unknown>;
if (predicate(record)) {
return record;
}
}
throw new Error(`expected ${label}`);
}
async function expectPathMissing(targetPath: string): Promise<void> {
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",
}),
},
);
});
});