Skip to content
Source
Sign inSign up

entities.test.ts

598 lines19.3 KB

1import {
2 EnvironmentId,
3 MessageId,
4 NodeId,
5 ProviderDriverKind,
6 ProviderSessionId,
7 ProviderThreadId,
8 RunId,
9 RuntimeRequestId,
10 TurnItemId,
11} from "@t3tools/contracts";
12import * as DateTime from "effect/DateTime";
13import { describe, expect, it } from "vite-plus/test";
14​
15import { presentThreadShell, resolveThreadWorkingStartedAt } from "./models.ts";
16import { v2Projection, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
17import { deriveLatestThreadRun, deriveThreadRuntime } from "./threadExecution.ts";
18import { isThreadWorking } from "./threadInbox.ts";
19import { derivePendingThreadRequests } from "./threadRequests.ts";
20import { canSnooze, effectiveSnoozed } from "./threadSettled.ts";
21​
22const environmentId = EnvironmentId.make("environment-v2");
23​
24describe("V2 client presentation", () => {
25 it("an async question allows snooze and wakes an older snooze", () => {
26 const runId = RunId.make("run-async-question");
27 const running = {
28 ...v2ThreadShell,
29 snoozedAt: DateTime.makeUnsafe("2026-06-19T23:59:00.000Z"),
30 snoozedUntil: DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"),
31 latestRunId: runId,
32 activeRunId: runId,
33 status: "running" as const,
34 pendingRuntimeRequest: {
35 id: RuntimeRequestId.make("request-async-question"),
36 kind: "user_input" as const,
37 createdAt: v2ThreadShell.updatedAt,
38 blocking: false,
39 },
40 };
41 const active = presentThreadShell(environmentId, running);
42 expect(active.hasPendingUserInput).toBe(true);
43 expect(isThreadWorking(active)).toBe(false);
44 expect(canSnooze(active, { now: "2026-06-20T00:05:00.000Z" })).toBe(true);
45​
46 const completed = presentThreadShell(environmentId, {
47 ...running,
48 activeRunId: null,
49 status: "completed",
50 });
51 expect(completed.hasPendingUserInput).toBe(true);
52 expect(isThreadWorking(completed)).toBe(false);
53 expect(effectiveSnoozed(active, { now: "2026-06-20T00:05:00.000Z" })).toBe(false);
54 });
55​
56 it("moves a running thread with any question out of Working", () => {
57 for (const capability of [{ blocking: false }, { blocking: true }, {}]) {
58 const shell = presentThreadShell(environmentId, {
59 ...v2ThreadShell,
60 latestRunId: RunId.make("run-blocking-question"),
61 activeRunId: RunId.make("run-blocking-question"),
62 status: "running",
63 pendingRuntimeRequest: {
64 id: RuntimeRequestId.make("request-blocking-question"),
65 kind: "user_input",
66 createdAt: v2ThreadShell.updatedAt,
67 ...capability,
68 },
69 });
70 expect(shell.hasPendingUserInput).toBe(true);
71 expect(isThreadWorking(shell)).toBe(false);
72 }
73 });
74​
75 it("parks a thread that never ran at idle while a pull request watch holds it", () => {
76 const watch = {
77 taskId: "pull-request-watch:github.com/acme/app#1",
78 kind: "monitor" as const,
79 awaited: true as const,
80 };
81 const watched = presentThreadShell(environmentId, {
82 ...v2ThreadShell,
83 latestRunId: null,
84 activeProviderThreadId: null,
85 activeRunId: null,
86 status: "idle",
87 pendingBackgroundTasks: [watch],
88 });
89 expect(watched.runtime).toMatchObject({ status: "idle", activeRunId: null });
90 expect(
91 presentThreadShell(environmentId, {
92 ...v2ThreadShell,
93 latestRunId: null,
94 activeProviderThreadId: null,
95 status: "idle",
96 pendingBackgroundTasks: [],
97 }).runtime,
98 ).toBeNull();
99 });
100​
101 it("keeps an imported thread that never ran out of Working", () => {
102 const imported = presentThreadShell(environmentId, {
103 ...v2ThreadShell,
104 historyOrigin: "v1_import",
105 activeProviderThreadId: ProviderThreadId.make("provider-thread-imported"),
106 latestRunId: null,
107 activeRunId: null,
108 status: "idle",
109 pendingBackgroundTasks: [],
110 });
111 expect(isThreadWorking(imported)).toBe(false);
112 });
113​
114 it.each([
115 { kinds: ["command"], expected: "completed" },
116 { kinds: ["command", "subagent"], expected: "idle" },
117 { kinds: ["background_task"], expected: "idle" },
118 ] as const)("presents a completed shell with $kinds as $expected", ({ kinds, expected }) => {
119 const pendingBackgroundTasks = kinds.map((kind, index) => ({
120 taskId: `bg-${index}`,
121 kind,
122 }));
123 const shell = presentThreadShell(environmentId, {
124 ...v2ThreadShell,
125 latestRunId: RunId.make("run-completed"),
126 activeRunId: null,
127 status: "completed",
128 pendingBackgroundTasks,
129 });
130​
131 expect(shell.latestRun?.status).toBe("completed");
132 expect(shell.runtime).toMatchObject({ status: expected, activeRunId: null });
133 expect(shell.pendingBackgroundTasks).toEqual(pendingBackgroundTasks);
134 });
135​
136 it.each(["running", "waiting"] as const)(
137 "preserves shell %s while only commands remain in the roster",
138 (status) => {
139 const runId = RunId.make("run-command");
140 const shell = presentThreadShell(environmentId, {
141 ...v2ThreadShell,
142 latestRunId: runId,
143 activeRunId: runId,
144 status,
145 pendingBackgroundTasks: [{ taskId: "dev-server", kind: "command" }],
146 });
147​
148 expect(shell.latestRun?.status).toBe(status);
149 expect(shell.runtime).toMatchObject({ status, activeRunId: runId });
150 expect(shell.pendingBackgroundTasks).toEqual([{ taskId: "dev-server", kind: "command" }]);
151 },
152 );
153​
154 it("keeps a failed latest run failed while background tasks are still pending", () => {
155 const shell = presentThreadShell(environmentId, {
156 ...v2ThreadShell,
157 latestRunId: RunId.make("run-failed"),
158 activeRunId: null,
159 status: "failed",
160 lastError: "Provider turn failed",
161 pendingBackgroundTasks: [
162 { taskId: "bg-1", description: "Watch build", kind: "monitor", awaited: true },
163 ],
164 });
165​
166 expect(shell.runtime).toMatchObject({ status: "failed", lastError: "Provider turn failed" });
167 expect(shell.pendingBackgroundTasks).toHaveLength(1);
168 });
169​
170 it("keeps an older active run visible over a newer cancelled run", () => {
171 const activeRunId = RunId.make("run-active");
172 const cancelledRunId = RunId.make("run-cancelled");
173 const shell = presentThreadShell(environmentId, {
174 ...v2ThreadShell,
175 latestRunId: cancelledRunId,
176 latestRunStartedAt: null,
177 latestRunCompletedAt: DateTime.makeUnsafe("2026-06-20T01:05:00.000Z"),
178 activeRunId,
179 activityRunStatus: "running",
180 status: "cancelled",
181 pendingBackgroundTasks: [],
182 });
183​
184 expect(shell.latestRun).toMatchObject({ runId: cancelledRunId, status: "cancelled" });
185 expect(shell.runtime).toMatchObject({
186 status: "running",
187 activeRunId,
188 });
189 });
190​
191 it("keeps a post-settlement background roster ahead of activity status", () => {
192 const activeRunId = RunId.make("run-active");
193 const shell = presentThreadShell(environmentId, {
194 ...v2ThreadShell,
195 latestRunId: RunId.make("run-cancelled"),
196 activeRunId,
197 activityRunStatus: "running",
198 status: "cancelled",
199 pendingBackgroundTasks: [
200 { taskId: "bg-activity", description: "background work", kind: "background_task" },
201 ],
202 });
203​
204 expect(shell.runtime).toMatchObject({ status: "idle", activeRunId });
205 });
206​
207 it("parks runtime idle over stale shell running when the roster is nonempty", () => {
208 const runId = RunId.make("run-stale-running");
209 const shell = presentThreadShell(environmentId, {
210 ...v2ThreadShell,
211 latestRunId: runId,
212 activeRunId: runId,
213 status: "running",
214 pendingBackgroundTasks: [
215 { taskId: "bg-1", description: "Watch build", kind: "monitor", awaited: true },
216 ],
217 });
218​
219 expect(shell.latestRun).toMatchObject({ runId, status: "running" });
220 expect(shell.runtime).toMatchObject({
221 status: "idle",
222 activeRunId: runId,
223 });
224 expect(shell.pendingBackgroundTasks).toEqual([
225 { taskId: "bg-1", description: "Watch build", kind: "monitor", awaited: true },
226 ]);
227 });
228​
229 it("derives execution summaries without wrapping or copying the projection", () => {
230 const runId = RunId.make("run-1");
231 const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z");
232 const projection = {
233 ...v2Projection,
234 runs: [
235 {
236 id: runId,
237 threadId: v2Projection.thread.id,
238 ordinal: 1,
239 providerInstanceId: v2Projection.thread.providerInstanceId,
240 modelSelection: v2Projection.thread.modelSelection,
241 providerThreadId: null,
242 userMessageId: MessageId.make("message-user"),
243 rootNodeId: null,
244 activeAttemptId: null,
245 status: "running" as const,
246 requestedAt: now,
247 startedAt: now,
248 completedAt: null,
249 checkpointId: null,
250 contextHandoffId: null,
251 },
252 ],
253 messages: [
254 {
255 id: MessageId.make("message-user"),
256 threadId: v2Projection.thread.id,
257 runId,
258 nodeId: null,
259 role: "user" as const,
260 text: "Hello",
261 attachments: [],
262 streaming: false,
263 createdBy: "user" as const,
264 creationSource: "web" as const,
265 createdAt: now,
266 updatedAt: now,
267 },
268 ],
269 updatedAt: now,
270 };
271​
272 expect(deriveLatestThreadRun(projection)).toMatchObject({
273 runId,
274 status: "running",
275 requestedAt: "2026-06-20T01:00:00.000Z",
276 assistantMessageId: null,
277 });
278 expect(deriveThreadRuntime(projection)).toMatchObject({
279 status: "running",
280 activeRunId: runId,
281 providerInstanceId: projection.thread.providerInstanceId,
282 });
283 for (const status of ["queued", "cancelled"] as const) {
284 const later = DateTime.add(now, { hours: 1 });
285 const latest = {
286 ...projection.runs[0]!,
287 id: RunId.make("newer-run"),
288 ordinal: 2,
289 status,
290 requestedAt: later,
291 startedAt: null,
292 completedAt: status === "cancelled" ? later : null,
293 };
294 const detail = { ...projection, runs: [...projection.runs, latest], updatedAt: later };
295 const shell = presentThreadShell(environmentId, {
296 ...v2ThreadShell,
297 latestRunId: latest.id,
298 latestRunStartedAt: null,
299 latestRunRequestedAt: later,
300 latestRunCompletedAt: latest.completedAt,
301 status,
302 activeRunId: runId,
303 activityRunStatus: "running",
304 activityRunStartedAt: now,
305 updatedAt: later,
306 });
307 expect(resolveThreadWorkingStartedAt(shell)).toBe(DateTime.formatIso(now));
308 expect(
309 resolveThreadWorkingStartedAt({
310 latestRun: deriveLatestThreadRun(detail),
311 runtime: deriveThreadRuntime(detail),
312 }),
313 ).toBe(resolveThreadWorkingStartedAt(shell));
314 const stopped = {
315 ...detail,
316 runs: detail.runs.map((run) => ({
317 ...run,
318 status: "completed" as const,
319 completedAt: later,
320 })),
321 };
322 expect(
323 resolveThreadWorkingStartedAt({
324 latestRun: deriveLatestThreadRun(stopped),
325 runtime: deriveThreadRuntime(stopped),
326 }),
327 ).toBeNull();
328 }
329 });
330​
331 it("parks waiting runtime for a post-settlement roster without hiding active running work", () => {
332 const runId = RunId.make("run-background-presentation");
333 const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z");
334 const run = {
335 id: runId,
336 threadId: v2Projection.thread.id,
337 ordinal: 1,
338 providerInstanceId: v2Projection.thread.providerInstanceId,
339 modelSelection: v2Projection.thread.modelSelection,
340 providerThreadId: null,
341 userMessageId: MessageId.make("message-background-presentation"),
342 rootNodeId: null,
343 activeAttemptId: null,
344 status: "waiting" as const,
345 requestedAt: now,
346 startedAt: now,
347 completedAt: null,
348 checkpointId: null,
349 contextHandoffId: null,
350 };
351 const backgroundItem = {
352 id: TurnItemId.make("item-background-subagent"),
353 threadId: v2Projection.thread.id,
354 runId,
355 nodeId: null,
356 providerThreadId: null,
357 providerTurnId: null,
358 nativeItemRef: null,
359 parentItemId: null,
360 ordinal: 0,
361 status: "running" as const,
362 title: "Background review",
363 startedAt: now,
364 completedAt: null,
365 updatedAt: now,
366 type: "subagent" as const,
367 subagentId: NodeId.make("subagent-review"),
368 origin: "provider_native" as const,
369 driver: ProviderDriverKind.make("codex"),
370 providerInstanceId: v2Projection.thread.providerInstanceId,
371 childThreadId: null,
372 prompt: "Review the changes",
373 result: null,
374 };
375​
376 expect(
377 deriveThreadRuntime({
378 ...v2Projection,
379 runs: [run],
380 turnItems: [backgroundItem],
381 }),
382 ).toMatchObject({
383 status: "idle",
384 activeRunId: null,
385 });
386 expect(
387 deriveThreadRuntime({
388 ...v2Projection,
389 runs: [{ ...run, status: "running" as const }],
390 turnItems: [backgroundItem],
391 }),
392 ).toMatchObject({
393 status: "running",
394 activeRunId: runId,
395 });
396 expect(
397 deriveThreadRuntime({
398 ...v2Projection,
399 runs: [{ ...run, status: "failed" as const, completedAt: now }],
400 turnItems: [backgroundItem],
401 }),
402 ).toMatchObject({ status: "failed", activeRunId: null });
403 const commandItem = {
404 ...backgroundItem,
405 id: TurnItemId.make("item-background-command"),
406 type: "command_execution" as const,
407 input: "npm run dev",
408 };
409 expect(
410 deriveThreadRuntime({ ...v2Projection, runs: [run], turnItems: [commandItem] }),
411 ).toMatchObject({ status: "waiting", activeRunId: null });
412​
413 for (const [turnItems, status] of [
414 [[commandItem], "completed"],
415 [[backgroundItem], "idle"],
416 [[commandItem, backgroundItem], "idle"],
417 ] as const) {
418 expect(
419 deriveThreadRuntime({
420 ...v2Projection,
421 runs: [{ ...run, status: "completed", completedAt: now }],
422 turnItems,
423 }),
424 ).toMatchObject({ status, activeRunId: null });
425 }
426​
427 for (const held of [{ kind: "monitor", awaited: true }, { kind: "background_task" }] as const) {
428 const providerThread = {
429 id: ProviderThreadId.make("provider-thread-background"),
430 driver: ProviderDriverKind.make("claudeCode"),
431 providerInstanceId: v2Projection.thread.providerInstanceId,
432 providerSessionId: null,
433 appThreadId: v2Projection.thread.id,
434 ownerNodeId: null,
435 nativeThreadRef: null,
436 nativeConversationHeadRef: null,
437 status: "idle" as const,
438 firstRunOrdinal: 1,
439 lastRunOrdinal: 1,
440 handoffIds: [],
441 forkedFrom: null,
442 pendingBackgroundTasks: [{ taskId: "background-task", ...held }],
443 createdAt: now,
444 updatedAt: now,
445 };
446 for (const status of ["completed", "failed"] as const) {
447 expect(
448 deriveThreadRuntime({
449 ...v2Projection,
450 runs: [{ ...run, status, completedAt: now }],
451 providerThreads: [providerThread],
452 }),
453 ).toMatchObject({ status: status === "failed" ? "failed" : "idle", activeRunId: null });
454 }
455 }
456 });
457​
458 it("joins pending request entities to their native turn-item display data", () => {
459 const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z");
460 const requestId = RuntimeRequestId.make("request-approval");
461 const item = {
462 id: TurnItemId.make("item-approval"),
463 threadId: v2Projection.thread.id,
464 runId: null,
465 nodeId: NodeId.make("node-root"),
466 providerThreadId: null,
467 providerTurnId: null,
468 nativeItemRef: null,
469 parentItemId: null,
470 ordinal: 0,
471 status: "completed" as const,
472 title: null,
473 startedAt: now,
474 completedAt: now,
475 updatedAt: now,
476 type: "approval_request" as const,
477 requestId,
478 requestKind: "command" as const,
479 prompt: "Allow command?",
480 };
481 const projection = {
482 ...v2Projection,
483 runtimeRequests: [
484 {
485 id: requestId,
486 nodeId: NodeId.make("node-root"),
487 providerTurnId: null,
488 nativeRequestRef: null,
489 kind: "command" as const,
490 status: "pending" as const,
491 responseCapability: {
492 type: "not_resumable" as const,
493 reason: "provider disconnected",
494 },
495 createdAt: now,
496 resolvedAt: null,
497 },
498 ],
499 turnItems: [item],
500 updatedAt: now,
501 };
502​
503 expect(derivePendingThreadRequests(projection).approvals).toEqual([
504 {
505 requestId,
506 requestKind: "command",
507 createdAt: "2026-06-20T01:00:00.000Z",
508 detail: "Allow command?",
509 responseCapability: "not_resumable",
510 },
511 ]);
512 expect(derivePendingThreadRequests(projection).userInputs).toEqual([]);
513 });
514​
515 it("preserves structured-question selection mode and defaults legacy payloads", () => {
516 const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z");
517 const multiRequestId = RuntimeRequestId.make("request-multi-select");
518 const legacyRequestId = RuntimeRequestId.make("request-legacy-single-select");
519 const runtimeRequests = [multiRequestId, legacyRequestId].map((id) => ({
520 id,
521 nodeId: NodeId.make(`node-${id}`),
522 providerTurnId: null,
523 nativeRequestRef: null,
524 kind: "user_input" as const,
525 status: "pending" as const,
526 responseCapability: {
527 type: "live" as const,
528 providerSessionId: ProviderSessionId.make("provider-session-questions"),
529 },
530 createdAt: now,
531 resolvedAt: null,
532 }));
533 const common = {
534 threadId: v2Projection.thread.id,
535 runId: null,
536 providerThreadId: null,
537 providerTurnId: null,
538 nativeItemRef: null,
539 parentItemId: null,
540 ordinal: 0,
541 status: "completed" as const,
542 title: null,
543 startedAt: now,
544 completedAt: now,
545 updatedAt: now,
546 type: "user_input_request" as const,
547 };
548 const projection = {
549 ...v2Projection,
550 runtimeRequests,
551 turnItems: [
552 {
553 ...common,
554 id: TurnItemId.make("item-multi-select"),
555 nodeId: NodeId.make("node-request-multi-select"),
556 requestId: multiRequestId,
557 questions: [
558 {
559 id: "regions",
560 header: "Regions",
561 question: "Which regions?",
562 options: [
563 { label: "US", description: "United States" },
564 { label: "EU", description: "European Union" },
565 ],
566 multiSelect: true,
567 },
568 ],
569 },
570 {
571 ...common,
572 id: TurnItemId.make("item-legacy-single-select"),
573 nodeId: NodeId.make("node-request-legacy-single-select"),
574 requestId: legacyRequestId,
575 questions: [
576 {
577 id: "target",
578 header: "Target",
579 question: "Which target?",
580 options: [{ label: "Staging", description: "Staging environment" }],
581 },
582 ],
583 },
584 ],
585 updatedAt: now,
586 };
587​
588 expect(
589 derivePendingThreadRequests(projection).userInputs.map((input) => ({
590 requestId: input.requestId,
591 multiSelect: input.questions[0]?.multiSelect,
592 })),
593 ).toEqual([
594 { requestId: multiRequestId, multiSelect: true },
595 { requestId: legacyRequestId, multiSelect: false },
596 ]);
597 });
598});