authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 02:08:33-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log317fcc659b80ebd651179db3eb8071c0c788a8b7
treed5ee0edb3a5042723034c86dd042c46b1b614fcc
parentff7e084a339d230afd974c1434773312d88f5d68

Add Claude checkpoint and revert reactor coverage

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 148 insertions(+), 3 deletions(-)

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+148-3
...@@ -45,7 +45,12 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId...@@ -45,7 +45,12 @@ const asProviderThreadId = (value: string): ProviderThreadId => ProviderThreadId
45const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);45const asProviderTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
46const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);46const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);
4747
48function createProviderServiceHarness(cwd: string, hasSession = true, sessionCwd = cwd) {48function 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 hasSession63 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 });
465472
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 });
796869
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();