| ... | ... | @@ -45,7 +45,12 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId |
| 45 | 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); |
| 46 | 46 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); |
| 47 | 47 | |
| 48 | | function createProviderServiceHarness(cwd: string, hasSession = true, sessionCwd = cwd) { |
| 48 | function createProviderServiceHarness( |
| 49 | cwd: string, |
| 50 | hasSession = true, |
| 51 | sessionCwd = cwd, |
| 52 | providerName: "codex" | "claudeCode" = "codex", |
| 53 | ) { |
| 49 | 54 | const now = new Date().toISOString(); |
| 50 | 55 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 51 | 56 | const rollbackConversation = vi.fn( |
| ... | ... | @@ -56,10 +61,10 @@ function createProviderServiceHarness(cwd: string, hasSession = true, sessionCwd |
| 56 | 61 | Effect.die(new Error("Unsupported provider call in test")) as Effect.Effect<A, never>; |
| 57 | 62 | const listSessions = () => |
| 58 | 63 | hasSession |
| 59 | | ? Effect.succeed([ |
| 64 | ? Effect.succeed([ |
| 60 | 65 | { |
| 61 | 66 | sessionId: asSessionId("sess-1"), |
| 62 | | provider: "codex", |
| 67 | provider: providerName, |
| 63 | 68 | status: "ready", |
| 64 | 69 | threadId: asProviderThreadId("provider-thread-1"), |
| 65 | 70 | cwd: sessionCwd, |
| ... | ... | @@ -220,6 +225,7 @@ describe("CheckpointReactor", () => { |
| 220 | 225 | readonly projectWorkspaceRoot?: string; |
| 221 | 226 | readonly threadWorktreePath?: string | null; |
| 222 | 227 | readonly providerSessionCwd?: string; |
| 228 | readonly providerName?: "codex" | "claudeCode"; |
| 223 | 229 | }) { |
| 224 | 230 | const cwd = createGitRepository(); |
| 225 | 231 | tempDirs.push(cwd); |
| ... | ... | @@ -227,6 +233,7 @@ describe("CheckpointReactor", () => { |
| 227 | 233 | cwd, |
| 228 | 234 | options?.hasSession ?? true, |
| 229 | 235 | options?.providerSessionCwd ?? cwd, |
| 236 | options?.providerName ?? "codex", |
| 230 | 237 | ); |
| 231 | 238 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 232 | 239 | Layer.provide(OrchestrationProjectionPipelineLive), |
| ... | ... | @@ -463,6 +470,72 @@ describe("CheckpointReactor", () => { |
| 463 | 470 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 464 | 471 | }); |
| 465 | 472 | |
| 473 | it("captures pre-turn and completion checkpoints for claudeCode runtime events", async () => { |
| 474 | const harness = await createHarness({ |
| 475 | seedFilesystemCheckpoints: false, |
| 476 | providerName: "claudeCode", |
| 477 | }); |
| 478 | const createdAt = new Date().toISOString(); |
| 479 | |
| 480 | await Effect.runPromise( |
| 481 | harness.engine.dispatch({ |
| 482 | type: "thread.session.set", |
| 483 | commandId: CommandId.makeUnsafe("cmd-session-set-capture-claude"), |
| 484 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 485 | session: { |
| 486 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 487 | status: "ready", |
| 488 | providerName: "claudeCode", |
| 489 | providerSessionId: asSessionId("sess-1"), |
| 490 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 491 | approvalPolicy: "on-request", |
| 492 | sandboxMode: "workspace-write", |
| 493 | activeTurnId: null, |
| 494 | lastError: null, |
| 495 | updatedAt: createdAt, |
| 496 | }, |
| 497 | createdAt, |
| 498 | }), |
| 499 | ); |
| 500 | |
| 501 | harness.provider.emit({ |
| 502 | type: "turn.started", |
| 503 | eventId: EventId.makeUnsafe("evt-turn-started-claude-1"), |
| 504 | provider: "claudeCode", |
| 505 | sessionId: asSessionId("sess-1"), |
| 506 | createdAt: new Date().toISOString(), |
| 507 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 508 | turnId: asProviderTurnId("turn-claude-1"), |
| 509 | }); |
| 510 | await waitForGitRefExists( |
| 511 | harness.cwd, |
| 512 | checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0), |
| 513 | ); |
| 514 | |
| 515 | fs.writeFileSync(path.join(harness.cwd, "README.md"), "v2\n", "utf8"); |
| 516 | harness.provider.emit({ |
| 517 | type: "turn.completed", |
| 518 | eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"), |
| 519 | provider: "claudeCode", |
| 520 | sessionId: asSessionId("sess-1"), |
| 521 | createdAt: new Date().toISOString(), |
| 522 | threadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 523 | turnId: asProviderTurnId("turn-claude-1"), |
| 524 | status: "completed", |
| 525 | }); |
| 526 | |
| 527 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); |
| 528 | const thread = await waitForThread( |
| 529 | harness.engine, |
| 530 | (entry) => entry.latestTurn?.turnId === "turn-claude-1" && entry.checkpoints.length === 1, |
| 531 | ); |
| 532 | |
| 533 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 534 | expect( |
| 535 | gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1)), |
| 536 | ).toBe(true); |
| 537 | }); |
| 538 | |
| 466 | 539 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { |
| 467 | 540 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 468 | 541 | const createdAt = new Date().toISOString(); |
| ... | ... | @@ -794,6 +867,78 @@ describe("CheckpointReactor", () => { |
| 794 | 867 | ).toBe(false); |
| 795 | 868 | }); |
| 796 | 869 | |
| 870 | it("executes provider revert and emits thread.reverted for claudeCode sessions", async () => { |
| 871 | const harness = await createHarness({ providerName: "claudeCode" }); |
| 872 | const createdAt = new Date().toISOString(); |
| 873 | |
| 874 | await Effect.runPromise( |
| 875 | harness.engine.dispatch({ |
| 876 | type: "thread.session.set", |
| 877 | commandId: CommandId.makeUnsafe("cmd-session-set-claude"), |
| 878 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 879 | session: { |
| 880 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 881 | status: "ready", |
| 882 | providerName: "claudeCode", |
| 883 | providerSessionId: asSessionId("sess-1"), |
| 884 | providerThreadId: ProviderThreadId.makeUnsafe("provider-thread-1"), |
| 885 | approvalPolicy: "on-request", |
| 886 | sandboxMode: "workspace-write", |
| 887 | activeTurnId: null, |
| 888 | lastError: null, |
| 889 | updatedAt: createdAt, |
| 890 | }, |
| 891 | createdAt, |
| 892 | }), |
| 893 | ); |
| 894 | |
| 895 | await Effect.runPromise( |
| 896 | harness.engine.dispatch({ |
| 897 | type: "thread.turn.diff.complete", |
| 898 | commandId: CommandId.makeUnsafe("cmd-diff-claude-1"), |
| 899 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 900 | turnId: asTurnId("turn-claude-1"), |
| 901 | completedAt: createdAt, |
| 902 | checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1), |
| 903 | status: "ready", |
| 904 | files: [], |
| 905 | checkpointTurnCount: 1, |
| 906 | createdAt, |
| 907 | }), |
| 908 | ); |
| 909 | await Effect.runPromise( |
| 910 | harness.engine.dispatch({ |
| 911 | type: "thread.turn.diff.complete", |
| 912 | commandId: CommandId.makeUnsafe("cmd-diff-claude-2"), |
| 913 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 914 | turnId: asTurnId("turn-claude-2"), |
| 915 | completedAt: createdAt, |
| 916 | checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 2), |
| 917 | status: "ready", |
| 918 | files: [], |
| 919 | checkpointTurnCount: 2, |
| 920 | createdAt, |
| 921 | }), |
| 922 | ); |
| 923 | |
| 924 | await Effect.runPromise( |
| 925 | harness.engine.dispatch({ |
| 926 | type: "thread.checkpoint.revert", |
| 927 | commandId: CommandId.makeUnsafe("cmd-revert-request-claude"), |
| 928 | threadId: ThreadId.makeUnsafe("thread-1"), |
| 929 | turnCount: 1, |
| 930 | createdAt, |
| 931 | }), |
| 932 | ); |
| 933 | |
| 934 | await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); |
| 935 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); |
| 936 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ |
| 937 | sessionId: asSessionId("sess-1"), |
| 938 | numTurns: 1, |
| 939 | }); |
| 940 | }); |
| 941 | |
| 797 | 942 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { |
| 798 | 943 | const harness = await createHarness(); |
| 799 | 944 | const createdAt = new Date().toISOString(); |