diff --git a/src/agents/sessions-spawn-hooks.test.ts b/src/agents/sessions-spawn-hooks.test.ts index 88d514de2ed..f8dc07ed3f6 100644 --- a/src/agents/sessions-spawn-hooks.test.ts +++ b/src/agents/sessions-spawn-hooks.test.ts @@ -54,6 +54,19 @@ function findGatewayRequest(method: string): GatewayRequest | undefined { return getGatewayRequests().find((request) => request.method === method); } +function requireRecord(value: unknown, label: string): Record { + expect(value, label).toBeTypeOf("object"); + expect(value, label).not.toBeNull(); + return value as Record; +} + +function expectFields(value: unknown, expected: Record, label = "object"): void { + const record = requireRecord(value, label); + for (const [key, expectedValue] of Object.entries(expected)) { + expect(record[key], `${label}.${key}`).toEqual(expectedValue); + } +} + function setConfig(next: Record) { hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next); } @@ -132,10 +145,14 @@ function expectThreadBindFailureCleanup( expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled(); expectSessionsDeleteWithoutAgentStart(); const deleteCall = findGatewayRequest("sessions.delete"); - expect(deleteCall?.params).toMatchObject({ - key: result.childSessionKey, - emitLifecycleHooks: false, - }); + expectFields( + deleteCall?.params, + { + key: result.childSessionKey, + emitLifecycleHooks: false, + }, + "delete params", + ); } beforeAll(async () => { @@ -215,7 +232,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => { context: "isolated", }); - expect(result).toMatchObject({ status: "accepted", runId: "run-1" }); + expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result"); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith( { @@ -242,25 +259,37 @@ describe("sessions_spawn subagent lifecycle hooks", () => { Record, Record, ]; - expect(event).toMatchObject({ - runId: "run-1", - agentId: "main", - label: "research", - mode: "session", - requester: { + expectFields( + event, + { + runId: "run-1", + agentId: "main", + label: "research", + mode: "session", + threadRequested: true, + }, + "spawned event", + ); + expectFields( + event.requester, + { channel: "discord", accountId: "work", to: "channel:123", threadId: 456, }, - threadRequested: true, - }); + "spawned requester", + ); expect(event.childSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/)); - expect(ctx).toMatchObject({ - runId: "run-1", - requesterSessionKey: "main", - childSessionKey: event.childSessionKey, - }); + expectFields( + ctx, + { + runId: "run-1", + requesterSessionKey: "main", + childSessionKey: event.childSessionKey, + }, + "spawned context", + ); }); it("emits subagent_spawned with threadRequested=false when not requested", async () => { @@ -269,20 +298,28 @@ describe("sessions_spawn subagent lifecycle hooks", () => { agentTo: "channel:123", }); - expect(result).toMatchObject({ status: "accepted", runId: "run-1" }); + expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result"); expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled(); expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1); const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [ Record, ]; - expect(event).toMatchObject({ - mode: "run", - threadRequested: false, - requester: { + expectFields( + event, + { + mode: "run", + threadRequested: false, + }, + "spawned event", + ); + expectFields( + event.requester, + { channel: "discord", to: "channel:123", }, - }); + "spawned requester", + ); }); it("respects explicit mode=run when thread binding is requested", async () => { @@ -294,13 +331,17 @@ describe("sessions_spawn subagent lifecycle hooks", () => { context: "isolated", }); - expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" }); + expectFields(result, { status: "accepted", runId: "run-1", mode: "run" }, "spawn result"); expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1); const event = getSpawnedEventCall(); - expect(event).toMatchObject({ - mode: "run", - threadRequested: true, - }); + expectFields( + event, + { + mode: "run", + threadRequested: true, + }, + "spawned event", + ); }); it("returns error when thread binding cannot be created", async () => { @@ -377,26 +418,34 @@ describe("sessions_spawn subagent lifecycle hooks", () => { context: "isolated", }); - expect(result).toMatchObject({ status: "error" }); + expect(result.status).toBe("error"); expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1); const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [ Record, ]; - expect(event).toMatchObject({ - targetSessionKey: expect.stringMatching(/^agent:main:subagent:/), - accountId: "work", - targetKind: "subagent", - reason: "spawn-failed", - sendFarewell: true, - outcome: "error", - error: "Session failed to start", - }); + expect(event.targetSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/)); + expectFields( + event, + { + accountId: "work", + targetKind: "subagent", + reason: "spawn-failed", + sendFarewell: true, + outcome: "error", + error: "Session failed to start", + }, + "ended event", + ); const deleteCall = findGatewayRequest("sessions.delete"); - expect(deleteCall?.params).toMatchObject({ - key: event.targetSessionKey, - deleteTranscript: true, - emitLifecycleHooks: false, - }); + expectFields( + deleteCall?.params, + { + key: event.targetSessionKey, + deleteTranscript: true, + emitLifecycleHooks: false, + }, + "delete params", + ); }); it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => { @@ -411,15 +460,19 @@ describe("sessions_spawn subagent lifecycle hooks", () => { context: "isolated", }); - expect(result).toMatchObject({ status: "error" }); + expect(result.status).toBe("error"); expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled(); const methods = getGatewayMethods(); expect(methods).toContain("sessions.delete"); const deleteCall = findGatewayRequest("sessions.delete"); - expect(deleteCall?.params).toMatchObject({ - deleteTranscript: true, - emitLifecycleHooks: true, - }); + expectFields( + deleteCall?.params, + { + deleteTranscript: true, + emitLifecycleHooks: true, + }, + "delete params", + ); }); it("cleans up the provisional session when lineage patching fails after thread binding", async () => { @@ -464,10 +517,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => { expect(methods).toContain("sessions.delete"); expect(methods).not.toContain("agent"); const deleteCall = findGatewayRequest("sessions.delete"); - expect(deleteCall?.params).toMatchObject({ - key: result.childSessionKey, - deleteTranscript: true, - emitLifecycleHooks: true, - }); + expectFields( + deleteCall?.params, + { + key: result.childSessionKey, + deleteTranscript: true, + emitLifecycleHooks: true, + }, + "delete params", + ); }); });