| 1 | import { |
| 2 | ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE, |
| 3 | RunId, |
| 4 | ThreadId, |
| 5 | TurnItemId, |
| 6 | type OrchestrationV2Run, |
| 7 | type OrchestrationV2TurnItem, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as DateTime from "effect/DateTime"; |
| 10 | import { describe, expect, it } from "vite-plus/test"; |
| 11 | |
| 12 | import { |
| 13 | turnItemIsWorkspacePreparation, |
| 14 | workspacePreparationRetryRunIds, |
| 15 | } from "./turnItemPresentation.ts"; |
| 16 | |
| 17 | function command(input: string): OrchestrationV2TurnItem { |
| 18 | const now = DateTime.makeUnsafe("2026-08-03T00:00:00.000Z"); |
| 19 | return { |
| 20 | id: TurnItemId.make("item-command"), |
| 21 | threadId: ThreadId.make("thread-1"), |
| 22 | runId: RunId.make("run-1"), |
| 23 | nodeId: null, |
| 24 | providerThreadId: null, |
| 25 | providerTurnId: null, |
| 26 | nativeItemRef: null, |
| 27 | parentItemId: null, |
| 28 | ordinal: 1, |
| 29 | status: "completed", |
| 30 | title: "Workspace ready", |
| 31 | startedAt: now, |
| 32 | completedAt: now, |
| 33 | updatedAt: now, |
| 34 | type: "command_execution", |
| 35 | input, |
| 36 | output: "Workspace preparation completed.", |
| 37 | exitCode: 0, |
| 38 | }; |
| 39 | } |
| 40 | |
| 41 | function preparationFailure( |
| 42 | status: OrchestrationV2TurnItem["status"], |
| 43 | code: string | null = ORCHESTRATION_V2_WORKSPACE_PREPARATION_FAILURE_CODE, |
| 44 | ): OrchestrationV2TurnItem { |
| 45 | const { |
| 46 | input: _input, |
| 47 | output: _output, |
| 48 | exitCode: _exitCode, |
| 49 | ...base |
| 50 | } = command("") as Extract<OrchestrationV2TurnItem, { type: "command_execution" }>; |
| 51 | return { |
| 52 | ...base, |
| 53 | id: TurnItemId.make("item-error"), |
| 54 | status, |
| 55 | title: "Workspace preparation failed", |
| 56 | type: "error", |
| 57 | failure: { class: "validation_error", message: "fetch failed", code, retryable: false }, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | const worktree = { type: "worktree", baseRef: "main" } as const; |
| 62 | function run( |
| 63 | status: OrchestrationV2Run["status"], |
| 64 | workspacePreparation?: typeof worktree, |
| 65 | ): Pick<OrchestrationV2Run, "id" | "status" | "workspacePreparation"> { |
| 66 | return { |
| 67 | id: RunId.make("run-1"), |
| 68 | status, |
| 69 | ...(workspacePreparation === undefined ? {} : { workspacePreparation }), |
| 70 | }; |
| 71 | } |
| 72 | |
| 73 | describe("turnItemIsWorkspacePreparation", () => { |
| 74 | it("identifies the synthetic workspace preparation command", () => { |
| 75 | expect(turnItemIsWorkspacePreparation(command("Preparing workspace"))).toBe(true); |
| 76 | expect(turnItemIsWorkspacePreparation(command("prepare workspace"))).toBe(false); |
| 77 | }); |
| 78 | |
| 79 | it("hides a preparation failure only once a retry cancelled it", () => { |
| 80 | expect(turnItemIsWorkspacePreparation(preparationFailure("failed"))).toBe(false); |
| 81 | expect(turnItemIsWorkspacePreparation(preparationFailure("cancelled"))).toBe(true); |
| 82 | expect(turnItemIsWorkspacePreparation(preparationFailure("cancelled", null))).toBe(false); |
| 83 | }); |
| 84 | }); |
| 85 | |
| 86 | describe("workspacePreparationRetryRunIds", () => { |
| 87 | it("offers a retry while the run still ends in its failed preparation", () => { |
| 88 | const failure = [preparationFailure("failed")]; |
| 89 | expect([...workspacePreparationRetryRunIds([run("failed", worktree)], failure)]).toEqual([ |
| 90 | RunId.make("run-1"), |
| 91 | ]); |
| 92 | // Retried: the run is preparing again and the old failure is cancelled. |
| 93 | expect(workspacePreparationRetryRunIds([run("preparing", worktree)], failure).size).toBe(0); |
| 94 | // An older server records no preparation, so there is nothing to repeat. |
| 95 | expect(workspacePreparationRetryRunIds([run("failed")], failure).size).toBe(0); |
| 96 | // A provider error on a run that did reach the provider is not a preparation failure. |
| 97 | expect( |
| 98 | workspacePreparationRetryRunIds( |
| 99 | [run("failed", worktree)], |
| 100 | [preparationFailure("failed", "provider_crashed")], |
| 101 | ).size, |
| 102 | ).toBe(0); |
| 103 | }); |
| 104 | }); |