| ... | @@ -45,7 +45,12 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId | ... | @@ -45,7 +45,12 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId |
| 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); | 45 | const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value); |
| 46 | const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value); | 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 | const now = new Date().toISOString(); | 54 | const now = new Date().toISOString(); |
| 50 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 55 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| 51 | const rollbackConversation = vi.fn( | 56 | const rollbackConversation = vi.fn( |
| ... | @@ -56,10 +61,10 @@ function createProviderServiceHarness(cwd: string, hasSession = true, sessionCwd | ... | @@ -56,10 +61,10 @@ function createProviderServiceHarness(cwd: string, hasSession = true, sessionCwd |
| 56 | Effect.die(new Error("Unsupported provider call in test")) as Effect.Effect<A, never>; | 61 | Effect.die(new Error("Unsupported provider call in test")) as Effect.Effect<A, never>; |
| 57 | const listSessions = () => | 62 | const listSessions = () => |
| 58 | hasSession | 63 | hasSession |
| 59 | ? Effect.succeed([ | 64 | ? Effect.succeed([ |
| 60 | { | 65 | { |
| 61 | sessionId: asSessionId("sess-1"), | 66 | sessionId: asSessionId("sess-1"), |
| 62 | provider: "codex", | 67 | provider: providerName, |
| 63 | status: "ready", | 68 | status: "ready", |
| 64 | threadId: asProviderThreadId("provider-thread-1"), | 69 | threadId: asProviderThreadId("provider-thread-1"), |
| 65 | cwd: sessionCwd, | 70 | cwd: sessionCwd, |
| ... | @@ -220,6 +225,7 @@ describe("CheckpointReactor", () => { | ... | @@ -220,6 +225,7 @@ describe("CheckpointReactor", () => { |
| 220 | readonly projectWorkspaceRoot?: string; | 225 | readonly projectWorkspaceRoot?: string; |
| 221 | readonly threadWorktreePath?: string | null; | 226 | readonly threadWorktreePath?: string | null; |
| 222 | readonly providerSessionCwd?: string; | 227 | readonly providerSessionCwd?: string; |
| | 228 | readonly providerName?: "codex" | "claudeCode"; |
| 223 | }) { | 229 | }) { |
| 224 | const cwd = createGitRepository(); | 230 | const cwd = createGitRepository(); |
| 225 | tempDirs.push(cwd); | 231 | tempDirs.push(cwd); |
| ... | @@ -227,6 +233,7 @@ describe("CheckpointReactor", () => { | ... | @@ -227,6 +233,7 @@ describe("CheckpointReactor", () => { |
| 227 | cwd, | 233 | cwd, |
| 228 | options?.hasSession ?? true, | 234 | options?.hasSession ?? true, |
| 229 | options?.providerSessionCwd ?? cwd, | 235 | options?.providerSessionCwd ?? cwd, |
| | 236 | options?.providerName ?? "codex", |
| 230 | ); | 237 | ); |
| 231 | const orchestrationLayer = OrchestrationEngineLive.pipe( | 238 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 232 | Layer.provide(OrchestrationProjectionPipelineLive), | 239 | Layer.provide(OrchestrationProjectionPipelineLive), |
| ... | @@ -463,6 +470,72 @@ describe("CheckpointReactor", () => { | ... | @@ -463,6 +470,72 @@ describe("CheckpointReactor", () => { |
| 463 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); | 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 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { | 539 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { |
| 467 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); | 540 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 468 | const createdAt = new Date().toISOString(); | 541 | const createdAt = new Date().toISOString(); |
| ... | @@ -794,6 +867,78 @@ describe("CheckpointReactor", () => { | ... | @@ -794,6 +867,78 @@ describe("CheckpointReactor", () => { |
| 794 | ).toBe(false); | 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 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { | 942 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { |
| 798 | const harness = await createHarness(); | 943 | const harness = await createHarness(); |
| 799 | const createdAt = new Date().toISOString(); | 944 | const createdAt = new Date().toISOString(); |