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