





















@@ -7,6 +7,67 @@ const noop = () => {};
77const waitForFast = <T>(callback: () => T | Promise<T>) =>
88vi.waitFor(callback, { timeout: 1_000, interval: 1 });
9910+function requireRecord(value: unknown, label: string): Record<string, unknown> {
11+if (typeof value !== "object" || value === null || Array.isArray(value)) {
12+throw new Error(`expected ${label} to be an object`);
13+}
14+return value as Record<string, unknown>;
15+}
16+17+function expectRecordFields(
18+value: unknown,
19+expected: Record<string, unknown>,
20+label: string,
21+): Record<string, unknown> {
22+const record = requireRecord(value, label);
23+for (const [key, expectedValue] of Object.entries(expected)) {
24+expect(record[key], `${label}.${key}`).toEqual(expectedValue);
25+}
26+return record;
27+}
28+29+function getMockCallArg(
30+mock: ReturnType<typeof vi.fn>,
31+callIndex: number,
32+argIndex: number,
33+label: string,
34+): unknown {
35+const call = (mock.mock.calls as unknown[][])[callIndex];
36+if (!call) {
37+throw new Error(`expected ${label} call ${callIndex}`);
38+}
39+return call[argIndex];
40+}
41+42+function findRecordCallArg(
43+mock: ReturnType<typeof vi.fn>,
44+argIndex: number,
45+label: string,
46+predicate: (record: Record<string, unknown>) => boolean,
47+): Record<string, unknown> {
48+for (const call of mock.mock.calls as unknown[][]) {
49+const value = call[argIndex];
50+if (typeof value !== "object" || value === null || Array.isArray(value)) {
51+continue;
52+}
53+const record = value as Record<string, unknown>;
54+if (predicate(record)) {
55+return record;
56+}
57+}
58+throw new Error(`expected ${label}`);
59+}
60+61+async function expectPathMissing(targetPath: string): Promise<void> {
62+try {
63+await fs.access(targetPath);
64+} catch (error) {
65+expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
66+return;
67+}
68+throw new Error(`expected ${targetPath} to be missing`);
69+}
70+1071const mocks = vi.hoisted(() => ({
1172callGateway: vi.fn(),
1273onAgentEvent: vi.fn(() => noop),
@@ -187,8 +248,10 @@ describe("subagent registry seam flow", () => {
187248});
188249189250await waitForFast(() => {
190-expect(mocks.scheduleOrphanRecovery).toHaveBeenCalledWith(
191-expect.objectContaining({ delayMs: 1_000 }),
251+expectRecordFields(
252+getMockCallArg(mocks.scheduleOrphanRecovery, 0, 0, "orphan recovery"),
253+{ delayMs: 1_000 },
254+"orphan recovery params",
192255);
193256});
194257expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled();
@@ -280,22 +343,36 @@ describe("subagent registry seam flow", () => {
280343await mod.__testing.sweepOnceForTests();
281344282345await waitForFast(() => {
283-expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith(
284-expect.objectContaining({
285-childRunId: "run-stale-terminal",
286-outcome: expect.objectContaining({ status: "ok", endedAt: persistedEndedAt }),
287-}),
346+const announceParams = findRecordCallArg(
347+mocks.runSubagentAnnounceFlow,
348+0,
349+"stale terminal announce",
350+(record) => record.childRunId === "run-stale-terminal",
351+);
352+expectRecordFields(
353+announceParams,
354+{ childRunId: "run-stale-terminal" },
355+"stale terminal announce",
356+);
357+expectRecordFields(
358+announceParams.outcome,
359+{ status: "ok", endedAt: persistedEndedAt },
360+"stale terminal announce outcome",
288361);
289362});
290363291364const run = mod
292365.listSubagentRunsForRequester("agent:main:main")
293366.find((entry) => entry.runId === "run-stale-terminal");
294367expect(run?.endedAt).toBe(persistedEndedAt);
295-expect(run?.outcome).toMatchObject({
296-status: "ok",
297-endedAt: persistedEndedAt,
298-});
368+expectRecordFields(
369+run?.outcome,
370+{
371+status: "ok",
372+endedAt: persistedEndedAt,
373+},
374+"stale terminal run outcome",
375+);
299376expect(run?.cleanupCompletedAt).toBeTypeOf("number");
300377});
301378@@ -328,8 +405,10 @@ describe("subagent registry seam flow", () => {
328405await mod.__testing.sweepOnceForTests();
329406330407await waitForFast(() => {
331-expect(mocks.scheduleOrphanRecovery).toHaveBeenCalledWith(
332-expect.objectContaining({ delayMs: 1_000 }),
408+expectRecordFields(
409+getMockCallArg(mocks.scheduleOrphanRecovery, 0, 0, "orphan recovery"),
410+{ delayMs: 1_000 },
411+"orphan recovery params",
333412);
334413});
335414expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled();
@@ -362,8 +441,9 @@ describe("subagent registry seam flow", () => {
362441label: undefined,
363442});
364443365-expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith(
366-expect.objectContaining({
444+expectRecordFields(
445+getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "completion announce"),
446+{
367447childSessionKey: "agent:main:subagent:child",
368448childRunId: "run-1",
369449requesterSessionKey: "agent:main:main",
@@ -377,13 +457,16 @@ describe("subagent registry seam flow", () => {
377457endedAt: 222,
378458elapsedMs: 111,
379459},
380-}),
460+},
461+"completion announce params",
381462);
382463383464expect(mocks.updateSessionStore).toHaveBeenCalledTimes(1);
384-expect(mocks.updateSessionStore).toHaveBeenCalledWith(
465+expect(getMockCallArg(mocks.updateSessionStore, 0, 0, "session store update")).toBe(
385466"/tmp/test-session-store.json",
386-expect.any(Function),
467+);
468+expect(getMockCallArg(mocks.updateSessionStore, 0, 1, "session store update")).toBeTypeOf(
469+"function",
387470);
388471389472const updateStore = mocks.updateSessionStore.mock.calls[0]?.[1] as
@@ -396,12 +479,16 @@ describe("subagent registry seam flow", () => {
396479},
397480};
398481updateStore?.(store);
399-expect(store["agent:main:subagent:child"]).toMatchObject({
400-startedAt: Date.parse("2026-03-24T12:00:00Z"),
401-endedAt: 222,
402-runtimeMs: 111,
403-status: "done",
404-});
482+expectRecordFields(
483+store["agent:main:subagent:child"],
484+{
485+startedAt: Date.parse("2026-03-24T12:00:00Z"),
486+endedAt: 222,
487+runtimeMs: 111,
488+status: "done",
489+},
490+"updated child session store entry",
491+);
405492406493expect(mocks.persistSubagentRunsToDisk).toHaveBeenCalled();
407494});
@@ -454,14 +541,18 @@ describe("subagent registry seam flow", () => {
454541await waitForFast(() => {
455542expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
456543});
457-expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith(
458-expect.objectContaining({
459-childRunId: "run-timeout-then-ok",
460-outcome: expect.objectContaining({
461-status: "ok",
462-endedAt: 1_250,
463-}),
464-}),
544+const timeoutAnnounce = expectRecordFields(
545+getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "timeout retry announce"),
546+{ childRunId: "run-timeout-then-ok" },
547+"timeout retry announce params",
548+);
549+expectRecordFields(
550+timeoutAnnounce.outcome,
551+{
552+status: "ok",
553+endedAt: 1_250,
554+},
555+"timeout retry announce outcome",
465556);
466557467558await vi.advanceTimersByTimeAsync(20_000);
@@ -489,11 +580,13 @@ describe("subagent registry seam flow", () => {
489580490581await vi.advanceTimersByTimeAsync(0);
491582expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
492-expect(
583+expectRecordFields(
493584mod
494585.listSubagentRunsForRequester("agent:main:main")
495586.find((entry) => entry.runId === "run-delete-give-up"),
496-).toMatchObject({ runId: "run-delete-give-up", cleanup: "delete" });
587+{ runId: "run-delete-give-up", cleanup: "delete" },
588+"delete give-up run",
589+);
497590498591await vi.advanceTimersByTimeAsync(1_000);
499592expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(2);
@@ -603,10 +696,10 @@ describe("subagent registry seam flow", () => {
603696});
604697605698expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledTimes(1);
606-expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith(
607-expect.objectContaining({
608- childRunId: "run-child-finished",
609-}),
699+expectRecordFields(
700+getMockCallArg(mocks.runSubagentAnnounceFlow, 0, 0, "child finished announce"),
701+{ childRunId: "run-child-finished" },
702+"child finished announce params",
610703);
611704await waitForFast(() => {
612705expect(mocks.onSubagentEnded).toHaveBeenCalledWith({
@@ -665,20 +758,26 @@ describe("subagent registry seam flow", () => {
665758allowGatewaySubagentBinding: true,
666759});
667760});
668-expect(mocks.runSubagentEnded).toHaveBeenCalledWith(
669-expect.objectContaining({
761+expectRecordFields(
762+getMockCallArg(mocks.runSubagentEnded, 0, 0, "subagent ended hook"),
763+{
670764targetSessionKey: "agent:main:subagent:killed",
671765reason: "subagent-killed",
672766accountId: "acct-1",
673767runId: "run-killed-init",
674768outcome: "killed",
675769error: "manual kill",
676-}),
677-expect.objectContaining({
770+},
771+"subagent ended hook params",
772+);
773+expectRecordFields(
774+getMockCallArg(mocks.runSubagentEnded, 0, 1, "subagent ended hook context"),
775+{
678776runId: "run-killed-init",
679777childSessionKey: "agent:main:subagent:killed",
680778requesterSessionKey: "agent:main:main",
681-}),
779+},
780+"subagent ended hook context",
682781);
683782});
684783@@ -739,7 +838,7 @@ describe("subagent registry seam flow", () => {
739838740839expect(updated).toBe(1);
741840await waitForFast(async () => {
742-await expect(fs.access(attachmentsDir)).rejects.toMatchObject({ code: "ENOENT" });
841+await expectPathMissing(attachmentsDir);
743842});
744843});
745844@@ -771,17 +870,27 @@ describe("subagent registry seam flow", () => {
771870772871expect(updated).toBe(1);
773872await waitForFast(() => {
774-expect(mocks.runSubagentAnnounceFlow).toHaveBeenCalledWith(
775-expect.objectContaining({
873+const announceParams = findRecordCallArg(
874+mocks.runSubagentAnnounceFlow,
875+0,
876+"interrupted announce",
877+(record) => record.childRunId === "run-interrupted",
878+);
879+expectRecordFields(
880+announceParams,
881+{
776882childRunId: "run-interrupted",
777883requesterSessionKey: "agent:main:main",
778884requesterOrigin: { channel: "quietchat", accountId: "acct-interrupted" },
779-outcome: expect.objectContaining({
780-status: "error",
781-error: expect.stringContaining("Automatic recovery failed after 2 attempts"),
782-}),
783-}),
885+},
886+"interrupted announce params",
784887);
888+const outcome = expectRecordFields(
889+announceParams.outcome,
890+{ status: "error" },
891+"interrupted announce outcome",
892+);
893+expect(String(outcome.error)).toContain("Automatic recovery failed after 2 attempts");
785894});
786895const run = mod
787896.listSubagentRunsForRequester("agent:main:main")
@@ -828,7 +937,7 @@ describe("subagent registry seam flow", () => {
828937mod.releaseSubagentRun("run-release-delete");
829938830939await waitForFast(async () => {
831-await expect(fs.access(attachmentsDir)).rejects.toMatchObject({ code: "ENOENT" });
940+await expectPathMissing(attachmentsDir);
832941});
833942await waitForFast(() => {
834943expect(mocks.onSubagentEnded).toHaveBeenCalledWith({
@@ -941,19 +1050,41 @@ describe("subagent registry seam flow", () => {
9411050await mod.__testing.sweepOnceForTests();
94210519431052await waitForFast(() => {
1053+findRecordCallArg(
1054+mocks.resolveContextEngine,
1055+1,
1056+"session context engine cleanup",
1057+(record) =>
1058+record.agentDir === "/tmp/agent-session" &&
1059+record.workspaceDir === "/tmp/workspace-session",
1060+);
1061+findRecordCallArg(
1062+mocks.resolveContextEngine,
1063+1,
1064+"archive context engine cleanup",
1065+(record) =>
1066+record.agentDir === "/tmp/agent-archive" &&
1067+record.workspaceDir === "/tmp/workspace-archive",
1068+);
9441069expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
945-expect.any(Object),
946-expect.objectContaining({
1070+{
1071+agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
1072+session: { mainKey: "main", scope: "per-sender" },
1073+},
1074+{
9471075agentDir: "/tmp/agent-session",
9481076workspaceDir: "/tmp/workspace-session",
949-}),
1077+},
9501078);
9511079expect(mocks.resolveContextEngine).toHaveBeenCalledWith(
952-expect.any(Object),
953-expect.objectContaining({
1080+{
1081+agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
1082+session: { mainKey: "main", scope: "per-sender" },
1083+},
1084+{
9541085agentDir: "/tmp/agent-archive",
9551086workspaceDir: "/tmp/workspace-archive",
956-}),
1087+},
9571088);
9581089});
9591090});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。