1import { describe, expect, it } from "vite-plus/test";
2import type { OrchestrationV2ThreadProjection } from "@t3tools/contracts";
3
4import {
5 canDetachThreadProviderSession,
6 canForkProjectedAssistantItem,
7 deriveThreadQueueWorkflowState,
8 resolveLatestMergeBackRun,
9 threadSupportsProviderHandoff,
10} from "./threadWorkflows.ts";
11
12const capabilities = (input?: {
13 readonly queued?: boolean;
14 readonly steer?: boolean;
15 readonly restartSteer?: boolean;
16 readonly nativeFork?: boolean;
17 readonly portableFork?: boolean;
18}) =>
19 ({
20 turns: {
21 supportsQueuedMessages: input?.queued ?? false,
22 supportsActiveSteering: input?.steer ?? false,
23 supportsSteeringByInterruptRestart: input?.restartSteer ?? false,
24 },
25 threads: {
26 canForkThread: input?.nativeFork ?? false,
27 canForkFromTurn: input?.nativeFork ?? false,
28 },
29 identity: { nativeThreadIds: input?.nativeFork ? "strong" : "none" },
30 context: { supportsFullThreadHandoff: input?.portableFork ?? false },
31 }) as never;
32
33describe("thread workflows", () => {
34 it("allows a completed thread to switch providers after its session detaches", () => {
35 const projection = {
36 thread: {
37 id: "thread",
38 activeProviderThreadId: "provider-thread",
39 modelSelection: { instanceId: "cursor", model: "grok-4.7" },
40 },
41 runs: [{ id: "run", status: "completed" }],
42 providerSessions: [],
43 providerThreads: [
44 {
45 id: "provider-thread",
46 appThreadId: "thread",
47 providerInstanceId: "cursor",
48 providerSessionId: "detached-session",
49 nativeThreadRef: { driver: "cursor", nativeId: "agent-123", strength: "strong" },
50 },
51 ],
52 } as unknown as OrchestrationV2ThreadProjection;
53
54 expect(threadSupportsProviderHandoff(projection)).toBe(true);
55 expect(
56 threadSupportsProviderHandoff({
57 ...projection,
58 runs: [{ ...projection.runs[0]!, status: "running" }],
59 }),
60 ).toBe(false);
61 expect(
62 threadSupportsProviderHandoff({
63 ...projection,
64 providerThreads: [{ ...projection.providerThreads[0]!, nativeThreadRef: null }],
65 }),
66 ).toBe(false);
67 });
68
69 it("sorts queued messages and gates reorder and promotion from capabilities", () => {
70 const state = deriveThreadQueueWorkflowState({
71 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
72 runs: [
73 {
74 id: "active",
75 status: "running",
76 providerThreadId: "provider-thread",
77 activeAttemptId: "attempt-active",
78 ordinal: 1,
79 },
80 { id: "later", status: "queued", userMessageId: "message-later", ordinal: 3 },
81 {
82 id: "first",
83 status: "queued",
84 userMessageId: "message-first",
85 ordinal: 2,
86 queuePosition: 1,
87 },
88 ],
89 messages: [
90 {
91 id: "message-first",
92 text: "First",
93 attachments: [
94 {
95 type: "image",
96 id: "attachment-first",
97 name: "first.png",
98 mimeType: "image/png",
99 sizeBytes: 64,
100 },
101 ],
102 },
103 { id: "message-later", text: "Later" },
104 ],
105 providerTurns: [
106 {
107 id: "provider-turn-active",
108 runAttemptId: "attempt-active",
109 status: "running",
110 },
111 ],
112 providerThreads: [
113 {
114 id: "provider-thread",
115 appThreadId: "thread",
116 providerSessionId: "provider-session",
117 },
118 ],
119 providerSessions: [
120 {
121 id: "provider-session",
122 status: "running",
123 capabilities: capabilities({ queued: true, restartSteer: true }),
124 },
125 ],
126 } as never);
127
128 expect(state.queuedRuns.map(({ run, text }) => [run.id, text])).toEqual([
129 ["first", "First"],
130 ["later", "Later"],
131 ]);
132 expect(state.queuedRuns.map(({ attachments }) => attachments.map(({ id }) => id))).toEqual([
133 ["attachment-first"],
134 [],
135 ]);
136 expect(state.activeRun?.id).toBe("active");
137 expect(state.canReorder).toBe(true);
138 expect(state.canPromoteToSteer).toBe(true);
139 });
140
141 it("keeps held messages visible and clears the hold when they leave the queue", () => {
142 for (const status of ["queued", "cancelled", "starting"] as const) {
143 const state = deriveThreadQueueWorkflowState({
144 thread: { id: "thread", activeProviderThreadId: null },
145 runs: [{ id: "held", status, userMessageId: "message", ordinal: 1, queueHeld: true }],
146 messages: [{ id: "message", text: "Saved message" }],
147 providerThreads: [],
148 providerTurns: [],
149 providerSessions: [],
150 } as never);
151 expect(state.isHeld).toBe(status === "queued");
152 expect(state.queuedRuns.map(({ text }) => text)).toEqual(
153 status === "queued" ? ["Saved message"] : [],
154 );
155 }
156 });
157
158 it("hides automatic completion delivery from the visible queue", () => {
159 const state = deriveThreadQueueWorkflowState({
160 thread: { id: "thread", activeProviderThreadId: null },
161 runs: [
162 {
163 id: "provider-wake",
164 status: "queued",
165 userMessageId: "provider-wake",
166 ordinal: 4,
167 queuePosition: 3,
168 },
169 {
170 id: "automatic",
171 status: "queued",
172 userMessageId: "message-automatic",
173 ordinal: 2,
174 queuePosition: 1,
175 },
176 {
177 id: "visible",
178 status: "queued",
179 userMessageId: "message-visible",
180 ordinal: 3,
181 queuePosition: 2,
182 },
183 ],
184 messages: [
185 {
186 id: "provider-wake",
187 text: "Model-facing text",
188 notification: {
189 source: { kind: "monitor" },
190 outcome: "updated",
191 summary: "Monitor updated",
192 },
193 },
194 {
195 id: "message-automatic",
196 text: "A delegated task reached a terminal state.",
197 delegatedCompletion: {
198 generation: 1,
199 parentRunId: "run:parent",
200 taskIds: ["task:child"],
201 },
202 },
203 { id: "message-visible", text: "Visible queued message" },
204 ],
205 providerTurns: [],
206 providerThreads: [],
207 providerSessions: [],
208 } as never);
209
210 expect(state.queuedRuns.map(({ run, text }) => [run.id, text])).toEqual([
211 ["visible", "Visible queued message"],
212 ]);
213 });
214
215 it("removes only the promoted head from the visible queue", () => {
216 const state = deriveThreadQueueWorkflowState({
217 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
218 runs: [
219 {
220 id: "promoted",
221 status: "starting",
222 userMessageId: "message-promoted",
223 providerThreadId: "provider-thread",
224 ordinal: 2,
225 queuePosition: null,
226 },
227 {
228 id: "still-queued",
229 status: "queued",
230 userMessageId: "message-still-queued",
231 providerThreadId: "provider-thread",
232 ordinal: 3,
233 queuePosition: 2,
234 },
235 ],
236 messages: [
237 { id: "message-promoted", text: "Run now" },
238 { id: "message-still-queued", text: "Wait longer" },
239 ],
240 providerTurns: [],
241 providerThreads: [],
242 providerSessions: [],
243 } as never);
244
245 expect(state.activeRun?.id).toBe("promoted");
246 expect(state.queuedRuns.map(({ run, text }) => [run.id, text])).toEqual([
247 ["still-queued", "Wait longer"],
248 ]);
249 });
250
251 it.each(["preparing", "starting", "waiting"] as const)(
252 "does not promote queued work into a %s run",
253 (status) => {
254 const state = deriveThreadQueueWorkflowState({
255 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
256 runs: [
257 {
258 id: "active",
259 status,
260 providerThreadId: "provider-thread",
261 activeAttemptId: "attempt-active",
262 ordinal: 1,
263 },
264 { id: "queued", status: "queued", userMessageId: "message", ordinal: 2 },
265 ],
266 messages: [{ id: "message", text: "Queued" }],
267 providerTurns: [
268 {
269 id: "provider-turn-active",
270 runAttemptId: "attempt-active",
271 status: status === "waiting" ? "completed" : "starting",
272 },
273 ],
274 providerThreads: [
275 {
276 id: "provider-thread",
277 appThreadId: "thread",
278 providerSessionId: "provider-session",
279 },
280 ],
281 providerSessions: [
282 {
283 id: "provider-session",
284 status: "running",
285 capabilities: capabilities({ queued: true, steer: true }),
286 },
287 ],
288 } as never);
289
290 expect(state.canPromoteToSteer).toBe(false);
291 },
292 );
293
294 it("does not promote queued work until the running provider turn is projected", () => {
295 const state = deriveThreadQueueWorkflowState({
296 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
297 runs: [
298 {
299 id: "active",
300 status: "running",
301 providerThreadId: "provider-thread",
302 activeAttemptId: "attempt-active",
303 ordinal: 1,
304 },
305 { id: "queued", status: "queued", userMessageId: "message", ordinal: 2 },
306 ],
307 messages: [{ id: "message", text: "Queued" }],
308 providerTurns: [],
309 providerThreads: [
310 {
311 id: "provider-thread",
312 appThreadId: "thread",
313 providerSessionId: "provider-session",
314 },
315 ],
316 providerSessions: [
317 {
318 id: "provider-session",
319 status: "running",
320 capabilities: capabilities({ queued: true, steer: true }),
321 },
322 ],
323 } as never);
324
325 expect(state.canPromoteToSteer).toBe(false);
326 });
327
328 it("does not expose known unsupported queue or fork actions", () => {
329 const projection = {
330 thread: { id: "thread", activeProviderThreadId: "provider-thread" },
331 runs: [{ id: "queued", status: "queued", userMessageId: "message", ordinal: 1 }],
332 messages: [],
333 providerThreads: [
334 {
335 id: "provider-thread",
336 appThreadId: "thread",
337 providerSessionId: "provider-session",
338 },
339 ],
340 providerSessions: [
341 {
342 id: "provider-session",
343 status: "ready",
344 capabilities: capabilities(),
345 },
346 ],
347 } as never;
348 const queue = deriveThreadQueueWorkflowState(projection);
349 const projectedItem = {
350 item: { type: "assistant_message", runId: "run", status: "completed" },
351 } as never;
352
353 expect(queue.canReorder).toBe(false);
354 expect(queue.canPromoteToSteer).toBe(false);
355 expect(canForkProjectedAssistantItem({ projectedItem, capabilities: capabilities() })).toBe(
356 false,
357 );
358 expect(canDetachThreadProviderSession(projection)).toBe(true);
359 });
360
361 it("allows native, portable, and capability-unknown exact-run forks", () => {
362 const projectedItem = {
363 item: { type: "assistant_message", runId: "run", status: "completed" },
364 } as never;
365
366 expect(
367 canForkProjectedAssistantItem({
368 projectedItem,
369 capabilities: capabilities({ nativeFork: true }),
370 }),
371 ).toBe(true);
372 expect(
373 canForkProjectedAssistantItem({
374 projectedItem,
375 capabilities: capabilities({ portableFork: true }),
376 }),
377 ).toBe(true);
378 expect(canForkProjectedAssistantItem({ projectedItem })).toBe(true);
379 expect(
380 canForkProjectedAssistantItem({
381 projectedItem: {
382 item: { type: "assistant_message", runId: "run", status: "running" },
383 } as never,
384 }),
385 ).toBe(false);
386 });
387
388 it("merges the newest provider-finished run while checkpoint capture is pending", () => {
389 const projection = {
390 runs: [
391 { id: "newest-queued", status: "queued", ordinal: 3 },
392 { id: "older-completed", status: "completed", ordinal: 1 },
393 { id: "newest-finished", status: "waiting", ordinal: 2 },
394 ],
395 } as never;
396
397 expect(resolveLatestMergeBackRun(projection)?.id).toBe("newest-finished");
398 });
399
400 it("does not let a stale completed run later in storage order hide the waiting checkpoint", () => {
401 const projection = {
402 runs: [
403 { id: "newest-finished", status: "waiting", ordinal: 2 },
404 { id: "older-completed", status: "completed", ordinal: 1 },
405 ],
406 } as never;
407
408 expect(resolveLatestMergeBackRun(projection)?.id).toBe("newest-finished");
409 });
410
411 it.each(["preparing", "starting", "running"] as const)(
412 "does not merge older history while a newer run is %s",
413 (status) => {
414 const projection = {
415 runs: [
416 { id: "older-completed", status: "completed", ordinal: 1 },
417 { id: "newer-active", status, ordinal: 2 },
418 ],
419 } as never;
420
421 expect(resolveLatestMergeBackRun(projection)).toBeNull();
422 },
423 );
424});