From 9a2eba684edb5124039c5d75aba23d107cdabdcd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 19:38:46 +0100 Subject: [PATCH] test: tighten subagent restart assertions --- .../subagent-registry.steer-restart.test.ts | 125 ++++++++++-------- 1 file changed, 68 insertions(+), 57 deletions(-) diff --git a/src/agents/subagent-registry.steer-restart.test.ts b/src/agents/subagent-registry.steer-restart.test.ts index 4b6acb8c457..8f3d679beff 100644 --- a/src/agents/subagent-registry.steer-restart.test.ts +++ b/src/agents/subagent-registry.steer-restart.test.ts @@ -78,6 +78,38 @@ function countMatching(items: readonly T[], predicate: (item: T) => boolean) return count; } +function requireRecord(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`expected ${label}`); + } + return value as Record; +} + +function requireSubagentEndedHookCall(runId: string): { + event: Record; + ctx: Record; +} { + const call = runSubagentEndedHookMock.mock.calls.find((candidate) => { + const ctx = candidate[1] as { runId?: string } | undefined; + return ctx?.runId === runId; + }); + if (!call) { + throw new Error(`expected subagent_ended hook call for ${runId}`); + } + return { + event: requireRecord(call[0], `${runId} subagent_ended event`), + ctx: requireRecord(call[1], `${runId} subagent_ended context`), + }; +} + +function requireSessionLifecycleEventCall(label: string): Record { + const call = emitSessionLifecycleEventMock.mock.calls[0]; + if (!call) { + throw new Error(`expected ${label}`); + } + return requireRecord(call[0], label); +} + const noopContextEngine = { info: { id: "test-context-engine", name: "Test context engine" }, ingest: async () => ({ ingested: false }), @@ -296,14 +328,9 @@ describe("subagent registry steer restarts", () => { }); expect(matchingCalls).toHaveLength(1); }); - expect(runSubagentEndedHookMock).toHaveBeenCalledWith( - expect.objectContaining({ - runId: "run-new", - }), - expect.objectContaining({ - runId: "run-new", - }), - ); + const hookCall = requireSubagentEndedHookCall("run-new"); + expect(hookCall.event.runId).toBe("run-new"); + expect(hookCall.ctx.runId).toBe("run-new"); const announce = (announceSpy.mock.calls[0]?.[0] ?? {}) as { childRunId?: string }; expect(announce.childRunId).toBe("run-new"); @@ -330,17 +357,12 @@ describe("subagent registry steer restarts", () => { await waitForRegistrySideEffect(() => { expect(runSubagentEndedHookMock).toHaveBeenCalledTimes(1); }); - expect(runSubagentEndedHookMock).toHaveBeenCalledWith( - expect.objectContaining({ - targetSessionKey: "agent:main:subagent:completion-delayed", - reason: "subagent-complete", - sendFarewell: true, - }), - expect.objectContaining({ - runId: "run-completion-delayed", - requesterSessionKey: MAIN_REQUESTER_SESSION_KEY, - }), - ); + const hookCall = requireSubagentEndedHookCall("run-completion-delayed"); + expect(hookCall.event.targetSessionKey).toBe("agent:main:subagent:completion-delayed"); + expect(hookCall.event.reason).toBe("subagent-complete"); + expect(hookCall.event.sendFarewell).toBe(true); + expect(hookCall.ctx.runId).toBe("run-completion-delayed"); + expect(hookCall.ctx.requesterSessionKey).toBe(MAIN_REQUESTER_SESSION_KEY); } }); @@ -423,21 +445,13 @@ describe("subagent registry steer restarts", () => { emitLifecycleEnd("run-terminal-state-new"); await waitForRegistrySideEffect(() => { - expect(runSubagentEndedHookMock).toHaveBeenCalledWith( - expect.objectContaining({ - runId: "run-terminal-state-new", - }), - expect.objectContaining({ - runId: "run-terminal-state-new", - }), - ); + const hookCall = requireSubagentEndedHookCall("run-terminal-state-new"); + expect(hookCall.event.runId).toBe("run-terminal-state-new"); + expect(hookCall.ctx.runId).toBe("run-terminal-state-new"); }); - expect(emitSessionLifecycleEventMock).toHaveBeenCalledWith( - expect.objectContaining({ - sessionKey: "agent:main:subagent:terminal-state", - reason: "subagent-status", - }), - ); + const lifecycleEvent = requireSessionLifecycleEventCall("terminal-state lifecycle event"); + expect(lifecycleEvent.sessionKey).toBe("agent:main:subagent:terminal-state"); + expect(lifecycleEvent.reason).toBe("subagent-status"); } }); @@ -532,11 +546,12 @@ describe("subagent registry steer restarts", () => { expect(replaced).toBe(true); const run = listMainRuns().find((entry) => entry.runId === "run-wake-new"); - expect(run).toMatchObject({ - frozenResultText: undefined, - fallbackFrozenResultText: "final summary before wake", - fallbackFrozenResultCapturedAt: 1234, - }); + if (!run) { + throw new Error("expected wake replacement run"); + } + expect(run.frozenResultText).toBeUndefined(); + expect(run.fallbackFrozenResultText).toBe("final summary before wake"); + expect(run.fallbackFrozenResultCapturedAt).toBe(1234); }); it("restores announce for a finished run when steer replacement dispatch fails", async () => { @@ -579,7 +594,8 @@ describe("subagent registry steer restarts", () => { expect(mod.isSubagentSessionRunActive(childSessionKey)).toBe(false); const run = listMainRuns()[0]; - expect(run?.outcome).toMatchObject({ status: "error", error: "manual kill" }); + expect(run?.outcome?.status).toBe("error"); + expect(run?.outcome?.error).toBe("manual kill"); expect(run?.outcome?.startedAt).toBeTypeOf("number"); expect(run?.outcome?.endedAt).toBeTypeOf("number"); expect(run?.outcome?.elapsedMs).toBeTypeOf("number"); @@ -588,24 +604,19 @@ describe("subagent registry steer restarts", () => { expect(run?.cleanupHandled).toBe(true); expect(typeof run?.cleanupCompletedAt).toBe("number"); await flushAnnounce(); - expect(runSubagentEndedHookMock).toHaveBeenCalledWith( - { - targetSessionKey: childSessionKey, - targetKind: "subagent", - reason: "subagent-killed", - sendFarewell: true, - accountId: undefined, - runId: "run-killed", - endedAt: expect.any(Number), - outcome: "killed", - error: "manual kill", - }, - { - runId: "run-killed", - childSessionKey, - requesterSessionKey: MAIN_REQUESTER_SESSION_KEY, - }, - ); + const hookCall = requireSubagentEndedHookCall("run-killed"); + expect(hookCall.event.targetSessionKey).toBe(childSessionKey); + expect(hookCall.event.targetKind).toBe("subagent"); + expect(hookCall.event.reason).toBe("subagent-killed"); + expect(hookCall.event.sendFarewell).toBe(true); + expect(hookCall.event.accountId).toBeUndefined(); + expect(hookCall.event.runId).toBe("run-killed"); + expect(typeof hookCall.event.endedAt).toBe("number"); + expect(hookCall.event.outcome).toBe("killed"); + expect(hookCall.event.error).toBe("manual kill"); + expect(hookCall.ctx.runId).toBe("run-killed"); + expect(hookCall.ctx.childSessionKey).toBe(childSessionKey); + expect(hookCall.ctx.requesterSessionKey).toBe(MAIN_REQUESTER_SESSION_KEY); }); it("treats a child session as inactive when only a stale older row is still unended", () => {