| ... | @@ -2597,6 +2597,161 @@ describe("CodexAdapterV2 post-settle continuation", () => { | ... | @@ -2597,6 +2597,161 @@ describe("CodexAdapterV2 post-settle continuation", () => { |
| 2597 | ), | 2597 | ), |
| 2598 | ); | 2598 | ); |
| 2599 | | 2599 | |
| | 2600 | it.effect.each( |
| | 2601 | ( |
| | 2602 | [ |
| | 2603 | ["recovers archived-session errors", "session saved-thread is archived", 0, ""], |
| | 2604 | [ |
| | 2605 | "recovers unarchive hints", |
| | 2606 | "Run `codex unarchive saved-thread` to unarchive it first.", |
| | 2607 | 0, |
| | 2608 | "", |
| | 2609 | ], |
| | 2610 | ["preserves missing-thread errors", "thread not found", 3, "thread not found"], |
| | 2611 | ["preserves missing-rollout errors", "no rollout found", 3, "no rollout found"], |
| | 2612 | ["preserves authentication errors", "authentication failed", 3, "authentication failed"], |
| | 2613 | [ |
| | 2614 | "preserves archived-workspace errors", |
| | 2615 | "workspace is archived", |
| | 2616 | 3, |
| | 2617 | "workspace is archived", |
| | 2618 | ], |
| | 2619 | [ |
| | 2620 | "preserves unrelated archive-path errors", |
| | 2621 | "permission denied reading archived_sessions/saved-thread", |
| | 2622 | 3, |
| | 2623 | "permission denied reading archived_sessions/saved-thread", |
| | 2624 | ], |
| | 2625 | [ |
| | 2626 | "propagates unarchive missing-thread errors", |
| | 2627 | "session saved-thread is archived", |
| | 2628 | 4, |
| | 2629 | "thread not found", |
| | 2630 | ], |
| | 2631 | [ |
| | 2632 | "propagates unarchive archived errors", |
| | 2633 | "session saved-thread is archived", |
| | 2634 | 4, |
| | 2635 | "session saved-thread is archived", |
| | 2636 | ], |
| | 2637 | [ |
| | 2638 | "propagates retry missing-thread errors", |
| | 2639 | "session saved-thread is archived", |
| | 2640 | 5, |
| | 2641 | "thread not found", |
| | 2642 | ], |
| | 2643 | [ |
| | 2644 | "does not retry archived errors twice", |
| | 2645 | "session saved-thread is archived", |
| | 2646 | 5, |
| | 2647 | "session saved-thread is archived", |
| | 2648 | ], |
| | 2649 | ] as const |
| | 2650 | ).map(([name, resumeError, failAt, finalError]) => ({ name, resumeError, failAt, finalError })), |
| | 2651 | )("$name", ({ name, resumeError, failAt, finalError }) => |
| | 2652 | Effect.scoped( |
| | 2653 | Effect.gen(function* () { |
| | 2654 | const nativeThreadId = "saved-thread"; |
| | 2655 | const params = { |
| | 2656 | threadId: nativeThreadId, |
| | 2657 | excludeTurns: true, |
| | 2658 | cwd: CODEX_TEST_RUNTIME_POLICY.cwd, |
| | 2659 | model: CODEX_TEST_MODEL_SELECTION.model, |
| | 2660 | config: CodexAdapterV2.CODEX_THREAD_CONFIG, |
| | 2661 | }; |
| | 2662 | const entries: Array<CodexReplay.CodexAppServerReplayEntry> = [ |
| | 2663 | ...codexReplayPreamble({ |
| | 2664 | nativeThreadId, |
| | 2665 | nativeTurnId: "unused-turn", |
| | 2666 | prompt: "unused-prompt", |
| | 2667 | }).slice(0, 5), |
| | 2668 | { |
| | 2669 | type: "expect_outbound", |
| | 2670 | label: "resume archived thread", |
| | 2671 | frame: { id: 3, method: "thread/resume", params }, |
| | 2672 | }, |
| | 2673 | { |
| | 2674 | type: "emit_inbound", |
| | 2675 | label: "resume error", |
| | 2676 | frame: { id: 3, error: { code: -32600, message: resumeError } }, |
| | 2677 | }, |
| | 2678 | ]; |
| | 2679 | if (failAt !== 3) { |
| | 2680 | entries.push( |
| | 2681 | { |
| | 2682 | type: "expect_outbound", |
| | 2683 | label: "unarchive same thread", |
| | 2684 | frame: { id: 4, method: "thread/unarchive", params: { threadId: nativeThreadId } }, |
| | 2685 | }, |
| | 2686 | { |
| | 2687 | type: "emit_inbound", |
| | 2688 | label: "unarchive result", |
| | 2689 | frame: |
| | 2690 | failAt === 4 |
| | 2691 | ? { id: 4, error: { code: -32600, message: finalError } } |
| | 2692 | : { id: 4, result: { thread: { turns: [{ type: "unknown-history-item" }] } } }, |
| | 2693 | }, |
| | 2694 | ); |
| | 2695 | } |
| | 2696 | if (failAt === 0 || failAt === 5) { |
| | 2697 | entries.push( |
| | 2698 | { |
| | 2699 | type: "expect_outbound", |
| | 2700 | label: "retry identical resume", |
| | 2701 | frame: { id: 5, method: "thread/resume", params }, |
| | 2702 | }, |
| | 2703 | { |
| | 2704 | type: "emit_inbound", |
| | 2705 | label: "retry result", |
| | 2706 | frame: |
| | 2707 | failAt === 5 |
| | 2708 | ? { id: 5, error: { code: -32600, message: finalError } } |
| | 2709 | : { |
| | 2710 | id: 5, |
| | 2711 | result: { |
| | 2712 | thread: { |
| | 2713 | id: nativeThreadId, |
| | 2714 | updatedAt: 1782622450, |
| | 2715 | turns: [{ type: "unknown-history-item" }], |
| | 2716 | }, |
| | 2717 | }, |
| | 2718 | }, |
| | 2719 | }, |
| | 2720 | ); |
| | 2721 | } |
| | 2722 | const harness = yield* makeCodexReplayHarness( |
| | 2723 | makeCodexReplayTranscript({ scenario: name, entries }), |
| | 2724 | ); |
| | 2725 | const resume = harness.runtime.resumeThread({ |
| | 2726 | providerThread: harness.providerThread, |
| | 2727 | modelSelection: CODEX_TEST_MODEL_SELECTION, |
| | 2728 | runtimePolicy: CODEX_TEST_RUNTIME_POLICY, |
| | 2729 | }); |
| | 2730 | if (failAt !== 0) { |
| | 2731 | const error = yield* Effect.flip(resume); |
| | 2732 | assert.equal(error._tag, "ProviderAdapterResumeThreadError"); |
| | 2733 | assert.nestedPropertyVal(error, "cause.errorMessage", finalError); |
| | 2734 | assert.nestedPropertyVal( |
| | 2735 | error, |
| | 2736 | "cause.method", |
| | 2737 | failAt === 4 ? "thread/unarchive" : "thread/resume", |
| | 2738 | ); |
| | 2739 | assert.nestedPropertyVal(error, "cause.requestId", String(failAt)); |
| | 2740 | return; |
| | 2741 | } |
| | 2742 | const resumed = yield* resume; |
| | 2743 | assert.equal(resumed.id, harness.providerThread.id); |
| | 2744 | assert.equal(resumed.nativeThreadRef?.nativeId, nativeThreadId); |
| | 2745 | assert.deepEqual( |
| | 2746 | resumed.nativeConversationHeadRef, |
| | 2747 | harness.providerThread.nativeConversationHeadRef, |
| | 2748 | ); |
| | 2749 | assert.equal(resumed.status, "idle"); |
| | 2750 | assert.equal(DateTime.toEpochMillis(resumed.updatedAt), 1782622450000); |
| | 2751 | }).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| | 2752 | ), |
| | 2753 | ); |
| | 2754 | |
| 2600 | it.effect("continues an interrupted native thread with empty input and reasoning summaries", () => | 2755 | it.effect("continues an interrupted native thread with empty input and reasoning summaries", () => |
| 2601 | Effect.scoped( | 2756 | Effect.scoped( |
| 2602 | Effect.gen(function* () { | 2757 | Effect.gen(function* () { |