Skip to content
Source
Sign inSign up

entities.test.ts

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