import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import type { ContextEngine } from "../context-engine/types.js" ;
const noop = () => {};
let lifecycleHandler:
| ((evt: {
stream?: string;
runId: string;
data?: {
phase?: string;
startedAt?: number;
endedAt?: number;
aborted?: boolean ;
error?: string;
};
}) => void )
| undefined;
vi.mock("../gateway/call.js" , () => ({
callGateway: vi.fn(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "agent.wait" ) {
return { status: "pending" };
}
return {};
}),
}));
vi.mock("../infra/agent-events.js" , () => ({
onAgentEvent: vi.fn((handler: typeof lifecycleHandler) => {
lifecycleHandler = handler;
return noop;
}),
}));
vi.mock("../config/config.js" , () => ({
loadConfig: vi.fn(() => ({
agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } },
})),
}));
vi.mock("../config/sessions.js" , () => {
const sessionStore = new Proxy<Record<string, { sessionId: string; updatedAt: number }>>(
{},
{
get(target, prop, receiver) {
if (typeof prop !== "string" || prop in target) {
return Reflect.get(target, prop, receiver);
}
return { sessionId: `sess-${prop}`, updatedAt: 1 };
},
},
);
return {
loadSessionStore: vi.fn(() => sessionStore),
resolveAgentIdFromSessionKey: (key: string) => {
const match = key.match(/^agent:([^:]+)/);
return match?.[1 ] ?? "main" ;
},
resolveMainSessionKey: () => "agent:main:main" ,
resolveStorePath: () => "/tmp/test-store" ,
updateSessionStore: vi.fn(),
};
});
const announceSpy = vi.fn(async (_params: unknown) => true );
const runSubagentEndedHookMock = vi.fn(async (_event?: unknown, _ctx?: unknown) => {});
const emitSessionLifecycleEventMock = vi.fn();
const noopContextEngine = {
info: { id: "test-context-engine" , name: "Test context engine" },
ingest: async () => ({ ingested: false }),
assemble: async () => ({ messages: [], estimatedTokens: 0 }),
compact: async () => ({ ok: true , compacted: false }),
} satisfies ContextEngine;
vi.mock("./subagent-announce.js" , () => ({
captureSubagentCompletionReply: vi.fn(async () => undefined),
runSubagentAnnounceFlow: announceSpy,
}));
vi.mock("../browser-lifecycle-cleanup.js" , () => ({
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
}));
vi.mock("../plugins/hook-runner-global.js" , () => ({
getGlobalHookRunner: vi.fn(() => ({
hasHooks: (hookName: string) => hookName === "subagent_ended" ,
runSubagentEnded: runSubagentEndedHookMock,
})),
getGlobalPluginRegistry: vi.fn(() => null ),
hasGlobalHooks: vi.fn((hookName: string) => hookName === "subagent_ended" ),
initializeGlobalHookRunner: vi.fn(),
resetGlobalHookRunner: vi.fn(),
}));
vi.mock("../sessions/session-lifecycle-events.js" , () => ({
emitSessionLifecycleEvent: emitSessionLifecycleEventMock,
}));
vi.mock("./subagent-registry.store.js" , () => ({
loadSubagentRegistryFromDisk: vi.fn(() => new Map()),
saveSubagentRegistryToDisk: vi.fn(() => {}),
}));
describe("subagent registry steer restarts" , () => {
let mod: typeof import ("./subagent-registry.js" );
type RegisterSubagentRunInput = Parameters<typeof mod.registerSubagentRun>[0 ];
const MAIN_REQUESTER_SESSION_KEY = "agent:main:main" ;
const MAIN_REQUESTER_DISPLAY_KEY = "main" ;
beforeAll(async () => {
mod = await import ("./subagent-registry.js" );
});
beforeEach(() => {
vi.useRealTimers();
lifecycleHandler = undefined;
mod.__testing.setDepsForTest({
ensureContextEnginesInitialized: () => {},
ensureRuntimePluginsLoaded: () => {},
resolveContextEngine: async () => noopContextEngine,
});
announceSpy.mockReset();
announceSpy.mockResolvedValue(true );
runSubagentEndedHookMock.mockReset();
runSubagentEndedHookMock.mockImplementation(async () => {});
emitSessionLifecycleEventMock.mockReset();
mod.resetSubagentRegistryForTests({ persist: false });
});
const flushAnnounce = async () => {
await new Promise<void >((resolve) => setImmediate(resolve));
};
const waitForRegistrySideEffect = async (assertion: () => void ) => {
await vi.waitFor(assertion, { interval: 1 , timeout: 1 _000 });
};
const createDeferredAnnounceResolver = (): ((value: boolean ) => void ) => {
let resolveAnnounce!: (value: boolean ) => void ;
announceSpy.mockImplementationOnce(
() =>
new Promise<boolean >((resolve) => {
resolveAnnounce = resolve;
}),
);
return (value: boolean ) => {
resolveAnnounce(value);
};
};
const registerCompletionModeRun = (
runId: string,
childSessionKey: string,
task: string,
options: Partial<Pick<RegisterSubagentRunInput, "spawnMode" >> = {},
): void => {
registerRun({
runId,
childSessionKey,
task,
expectsCompletionMessage: true ,
requesterOrigin: {
channel: "discord" ,
to: "channel:123" ,
accountId: "work" ,
},
...options,
});
};
const registerRun = (
params: {
runId: string;
childSessionKey: string;
task: string;
requesterSessionKey?: string;
requesterDisplayKey?: string;
} & Partial<
Pick<RegisterSubagentRunInput, "spawnMode" | "requesterOrigin" | "expectsCompletionMessage" >
>,
): void => {
mod.registerSubagentRun({
runId: params.runId,
childSessionKey: params.childSessionKey,
requesterSessionKey: params.requesterSessionKey ?? MAIN_REQUESTER_SESSION_KEY,
requesterDisplayKey: params.requesterDisplayKey ?? MAIN_REQUESTER_DISPLAY_KEY,
requesterOrigin: params.requesterOrigin,
task: params.task,
cleanup: "keep" ,
spawnMode: params.spawnMode,
expectsCompletionMessage: params.expectsCompletionMessage,
});
};
const listMainRuns = () => mod.listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY);
const emitLifecycleEnd = (
runId: string,
data: {
startedAt?: number;
endedAt?: number;
aborted?: boolean ;
error?: string;
} = {},
) => {
lifecycleHandler?.({
stream: "lifecycle" ,
runId,
data: {
phase: "end" ,
...data,
},
});
};
const replaceRunAfterSteer = (params: {
previousRunId: string;
nextRunId: string;
fallback?: ReturnType<typeof listMainRuns>[number];
}) => {
const replaced = mod.replaceSubagentRunAfterSteer({
previousRunId: params.previousRunId,
nextRunId: params.nextRunId,
fallback: params.fallback,
});
expect(replaced).toBe(true );
const runs = listMainRuns();
expect(runs).toHaveLength(1 );
expect(runs[0 ].runId).toBe(params.nextRunId);
return runs[0 ];
};
afterEach(async () => {
vi.useRealTimers();
mod.__testing.setDepsForTest();
announceSpy.mockReset();
announceSpy.mockResolvedValue(true );
runSubagentEndedHookMock.mockReset();
runSubagentEndedHookMock.mockImplementation(async () => {});
emitSessionLifecycleEventMock.mockReset();
lifecycleHandler = undefined;
mod.resetSubagentRegistryForTests({ persist: false });
});
it("suppresses announce for interrupted runs and only announces the replacement run" , async () => {
{
registerRun({
runId: "run-old" ,
childSessionKey: "agent:main:subagent:steer" ,
task: "initial task" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-old" );
const marked = mod.markSubagentRunForSteerRestart("run-old" );
expect(marked).toBe(true );
emitLifecycleEnd("run-old" );
await flushAnnounce();
expect(announceSpy).not.toHaveBeenCalled();
expect(runSubagentEndedHookMock).not.toHaveBeenCalled();
expect(emitSessionLifecycleEventMock).not.toHaveBeenCalled();
replaceRunAfterSteer({
previousRunId: "run-old" ,
nextRunId: "run-new" ,
fallback: previous,
});
emitLifecycleEnd("run-new" );
await waitForRegistrySideEffect(() => {
expect(announceSpy).toHaveBeenCalledTimes(1 );
});
await waitForRegistrySideEffect(() => {
const matchingCalls = runSubagentEndedHookMock.mock.calls.filter((call) => {
const ctx = call[1 ] as { runId?: string } | undefined;
return ctx?.runId === "run-new" ;
});
expect(matchingCalls).toHaveLength(1 );
});
expect(runSubagentEndedHookMock).toHaveBeenCalledWith(
expect.objectContaining({
runId: "run-new" ,
}),
expect.objectContaining({
runId: "run-new" ,
}),
);
const announce = (announceSpy.mock.calls[0 ]?.[0 ] ?? {}) as { childRunId?: string };
expect(announce.childRunId).toBe("run-new" );
}
});
it("defers subagent_ended hook for completion-mode runs until announce delivery resolves" , async () => {
{
const resolveAnnounce = createDeferredAnnounceResolver();
registerCompletionModeRun(
"run-completion-delayed" ,
"agent:main:subagent:completion-delayed" ,
"completion-mode task" ,
);
emitLifecycleEnd("run-completion-delayed" );
await waitForRegistrySideEffect(() => {
expect(announceSpy).toHaveBeenCalledTimes(1 );
});
expect(runSubagentEndedHookMock).not.toHaveBeenCalled();
resolveAnnounce(true );
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,
}),
);
}
});
it("does not emit subagent_ended on completion for persistent session-mode runs" , async () => {
{
const resolveAnnounce = createDeferredAnnounceResolver();
registerCompletionModeRun(
"run-persistent-session" ,
"agent:main:subagent:persistent-session" ,
"persistent session task" ,
{ spawnMode: "session" },
);
emitLifecycleEnd("run-persistent-session" );
await flushAnnounce();
expect(runSubagentEndedHookMock).not.toHaveBeenCalled();
resolveAnnounce(true );
await flushAnnounce();
expect(runSubagentEndedHookMock).not.toHaveBeenCalled();
const run = listMainRuns()[0 ];
expect(run?.runId).toBe("run-persistent-session" );
expect(run?.cleanupCompletedAt).toBeTypeOf("number" );
expect(run?.endedHookEmittedAt).toBeUndefined();
}
});
it("clears announce retry state when replacing after steer restart" , async () => {
{
registerRun({
runId: "run-retry-reset-old" ,
childSessionKey: "agent:main:subagent:retry-reset" ,
task: "retry reset" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-retry-reset-old" );
if (previous) {
previous.announceRetryCount = 2 ;
previous.lastAnnounceRetryAt = Date.now();
}
const run = replaceRunAfterSteer({
previousRunId: "run-retry-reset-old" ,
nextRunId: "run-retry-reset-new" ,
fallback: previous,
});
expect(run.announceRetryCount).toBeUndefined();
expect(run.lastAnnounceRetryAt).toBeUndefined();
}
});
it("clears terminal lifecycle state when replacing after steer restart" , async () => {
{
registerRun({
runId: "run-terminal-state-old" ,
childSessionKey: "agent:main:subagent:terminal-state" ,
task: "terminal state" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-terminal-state-old" );
if (previous) {
previous.endedHookEmittedAt = Date.now();
previous.endedReason = "subagent-complete" ;
previous.endedAt = Date.now();
previous.outcome = { status: "ok" };
}
const run = replaceRunAfterSteer({
previousRunId: "run-terminal-state-old" ,
nextRunId: "run-terminal-state-new" ,
fallback: previous,
});
expect(run.endedHookEmittedAt).toBeUndefined();
expect(run.endedReason).toBeUndefined();
emitLifecycleEnd("run-terminal-state-new" );
await waitForRegistrySideEffect(() => {
expect(runSubagentEndedHookMock).toHaveBeenCalledWith(
expect.objectContaining({
runId: "run-terminal-state-new" ,
}),
expect.objectContaining({
runId: "run-terminal-state-new" ,
}),
);
});
expect(emitSessionLifecycleEventMock).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "agent:main:subagent:terminal-state" ,
reason: "subagent-status" ,
}),
);
}
});
it("clears frozen completion fields when replacing after steer restart" , () => {
registerRun({
runId: "run-frozen-old" ,
childSessionKey: "agent:main:subagent:frozen" ,
task: "frozen result reset" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-frozen-old" );
if (previous) {
previous.frozenResultText = "stale frozen completion" ;
previous.frozenResultCapturedAt = Date.now();
previous.cleanupCompletedAt = Date.now();
previous.cleanupHandled = true ;
}
const run = replaceRunAfterSteer({
previousRunId: "run-frozen-old" ,
nextRunId: "run-frozen-new" ,
fallback: previous,
});
expect(run.frozenResultText).toBeUndefined();
expect(run.frozenResultCapturedAt).toBeUndefined();
expect(run.cleanupCompletedAt).toBeUndefined();
expect(run.cleanupHandled).toBe(false );
});
it("preserves cumulative session timing across steer replacement runs" , () => {
registerRun({
runId: "run-runtime-old" ,
childSessionKey: "agent:main:subagent:runtime" ,
task: "keep timing stable" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-runtime-old" );
if (!previous) {
throw new Error("missing previous run" );
}
previous.startedAt = 1 _000 ;
previous.sessionStartedAt = 1 _000 ;
previous.endedAt = 121 _000 ;
previous.accumulatedRuntimeMs = 0 ;
previous.outcome = { status: "ok" };
const replaced = mod.replaceSubagentRunAfterSteer({
previousRunId: "run-runtime-old" ,
nextRunId: "run-runtime-new" ,
fallback: previous,
});
expect(replaced).toBe(true );
const next = listMainRuns().find((entry) => entry.runId === "run-runtime-new" );
expect(next).toBeDefined();
expect(mod.getSubagentSessionStartedAt(next)).toBe(1 _000 );
expect(next?.accumulatedRuntimeMs).toBe(120 _000 );
if (!next?.startedAt) {
throw new Error("missing next startedAt" );
}
next.endedAt = next.startedAt + 30 _000 ;
expect(mod.getSubagentSessionRuntimeMs(next, next.endedAt)).toBe(150 _000 );
});
it("preserves frozen completion as fallback when replacing for wake continuation" , () => {
registerRun({
runId: "run-wake-old" ,
childSessionKey: "agent:main:subagent:wake" ,
task: "wake result fallback" ,
});
const previous = listMainRuns()[0 ];
expect(previous?.runId).toBe("run-wake-old" );
if (previous) {
previous.frozenResultText = "final summary before wake" ;
previous.frozenResultCapturedAt = 1234 ;
}
const replaced = mod.replaceSubagentRunAfterSteer({
previousRunId: "run-wake-old" ,
nextRunId: "run-wake-new" ,
fallback: previous,
preserveFrozenResultFallback: true ,
});
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 ,
});
});
it("restores announce for a finished run when steer replacement dispatch fails" , async () => {
registerRun({
runId: "run-failed-restart" ,
childSessionKey: "agent:main:subagent:failed-restart" ,
task: "initial task" ,
});
expect(mod.markSubagentRunForSteerRestart("run-failed-restart" )).toBe(true );
emitLifecycleEnd("run-failed-restart" );
await flushAnnounce();
expect(announceSpy).not.toHaveBeenCalled();
expect(mod.clearSubagentRunSteerRestart("run-failed-restart" )).toBe(true );
await flushAnnounce();
expect(announceSpy).toHaveBeenCalledTimes(1 );
const announce = (announceSpy.mock.calls[0 ]?.[0 ] ?? {}) as { childRunId?: string };
expect(announce.childRunId).toBe("run-failed-restart" );
});
it("marks killed runs terminated and inactive" , async () => {
const childSessionKey = "agent:main:subagent:killed" ;
registerRun({
runId: "run-killed" ,
childSessionKey,
task: "kill me" ,
});
expect(mod.isSubagentSessionRunActive(childSessionKey)).toBe(true );
const updated = mod.markSubagentRunTerminated({
childSessionKey,
reason: "manual kill" ,
});
expect(updated).toBe(1 );
expect(mod.isSubagentSessionRunActive(childSessionKey)).toBe(false );
const run = listMainRuns()[0 ];
expect(run?.outcome).toMatchObject({ status: "error" , error: "manual kill" });
expect(run?.outcome?.startedAt).toEqual(expect.any(Number));
expect(run?.outcome?.endedAt).toEqual(expect.any(Number));
expect(run?.outcome?.elapsedMs).toEqual(expect.any(Number));
expect(run?.outcome?.endedAt).toBeGreaterThanOrEqual(run?.outcome?.startedAt ?? 0 );
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,
},
);
});
it("treats a child session as inactive when only a stale older row is still unended" , async () => {
const childSessionKey = "agent:main:subagent:stale-active-older-row" ;
mod.addSubagentRunForTests({
runId: "run-stale-older" ,
childSessionKey,
requesterSessionKey: MAIN_REQUESTER_SESSION_KEY,
requesterDisplayKey: MAIN_REQUESTER_DISPLAY_KEY,
task: "older stale row" ,
startedAt: 100 ,
createdAt: 100 ,
cleanup: "keep" ,
});
mod.addSubagentRunForTests({
runId: "run-current-ended" ,
childSessionKey,
requesterSessionKey: MAIN_REQUESTER_SESSION_KEY,
requesterDisplayKey: MAIN_REQUESTER_DISPLAY_KEY,
task: "current ended row" ,
startedAt: 200 ,
createdAt: 200 ,
endedAt: 250 ,
outcome: { status: "ok" },
cleanup: "keep" ,
});
expect(mod.isSubagentSessionRunActive(childSessionKey)).toBe(false );
});
it("recovers announce cleanup when completion arrives after a kill marker" , async () => {
const childSessionKey = "agent:main:subagent:kill-race" ;
registerRun({
runId: "run-kill-race" ,
childSessionKey,
task: "race test" ,
});
expect(mod.markSubagentRunTerminated({ runId: "run-kill-race" , reason: "manual kill" })).toBe(
1 ,
);
expect(listMainRuns()[0 ]?.suppressAnnounceReason).toBe("killed" );
expect(listMainRuns()[0 ]?.cleanupHandled).toBe(true );
expect(typeof listMainRuns()[0 ]?.cleanupCompletedAt).toBe("number" );
emitLifecycleEnd("run-kill-race" );
await flushAnnounce();
await flushAnnounce();
expect(announceSpy).toHaveBeenCalledTimes(1 );
const announce = (announceSpy.mock.calls[0 ]?.[0 ] ?? {}) as { childRunId?: string };
expect(announce.childRunId).toBe("run-kill-race" );
const run = listMainRuns()[0 ];
expect(run?.endedReason).toBe("subagent-complete" );
expect(run?.outcome?.status).not.toBe("error" );
expect(run?.suppressAnnounceReason).toBeUndefined();
expect(run?.cleanupHandled).toBe(true );
expect(typeof run?.cleanupCompletedAt).toBe("number" );
expect(runSubagentEndedHookMock).toHaveBeenCalledTimes(1 );
});
it("retries deferred parent cleanup after a descendant announces" , async () => {
let parentAttempts = 0 ;
announceSpy.mockImplementation(async (params: unknown) => {
const typed = params as { childRunId?: string };
if (typed.childRunId === "run-parent" ) {
parentAttempts += 1 ;
return parentAttempts >= 2 ;
}
return true ;
});
registerRun({
runId: "run-parent" ,
childSessionKey: "agent:main:subagent:parent" ,
task: "parent task" ,
});
registerRun({
runId: "run-child" ,
childSessionKey: "agent:main:subagent:parent:subagent:child" ,
requesterSessionKey: "agent:main:subagent:parent" ,
requesterDisplayKey: "parent" ,
task: "child task" ,
});
emitLifecycleEnd("run-parent" );
await waitForRegistrySideEffect(() => {
const childRunIds = announceSpy.mock.calls.map(
(call) => ((call[0 ] ?? {}) as { childRunId?: string }).childRunId,
);
expect(childRunIds.filter((id) => id === "run-parent" )).toHaveLength(1 );
});
emitLifecycleEnd("run-child" );
await waitForRegistrySideEffect(() => {
const childRunIds = announceSpy.mock.calls.map(
(call) => ((call[0 ] ?? {}) as { childRunId?: string }).childRunId,
);
expect(childRunIds.filter((id) => id === "run-parent" )).toHaveLength(2 );
expect(childRunIds.filter((id) => id === "run-child" )).toHaveLength(1 );
});
const childRunIds = announceSpy.mock.calls.map(
(call) => ((call[0 ] ?? {}) as { childRunId?: string }).childRunId,
);
expect(childRunIds.filter((id) => id === "run-parent" )).toHaveLength(2 );
expect(childRunIds.filter((id) => id === "run-child" )).toHaveLength(1 );
});
it("retries completion-mode announce delivery with backoff and then gives up after retry limit" , async () => {
{
vi.useFakeTimers();
try {
announceSpy.mockResolvedValue(false );
registerCompletionModeRun(
"run-completion-retry" ,
"agent:main:subagent:completion" ,
"completion retry" ,
);
emitLifecycleEnd("run-completion-retry" );
await vi.advanceTimersByTimeAsync(0 );
expect(announceSpy).toHaveBeenCalledTimes(1 );
expect(listMainRuns()[0 ]?.announceRetryCount).toBe(1 );
await vi.advanceTimersByTimeAsync(999 );
expect(announceSpy).toHaveBeenCalledTimes(1 );
await vi.advanceTimersByTimeAsync(1 );
expect(announceSpy).toHaveBeenCalledTimes(2 );
expect(listMainRuns()[0 ]?.announceRetryCount).toBe(2 );
await vi.advanceTimersByTimeAsync(1 _999 );
expect(announceSpy).toHaveBeenCalledTimes(2 );
await vi.advanceTimersByTimeAsync(1 );
expect(announceSpy).toHaveBeenCalledTimes(3 );
expect(listMainRuns()[0 ]?.announceRetryCount).toBe(3 );
await vi.advanceTimersByTimeAsync(4 _001 );
expect(announceSpy).toHaveBeenCalledTimes(3 );
expect(listMainRuns()[0 ]?.cleanupCompletedAt).toBeTypeOf("number" );
} finally {
vi.useRealTimers();
}
}
});
it("keeps completion cleanup pending while descendants are still active" , async () => {
announceSpy.mockResolvedValue(false );
registerCompletionModeRun(
"run-parent-expiry" ,
"agent:main:subagent:parent-expiry" ,
"parent completion expiry" ,
);
registerRun({
runId: "run-child-active" ,
childSessionKey: "agent:main:subagent:parent-expiry:subagent:child-active" ,
requesterSessionKey: "agent:main:subagent:parent-expiry" ,
requesterDisplayKey: "parent-expiry" ,
task: "child still running" ,
});
emitLifecycleEnd("run-parent-expiry" , {
startedAt: Date.now() - 7 * 60 _000 ,
endedAt: Date.now() - 6 * 60 _000 ,
});
await flushAnnounce();
const parentHookCall = runSubagentEndedHookMock.mock.calls.find((call) => {
const event = call[0 ] as { runId?: string; reason?: string };
return event.runId === "run-parent-expiry" && event.reason === "subagent-complete" ;
});
expect(parentHookCall).toBeUndefined();
const parent = mod
.listSubagentRunsForRequester(MAIN_REQUESTER_SESSION_KEY)
.find((entry) => entry.runId === "run-parent-expiry" );
expect(parent?.cleanupCompletedAt).toBeUndefined();
expect(parent?.cleanupHandled).toBe(false );
});
});
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland