| 1 | import { |
| 2 | EnvironmentId, |
| 3 | MessageId, |
| 4 | NodeId, |
| 5 | ProviderDriverKind, |
| 6 | ProviderInstanceId, |
| 7 | ProviderSessionId, |
| 8 | ProviderThreadId, |
| 9 | RunId, |
| 10 | RuntimeRequestId, |
| 11 | TurnItemId, |
| 12 | } from "@t3tools/contracts"; |
| 13 | import * as DateTime from "effect/DateTime"; |
| 14 | import { describe, expect, it } from "vite-plus/test"; |
| 15 | |
| 16 | import { |
| 17 | InvalidScopedProjectRefCollectionKeyError, |
| 18 | InvalidScopedThreadKeyError, |
| 19 | parseProjectRefCollectionKey, |
| 20 | parseThreadKey, |
| 21 | } from "./entities.ts"; |
| 22 | import { |
| 23 | presentThreadShell, |
| 24 | resolveThreadProviderStack, |
| 25 | resolveThreadWorkingStartedAt, |
| 26 | } from "./models.ts"; |
| 27 | import { v2Projection, v2ThreadShell } from "./orchestrationV2TestFixtures.ts"; |
| 28 | import { deriveLatestThreadRun, deriveThreadRuntime } from "./threadExecution.ts"; |
| 29 | import { derivePendingThreadRequests } from "./threadRequests.ts"; |
| 30 | |
| 31 | const environmentId = EnvironmentId.make("environment-v2"); |
| 32 | |
| 33 | describe("scoped entity keys", () => { |
| 34 | it("preserves an invalid thread key as structured error data", () => { |
| 35 | const key = "missing-thread-key-separator"; |
| 36 | let error: unknown; |
| 37 | |
| 38 | try { |
| 39 | parseThreadKey(key); |
| 40 | } catch (cause) { |
| 41 | error = cause; |
| 42 | } |
| 43 | |
| 44 | expect(error).toEqual(new InvalidScopedThreadKeyError({ key })); |
| 45 | }); |
| 46 | |
| 47 | it("preserves malformed project reference collection input and its cause", () => { |
| 48 | const key = "not-json"; |
| 49 | let error: unknown; |
| 50 | |
| 51 | try { |
| 52 | parseProjectRefCollectionKey(key); |
| 53 | } catch (cause) { |
| 54 | error = cause; |
| 55 | } |
| 56 | |
| 57 | expect(error).toBeInstanceOf(InvalidScopedProjectRefCollectionKeyError); |
| 58 | expect(error).toMatchObject({ key, cause: expect.anything() }); |
| 59 | }); |
| 60 | |
| 61 | it("rejects invalid project reference collection shapes", () => { |
| 62 | const key = JSON.stringify([["environment-1"]]); |
| 63 | |
| 64 | expect(() => parseProjectRefCollectionKey(key)).toThrowError( |
| 65 | InvalidScopedProjectRefCollectionKeyError, |
| 66 | ); |
| 67 | }); |
| 68 | }); |
| 69 | |
| 70 | describe("V2 client presentation", () => { |
| 71 | it("presents shell timestamps and status without constructing V1 state", () => { |
| 72 | const shell = presentThreadShell(environmentId, v2ThreadShell); |
| 73 | expect(shell.environmentId).toBe(environmentId); |
| 74 | expect(shell.createdAt).toBe("2026-06-20T00:00:00.000Z"); |
| 75 | expect(shell.runtime).toBeNull(); |
| 76 | expect(shell.source).toBe(v2ThreadShell); |
| 77 | }); |
| 78 | |
| 79 | it("preserves active ordering and both pull-request sources", () => { |
| 80 | const linkedPullRequest = { |
| 81 | projectId: v2ThreadShell.projectId, |
| 82 | repository: "pingdotgg/t3code", |
| 83 | number: 42, |
| 84 | url: "https://github.com/pingdotgg/t3code/pull/42", |
| 85 | }; |
| 86 | const branchPullRequest = { ...linkedPullRequest, number: 43 }; |
| 87 | const shell = presentThreadShell(environmentId, { |
| 88 | ...v2ThreadShell, |
| 89 | activeOrderKey: "m", |
| 90 | linkedPullRequest, |
| 91 | branchPullRequest, |
| 92 | }); |
| 93 | |
| 94 | expect(shell.activeOrderKey).toBe("m"); |
| 95 | expect(shell.linkedPullRequest).toEqual(linkedPullRequest); |
| 96 | expect(shell.branchPullRequest).toEqual(branchPullRequest); |
| 97 | }); |
| 98 | |
| 99 | it("presents provider errors carried by failed thread shells", () => { |
| 100 | const runId = RunId.make("run-failed"); |
| 101 | const shell = presentThreadShell(environmentId, { |
| 102 | ...v2ThreadShell, |
| 103 | latestRunId: runId, |
| 104 | status: "failed", |
| 105 | lastError: "provider process exited", |
| 106 | }); |
| 107 | |
| 108 | expect(shell.runtime).toMatchObject({ |
| 109 | status: "failed", |
| 110 | lastError: "provider process exited", |
| 111 | }); |
| 112 | }); |
| 113 | |
| 114 | it("preserves shell run timestamps for sidebar activity clocks", () => { |
| 115 | const runId = RunId.make("run-working-clock"); |
| 116 | const requestedAt = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"); |
| 117 | const startedAt = DateTime.makeUnsafe("2026-06-20T01:00:02.000Z"); |
| 118 | const shell = presentThreadShell(environmentId, { |
| 119 | ...v2ThreadShell, |
| 120 | latestRunId: runId, |
| 121 | latestRunRequestedAt: requestedAt, |
| 122 | latestRunStartedAt: startedAt, |
| 123 | latestRunCompletedAt: null, |
| 124 | activeRunId: runId, |
| 125 | status: "running", |
| 126 | updatedAt: DateTime.makeUnsafe("2026-06-20T01:04:45.000Z"), |
| 127 | }); |
| 128 | |
| 129 | expect(shell.latestRun).toMatchObject({ |
| 130 | runId, |
| 131 | status: "running", |
| 132 | requestedAt: "2026-06-20T01:00:00.000Z", |
| 133 | startedAt: "2026-06-20T01:00:02.000Z", |
| 134 | completedAt: null, |
| 135 | }); |
| 136 | }); |
| 137 | |
| 138 | it("parks presented runtime at idle when a settled shell has pending background tasks", () => { |
| 139 | const runId = RunId.make("run-completed"); |
| 140 | const shell = presentThreadShell(environmentId, { |
| 141 | ...v2ThreadShell, |
| 142 | latestRunId: runId, |
| 143 | activeRunId: null, |
| 144 | status: "completed", |
| 145 | pendingBackgroundTasks: [{ taskId: "bg-1", description: "Watch build", kind: "monitor" }], |
| 146 | }); |
| 147 | |
| 148 | expect(shell.latestRun).toMatchObject({ runId, status: "completed" }); |
| 149 | expect(shell.runtime).toMatchObject({ |
| 150 | status: "idle", |
| 151 | activeRunId: null, |
| 152 | }); |
| 153 | expect(shell.pendingBackgroundTasks).toEqual([ |
| 154 | { taskId: "bg-1", description: "Watch build", kind: "monitor" }, |
| 155 | ]); |
| 156 | }); |
| 157 | |
| 158 | it("parks a thread that never ran at idle while a pull request watch holds it", () => { |
| 159 | const watch = { taskId: "pull-request-watch:github.com/acme/app#1", kind: "monitor" as const }; |
| 160 | const watched = presentThreadShell(environmentId, { |
| 161 | ...v2ThreadShell, |
| 162 | latestRunId: null, |
| 163 | activeProviderThreadId: null, |
| 164 | activeRunId: null, |
| 165 | status: "idle", |
| 166 | pendingBackgroundTasks: [watch], |
| 167 | }); |
| 168 | expect(watched.runtime).toMatchObject({ status: "idle", activeRunId: null }); |
| 169 | expect( |
| 170 | presentThreadShell(environmentId, { |
| 171 | ...v2ThreadShell, |
| 172 | latestRunId: null, |
| 173 | activeProviderThreadId: null, |
| 174 | status: "idle", |
| 175 | pendingBackgroundTasks: [], |
| 176 | }).runtime, |
| 177 | ).toBeNull(); |
| 178 | }); |
| 179 | |
| 180 | it.each([ |
| 181 | { kinds: ["command"], expected: "completed" }, |
| 182 | { kinds: ["command", "subagent"], expected: "idle" }, |
| 183 | { kinds: ["background_task"], expected: "idle" }, |
| 184 | ] as const)("presents a completed shell with $kinds as $expected", ({ kinds, expected }) => { |
| 185 | const pendingBackgroundTasks = kinds.map((kind, index) => ({ |
| 186 | taskId: `bg-${index}`, |
| 187 | kind, |
| 188 | })); |
| 189 | const shell = presentThreadShell(environmentId, { |
| 190 | ...v2ThreadShell, |
| 191 | latestRunId: RunId.make("run-completed"), |
| 192 | activeRunId: null, |
| 193 | status: "completed", |
| 194 | pendingBackgroundTasks, |
| 195 | }); |
| 196 | |
| 197 | expect(shell.latestRun?.status).toBe("completed"); |
| 198 | expect(shell.runtime).toMatchObject({ status: expected, activeRunId: null }); |
| 199 | expect(shell.pendingBackgroundTasks).toEqual(pendingBackgroundTasks); |
| 200 | }); |
| 201 | |
| 202 | it.each(["running", "waiting"] as const)( |
| 203 | "preserves shell %s while only commands remain in the roster", |
| 204 | (status) => { |
| 205 | const runId = RunId.make("run-command"); |
| 206 | const shell = presentThreadShell(environmentId, { |
| 207 | ...v2ThreadShell, |
| 208 | latestRunId: runId, |
| 209 | activeRunId: runId, |
| 210 | status, |
| 211 | pendingBackgroundTasks: [{ taskId: "dev-server", kind: "command" }], |
| 212 | }); |
| 213 | |
| 214 | expect(shell.latestRun?.status).toBe(status); |
| 215 | expect(shell.runtime).toMatchObject({ status, activeRunId: runId }); |
| 216 | expect(shell.pendingBackgroundTasks).toEqual([{ taskId: "dev-server", kind: "command" }]); |
| 217 | }, |
| 218 | ); |
| 219 | |
| 220 | it("keeps a failed latest run failed while background tasks are still pending", () => { |
| 221 | const shell = presentThreadShell(environmentId, { |
| 222 | ...v2ThreadShell, |
| 223 | latestRunId: RunId.make("run-failed"), |
| 224 | activeRunId: null, |
| 225 | status: "failed", |
| 226 | lastError: "Provider turn failed", |
| 227 | pendingBackgroundTasks: [{ taskId: "bg-1", description: "Watch build", kind: "monitor" }], |
| 228 | }); |
| 229 | |
| 230 | // Sidebar and mobile list read runtime "idle" as Waiting before failure. |
| 231 | expect(shell.runtime).toMatchObject({ status: "failed", lastError: "Provider turn failed" }); |
| 232 | expect(shell.pendingBackgroundTasks).toHaveLength(1); |
| 233 | }); |
| 234 | |
| 235 | it("stacks earlier provider owners behind the current one, newest history first to go", () => { |
| 236 | const codex = ProviderInstanceId.make("codex"); |
| 237 | const claude = ProviderInstanceId.make("claude"); |
| 238 | const cursor = ProviderInstanceId.make("cursor"); |
| 239 | const grok = ProviderInstanceId.make("grok"); |
| 240 | const shell = presentThreadShell(environmentId, { |
| 241 | ...v2ThreadShell, |
| 242 | providerInstanceId: grok, |
| 243 | modelSelection: { instanceId: grok, model: "grok-4" }, |
| 244 | providerInstanceHistory: [codex, claude, cursor, grok], |
| 245 | }); |
| 246 | |
| 247 | // Three slots: the two most recent earlier owners, then the current one. |
| 248 | expect(resolveThreadProviderStack(shell)).toEqual([claude, cursor, grok]); |
| 249 | expect( |
| 250 | resolveThreadProviderStack({ ...shell, providerInstanceHistory: [codex, grok] }), |
| 251 | ).toEqual([codex, grok]); |
| 252 | expect(resolveThreadProviderStack({ ...shell, providerInstanceHistory: [] })).toEqual([grok]); |
| 253 | // Servers that predate the field decode to an empty history. |
| 254 | expect( |
| 255 | presentThreadShell(environmentId, { ...v2ThreadShell, providerInstanceHistory: undefined }) |
| 256 | .providerInstanceHistory, |
| 257 | ).toEqual([]); |
| 258 | }); |
| 259 | |
| 260 | it("keeps terminal runtime completed when there are no pending background tasks", () => { |
| 261 | const runId = RunId.make("run-completed"); |
| 262 | const shell = presentThreadShell(environmentId, { |
| 263 | ...v2ThreadShell, |
| 264 | latestRunId: runId, |
| 265 | activeRunId: null, |
| 266 | status: "completed", |
| 267 | pendingBackgroundTasks: [], |
| 268 | }); |
| 269 | |
| 270 | expect(shell.latestRun).toMatchObject({ runId, status: "completed" }); |
| 271 | expect(shell.runtime).toMatchObject({ |
| 272 | status: "completed", |
| 273 | activeRunId: null, |
| 274 | }); |
| 275 | }); |
| 276 | |
| 277 | it("keeps runtime running when there is no background roster", () => { |
| 278 | const runId = RunId.make("run-running"); |
| 279 | const shell = presentThreadShell(environmentId, { |
| 280 | ...v2ThreadShell, |
| 281 | latestRunId: runId, |
| 282 | activeRunId: runId, |
| 283 | status: "running", |
| 284 | pendingBackgroundTasks: [], |
| 285 | }); |
| 286 | |
| 287 | expect(shell.latestRun).toMatchObject({ runId, status: "running" }); |
| 288 | expect(shell.runtime).toMatchObject({ |
| 289 | status: "running", |
| 290 | activeRunId: runId, |
| 291 | }); |
| 292 | }); |
| 293 | |
| 294 | it("keeps an older active run visible over a newer cancelled run", () => { |
| 295 | const activeRunId = RunId.make("run-active"); |
| 296 | const cancelledRunId = RunId.make("run-cancelled"); |
| 297 | const shell = presentThreadShell(environmentId, { |
| 298 | ...v2ThreadShell, |
| 299 | latestRunId: cancelledRunId, |
| 300 | latestRunStartedAt: null, |
| 301 | latestRunCompletedAt: DateTime.makeUnsafe("2026-06-20T01:05:00.000Z"), |
| 302 | activeRunId, |
| 303 | activityRunStatus: "running", |
| 304 | status: "cancelled", |
| 305 | pendingBackgroundTasks: [], |
| 306 | }); |
| 307 | |
| 308 | expect(shell.latestRun).toMatchObject({ runId: cancelledRunId, status: "cancelled" }); |
| 309 | expect(shell.runtime).toMatchObject({ |
| 310 | status: "running", |
| 311 | activeRunId, |
| 312 | }); |
| 313 | }); |
| 314 | |
| 315 | it("keeps an older waiting run visible over a newer cancelled run", () => { |
| 316 | const shell = presentThreadShell(environmentId, { |
| 317 | ...v2ThreadShell, |
| 318 | latestRunId: RunId.make("run-cancelled"), |
| 319 | activeRunId: null, |
| 320 | activityRunStatus: "waiting", |
| 321 | status: "cancelled", |
| 322 | pendingBackgroundTasks: [], |
| 323 | }); |
| 324 | |
| 325 | expect(shell.runtime).toMatchObject({ status: "waiting", activeRunId: null }); |
| 326 | }); |
| 327 | |
| 328 | it("keeps a post-settlement background roster ahead of activity status", () => { |
| 329 | const activeRunId = RunId.make("run-active"); |
| 330 | const shell = presentThreadShell(environmentId, { |
| 331 | ...v2ThreadShell, |
| 332 | latestRunId: RunId.make("run-cancelled"), |
| 333 | activeRunId, |
| 334 | activityRunStatus: "running", |
| 335 | status: "cancelled", |
| 336 | pendingBackgroundTasks: [ |
| 337 | { taskId: "bg-activity", description: "background work", kind: "background_task" }, |
| 338 | ], |
| 339 | }); |
| 340 | |
| 341 | expect(shell.runtime).toMatchObject({ status: "idle", activeRunId }); |
| 342 | }); |
| 343 | |
| 344 | it("parks runtime idle over stale shell running when the roster is nonempty", () => { |
| 345 | const runId = RunId.make("run-stale-running"); |
| 346 | const shell = presentThreadShell(environmentId, { |
| 347 | ...v2ThreadShell, |
| 348 | latestRunId: runId, |
| 349 | activeRunId: runId, |
| 350 | // Stale: server already projected a post-settlement roster, but shell |
| 351 | // status still says running (packaged orchestrator-v2 bug). |
| 352 | status: "running", |
| 353 | pendingBackgroundTasks: [{ taskId: "bg-1", description: "Watch build", kind: "monitor" }], |
| 354 | }); |
| 355 | |
| 356 | expect(shell.latestRun).toMatchObject({ runId, status: "running" }); |
| 357 | expect(shell.runtime).toMatchObject({ |
| 358 | status: "idle", |
| 359 | activeRunId: runId, |
| 360 | }); |
| 361 | expect(shell.pendingBackgroundTasks).toEqual([ |
| 362 | { taskId: "bg-1", description: "Watch build", kind: "monitor" }, |
| 363 | ]); |
| 364 | }); |
| 365 | |
| 366 | it("parks runtime idle over stale checkpoint waiting when the roster is nonempty", () => { |
| 367 | const runId = RunId.make("run-stale-waiting"); |
| 368 | const shell = presentThreadShell(environmentId, { |
| 369 | ...v2ThreadShell, |
| 370 | latestRunId: runId, |
| 371 | activeRunId: runId, |
| 372 | // Stale: checkpoint-oriented waiting masks post-settlement background work. |
| 373 | status: "waiting", |
| 374 | pendingBackgroundTasks: [{ taskId: "bg-2", description: "Watch build", kind: "monitor" }], |
| 375 | }); |
| 376 | |
| 377 | expect(shell.latestRun).toMatchObject({ runId, status: "waiting" }); |
| 378 | expect(shell.runtime).toMatchObject({ |
| 379 | status: "idle", |
| 380 | activeRunId: runId, |
| 381 | }); |
| 382 | expect(shell.pendingBackgroundTasks).toEqual([ |
| 383 | { taskId: "bg-2", description: "Watch build", kind: "monitor" }, |
| 384 | ]); |
| 385 | }); |
| 386 | |
| 387 | it("derives execution summaries without wrapping or copying the projection", () => { |
| 388 | const runId = RunId.make("run-1"); |
| 389 | const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"); |
| 390 | const projection = { |
| 391 | ...v2Projection, |
| 392 | runs: [ |
| 393 | { |
| 394 | id: runId, |
| 395 | threadId: v2Projection.thread.id, |
| 396 | ordinal: 1, |
| 397 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 398 | modelSelection: v2Projection.thread.modelSelection, |
| 399 | providerThreadId: null, |
| 400 | userMessageId: MessageId.make("message-user"), |
| 401 | rootNodeId: null, |
| 402 | activeAttemptId: null, |
| 403 | status: "running" as const, |
| 404 | requestedAt: now, |
| 405 | startedAt: now, |
| 406 | completedAt: null, |
| 407 | checkpointId: null, |
| 408 | contextHandoffId: null, |
| 409 | }, |
| 410 | ], |
| 411 | messages: [ |
| 412 | { |
| 413 | id: MessageId.make("message-user"), |
| 414 | threadId: v2Projection.thread.id, |
| 415 | runId, |
| 416 | nodeId: null, |
| 417 | role: "user" as const, |
| 418 | text: "Hello", |
| 419 | attachments: [], |
| 420 | streaming: false, |
| 421 | createdBy: "user" as const, |
| 422 | creationSource: "web" as const, |
| 423 | createdAt: now, |
| 424 | updatedAt: now, |
| 425 | }, |
| 426 | ], |
| 427 | updatedAt: now, |
| 428 | }; |
| 429 | |
| 430 | expect(deriveLatestThreadRun(projection)).toMatchObject({ |
| 431 | runId, |
| 432 | status: "running", |
| 433 | requestedAt: "2026-06-20T01:00:00.000Z", |
| 434 | assistantMessageId: null, |
| 435 | }); |
| 436 | expect(deriveThreadRuntime(projection)).toMatchObject({ |
| 437 | status: "running", |
| 438 | activeRunId: runId, |
| 439 | providerInstanceId: projection.thread.providerInstanceId, |
| 440 | }); |
| 441 | for (const status of ["queued", "cancelled"] as const) { |
| 442 | const later = DateTime.add(now, { hours: 1 }); |
| 443 | const latest = { |
| 444 | ...projection.runs[0]!, |
| 445 | id: RunId.make("newer-run"), |
| 446 | ordinal: 2, |
| 447 | status, |
| 448 | requestedAt: later, |
| 449 | startedAt: null, |
| 450 | completedAt: status === "cancelled" ? later : null, |
| 451 | }; |
| 452 | const detail = { ...projection, runs: [...projection.runs, latest], updatedAt: later }; |
| 453 | const shell = presentThreadShell(environmentId, { |
| 454 | ...v2ThreadShell, |
| 455 | latestRunId: latest.id, |
| 456 | latestRunStartedAt: null, |
| 457 | latestRunRequestedAt: later, |
| 458 | latestRunCompletedAt: latest.completedAt, |
| 459 | status, |
| 460 | activeRunId: runId, |
| 461 | activityRunStatus: "running", |
| 462 | activityRunStartedAt: now, |
| 463 | updatedAt: later, |
| 464 | }); |
| 465 | expect(resolveThreadWorkingStartedAt(shell)).toBe(DateTime.formatIso(now)); |
| 466 | expect( |
| 467 | resolveThreadWorkingStartedAt({ |
| 468 | latestRun: deriveLatestThreadRun(detail), |
| 469 | runtime: deriveThreadRuntime(detail), |
| 470 | }), |
| 471 | ).toBe(resolveThreadWorkingStartedAt(shell)); |
| 472 | const stopped = { |
| 473 | ...detail, |
| 474 | runs: detail.runs.map((run) => ({ |
| 475 | ...run, |
| 476 | status: "completed" as const, |
| 477 | completedAt: later, |
| 478 | })), |
| 479 | }; |
| 480 | expect( |
| 481 | resolveThreadWorkingStartedAt({ |
| 482 | latestRun: deriveLatestThreadRun(stopped), |
| 483 | runtime: deriveThreadRuntime(stopped), |
| 484 | }), |
| 485 | ).toBeNull(); |
| 486 | } |
| 487 | }); |
| 488 | |
| 489 | it("parks waiting runtime for a post-settlement roster without hiding active running work", () => { |
| 490 | const runId = RunId.make("run-background-presentation"); |
| 491 | const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"); |
| 492 | const run = { |
| 493 | id: runId, |
| 494 | threadId: v2Projection.thread.id, |
| 495 | ordinal: 1, |
| 496 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 497 | modelSelection: v2Projection.thread.modelSelection, |
| 498 | providerThreadId: null, |
| 499 | userMessageId: MessageId.make("message-background-presentation"), |
| 500 | rootNodeId: null, |
| 501 | activeAttemptId: null, |
| 502 | status: "waiting" as const, |
| 503 | requestedAt: now, |
| 504 | startedAt: now, |
| 505 | completedAt: null, |
| 506 | checkpointId: null, |
| 507 | contextHandoffId: null, |
| 508 | }; |
| 509 | const backgroundItem = { |
| 510 | id: TurnItemId.make("item-background-subagent"), |
| 511 | threadId: v2Projection.thread.id, |
| 512 | runId, |
| 513 | nodeId: null, |
| 514 | providerThreadId: null, |
| 515 | providerTurnId: null, |
| 516 | nativeItemRef: null, |
| 517 | parentItemId: null, |
| 518 | ordinal: 0, |
| 519 | status: "running" as const, |
| 520 | title: "Background review", |
| 521 | startedAt: now, |
| 522 | completedAt: null, |
| 523 | updatedAt: now, |
| 524 | type: "subagent" as const, |
| 525 | subagentId: NodeId.make("subagent-review"), |
| 526 | origin: "provider_native" as const, |
| 527 | driver: ProviderDriverKind.make("codex"), |
| 528 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 529 | childThreadId: null, |
| 530 | prompt: "Review the changes", |
| 531 | result: null, |
| 532 | }; |
| 533 | |
| 534 | expect( |
| 535 | deriveThreadRuntime({ |
| 536 | ...v2Projection, |
| 537 | runs: [run], |
| 538 | turnItems: [backgroundItem], |
| 539 | }), |
| 540 | ).toMatchObject({ |
| 541 | status: "idle", |
| 542 | activeRunId: null, |
| 543 | }); |
| 544 | expect( |
| 545 | deriveThreadRuntime({ |
| 546 | ...v2Projection, |
| 547 | runs: [{ ...run, status: "running" as const }], |
| 548 | turnItems: [backgroundItem], |
| 549 | }), |
| 550 | ).toMatchObject({ |
| 551 | status: "running", |
| 552 | activeRunId: runId, |
| 553 | }); |
| 554 | expect( |
| 555 | deriveThreadRuntime({ |
| 556 | ...v2Projection, |
| 557 | runs: [{ ...run, status: "failed" as const, completedAt: now }], |
| 558 | turnItems: [backgroundItem], |
| 559 | }), |
| 560 | ).toMatchObject({ status: "failed", activeRunId: null }); |
| 561 | const commandItem = { |
| 562 | ...backgroundItem, |
| 563 | id: TurnItemId.make("item-background-command"), |
| 564 | type: "command_execution" as const, |
| 565 | input: "npm run dev", |
| 566 | }; |
| 567 | expect( |
| 568 | deriveThreadRuntime({ ...v2Projection, runs: [run], turnItems: [commandItem] }), |
| 569 | ).toMatchObject({ status: "waiting", activeRunId: null }); |
| 570 | |
| 571 | for (const [turnItems, status] of [ |
| 572 | [[commandItem], "completed"], |
| 573 | [[backgroundItem], "idle"], |
| 574 | [[commandItem, backgroundItem], "idle"], |
| 575 | ] as const) { |
| 576 | expect( |
| 577 | deriveThreadRuntime({ |
| 578 | ...v2Projection, |
| 579 | runs: [{ ...run, status: "completed", completedAt: now }], |
| 580 | turnItems, |
| 581 | }), |
| 582 | ).toMatchObject({ status, activeRunId: null }); |
| 583 | } |
| 584 | |
| 585 | for (const kind of ["monitor", "background_task"] as const) { |
| 586 | const providerThread = { |
| 587 | id: ProviderThreadId.make("provider-thread-background"), |
| 588 | driver: ProviderDriverKind.make("claudeCode"), |
| 589 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 590 | providerSessionId: null, |
| 591 | appThreadId: v2Projection.thread.id, |
| 592 | ownerNodeId: null, |
| 593 | nativeThreadRef: null, |
| 594 | nativeConversationHeadRef: null, |
| 595 | status: "idle" as const, |
| 596 | firstRunOrdinal: 1, |
| 597 | lastRunOrdinal: 1, |
| 598 | handoffIds: [], |
| 599 | forkedFrom: null, |
| 600 | pendingBackgroundTasks: [{ taskId: "background-task", kind }], |
| 601 | createdAt: now, |
| 602 | updatedAt: now, |
| 603 | }; |
| 604 | for (const status of ["completed", "failed"] as const) { |
| 605 | expect( |
| 606 | deriveThreadRuntime({ |
| 607 | ...v2Projection, |
| 608 | runs: [{ ...run, status, completedAt: now }], |
| 609 | providerThreads: [providerThread], |
| 610 | }), |
| 611 | ).toMatchObject({ status: status === "failed" ? "failed" : "idle", activeRunId: null }); |
| 612 | } |
| 613 | } |
| 614 | }); |
| 615 | |
| 616 | it("joins pending request entities to their native turn-item display data", () => { |
| 617 | const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"); |
| 618 | const requestId = RuntimeRequestId.make("request-approval"); |
| 619 | const item = { |
| 620 | id: TurnItemId.make("item-approval"), |
| 621 | threadId: v2Projection.thread.id, |
| 622 | runId: null, |
| 623 | nodeId: NodeId.make("node-root"), |
| 624 | providerThreadId: null, |
| 625 | providerTurnId: null, |
| 626 | nativeItemRef: null, |
| 627 | parentItemId: null, |
| 628 | ordinal: 0, |
| 629 | status: "completed" as const, |
| 630 | title: null, |
| 631 | startedAt: now, |
| 632 | completedAt: now, |
| 633 | updatedAt: now, |
| 634 | type: "approval_request" as const, |
| 635 | requestId, |
| 636 | requestKind: "command" as const, |
| 637 | prompt: "Allow command?", |
| 638 | }; |
| 639 | const projection = { |
| 640 | ...v2Projection, |
| 641 | runtimeRequests: [ |
| 642 | { |
| 643 | id: requestId, |
| 644 | nodeId: NodeId.make("node-root"), |
| 645 | providerTurnId: null, |
| 646 | nativeRequestRef: null, |
| 647 | kind: "command" as const, |
| 648 | status: "pending" as const, |
| 649 | responseCapability: { |
| 650 | type: "not_resumable" as const, |
| 651 | reason: "provider disconnected", |
| 652 | }, |
| 653 | createdAt: now, |
| 654 | resolvedAt: null, |
| 655 | }, |
| 656 | ], |
| 657 | turnItems: [item], |
| 658 | updatedAt: now, |
| 659 | }; |
| 660 | |
| 661 | expect(derivePendingThreadRequests(projection).approvals).toEqual([ |
| 662 | { |
| 663 | requestId, |
| 664 | requestKind: "command", |
| 665 | createdAt: "2026-06-20T01:00:00.000Z", |
| 666 | detail: "Allow command?", |
| 667 | responseCapability: "not_resumable", |
| 668 | }, |
| 669 | ]); |
| 670 | expect(derivePendingThreadRequests(projection).userInputs).toEqual([]); |
| 671 | }); |
| 672 | |
| 673 | it("preserves structured-question selection mode and defaults legacy payloads", () => { |
| 674 | const now = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"); |
| 675 | const multiRequestId = RuntimeRequestId.make("request-multi-select"); |
| 676 | const legacyRequestId = RuntimeRequestId.make("request-legacy-single-select"); |
| 677 | const runtimeRequests = [multiRequestId, legacyRequestId].map((id) => ({ |
| 678 | id, |
| 679 | nodeId: NodeId.make(`node-${id}`), |
| 680 | providerTurnId: null, |
| 681 | nativeRequestRef: null, |
| 682 | kind: "user_input" as const, |
| 683 | status: "pending" as const, |
| 684 | responseCapability: { |
| 685 | type: "live" as const, |
| 686 | providerSessionId: ProviderSessionId.make("provider-session-questions"), |
| 687 | }, |
| 688 | createdAt: now, |
| 689 | resolvedAt: null, |
| 690 | })); |
| 691 | const common = { |
| 692 | threadId: v2Projection.thread.id, |
| 693 | runId: null, |
| 694 | providerThreadId: null, |
| 695 | providerTurnId: null, |
| 696 | nativeItemRef: null, |
| 697 | parentItemId: null, |
| 698 | ordinal: 0, |
| 699 | status: "completed" as const, |
| 700 | title: null, |
| 701 | startedAt: now, |
| 702 | completedAt: now, |
| 703 | updatedAt: now, |
| 704 | type: "user_input_request" as const, |
| 705 | }; |
| 706 | const projection = { |
| 707 | ...v2Projection, |
| 708 | runtimeRequests, |
| 709 | turnItems: [ |
| 710 | { |
| 711 | ...common, |
| 712 | id: TurnItemId.make("item-multi-select"), |
| 713 | nodeId: NodeId.make("node-request-multi-select"), |
| 714 | requestId: multiRequestId, |
| 715 | questions: [ |
| 716 | { |
| 717 | id: "regions", |
| 718 | header: "Regions", |
| 719 | question: "Which regions?", |
| 720 | options: [ |
| 721 | { label: "US", description: "United States" }, |
| 722 | { label: "EU", description: "European Union" }, |
| 723 | ], |
| 724 | multiSelect: true, |
| 725 | }, |
| 726 | ], |
| 727 | }, |
| 728 | { |
| 729 | ...common, |
| 730 | id: TurnItemId.make("item-legacy-single-select"), |
| 731 | nodeId: NodeId.make("node-request-legacy-single-select"), |
| 732 | requestId: legacyRequestId, |
| 733 | questions: [ |
| 734 | { |
| 735 | id: "target", |
| 736 | header: "Target", |
| 737 | question: "Which target?", |
| 738 | options: [{ label: "Staging", description: "Staging environment" }], |
| 739 | }, |
| 740 | ], |
| 741 | }, |
| 742 | ], |
| 743 | updatedAt: now, |
| 744 | }; |
| 745 | |
| 746 | expect( |
| 747 | derivePendingThreadRequests(projection).userInputs.map((input) => ({ |
| 748 | requestId: input.requestId, |
| 749 | multiSelect: input.questions[0]?.multiSelect, |
| 750 | })), |
| 751 | ).toEqual([ |
| 752 | { requestId: multiRequestId, multiSelect: true }, |
| 753 | { requestId: legacyRequestId, multiSelect: false }, |
| 754 | ]); |
| 755 | }); |
| 756 | }); |