mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-27 08:31:17 +00:00
* fix(codex): preserve subagent completions across sessions_yield * fix(codex): confirm steering delivery before yield Co-authored-by: Alexis Gallagher <alexis@alexisgallagher.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createCodexAttemptLifecycleController } from "./run-attempt-lifecycle-controller.js";
|
|
import { buildCodexLifecycleTerminalMeta } from "./run-attempt-lifecycle-terminal.js";
|
|
|
|
function createTerminalReleaseHarness() {
|
|
const order: string[] = [];
|
|
const cancel = vi.fn(() => order.push("cancel"));
|
|
const request = vi.fn(async (method: string) => {
|
|
order.push(method);
|
|
return {};
|
|
});
|
|
const resolveCompletion = vi.fn();
|
|
const state = {
|
|
completed: false,
|
|
activeAppServerTurnRequests: 0,
|
|
currentTurnHadNonTerminalDynamicToolResult: false,
|
|
pendingTerminalDynamicToolRelease: undefined,
|
|
terminalDynamicToolReleaseCheckScheduled: false,
|
|
resolveCompletion,
|
|
};
|
|
const controller = createCodexAttemptLifecycleController(
|
|
{
|
|
prompt: {
|
|
context: {
|
|
runtime: {
|
|
connection: {
|
|
params: {},
|
|
attemptStartedAt: 0,
|
|
runAbortController: new AbortController(),
|
|
fastModeAutoProgressState: {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
state: { client: { request } },
|
|
} as never,
|
|
{
|
|
state,
|
|
activeTurnItemIds: new Set(),
|
|
pendingOpenClawDynamicToolCompletionIds: new Set(),
|
|
steeringQueueRef: { current: { cancel } },
|
|
turnWatches: {
|
|
clearCompletionIdleTimer: vi.fn(),
|
|
clearAssistantCompletionIdleTimer: vi.fn(),
|
|
clearTerminalIdleTimer: vi.fn(),
|
|
},
|
|
} as never,
|
|
);
|
|
return { cancel, controller, order, request, resolveCompletion, state };
|
|
}
|
|
|
|
function terminalYieldResult(success: boolean) {
|
|
return {
|
|
call: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
callId: "call-yield",
|
|
tool: "sessions_yield",
|
|
arguments: {},
|
|
},
|
|
response: { success, terminate: true, contentItems: [] },
|
|
durationMs: 1,
|
|
};
|
|
}
|
|
|
|
describe("buildCodexLifecycleTerminalMeta", () => {
|
|
it("marks sessions_yield as a paused parent continuation", () => {
|
|
expect(
|
|
buildCodexLifecycleTerminalMeta({
|
|
aborted: false,
|
|
timedOut: false,
|
|
yielded: true,
|
|
}),
|
|
).toEqual({
|
|
yielded: true,
|
|
livenessState: "paused",
|
|
stopReason: "end_turn",
|
|
});
|
|
});
|
|
|
|
it("keeps ordinary successful turns terminal", () => {
|
|
expect(
|
|
buildCodexLifecycleTerminalMeta({
|
|
aborted: false,
|
|
timedOut: false,
|
|
yielded: false,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("keeps cancellation stronger than a stale yield signal", () => {
|
|
expect(
|
|
buildCodexLifecycleTerminalMeta({
|
|
aborted: true,
|
|
timedOut: false,
|
|
yielded: true,
|
|
}),
|
|
).toEqual({
|
|
aborted: true,
|
|
status: "cancelled",
|
|
stopReason: "stop",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Codex terminal dynamic-tool release", () => {
|
|
it("fences unconsumed steering before interrupting a successful yield", async () => {
|
|
const harness = createTerminalReleaseHarness();
|
|
|
|
harness.controller.scheduleTurnReleaseAfterTerminalDynamicTool(terminalYieldResult(true));
|
|
await new Promise<void>((resolve) => {
|
|
setImmediate(resolve);
|
|
});
|
|
|
|
expect(harness.cancel).toHaveBeenCalled();
|
|
expect(harness.request).toHaveBeenCalledWith(
|
|
"turn/interrupt",
|
|
{ threadId: "thread-1", turnId: "turn-1" },
|
|
{ timeoutMs: 5_000 },
|
|
);
|
|
expect(harness.order.indexOf("cancel")).toBeLessThan(harness.order.indexOf("turn/interrupt"));
|
|
expect(harness.state.completed).toBe(true);
|
|
expect(harness.resolveCompletion).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("keeps steering open when the yield result fails", async () => {
|
|
const harness = createTerminalReleaseHarness();
|
|
|
|
harness.controller.scheduleTurnReleaseAfterTerminalDynamicTool(terminalYieldResult(false));
|
|
await new Promise<void>((resolve) => {
|
|
setImmediate(resolve);
|
|
});
|
|
|
|
expect(harness.cancel).not.toHaveBeenCalled();
|
|
expect(harness.request).not.toHaveBeenCalled();
|
|
expect(harness.state.completed).toBe(false);
|
|
expect(harness.resolveCompletion).not.toHaveBeenCalled();
|
|
});
|
|
});
|