| 1 | import { |
| 2 | CheckpointId, |
| 3 | CheckpointRef, |
| 4 | CheckpointScopeId, |
| 5 | CommandId, |
| 6 | EnvironmentId, |
| 7 | MessageId, |
| 8 | NodeId, |
| 9 | ORCHESTRATION_V2_WS_METHODS, |
| 10 | PlanId, |
| 11 | ProjectId, |
| 12 | ProviderInstanceId, |
| 13 | ProviderDriverKind, |
| 14 | RunId, |
| 15 | RuntimeRequestId, |
| 16 | ThreadId, |
| 17 | TurnItemId, |
| 18 | WS_METHODS, |
| 19 | type OrchestrationV2Command, |
| 20 | type OrchestrationV2ThreadLaunchInput, |
| 21 | type OrchestrationV2ThreadProjection, |
| 22 | type ProjectMutation, |
| 23 | } from "@t3tools/contracts"; |
| 24 | import { describe, expect, it } from "@effect/vitest"; |
| 25 | import * as Crypto from "effect/Crypto"; |
| 26 | import * as Effect from "effect/Effect"; |
| 27 | import * as Layer from "effect/Layer"; |
| 28 | import * as Option from "effect/Option"; |
| 29 | import * as SubscriptionRef from "effect/SubscriptionRef"; |
| 30 | |
| 31 | import { |
| 32 | AVAILABLE_CONNECTION_STATE, |
| 33 | PrimaryConnectionTarget, |
| 34 | type PreparedConnection, |
| 35 | } from "../connection/model.ts"; |
| 36 | import * as EnvironmentSupervisor from "../connection/supervisor.ts"; |
| 37 | import * as RpcSession from "../rpc/session.ts"; |
| 38 | import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; |
| 39 | import { v2Now, v2Projection, v2ThreadId } from "../state/orchestrationV2TestFixtures.ts"; |
| 40 | import { |
| 41 | archiveThread, |
| 42 | cancelQueuedRun, |
| 43 | createProject, |
| 44 | dismissThreadUserInput, |
| 45 | editQueuedRun, |
| 46 | forkThreadFromRun, |
| 47 | interruptThreadTurn, |
| 48 | mergeThreadBack, |
| 49 | promoteQueuedRun, |
| 50 | reorderActiveThread, |
| 51 | reorderQueuedRun, |
| 52 | revertThreadCheckpoint, |
| 53 | settleThread, |
| 54 | startThreadTurn, |
| 55 | unsettleThread, |
| 56 | updateProject, |
| 57 | updateThreadMetadata, |
| 58 | } from "./commands.ts"; |
| 59 | |
| 60 | const layerTestCrypto = Layer.succeed( |
| 61 | Crypto.Crypto, |
| 62 | Crypto.make({ |
| 63 | randomBytes: (size) => new Uint8Array(size), |
| 64 | digest: (_algorithm, data) => Effect.succeed(data), |
| 65 | }), |
| 66 | ); |
| 67 | |
| 68 | const TARGET = new PrimaryConnectionTarget({ |
| 69 | environmentId: EnvironmentId.make("environment-1"), |
| 70 | label: "Test environment", |
| 71 | httpBaseUrl: "https://environment.example.test", |
| 72 | wsBaseUrl: "wss://environment.example.test", |
| 73 | }); |
| 74 | |
| 75 | const makeSupervisor = Effect.fn("TestEnvironmentCommands.makeSupervisor")(function* (input: { |
| 76 | readonly commands: OrchestrationV2Command[]; |
| 77 | readonly projects: ProjectMutation[]; |
| 78 | readonly launches?: OrchestrationV2ThreadLaunchInput[]; |
| 79 | readonly projection?: OrchestrationV2ThreadProjection; |
| 80 | readonly projectionRequests?: ThreadId[]; |
| 81 | readonly advertiseServerResolvedCommandContext?: boolean; |
| 82 | }) { |
| 83 | const client = { |
| 84 | [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: (command: OrchestrationV2Command) => |
| 85 | Effect.sync(() => { |
| 86 | input.commands.push(command); |
| 87 | return { sequence: input.commands.length }; |
| 88 | }), |
| 89 | [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: (requestInput: { |
| 90 | readonly threadId: ThreadId; |
| 91 | }) => |
| 92 | Effect.sync(() => { |
| 93 | input.projectionRequests?.push(requestInput.threadId); |
| 94 | return input.projection ?? v2Projection; |
| 95 | }), |
| 96 | [ORCHESTRATION_V2_WS_METHODS.launchThread]: (launchInput: OrchestrationV2ThreadLaunchInput) => |
| 97 | Effect.sync(() => { |
| 98 | input.launches?.push(launchInput); |
| 99 | return { |
| 100 | threadId: launchInput.threadId ?? v2ThreadId, |
| 101 | projection: input.projection ?? v2Projection, |
| 102 | resumed: false, |
| 103 | }; |
| 104 | }), |
| 105 | [WS_METHODS.projectsMutate]: (mutation: ProjectMutation) => |
| 106 | Effect.sync(() => { |
| 107 | input.projects.push(mutation); |
| 108 | return { |
| 109 | id: mutation.projectId, |
| 110 | title: mutation.type === "project.create" ? mutation.title : "Project", |
| 111 | workspaceRoot: |
| 112 | mutation.type === "project.create" ? mutation.workspaceRoot : "/workspace/project", |
| 113 | repositoryIdentity: null, |
| 114 | faviconPath: null, |
| 115 | defaultModelSelection: null, |
| 116 | scripts: [], |
| 117 | createdAt: "2026-06-06T00:00:00.000Z", |
| 118 | updatedAt: "2026-06-06T00:00:00.000Z", |
| 119 | deletedAt: null, |
| 120 | }; |
| 121 | }), |
| 122 | } as unknown as WsRpcProtocolClient; |
| 123 | const session: RpcSession.RpcSession = { |
| 124 | client, |
| 125 | initialConfig: Effect.succeed({ |
| 126 | environment: { |
| 127 | capabilities: { |
| 128 | repositoryIdentity: true, |
| 129 | ...(input.advertiseServerResolvedCommandContext === false |
| 130 | ? {} |
| 131 | : { serverResolvedCommandContext: true }), |
| 132 | }, |
| 133 | }, |
| 134 | } as never), |
| 135 | subscribeServerConfig: (input) => client.subscribeServerConfig(input), |
| 136 | ready: Effect.void, |
| 137 | probe: Effect.void, |
| 138 | closed: Effect.never, |
| 139 | }; |
| 140 | return EnvironmentSupervisor.EnvironmentSupervisor.of({ |
| 141 | target: TARGET, |
| 142 | state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), |
| 143 | session: yield* SubscriptionRef.make(Option.some(session)), |
| 144 | prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()), |
| 145 | connect: Effect.void, |
| 146 | disconnect: Effect.void, |
| 147 | retryNow: Effect.void, |
| 148 | } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); |
| 149 | }); |
| 150 | |
| 151 | describe("V2 environment commands", () => { |
| 152 | it.effect("routes projects through the event-sourced project transport", () => |
| 153 | Effect.gen(function* () { |
| 154 | const projects: ProjectMutation[] = []; |
| 155 | const supervisor = yield* makeSupervisor({ commands: [], projects }); |
| 156 | |
| 157 | yield* createProject({ |
| 158 | projectId: ProjectId.make("project-1"), |
| 159 | title: "Project", |
| 160 | workspaceRoot: "/workspace/project", |
| 161 | createWorkspaceRootIfMissing: true, |
| 162 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 163 | |
| 164 | expect(projects).toEqual([ |
| 165 | { |
| 166 | type: "project.create", |
| 167 | commandId: "00000000-0000-4000-8000-000000000000", |
| 168 | projectId: "project-1", |
| 169 | title: "Project", |
| 170 | workspaceRoot: "/workspace/project", |
| 171 | createWorkspaceRootIfMissing: true, |
| 172 | }, |
| 173 | ]); |
| 174 | }).pipe(Effect.provide(layerTestCrypto)), |
| 175 | ); |
| 176 | |
| 177 | it.effect("persists and clears project presentation and environment settings", () => |
| 178 | Effect.gen(function* () { |
| 179 | const projects: ProjectMutation[] = []; |
| 180 | const supervisor = yield* makeSupervisor({ commands: [], projects }); |
| 181 | const projectId = ProjectId.make("project-1"); |
| 182 | const projectIcon = { kind: "emoji", emoji: "🌲" } as const; |
| 183 | yield* updateProject({ |
| 184 | projectId, |
| 185 | autoPull: true, |
| 186 | projectIcon, |
| 187 | faviconPath: "/workspace/project/icon.png", |
| 188 | defaultThreadEnvMode: "worktree", |
| 189 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 190 | yield* updateProject({ |
| 191 | projectId, |
| 192 | autoPull: false, |
| 193 | projectIcon: null, |
| 194 | faviconPath: null, |
| 195 | defaultThreadEnvMode: null, |
| 196 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 197 | |
| 198 | expect(projects).toEqual([ |
| 199 | { |
| 200 | type: "project.update", |
| 201 | commandId: "00000000-0000-4000-8000-000000000000", |
| 202 | projectId, |
| 203 | autoPull: true, |
| 204 | projectIcon, |
| 205 | faviconPath: "/workspace/project/icon.png", |
| 206 | defaultThreadEnvMode: "worktree", |
| 207 | }, |
| 208 | { |
| 209 | type: "project.update", |
| 210 | commandId: "00000000-0000-4000-8000-000000000000", |
| 211 | projectId, |
| 212 | autoPull: false, |
| 213 | projectIcon: null, |
| 214 | faviconPath: null, |
| 215 | defaultThreadEnvMode: null, |
| 216 | }, |
| 217 | ]); |
| 218 | }).pipe(Effect.provide(layerTestCrypto)), |
| 219 | ); |
| 220 | |
| 221 | it.effect("preserves caller command ids for idempotent V2 commands", () => |
| 222 | Effect.gen(function* () { |
| 223 | const commands: OrchestrationV2Command[] = []; |
| 224 | const supervisor = yield* makeSupervisor({ commands, projects: [] }); |
| 225 | |
| 226 | yield* archiveThread({ |
| 227 | commandId: CommandId.make("queued-command"), |
| 228 | threadId: ThreadId.make("thread-1"), |
| 229 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 230 | |
| 231 | expect(commands).toEqual([ |
| 232 | { type: "thread.archive", commandId: "queued-command", threadId: "thread-1" }, |
| 233 | ]); |
| 234 | }).pipe(Effect.provide(layerTestCrypto)), |
| 235 | ); |
| 236 | |
| 237 | it.effect("preserves the settled shelf snapshot guard when archiving", () => |
| 238 | Effect.gen(function* () { |
| 239 | const commands: OrchestrationV2Command[] = []; |
| 240 | const supervisor = yield* makeSupervisor({ commands, projects: [] }); |
| 241 | yield* archiveThread({ |
| 242 | commandId: CommandId.make("guarded-archive"), |
| 243 | threadId: ThreadId.make("settled-thread"), |
| 244 | snapshotSequence: 137, |
| 245 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 246 | expect(commands).toEqual([ |
| 247 | { |
| 248 | type: "thread.archive", |
| 249 | commandId: "guarded-archive", |
| 250 | threadId: "settled-thread", |
| 251 | snapshotSequence: 137, |
| 252 | }, |
| 253 | ]); |
| 254 | }).pipe(Effect.provide(layerTestCrypto)), |
| 255 | ); |
| 256 | |
| 257 | it.effect("resolves run ordinal zero to the persisted thread-start checkpoint", () => |
| 258 | Effect.gen(function* () { |
| 259 | const scopeId = CheckpointScopeId.make("checkpoint-scope-root"); |
| 260 | const checkpointId = CheckpointId.make("checkpoint-thread-start"); |
| 261 | const projection: OrchestrationV2ThreadProjection = { |
| 262 | ...v2Projection, |
| 263 | checkpoints: [ |
| 264 | { |
| 265 | id: checkpointId, |
| 266 | threadId: v2ThreadId, |
| 267 | scopeId, |
| 268 | runId: null, |
| 269 | nodeId: NodeId.make("node-run-1"), |
| 270 | parentCheckpointId: null, |
| 271 | ordinalWithinScope: 0, |
| 272 | appRunOrdinal: null, |
| 273 | ref: CheckpointRef.make("refs/t3/thread-start"), |
| 274 | status: "ready", |
| 275 | files: [], |
| 276 | capturedAt: v2Now, |
| 277 | }, |
| 278 | ], |
| 279 | }; |
| 280 | const commands: OrchestrationV2Command[] = []; |
| 281 | const supervisor = yield* makeSupervisor({ commands, projects: [], projection }); |
| 282 | |
| 283 | yield* revertThreadCheckpoint({ |
| 284 | commandId: CommandId.make("rollback-thread-start"), |
| 285 | threadId: v2ThreadId, |
| 286 | turnCount: 0, |
| 287 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 288 | |
| 289 | expect(commands).toEqual([ |
| 290 | { |
| 291 | type: "checkpoint.rollback", |
| 292 | commandId: "rollback-thread-start", |
| 293 | threadId: v2ThreadId, |
| 294 | scopeId, |
| 295 | checkpointId, |
| 296 | }, |
| 297 | ]); |
| 298 | }).pipe(Effect.provide(layerTestCrypto)), |
| 299 | ); |
| 300 | |
| 301 | it.effect("preserves plan implementation provenance on V2 runs", () => |
| 302 | Effect.gen(function* () { |
| 303 | const commands: OrchestrationV2Command[] = []; |
| 304 | const supervisor = yield* makeSupervisor({ commands, projects: [] }); |
| 305 | |
| 306 | yield* startThreadTurn({ |
| 307 | commandId: CommandId.make("implement-plan"), |
| 308 | threadId: v2ThreadId, |
| 309 | message: { |
| 310 | messageId: MessageId.make("message-implementation"), |
| 311 | role: "user", |
| 312 | text: "Implement the plan", |
| 313 | attachments: [], |
| 314 | }, |
| 315 | modelSelection: { |
| 316 | instanceId: ProviderInstanceId.make("codex"), |
| 317 | model: "gpt-5.4", |
| 318 | }, |
| 319 | runtimeMode: "full-access", |
| 320 | interactionMode: "default", |
| 321 | titleSeed: "Implement the plan", |
| 322 | sourceProposedPlan: { |
| 323 | threadId: ThreadId.make("thread-plan"), |
| 324 | planId: PlanId.make("plan-1"), |
| 325 | }, |
| 326 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 327 | |
| 328 | expect(commands).toHaveLength(1); |
| 329 | expect(commands[0]).toMatchObject({ |
| 330 | type: "message.dispatch", |
| 331 | commandId: "implement-plan", |
| 332 | threadId: v2ThreadId, |
| 333 | titleSeed: "Implement the plan", |
| 334 | sourcePlanRef: { threadId: "thread-plan", planId: "plan-1" }, |
| 335 | deliveryIntent: "auto", |
| 336 | dispatchMode: { type: "start_immediately" }, |
| 337 | }); |
| 338 | }).pipe(Effect.provide(layerTestCrypto)), |
| 339 | ); |
| 340 | |
| 341 | it.effect("preserves an existing worktree and branch during first-message launch", () => |
| 342 | Effect.gen(function* () { |
| 343 | const launches: OrchestrationV2ThreadLaunchInput[] = []; |
| 344 | const supervisor = yield* makeSupervisor({ commands: [], projects: [], launches }); |
| 345 | |
| 346 | yield* startThreadTurn({ |
| 347 | commandId: CommandId.make("launch-existing-worktree"), |
| 348 | threadId: v2ThreadId, |
| 349 | message: { |
| 350 | messageId: MessageId.make("message-existing-worktree"), |
| 351 | role: "user", |
| 352 | text: "Continue here", |
| 353 | attachments: [], |
| 354 | }, |
| 355 | runtimeMode: "full-access", |
| 356 | interactionMode: "default", |
| 357 | titleSeed: "Continue here", |
| 358 | bootstrap: { |
| 359 | createThread: { |
| 360 | projectId: ProjectId.make("project-1"), |
| 361 | title: "Thread", |
| 362 | modelSelection: v2Projection.thread.modelSelection, |
| 363 | runtimeMode: "full-access", |
| 364 | interactionMode: "default", |
| 365 | branch: "feature", |
| 366 | worktreePath: "/workspace/project-worktrees/feature", |
| 367 | createdAt: "2026-06-20T00:00:00.000Z", |
| 368 | }, |
| 369 | }, |
| 370 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 371 | |
| 372 | expect(launches[0]).toMatchObject({ |
| 373 | threadId: v2ThreadId, |
| 374 | title: "Continue here", |
| 375 | generateTitle: true, |
| 376 | workspaceStrategy: { |
| 377 | type: "existing_worktree", |
| 378 | worktreePath: "/workspace/project-worktrees/feature", |
| 379 | branch: "feature", |
| 380 | }, |
| 381 | }); |
| 382 | }).pipe(Effect.provide(layerTestCrypto)), |
| 383 | ); |
| 384 | |
| 385 | it.effect("provisions an origin-based worktree for an existing empty thread", () => |
| 386 | Effect.gen(function* () { |
| 387 | const launches: OrchestrationV2ThreadLaunchInput[] = []; |
| 388 | const supervisor = yield* makeSupervisor({ commands: [], projects: [], launches }); |
| 389 | |
| 390 | yield* startThreadTurn({ |
| 391 | commandId: CommandId.make("launch-existing-thread-worktree"), |
| 392 | threadId: v2ThreadId, |
| 393 | message: { |
| 394 | messageId: MessageId.make("message-existing-thread-worktree"), |
| 395 | role: "user", |
| 396 | text: "Move to a worktree", |
| 397 | attachments: [], |
| 398 | }, |
| 399 | runtimeMode: "full-access", |
| 400 | interactionMode: "default", |
| 401 | bootstrap: { |
| 402 | prepareWorktree: { |
| 403 | projectCwd: "/workspace/project", |
| 404 | baseBranch: "main", |
| 405 | branch: "feature", |
| 406 | startFromOrigin: true, |
| 407 | }, |
| 408 | }, |
| 409 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 410 | |
| 411 | expect(launches[0]).toMatchObject({ |
| 412 | threadId: v2ThreadId, |
| 413 | reuseExistingThread: true, |
| 414 | projectId: v2Projection.thread.projectId, |
| 415 | workspaceStrategy: { |
| 416 | type: "worktree", |
| 417 | baseRef: "main", |
| 418 | branch: "feature", |
| 419 | startFromOrigin: true, |
| 420 | }, |
| 421 | }); |
| 422 | }).pipe(Effect.provide(layerTestCrypto)), |
| 423 | ); |
| 424 | |
| 425 | it.effect("uses server-resolved delivery intent without fetching the full projection", () => |
| 426 | Effect.gen(function* () { |
| 427 | const commands: OrchestrationV2Command[] = []; |
| 428 | const projectionRequests: ThreadId[] = []; |
| 429 | const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests }); |
| 430 | |
| 431 | for (const [mode, expectedType] of [ |
| 432 | ["queue", "queue_after_active"], |
| 433 | ["auto", "start_immediately"], |
| 434 | ["steer", "start_immediately"], |
| 435 | ["restart", "start_immediately"], |
| 436 | ] as const) { |
| 437 | yield* startThreadTurn({ |
| 438 | commandId: CommandId.make(`command-${mode}`), |
| 439 | threadId: v2ThreadId, |
| 440 | message: { |
| 441 | messageId: MessageId.make(`message-${mode}`), |
| 442 | role: "user", |
| 443 | text: mode, |
| 444 | attachments: [], |
| 445 | }, |
| 446 | runtimeMode: "full-access", |
| 447 | interactionMode: "default", |
| 448 | dispatchMode: mode, |
| 449 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 450 | |
| 451 | expect(commands.at(-1)).toMatchObject({ |
| 452 | type: "message.dispatch", |
| 453 | dispatchMode: { type: expectedType }, |
| 454 | ...(mode === "queue" ? {} : { deliveryIntent: mode }), |
| 455 | }); |
| 456 | } |
| 457 | expect(projectionRequests).toEqual([]); |
| 458 | }).pipe(Effect.provide(layerTestCrypto)), |
| 459 | ); |
| 460 | |
| 461 | it.effect("retains projection-shaped delivery for servers without command context support", () => |
| 462 | Effect.gen(function* () { |
| 463 | const activeRunId = RunId.make("legacy-active-run"); |
| 464 | const projection: OrchestrationV2ThreadProjection = { |
| 465 | ...v2Projection, |
| 466 | runs: [ |
| 467 | { |
| 468 | id: activeRunId, |
| 469 | threadId: v2ThreadId, |
| 470 | ordinal: 1, |
| 471 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 472 | modelSelection: v2Projection.thread.modelSelection, |
| 473 | providerThreadId: null, |
| 474 | userMessageId: MessageId.make("legacy-active-message"), |
| 475 | rootNodeId: null, |
| 476 | activeAttemptId: null, |
| 477 | status: "running", |
| 478 | requestedAt: v2Now, |
| 479 | startedAt: v2Now, |
| 480 | completedAt: null, |
| 481 | checkpointId: null, |
| 482 | contextHandoffId: null, |
| 483 | }, |
| 484 | ], |
| 485 | }; |
| 486 | const commands: OrchestrationV2Command[] = []; |
| 487 | const projectionRequests: ThreadId[] = []; |
| 488 | const supervisor = yield* makeSupervisor({ |
| 489 | commands, |
| 490 | projects: [], |
| 491 | projection, |
| 492 | projectionRequests, |
| 493 | advertiseServerResolvedCommandContext: false, |
| 494 | }); |
| 495 | |
| 496 | for (const [mode, expectedMode] of [ |
| 497 | ["auto", { type: "queue_after_active" }], |
| 498 | ["queue", { type: "queue_after_active" }], |
| 499 | ["steer", { type: "steer_active", targetRunId: activeRunId }], |
| 500 | ["restart", { type: "restart_active", targetRunId: activeRunId }], |
| 501 | ] as const) { |
| 502 | yield* startThreadTurn({ |
| 503 | commandId: CommandId.make(`legacy-command-${mode}`), |
| 504 | threadId: v2ThreadId, |
| 505 | message: { |
| 506 | messageId: MessageId.make(`legacy-message-${mode}`), |
| 507 | role: "user", |
| 508 | text: mode, |
| 509 | attachments: [], |
| 510 | }, |
| 511 | runtimeMode: "full-access", |
| 512 | interactionMode: "default", |
| 513 | dispatchMode: mode, |
| 514 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 515 | |
| 516 | expect(commands.at(-1)).toMatchObject({ |
| 517 | type: "message.dispatch", |
| 518 | dispatchMode: expectedMode, |
| 519 | }); |
| 520 | expect(commands.at(-1)).not.toHaveProperty("deliveryIntent"); |
| 521 | } |
| 522 | expect(projectionRequests).toEqual([v2ThreadId, v2ThreadId, v2ThreadId, v2ThreadId]); |
| 523 | }).pipe(Effect.provide(layerTestCrypto)), |
| 524 | ); |
| 525 | |
| 526 | it.effect("Stop with no run left ends the thread's pull request watches", () => |
| 527 | Effect.gen(function* () { |
| 528 | const link = { |
| 529 | host: "github.com", |
| 530 | repository: "pingdotgg/t3code", |
| 531 | url: "https://github.com/pingdotgg/t3code/pull/7", |
| 532 | source: "agent" as const, |
| 533 | linkedAt: "2026-10-05T00:00:00.000Z", |
| 534 | snapshot: null, |
| 535 | stack: null, |
| 536 | }; |
| 537 | const watch = { |
| 538 | startedAt: "2026-10-05T00:00:00.000Z", |
| 539 | headSha: null, |
| 540 | failedChecks: [], |
| 541 | passed: false, |
| 542 | passedChecks: [], |
| 543 | remarksThrough: "2026-10-05T00:00:00.000Z", |
| 544 | remarkIds: [], |
| 545 | conflicting: false, |
| 546 | wakes: 0, |
| 547 | }; |
| 548 | const projection: OrchestrationV2ThreadProjection = { |
| 549 | ...v2Projection, |
| 550 | thread: { |
| 551 | ...v2Projection.thread, |
| 552 | pullRequests: [ |
| 553 | { ...link, number: 7, watch }, |
| 554 | { ...link, number: 8 }, |
| 555 | { ...link, number: 9, source: "stack-dismissed", watch }, |
| 556 | ], |
| 557 | }, |
| 558 | }; |
| 559 | const commands: OrchestrationV2Command[] = []; |
| 560 | const supervisor = yield* makeSupervisor({ commands, projects: [], projection }); |
| 561 | |
| 562 | yield* interruptThreadTurn({ threadId: v2ThreadId }).pipe( |
| 563 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 564 | ); |
| 565 | |
| 566 | expect(commands).toEqual([ |
| 567 | { |
| 568 | type: "thread.pull-request.watch", |
| 569 | commandId: expect.any(String), |
| 570 | threadId: v2ThreadId, |
| 571 | host: "github.com", |
| 572 | repository: "pingdotgg/t3code", |
| 573 | number: 7, |
| 574 | watching: false, |
| 575 | }, |
| 576 | ]); |
| 577 | }).pipe(Effect.provide(layerTestCrypto)), |
| 578 | ); |
| 579 | |
| 580 | it.effect.each([ |
| 581 | "waiting", |
| 582 | "completed", |
| 583 | "failed", |
| 584 | "interrupted", |
| 585 | "cancelled", |
| 586 | "rolled_back", |
| 587 | ] as const)("dispatches Stop for %s runs with background commands except rollback", (status) => |
| 588 | Effect.gen(function* () { |
| 589 | const waitingRunId = RunId.make("run-waiting"); |
| 590 | const projection: OrchestrationV2ThreadProjection = { |
| 591 | ...v2Projection, |
| 592 | runs: [ |
| 593 | { |
| 594 | id: waitingRunId, |
| 595 | threadId: v2ThreadId, |
| 596 | ordinal: 1, |
| 597 | providerInstanceId: v2Projection.thread.providerInstanceId, |
| 598 | modelSelection: v2Projection.thread.modelSelection, |
| 599 | providerThreadId: null, |
| 600 | userMessageId: MessageId.make("message-waiting"), |
| 601 | rootNodeId: null, |
| 602 | activeAttemptId: null, |
| 603 | status, |
| 604 | requestedAt: v2Now, |
| 605 | startedAt: v2Now, |
| 606 | completedAt: null, |
| 607 | checkpointId: null, |
| 608 | contextHandoffId: null, |
| 609 | }, |
| 610 | ], |
| 611 | turnItems: [ |
| 612 | { |
| 613 | id: TurnItemId.make("background-command"), |
| 614 | threadId: v2ThreadId, |
| 615 | runId: waitingRunId, |
| 616 | nodeId: null, |
| 617 | providerThreadId: null, |
| 618 | providerTurnId: null, |
| 619 | nativeItemRef: null, |
| 620 | parentItemId: null, |
| 621 | ordinal: 1, |
| 622 | status: "running", |
| 623 | title: null, |
| 624 | startedAt: v2Now, |
| 625 | completedAt: null, |
| 626 | updatedAt: v2Now, |
| 627 | type: "command_execution", |
| 628 | input: "vp run dev", |
| 629 | }, |
| 630 | ], |
| 631 | }; |
| 632 | const commands: OrchestrationV2Command[] = []; |
| 633 | const supervisor = yield* makeSupervisor({ commands, projects: [], projection }); |
| 634 | |
| 635 | const result = yield* interruptThreadTurn({ threadId: v2ThreadId }).pipe( |
| 636 | Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), |
| 637 | ); |
| 638 | |
| 639 | expect(result).toEqual({ sequence: status === "rolled_back" ? 0 : 1 }); |
| 640 | expect(commands).toEqual( |
| 641 | status === "rolled_back" |
| 642 | ? [] |
| 643 | : [ |
| 644 | { |
| 645 | type: "run.interrupt", |
| 646 | commandId: expect.any(String), |
| 647 | threadId: v2ThreadId, |
| 648 | runId: waitingRunId, |
| 649 | holdQueue: true, |
| 650 | }, |
| 651 | ], |
| 652 | ); |
| 653 | }).pipe(Effect.provide(layerTestCrypto)), |
| 654 | ); |
| 655 | |
| 656 | it.effect( |
| 657 | "dispatches V2-native relationship and queue commands without compatibility shaping", |
| 658 | () => |
| 659 | Effect.gen(function* () { |
| 660 | const commands: OrchestrationV2Command[] = []; |
| 661 | const supervisor = yield* makeSupervisor({ commands, projects: [] }); |
| 662 | const provide = Effect.provideService( |
| 663 | EnvironmentSupervisor.EnvironmentSupervisor, |
| 664 | supervisor, |
| 665 | ); |
| 666 | |
| 667 | yield* forkThreadFromRun({ |
| 668 | commandId: CommandId.make("fork"), |
| 669 | sourceThreadId: v2ThreadId, |
| 670 | targetThreadId: ThreadId.make("thread-fork"), |
| 671 | runId: RunId.make("run-1"), |
| 672 | }).pipe(provide); |
| 673 | yield* mergeThreadBack({ |
| 674 | commandId: CommandId.make("merge"), |
| 675 | sourceThreadId: ThreadId.make("thread-fork"), |
| 676 | targetThreadId: v2ThreadId, |
| 677 | runId: RunId.make("run-2"), |
| 678 | }).pipe(provide); |
| 679 | yield* reorderQueuedRun({ |
| 680 | commandId: CommandId.make("reorder"), |
| 681 | threadId: v2ThreadId, |
| 682 | runId: RunId.make("run-3"), |
| 683 | beforeRunId: RunId.make("run-4"), |
| 684 | }).pipe(provide); |
| 685 | yield* promoteQueuedRun({ |
| 686 | commandId: CommandId.make("promote"), |
| 687 | threadId: v2ThreadId, |
| 688 | queuedRunId: RunId.make("run-3"), |
| 689 | targetRunId: RunId.make("run-active"), |
| 690 | }).pipe(provide); |
| 691 | yield* cancelQueuedRun({ |
| 692 | commandId: CommandId.make("cancel"), |
| 693 | threadId: v2ThreadId, |
| 694 | runId: RunId.make("run-3"), |
| 695 | }).pipe(provide); |
| 696 | yield* editQueuedRun({ |
| 697 | commandId: CommandId.make("edit"), |
| 698 | threadId: v2ThreadId, |
| 699 | runId: RunId.make("run-3"), |
| 700 | text: "updated queued text", |
| 701 | }).pipe(provide); |
| 702 | yield* editQueuedRun({ |
| 703 | commandId: CommandId.make("edit-attachments"), |
| 704 | threadId: v2ThreadId, |
| 705 | runId: RunId.make("run-3"), |
| 706 | text: "updated queued text with attachments", |
| 707 | edit: { |
| 708 | messageId: MessageId.make("message-3"), |
| 709 | attachments: [ |
| 710 | { |
| 711 | type: "image", |
| 712 | id: "attachment-kept", |
| 713 | name: "kept.png", |
| 714 | mimeType: "image/png", |
| 715 | sizeBytes: 64, |
| 716 | }, |
| 717 | ], |
| 718 | }, |
| 719 | }).pipe(provide); |
| 720 | |
| 721 | expect(commands).toMatchObject([ |
| 722 | { type: "thread.fork", sourcePoint: { type: "run", runId: "run-1" } }, |
| 723 | { type: "thread.merge_back", sourcePoint: { type: "run", runId: "run-2" } }, |
| 724 | { type: "queued-run.reorder", runId: "run-3", beforeRunId: "run-4" }, |
| 725 | { |
| 726 | type: "queued-message.promote-to-steer", |
| 727 | queuedRunId: "run-3", |
| 728 | targetRunId: "run-active", |
| 729 | }, |
| 730 | { type: "queued-run.cancel", runId: "run-3" }, |
| 731 | { type: "queued-run.edit", runId: "run-3", text: "updated queued text" }, |
| 732 | { |
| 733 | type: "queued-run.edit", |
| 734 | runId: "run-3", |
| 735 | text: "updated queued text with attachments", |
| 736 | attachments: [{ id: "attachment-kept" }], |
| 737 | }, |
| 738 | ]); |
| 739 | // A text-only edit must not send an attachments replacement list. |
| 740 | expect(commands[5]).not.toHaveProperty("attachments"); |
| 741 | }).pipe(Effect.provide(layerTestCrypto)), |
| 742 | ); |
| 743 | |
| 744 | it.effect("delegates model selection to the server without fetching the full projection", () => |
| 745 | Effect.gen(function* () { |
| 746 | const commands: OrchestrationV2Command[] = []; |
| 747 | const projectionRequests: ThreadId[] = []; |
| 748 | const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests }); |
| 749 | |
| 750 | yield* updateThreadMetadata({ |
| 751 | commandId: CommandId.make("same-provider"), |
| 752 | threadId: v2ThreadId, |
| 753 | modelSelection: { |
| 754 | instanceId: ProviderInstanceId.make("codex"), |
| 755 | model: "another-model", |
| 756 | }, |
| 757 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 758 | |
| 759 | yield* updateThreadMetadata({ |
| 760 | commandId: CommandId.make("switch-provider"), |
| 761 | threadId: v2ThreadId, |
| 762 | modelSelection: { |
| 763 | instanceId: ProviderInstanceId.make("claude"), |
| 764 | model: "claude-sonnet-4-6", |
| 765 | }, |
| 766 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 767 | |
| 768 | expect(commands).toEqual([ |
| 769 | { |
| 770 | type: "thread.model-selection.set", |
| 771 | commandId: "same-provider", |
| 772 | threadId: v2ThreadId, |
| 773 | modelSelection: { instanceId: "codex", model: "another-model" }, |
| 774 | }, |
| 775 | { |
| 776 | type: "thread.model-selection.set", |
| 777 | commandId: "switch-provider", |
| 778 | threadId: v2ThreadId, |
| 779 | modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" }, |
| 780 | }, |
| 781 | ]); |
| 782 | expect(projectionRequests).toEqual([]); |
| 783 | }).pipe(Effect.provide(layerTestCrypto)), |
| 784 | ); |
| 785 | |
| 786 | it.effect("retains provider-switch shaping for servers without command context support", () => |
| 787 | Effect.gen(function* () { |
| 788 | const commands: OrchestrationV2Command[] = []; |
| 789 | const projectionRequests: ThreadId[] = []; |
| 790 | const supervisor = yield* makeSupervisor({ |
| 791 | commands, |
| 792 | projects: [], |
| 793 | projectionRequests, |
| 794 | advertiseServerResolvedCommandContext: false, |
| 795 | }); |
| 796 | |
| 797 | yield* updateThreadMetadata({ |
| 798 | commandId: CommandId.make("legacy-switch-provider"), |
| 799 | threadId: v2ThreadId, |
| 800 | modelSelection: { |
| 801 | instanceId: ProviderInstanceId.make("claude"), |
| 802 | model: "claude-sonnet-4-6", |
| 803 | }, |
| 804 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 805 | |
| 806 | expect(projectionRequests).toEqual([v2ThreadId]); |
| 807 | expect(commands).toEqual([ |
| 808 | { |
| 809 | type: "provider.switch", |
| 810 | commandId: "legacy-switch-provider", |
| 811 | threadId: v2ThreadId, |
| 812 | modelSelection: { instanceId: "claude", model: "claude-sonnet-4-6" }, |
| 813 | }, |
| 814 | ]); |
| 815 | }).pipe(Effect.provide(layerTestCrypto)), |
| 816 | ); |
| 817 | |
| 818 | it.effect.each([true, false])( |
| 819 | "rolls back an identified checkpoint without fetching the full projection, restoreFiles=%s", |
| 820 | (restoreFiles) => |
| 821 | Effect.gen(function* () { |
| 822 | const commands: OrchestrationV2Command[] = []; |
| 823 | const projectionRequests: ThreadId[] = []; |
| 824 | const supervisor = yield* makeSupervisor({ commands, projects: [], projectionRequests }); |
| 825 | |
| 826 | yield* revertThreadCheckpoint({ |
| 827 | commandId: CommandId.make("rollback-known-checkpoint"), |
| 828 | threadId: v2ThreadId, |
| 829 | checkpointId: CheckpointId.make("checkpoint-known"), |
| 830 | scopeId: CheckpointScopeId.make("scope-known"), |
| 831 | restoreFiles, |
| 832 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 833 | |
| 834 | expect(projectionRequests).toEqual([]); |
| 835 | expect(commands).toEqual([ |
| 836 | { |
| 837 | type: "checkpoint.rollback", |
| 838 | commandId: "rollback-known-checkpoint", |
| 839 | threadId: v2ThreadId, |
| 840 | checkpointId: "checkpoint-known", |
| 841 | scopeId: "scope-known", |
| 842 | restoreFiles, |
| 843 | }, |
| 844 | ]); |
| 845 | }).pipe(Effect.provide(layerTestCrypto)), |
| 846 | ); |
| 847 | |
| 848 | it.effect("validates identified checkpoints locally for older servers", () => |
| 849 | Effect.gen(function* () { |
| 850 | const checkpointId = CheckpointId.make("legacy-checkpoint"); |
| 851 | const scopeId = CheckpointScopeId.make("legacy-checkpoint-scope"); |
| 852 | for (const status of ["ready", "missing", "error", "stale", null] as const) { |
| 853 | const commands: OrchestrationV2Command[] = []; |
| 854 | const projectionRequests: ThreadId[] = []; |
| 855 | const supervisor = yield* makeSupervisor({ |
| 856 | commands, |
| 857 | projects: [], |
| 858 | projectionRequests, |
| 859 | advertiseServerResolvedCommandContext: false, |
| 860 | projection: { |
| 861 | ...v2Projection, |
| 862 | checkpoints: |
| 863 | status === null |
| 864 | ? [] |
| 865 | : [ |
| 866 | { |
| 867 | id: checkpointId, |
| 868 | threadId: v2ThreadId, |
| 869 | scopeId, |
| 870 | runId: null, |
| 871 | nodeId: NodeId.make("legacy-checkpoint-node"), |
| 872 | parentCheckpointId: null, |
| 873 | ordinalWithinScope: 0, |
| 874 | appRunOrdinal: null, |
| 875 | ref: CheckpointRef.make("refs/t3/legacy-checkpoint"), |
| 876 | status, |
| 877 | files: [], |
| 878 | capturedAt: v2Now, |
| 879 | }, |
| 880 | ], |
| 881 | }, |
| 882 | }); |
| 883 | const rollback = revertThreadCheckpoint({ |
| 884 | commandId: CommandId.make(`legacy-rollback-${status}`), |
| 885 | threadId: v2ThreadId, |
| 886 | checkpointId, |
| 887 | scopeId, |
| 888 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 889 | |
| 890 | if (status === "ready") { |
| 891 | yield* rollback; |
| 892 | expect(commands).toEqual([ |
| 893 | { |
| 894 | type: "checkpoint.rollback", |
| 895 | commandId: "legacy-rollback-ready", |
| 896 | threadId: v2ThreadId, |
| 897 | checkpointId, |
| 898 | scopeId, |
| 899 | }, |
| 900 | ]); |
| 901 | } else { |
| 902 | const error = yield* rollback.pipe(Effect.flip); |
| 903 | expect(error._tag).toBe("OrchestrationV2CheckpointUnavailableError"); |
| 904 | expect(commands).toEqual([]); |
| 905 | } |
| 906 | expect(projectionRequests).toEqual([v2ThreadId]); |
| 907 | } |
| 908 | }).pipe(Effect.provide(layerTestCrypto)), |
| 909 | ); |
| 910 | |
| 911 | it.effect("dispatches settle and unsettle commands without timestamps", () => |
| 912 | Effect.gen(function* () { |
| 913 | const dispatched: OrchestrationV2Command[] = []; |
| 914 | const supervisor = yield* makeSupervisor({ commands: dispatched, projects: [] }); |
| 915 | |
| 916 | yield* settleThread({ |
| 917 | commandId: CommandId.make("settle-command"), |
| 918 | threadId: ThreadId.make("thread-1"), |
| 919 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 920 | yield* unsettleThread({ |
| 921 | commandId: CommandId.make("unsettle-command"), |
| 922 | threadId: ThreadId.make("thread-1"), |
| 923 | reason: "user", |
| 924 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 925 | |
| 926 | expect(dispatched).toEqual([ |
| 927 | { |
| 928 | type: "thread.settle", |
| 929 | commandId: "settle-command", |
| 930 | threadId: "thread-1", |
| 931 | }, |
| 932 | { |
| 933 | type: "thread.unsettle", |
| 934 | commandId: "unsettle-command", |
| 935 | threadId: "thread-1", |
| 936 | reason: "user", |
| 937 | }, |
| 938 | ]); |
| 939 | }).pipe(Effect.provide(layerTestCrypto)), |
| 940 | ); |
| 941 | |
| 942 | it.effect("sends an active order key without changing activity timestamps", () => |
| 943 | Effect.gen(function* () { |
| 944 | const dispatched: OrchestrationV2Command[] = []; |
| 945 | const supervisor = yield* makeSupervisor({ commands: dispatched, projects: [] }); |
| 946 | yield* reorderActiveThread({ |
| 947 | commandId: CommandId.make("reorder-command"), |
| 948 | threadId: ThreadId.make("thread-1"), |
| 949 | orderKey: "mf", |
| 950 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 951 | expect(dispatched).toEqual([ |
| 952 | { |
| 953 | type: "thread.active.reorder", |
| 954 | commandId: "reorder-command", |
| 955 | threadId: "thread-1", |
| 956 | orderKey: "mf", |
| 957 | }, |
| 958 | ]); |
| 959 | }).pipe(Effect.provide(layerTestCrypto)), |
| 960 | ); |
| 961 | |
| 962 | it.effect("dismisses a pending user-input request", () => |
| 963 | Effect.gen(function* () { |
| 964 | const commands: OrchestrationV2Command[] = []; |
| 965 | const supervisor = yield* makeSupervisor({ commands, projects: [] }); |
| 966 | |
| 967 | yield* dismissThreadUserInput({ |
| 968 | commandId: CommandId.make("dismiss-command"), |
| 969 | threadId: v2ThreadId, |
| 970 | requestId: RuntimeRequestId.make("request-1"), |
| 971 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 972 | |
| 973 | expect(commands).toEqual([ |
| 974 | { |
| 975 | type: "thread.user-input.dismiss", |
| 976 | commandId: "dismiss-command", |
| 977 | threadId: v2ThreadId, |
| 978 | requestId: "request-1", |
| 979 | }, |
| 980 | ]); |
| 981 | }).pipe(Effect.provide(layerTestCrypto)), |
| 982 | ); |
| 983 | |
| 984 | it.effect("dispatches an explicit idle start without fetching the full projection", () => |
| 985 | Effect.gen(function* () { |
| 986 | const commands: OrchestrationV2Command[] = []; |
| 987 | const projectionRequests: ThreadId[] = []; |
| 988 | const supervisor = yield* makeSupervisor({ |
| 989 | commands, |
| 990 | projects: [], |
| 991 | projectionRequests, |
| 992 | }); |
| 993 | |
| 994 | yield* startThreadTurn({ |
| 995 | commandId: CommandId.make("direct-start"), |
| 996 | threadId: v2ThreadId, |
| 997 | message: { |
| 998 | messageId: MessageId.make("direct-start-message"), |
| 999 | role: "user", |
| 1000 | text: "continue", |
| 1001 | attachments: [], |
| 1002 | }, |
| 1003 | runtimeMode: "full-access", |
| 1004 | interactionMode: "default", |
| 1005 | dispatchMode: "start", |
| 1006 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 1007 | |
| 1008 | expect(projectionRequests).toEqual([]); |
| 1009 | expect(commands).toMatchObject([ |
| 1010 | { |
| 1011 | type: "message.dispatch", |
| 1012 | threadId: v2ThreadId, |
| 1013 | dispatchMode: { type: "start_immediately" }, |
| 1014 | }, |
| 1015 | ]); |
| 1016 | }).pipe(Effect.provide(layerTestCrypto)), |
| 1017 | ); |
| 1018 | |
| 1019 | it.effect( |
| 1020 | "a native subagent stop targets its owning run rather than holding the parent queue", |
| 1021 | () => |
| 1022 | Effect.gen(function* () { |
| 1023 | const subagentId = NodeId.make("native-subagent"); |
| 1024 | const runId = RunId.make("settled-owner-run"); |
| 1025 | const commands: OrchestrationV2Command[] = []; |
| 1026 | const supervisor = yield* makeSupervisor({ |
| 1027 | commands, |
| 1028 | projects: [], |
| 1029 | projection: { |
| 1030 | ...v2Projection, |
| 1031 | subagents: [ |
| 1032 | { |
| 1033 | id: subagentId, |
| 1034 | threadId: v2ThreadId, |
| 1035 | runId, |
| 1036 | parentNodeId: NodeId.make("owner-node"), |
| 1037 | origin: "provider_native", |
| 1038 | createdBy: "agent", |
| 1039 | driver: ProviderDriverKind.make("claudeAgent"), |
| 1040 | providerInstanceId: ProviderInstanceId.make("claudeAgent"), |
| 1041 | providerThreadId: null, |
| 1042 | childThreadId: ThreadId.make("native-child-thread"), |
| 1043 | nativeTaskRef: null, |
| 1044 | prompt: "Work", |
| 1045 | title: null, |
| 1046 | model: null, |
| 1047 | result: null, |
| 1048 | status: "running", |
| 1049 | startedAt: v2Now, |
| 1050 | completedAt: null, |
| 1051 | updatedAt: v2Now, |
| 1052 | }, |
| 1053 | ], |
| 1054 | }, |
| 1055 | }); |
| 1056 | yield* interruptThreadTurn({ |
| 1057 | commandId: CommandId.make("native-stop"), |
| 1058 | threadId: v2ThreadId, |
| 1059 | subagentId, |
| 1060 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 1061 | expect(commands).toEqual([ |
| 1062 | { |
| 1063 | type: "subagent.stop", |
| 1064 | commandId: "native-stop", |
| 1065 | threadId: v2ThreadId, |
| 1066 | runId, |
| 1067 | subagentId, |
| 1068 | }, |
| 1069 | ]); |
| 1070 | }).pipe(Effect.provide(layerTestCrypto)), |
| 1071 | ); |
| 1072 | |
| 1073 | it.effect("interrupts a known run without fetching the full projection", () => |
| 1074 | Effect.gen(function* () { |
| 1075 | const commands: OrchestrationV2Command[] = []; |
| 1076 | const projectionRequests: ThreadId[] = []; |
| 1077 | const supervisor = yield* makeSupervisor({ |
| 1078 | commands, |
| 1079 | projects: [], |
| 1080 | projectionRequests, |
| 1081 | }); |
| 1082 | |
| 1083 | yield* interruptThreadTurn({ |
| 1084 | commandId: CommandId.make("direct-interrupt"), |
| 1085 | threadId: v2ThreadId, |
| 1086 | runId: RunId.make("active-run"), |
| 1087 | }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor)); |
| 1088 | |
| 1089 | expect(projectionRequests).toEqual([]); |
| 1090 | expect(commands).toEqual([ |
| 1091 | { |
| 1092 | type: "run.interrupt", |
| 1093 | commandId: "direct-interrupt", |
| 1094 | threadId: v2ThreadId, |
| 1095 | runId: "active-run", |
| 1096 | holdQueue: true, |
| 1097 | }, |
| 1098 | ]); |
| 1099 | }).pipe(Effect.provide(layerTestCrypto)), |
| 1100 | ); |
| 1101 | }); |