

























@@ -54,6 +54,19 @@ function findGatewayRequest(method: string): GatewayRequest | undefined {
5454return getGatewayRequests().find((request) => request.method === method);
5555}
565657+function requireRecord(value: unknown, label: string): Record<string, unknown> {
58+expect(value, label).toBeTypeOf("object");
59+expect(value, label).not.toBeNull();
60+return value as Record<string, unknown>;
61+}
62+63+function expectFields(value: unknown, expected: Record<string, unknown>, label = "object"): void {
64+const record = requireRecord(value, label);
65+for (const [key, expectedValue] of Object.entries(expected)) {
66+expect(record[key], `${label}.${key}`).toEqual(expectedValue);
67+}
68+}
69+5770function setConfig(next: Record<string, unknown>) {
5871hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next);
5972}
@@ -132,10 +145,14 @@ function expectThreadBindFailureCleanup(
132145expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
133146expectSessionsDeleteWithoutAgentStart();
134147const deleteCall = findGatewayRequest("sessions.delete");
135-expect(deleteCall?.params).toMatchObject({
136-key: result.childSessionKey,
137-emitLifecycleHooks: false,
138-});
148+expectFields(
149+deleteCall?.params,
150+{
151+key: result.childSessionKey,
152+emitLifecycleHooks: false,
153+},
154+"delete params",
155+);
139156}
140157141158beforeAll(async () => {
@@ -215,7 +232,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
215232context: "isolated",
216233});
217234218-expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
235+expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");
219236expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
220237expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith(
221238{
@@ -242,25 +259,37 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
242259Record<string, unknown>,
243260Record<string, unknown>,
244261];
245-expect(event).toMatchObject({
246-runId: "run-1",
247-agentId: "main",
248-label: "research",
249-mode: "session",
250-requester: {
262+expectFields(
263+event,
264+{
265+runId: "run-1",
266+agentId: "main",
267+label: "research",
268+mode: "session",
269+threadRequested: true,
270+},
271+"spawned event",
272+);
273+expectFields(
274+event.requester,
275+{
251276channel: "discord",
252277accountId: "work",
253278to: "channel:123",
254279threadId: 456,
255280},
256-threadRequested: true,
257-});
281+"spawned requester",
282+);
258283expect(event.childSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));
259-expect(ctx).toMatchObject({
260-runId: "run-1",
261-requesterSessionKey: "main",
262-childSessionKey: event.childSessionKey,
263-});
284+expectFields(
285+ctx,
286+{
287+runId: "run-1",
288+requesterSessionKey: "main",
289+childSessionKey: event.childSessionKey,
290+},
291+"spawned context",
292+);
264293});
265294266295it("emits subagent_spawned with threadRequested=false when not requested", async () => {
@@ -269,20 +298,28 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
269298agentTo: "channel:123",
270299});
271300272-expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
301+expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");
273302expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();
274303expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1);
275304const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
276305Record<string, unknown>,
277306];
278-expect(event).toMatchObject({
279-mode: "run",
280-threadRequested: false,
281-requester: {
307+expectFields(
308+event,
309+{
310+mode: "run",
311+threadRequested: false,
312+},
313+"spawned event",
314+);
315+expectFields(
316+event.requester,
317+{
282318channel: "discord",
283319to: "channel:123",
284320},
285-});
321+"spawned requester",
322+);
286323});
287324288325it("respects explicit mode=run when thread binding is requested", async () => {
@@ -294,13 +331,17 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
294331context: "isolated",
295332});
296333297-expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });
334+expectFields(result, { status: "accepted", runId: "run-1", mode: "run" }, "spawn result");
298335expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
299336const event = getSpawnedEventCall();
300-expect(event).toMatchObject({
301-mode: "run",
302-threadRequested: true,
303-});
337+expectFields(
338+event,
339+{
340+mode: "run",
341+threadRequested: true,
342+},
343+"spawned event",
344+);
304345});
305346306347it("returns error when thread binding cannot be created", async () => {
@@ -377,26 +418,34 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
377418context: "isolated",
378419});
379420380-expect(result).toMatchObject({ status: "error" });
421+expect(result.status).toBe("error");
381422expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1);
382423const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [
383424Record<string, unknown>,
384425];
385-expect(event).toMatchObject({
386-targetSessionKey: expect.stringMatching(/^agent:main:subagent:/),
387-accountId: "work",
388-targetKind: "subagent",
389-reason: "spawn-failed",
390-sendFarewell: true,
391-outcome: "error",
392-error: "Session failed to start",
393-});
426+expect(event.targetSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));
427+expectFields(
428+event,
429+{
430+accountId: "work",
431+targetKind: "subagent",
432+reason: "spawn-failed",
433+sendFarewell: true,
434+outcome: "error",
435+error: "Session failed to start",
436+},
437+"ended event",
438+);
394439const deleteCall = findGatewayRequest("sessions.delete");
395-expect(deleteCall?.params).toMatchObject({
396-key: event.targetSessionKey,
397-deleteTranscript: true,
398-emitLifecycleHooks: false,
399-});
440+expectFields(
441+deleteCall?.params,
442+{
443+key: event.targetSessionKey,
444+deleteTranscript: true,
445+emitLifecycleHooks: false,
446+},
447+"delete params",
448+);
400449});
401450402451it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => {
@@ -411,15 +460,19 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
411460context: "isolated",
412461});
413462414-expect(result).toMatchObject({ status: "error" });
463+expect(result.status).toBe("error");
415464expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled();
416465const methods = getGatewayMethods();
417466expect(methods).toContain("sessions.delete");
418467const deleteCall = findGatewayRequest("sessions.delete");
419-expect(deleteCall?.params).toMatchObject({
420-deleteTranscript: true,
421-emitLifecycleHooks: true,
422-});
468+expectFields(
469+deleteCall?.params,
470+{
471+deleteTranscript: true,
472+emitLifecycleHooks: true,
473+},
474+"delete params",
475+);
423476});
424477425478it("cleans up the provisional session when lineage patching fails after thread binding", async () => {
@@ -464,10 +517,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
464517expect(methods).toContain("sessions.delete");
465518expect(methods).not.toContain("agent");
466519const deleteCall = findGatewayRequest("sessions.delete");
467-expect(deleteCall?.params).toMatchObject({
468-key: result.childSessionKey,
469-deleteTranscript: true,
470-emitLifecycleHooks: true,
471-});
520+expectFields(
521+deleteCall?.params,
522+{
523+key: result.childSessionKey,
524+deleteTranscript: true,
525+emitLifecycleHooks: true,
526+},
527+"delete params",
528+);
472529});
473530});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。