| ... | ... | @@ -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 | 2755 | it.effect("continues an interrupted native thread with empty input and reasoning summaries", () => |
| 2601 | 2756 | Effect.scoped( |
| 2602 | 2757 | Effect.gen(function* () { |