












@@ -303,41 +303,6 @@ function mockCall(mock: unknown, label: string, index = 0): unknown[] {
303303return call;
304304}
305305306-async function waitForPromiseForTest<T>(
307-promise: Promise<T>,
308-timeoutMs: number,
309-label: string,
310-): Promise<T> {
311-let timer: ReturnType<typeof setTimeout> | undefined;
312-try {
313-return await Promise.race([
314-promise,
315-new Promise<never>((_resolve, reject) => {
316-timer = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
317-}),
318-]);
319-} finally {
320-if (timer) {
321-clearTimeout(timer);
322-}
323-}
324-}
325-326-async function drainPromiseForTest(
327-promise: Promise<unknown>,
328-timeoutMs: number,
329-label: string,
330-): Promise<void> {
331-await waitForPromiseForTest(
332-promise.then(
333-() => undefined,
334-() => undefined,
335-),
336-timeoutMs,
337-label,
338-);
339-}
340-341306function openSocket(url: string): Promise<WebSocket> {
342307return new Promise((resolve, reject) => {
343308const socket = new WebSocket(url);
@@ -3243,84 +3208,51 @@ describe("runCodexAppServerAttempt", () => {
32433208}
32443209});
324532103246-it("releases the turn after terminal dynamic tool responses", async () => {
3247-const harness = createStartedThreadHarness();
3248-testing.setOpenClawCodingToolsFactoryForTests((options) => [
3249-{
3250- ...createRuntimeDynamicTool("image_generate"),
3251-execute: vi.fn(async () => {
3252-await options?.onYield?.("Image generation started; wait for completion.");
3253-return {
3254-content: [{ type: "text" as const, text: "Background task started." }],
3255-details: { async: true, status: "started", taskId: "task-1" },
3256-terminate: true,
3257-};
3258-}),
3259-},
3260-]);
3261-3262-const params = createParams(
3263-path.join(tempDir, "session.jsonl"),
3264-path.join(tempDir, "workspace"),
3265-);
3266-const abortController = new AbortController();
3267-params.abortSignal = abortController.signal;
3268-params.disableTools = false;
3269-params.runtimePlan = createCodexRuntimePlanFixture();
3270-3271-const run = runCodexAppServerAttempt(params);
3272-let completed = false;
3273-try {
3274-await harness.waitForMethod("turn/start", 10_000);
3275-3276-const toolResult = (await harness.handleServerRequest({
3277-id: "request-image-generate",
3278-method: "item/tool/call",
3279-params: {
3280-threadId: "thread-1",
3281-turnId: "turn-1",
3282-callId: "call-image-1",
3283-namespace: null,
3284-tool: "image_generate",
3285-arguments: { prompt: "lighthouse" },
3286-},
3287-})) as {
3288-contentItems?: Array<{ text?: string; type?: string }>;
3289-success?: boolean;
3290-};
3291-3292-expect(toolResult).toEqual({
3293-success: true,
3294-contentItems: [{ type: "inputText", text: "Background task started." }],
3295-});
3296-expect(harness.requests.some((request) => request.method === "turn/interrupt")).toBe(false);
3297-const result = await waitForPromiseForTest(run, 20_000, "Codex terminal dynamic tool run");
3298-completed = true;
3299-3300-expect(result.timedOut).toBe(false);
3301-expect(result.promptError).toBeNull();
3302-expect(result.yieldDetected).toBe(true);
3303-expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
3304-"user",
3305-"assistant",
3306-"toolResult",
3307-]);
3308-expect(
3309-harness.requests.some(
3310-(request) =>
3311-request.method === "turn/interrupt" &&
3312-(request.params as { turnId?: string } | undefined)?.turnId === "turn-1",
3313-),
3314-).toBe(true);
3315-} finally {
3316-if (!completed) {
3317-harness.close();
3318-abortController.abort(new Error("test cleanup"));
3319-await drainPromiseForTest(run, 1_000, "Codex terminal dynamic tool cleanup").catch(
3320-() => undefined,
3321-);
3322-}
3323-}
3211+it("allows turn release after successful terminal dynamic tool responses", () => {
3212+expect(
3213+testing.shouldReleaseTurnAfterTerminalDynamicTool({
3214+completed: false,
3215+aborted: false,
3216+responseSuccess: true,
3217+currentTurnHadNonTerminalDynamicToolResult: false,
3218+activeAppServerTurnRequests: 0,
3219+activeTurnItemIdsCount: 0,
3220+pendingOpenClawDynamicToolCompletionIdsCount: 0,
3221+}),
3222+).toBe(true);
3223+expect(
3224+testing.shouldReleaseTurnAfterTerminalDynamicTool({
3225+completed: false,
3226+aborted: false,
3227+responseSuccess: true,
3228+currentTurnHadNonTerminalDynamicToolResult: true,
3229+activeAppServerTurnRequests: 0,
3230+activeTurnItemIdsCount: 0,
3231+pendingOpenClawDynamicToolCompletionIdsCount: 0,
3232+}),
3233+).toBe(false);
3234+expect(
3235+testing.shouldReleaseTurnAfterTerminalDynamicTool({
3236+completed: false,
3237+aborted: false,
3238+responseSuccess: true,
3239+currentTurnHadNonTerminalDynamicToolResult: false,
3240+activeAppServerTurnRequests: 1,
3241+activeTurnItemIdsCount: 0,
3242+pendingOpenClawDynamicToolCompletionIdsCount: 0,
3243+}),
3244+).toBe(false);
3245+expect(
3246+testing.shouldReleaseTurnAfterTerminalDynamicTool({
3247+completed: false,
3248+aborted: false,
3249+responseSuccess: true,
3250+currentTurnHadNonTerminalDynamicToolResult: false,
3251+activeAppServerTurnRequests: 0,
3252+activeTurnItemIdsCount: 0,
3253+pendingOpenClawDynamicToolCompletionIdsCount: 1,
3254+}),
3255+).toBe(false);
33243256});
3325325733263258it("keeps mixed dynamic tool batches running after one terminal result", async () => {
此內容由慣性聚合(RSS閱讀器)自動聚合整理,僅供閱讀參考。 原文來自 — 版權歸原作者所有。