1import { presentThreadShell } from "@t3tools/client-runtime/state/shell";
2import {
3 EnvironmentId,
4 MessageId,
5 RunId,
6 TurnItemId,
7 type OrchestrationV2ProjectedTurnItem,
8 type OrchestrationV2TurnItem,
9 ProjectId,
10 ProviderInstanceId,
11 ThreadId,
12 type OrchestrationV2ThreadProjection,
13} from "@t3tools/contracts";
14import * as DateTime from "effect/DateTime";
15
16import type { ChatMessage, Thread } from "./types";
17
18const DEFAULT_TIMESTAMP = "2026-01-01T00:00:00.000Z";
19
20export type ThreadFixtureOverrides = Partial<Thread> & {
21 /** Test-only fallback data for the generic thread-sort contract. */
22 readonly messages?: ReadonlyArray<ChatMessage>;
23 /** Accepted while older unit cases are migrated; never consumed by production code. */
24 readonly proposedPlans?: ReadonlyArray<unknown>;
25};
26
27export function makeThreadProjectionFixture(): OrchestrationV2ThreadProjection {
28 const now = DateTime.makeUnsafe(DEFAULT_TIMESTAMP);
29 const id = ThreadId.make("thread-test");
30 const providerInstanceId = ProviderInstanceId.make("codex");
31 return {
32 thread: {
33 id,
34 projectId: ProjectId.make("project-test"),
35 title: "Thread",
36 providerInstanceId,
37 modelSelection: { instanceId: providerInstanceId, model: "gpt-5.4" },
38 runtimeMode: "full-access",
39 interactionMode: "default",
40 branch: null,
41 worktreePath: null,
42 activeProviderThreadId: null,
43 lineage: { rootThreadId: id, parentThreadId: null, relationshipToParent: null },
44 forkedFrom: null,
45 createdBy: "user",
46 creationSource: "web",
47 createdAt: now,
48 updatedAt: now,
49 archivedAt: null,
50 settledOverride: null,
51 settledAt: null,
52 lastVisitedAt: null,
53 deletedAt: null,
54 },
55 runs: [],
56 attempts: [],
57 nodes: [],
58 subagents: [],
59 providerSessions: [],
60 providerThreads: [],
61 providerTurns: [],
62 runtimeRequests: [],
63 messages: [],
64 plans: [],
65 turnItems: [],
66 checkpointScopes: [],
67 checkpoints: [],
68 contextHandoffs: [],
69 contextTransfers: [],
70 visibleTurnItems: [],
71 updatedAt: now,
72 };
73}
74
75/** Creates a structurally complete V2 shell for UI unit tests. */
76export function makeThreadFixture(overrides: ThreadFixtureOverrides = {}): Thread {
77 const environmentId = overrides.environmentId ?? EnvironmentId.make("environment-test");
78 const id = overrides.id ?? ThreadId.make("thread-test");
79 const projectId = overrides.projectId ?? ProjectId.make("project-test");
80 const providerInstanceId =
81 overrides.providerInstanceId ??
82 overrides.modelSelection?.instanceId ??
83 ProviderInstanceId.make("codex");
84 const modelSelection = overrides.modelSelection ?? {
85 instanceId: providerInstanceId,
86 model: "gpt-5.4",
87 };
88 const createdAt = DateTime.makeUnsafe(DEFAULT_TIMESTAMP);
89 const updatedAt = DateTime.makeUnsafe(DEFAULT_TIMESTAMP);
90 const archivedAt =
91 overrides.archivedAt === null || overrides.archivedAt === undefined
92 ? null
93 : DateTime.makeUnsafe(overrides.archivedAt);
94 const deletedAt =
95 overrides.deletedAt === null || overrides.deletedAt === undefined
96 ? null
97 : DateTime.makeUnsafe(overrides.deletedAt);
98 const shell = presentThreadShell(environmentId, {
99 id,
100 projectId,
101 title: overrides.title ?? "Thread",
102 providerInstanceId,
103 modelSelection,
104 runtimeMode: overrides.runtimeMode ?? "full-access",
105 interactionMode: overrides.interactionMode ?? "default",
106 branch: overrides.branch ?? null,
107 worktreePath: overrides.worktreePath ?? null,
108 activeProviderThreadId: overrides.activeProviderThreadId ?? null,
109 lineage: overrides.lineage ?? {
110 rootThreadId: id,
111 parentThreadId: null,
112 relationshipToParent: null,
113 },
114 forkedFrom: overrides.forkedFrom ?? null,
115 createdBy: "user",
116 creationSource: "web",
117 latestRunId: overrides.latestRun?.runId ?? null,
118 activeRunId: overrides.runtime?.activeRunId ?? null,
119 status: overrides.runtime?.status ?? "idle",
120 pendingRuntimeRequest: null,
121 latestVisibleMessage: null,
122 latestUserMessageAt:
123 overrides.latestUserMessageAt === null || overrides.latestUserMessageAt === undefined
124 ? null
125 : DateTime.makeUnsafe(overrides.latestUserMessageAt),
126 hasActionableProposedPlan: overrides.hasActionableProposedPlan ?? false,
127 itemCount: overrides.itemCount ?? 0,
128 visibleItemCount: overrides.visibleItemCount ?? 0,
129 createdAt,
130 updatedAt,
131 archivedAt,
132 settledOverride: overrides.settledOverride ?? null,
133 settledAt:
134 overrides.settledAt === null || overrides.settledAt === undefined
135 ? null
136 : DateTime.makeUnsafe(overrides.settledAt),
137 deletedAt,
138 });
139
140 return { ...shell, ...overrides };
141}
142
143/** Canonical v2 history and a live response for streaming projection tests. */
144export function makeStreamingTimelineFixture(text = "") {
145 const threadId = ThreadId.make("streaming-thread");
146 const runId = RunId.make("live-run");
147 const historyRunId = RunId.make("history-run");
148 const time = (second: number) => new Date(Date.UTC(2026, 8, 4, 0, 0, second)).toISOString();
149 const base = (id: string, run: typeof runId, second: number) => ({
150 id: TurnItemId.make(id),
151 threadId,
152 runId: run,
153 nodeId: null,
154 providerThreadId: null,
155 providerTurnId: null,
156 nativeItemRef: null,
157 parentItemId: null,
158 ordinal: second,
159 status: "completed" as const,
160 title: null,
161 startedAt: DateTime.makeUnsafe(time(second)),
162 completedAt: DateTime.makeUnsafe(time(second)),
163 updatedAt: DateTime.makeUnsafe(time(second)),
164 });
165 const items: OrchestrationV2TurnItem[] = [
166 {
167 ...base("history-user-item", historyRunId, 0),
168 type: "user_message",
169 messageId: MessageId.make("history-user"),
170 text: "Inspect",
171 attachments: [],
172 inputIntent: "turn_start",
173 createdBy: "user",
174 creationSource: "web",
175 },
176 {
177 ...base("history-work", historyRunId, 1),
178 type: "command_execution",
179 status: "failed",
180 input: "vp test",
181 output: "Test failed",
182 exitCode: 1,
183 },
184 {
185 ...base("history-assistant-item", historyRunId, 3),
186 type: "assistant_message",
187 messageId: MessageId.make("history-assistant"),
188 text: "Done",
189 streaming: false,
190 updatedAt: DateTime.makeUnsafe(time(4)),
191 },
192 {
193 ...base("live-user-item", runId, 5),
194 type: "user_message",
195 messageId: MessageId.make("live-user"),
196 text: "Continue",
197 attachments: [],
198 inputIntent: "turn_start",
199 createdBy: "user",
200 creationSource: "web",
201 },
202 {
203 ...base("live-work", runId, 6),
204 type: "dynamic_tool",
205 toolName: "read_file",
206 input: { path: "/repo/src/index.ts" },
207 output: "Contents",
208 },
209 {
210 ...base("live-assistant-item", runId, 7),
211 type: "assistant_message",
212 messageId: MessageId.make("live-assistant"),
213 text,
214 streaming: true,
215 status: "running",
216 completedAt: null,
217 },
218 ];
219 const visibleTurnItems: OrchestrationV2ProjectedTurnItem[] = items.map((item, position) => ({
220 position,
221 visibility: "local",
222 sourceThreadId: threadId,
223 sourceItemId: item.id,
224 item,
225 }));
226 return { threadId, runId, historyRunId, visibleTurnItems, time };
227}