test: tighten subagent restart assertions

This commit is contained in:
Peter Steinberger
2026-05-09 19:38:46 +01:00
parent 7b8a995a07
commit 9a2eba684e

View File

@@ -78,6 +78,38 @@ function countMatching<T>(items: readonly T[], predicate: (item: T) => boolean)
return count;
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected ${label}`);
}
return value as Record<string, unknown>;
}
function requireSubagentEndedHookCall(runId: string): {
event: Record<string, unknown>;
ctx: Record<string, unknown>;
} {
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<string, unknown> {
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", () => {