

























@@ -99,6 +99,21 @@ function getResumeMessage() {
9999return params.message as string;
100100}
101101102+function firstCallParam(calls: ReadonlyArray<readonly unknown[]>, label: string) {
103+const call = calls[0];
104+if (call === undefined) {
105+throw new Error(`expected ${label} call`);
106+}
107+return call[0];
108+}
109+110+function requireRecord(value: unknown, label: string): Record<string, unknown> {
111+if (value === null || typeof value !== "object" || Array.isArray(value)) {
112+throw new Error(`expected ${label} to be a record`);
113+}
114+return value as Record<string, unknown>;
115+}
116+102117describe("subagent-orphan-recovery", () => {
103118beforeEach(() => {
104119vi.useFakeTimers();
@@ -142,13 +157,17 @@ describe("subagent-orphan-recovery", () => {
142157expect(params.sessionKey).toBe("agent:main:subagent:test-session-1");
143158expect(params.message).toContain("gateway reload");
144159expect(params.message).toContain("Test task: implement feature X");
145-expect(subagentRegistrySteerRuntime.replaceSubagentRunAfterSteer).toHaveBeenCalledWith(
146-expect.objectContaining({
147-previousRunId: "run-1",
148-nextRunId: "test-run-id",
149-fallback: run,
150-}),
160+expect(subagentRegistrySteerRuntime.replaceSubagentRunAfterSteer).toHaveBeenCalledOnce();
161+const replaceParams = requireRecord(
162+firstCallParam(
163+vi.mocked(subagentRegistrySteerRuntime.replaceSubagentRunAfterSteer).mock.calls,
164+"run replacement",
165+),
166+"run replacement params",
151167);
168+expect(replaceParams.previousRunId).toBe("run-1");
169+expect(replaceParams.nextRunId).toBe("test-run-id");
170+expect(replaceParams.fallback).toBe(run);
152171});
153172154173it("skips sessions that are not aborted", async () => {
@@ -281,13 +300,11 @@ describe("subagent-orphan-recovery", () => {
281300282301expect(result.recovered).toBe(0);
283302expect(result.failed).toBe(1);
284-expect(result.failedRuns).toEqual([
285-expect.objectContaining({
286-runId: "run-1",
287-childSessionKey: "agent:main:subagent:test-session-1",
288-error: "gateway unavailable",
289-}),
290-]);
303+expect(result.failedRuns).toHaveLength(1);
304+const failedRun = requireRecord(result.failedRuns[0], "failed run");
305+expect(failedRun.runId).toBe("run-1");
306+expect(failedRun.childSessionKey).toBe("agent:main:subagent:test-session-1");
307+expect(failedRun.error).toBe("gateway unavailable");
291308292309// abortedLastRun flag should NOT be cleared on failure,
293310// so the next restart can retry the recovery
@@ -361,14 +378,15 @@ describe("subagent-orphan-recovery", () => {
361378},
362379};
363380await updater(mockStore);
364-expect(mockStore["agent:main:subagent:test-session-1"]).toMatchObject({
365-abortedLastRun: false,
366-subagentRecovery: {
367-automaticAttempts: 1,
368-lastRunId: "run-1",
369-lastAttemptAt: expect.any(Number),
370-},
371-});
381+const sessionEntry = requireRecord(
382+mockStore["agent:main:subagent:test-session-1"],
383+"updated session entry",
384+);
385+expect(sessionEntry.abortedLastRun).toBe(false);
386+const recovery = requireRecord(sessionEntry.subagentRecovery, "subagent recovery");
387+expect(recovery.automaticAttempts).toBe(1);
388+expect(recovery.lastRunId).toBe("run-1");
389+expect(recovery.lastAttemptAt).toBeTypeOf("number");
372390});
373391374392it("tombstones rapid repeated accepted recovery before resuming again", async () => {
@@ -385,18 +403,14 @@ describe("subagent-orphan-recovery", () => {
385403getActiveRuns: () => createActiveRuns(createTestRunRecord()),
386404});
387405388-expect(result).toMatchObject({
389-recovered: 0,
390-failed: 0,
391-skipped: 1,
392-failedRuns: [
393-expect.objectContaining({
394-runId: "run-1",
395-childSessionKey: "agent:main:subagent:test-session-1",
396-error: expect.stringContaining("recovery blocked after 2 rapid accepted resume attempts"),
397-}),
398-],
399-});
406+expect(result.recovered).toBe(0);
407+expect(result.failed).toBe(0);
408+expect(result.skipped).toBe(1);
409+expect(result.failedRuns).toHaveLength(1);
410+const blockedRun = requireRecord(result.failedRuns[0], "blocked run");
411+expect(blockedRun.runId).toBe("run-1");
412+expect(blockedRun.childSessionKey).toBe("agent:main:subagent:test-session-1");
413+expect(blockedRun.error).toContain("recovery blocked after 2 rapid accepted resume attempts");
400414expect(gateway.callGateway).not.toHaveBeenCalled();
401415expect(sessions.updateSessionStore).toHaveBeenCalledOnce();
402416@@ -414,15 +428,16 @@ describe("subagent-orphan-recovery", () => {
414428},
415429};
416430await updater(mockStore);
417-expect(mockStore["agent:main:subagent:test-session-1"]).toMatchObject({
418-abortedLastRun: false,
419-subagentRecovery: {
420-automaticAttempts: 2,
421-lastRunId: "run-1",
422-wedgedAt: expect.any(Number),
423-wedgedReason: expect.stringContaining("recovery blocked"),
424-},
425-});
431+const sessionEntry = requireRecord(
432+mockStore["agent:main:subagent:test-session-1"],
433+"wedged session entry",
434+);
435+expect(sessionEntry.abortedLastRun).toBe(false);
436+const recovery = requireRecord(sessionEntry.subagentRecovery, "wedged recovery");
437+expect(recovery.automaticAttempts).toBe(2);
438+expect(recovery.lastRunId).toBe("run-1");
439+expect(recovery.wedgedAt).toBeTypeOf("number");
440+expect(recovery.wedgedReason).toContain("recovery blocked");
426441});
427442428443it("skips already tombstoned wedged sessions without rewriting them", async () => {
@@ -513,12 +528,15 @@ describe("subagent-orphan-recovery", () => {
513528});
514529515530expect(announceDelivery.deliverSubagentAnnouncement).toHaveBeenCalledOnce();
516-expect(announceDelivery.deliverSubagentAnnouncement).toHaveBeenCalledWith(
517-expect.objectContaining({
518-requesterSessionKey: "agent:main:quietchat:direct:+1234567890",
519-triggerMessage: expect.stringContaining("Automatic recovery is already in progress"),
520-}),
531+const announcement = requireRecord(
532+firstCallParam(
533+vi.mocked(announceDelivery.deliverSubagentAnnouncement).mock.calls,
534+"recovery announcement",
535+),
536+"recovery announcement params",
521537);
538+expect(announcement.requesterSessionKey).toBe("agent:main:quietchat:direct:+1234567890");
539+expect(announcement.triggerMessage).toContain("Automatic recovery is already in progress");
522540expect(notifiedRecoverySessionKeys).toEqual(new Set(["agent:main:subagent:test-session-1"]));
523541524542await recoverOrphanedSubagentSessions({
@@ -615,17 +633,17 @@ describe("subagent-orphan-recovery", () => {
615633await vi.advanceTimersByTimeAsync(2);
616634await Promise.resolve();
617635618-expect(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).toHaveBeenCalledWith(
619-expect.objectContaining({
620-runId: "run-1",
621-childSessionKey: "agent:main:subagent:test-session-1",
622-error: expect.stringContaining("Automatic recovery failed after 2 attempts"),
623-}),
624-);
625-expect(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).toHaveBeenCalledWith(
626-expect.objectContaining({
627-error: expect.stringContaining("service restart"),
628-}),
636+expect(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).toHaveBeenCalledOnce();
637+const finalizeParams = requireRecord(
638+firstCallParam(
639+vi.mocked(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).mock.calls,
640+"interrupted run finalization",
641+),
642+"interrupted run finalization params",
629643);
644+expect(finalizeParams.runId).toBe("run-1");
645+expect(finalizeParams.childSessionKey).toBe("agent:main:subagent:test-session-1");
646+expect(finalizeParams.error).toContain("Automatic recovery failed after 2 attempts");
647+expect(finalizeParams.error).toContain("service restart");
630648});
631649});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。