Skip to content

Commit 1beeff2

Browse files
juliusmarmingecodex
andcommitted
test: restore Claude coverage on sibling stack
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent b47976c commit 1beeff2

4 files changed

Lines changed: 312 additions & 2 deletions

File tree

apps/server/src/orchestration/Layers/CheckpointReactor.test.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,15 @@ describe("CheckpointReactor", () => {
235235
readonly projectWorkspaceRoot?: string;
236236
readonly threadWorktreePath?: string | null;
237237
readonly providerSessionCwd?: string;
238+
readonly providerName?: "codex" | "claudeCode";
238239
}) {
239240
const cwd = createGitRepository();
240241
tempDirs.push(cwd);
241242
const provider = createProviderServiceHarness(
242243
cwd,
243244
options?.hasSession ?? true,
244245
options?.providerSessionCwd ?? cwd,
245-
"codex",
246+
options?.providerName ?? "codex",
246247
);
247248
const orchestrationLayer = OrchestrationEngineLive.pipe(
248249
Layer.provide(OrchestrationProjectionPipelineLive),
@@ -475,6 +476,67 @@ describe("CheckpointReactor", () => {
475476
expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
476477
});
477478

479+
it("captures pre-turn and completion checkpoints for claudeCode runtime events", async () => {
480+
const harness = await createHarness({
481+
seedFilesystemCheckpoints: false,
482+
providerName: "claudeCode",
483+
});
484+
const createdAt = new Date().toISOString();
485+
486+
await Effect.runPromise(
487+
harness.engine.dispatch({
488+
type: "thread.session.set",
489+
commandId: CommandId.makeUnsafe("cmd-session-set-capture-claude"),
490+
threadId: ThreadId.makeUnsafe("thread-1"),
491+
session: {
492+
threadId: ThreadId.makeUnsafe("thread-1"),
493+
status: "ready",
494+
providerName: "claudeCode",
495+
runtimeMode: "approval-required",
496+
activeTurnId: null,
497+
lastError: null,
498+
updatedAt: createdAt,
499+
},
500+
createdAt,
501+
}),
502+
);
503+
504+
harness.provider.emit({
505+
type: "turn.started",
506+
eventId: EventId.makeUnsafe("evt-turn-started-claude-1"),
507+
provider: "claudeCode",
508+
createdAt: new Date().toISOString(),
509+
threadId: ThreadId.makeUnsafe("thread-1"),
510+
turnId: asTurnId("turn-claude-1"),
511+
});
512+
await waitForGitRefExists(
513+
harness.cwd,
514+
checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0),
515+
);
516+
517+
fs.writeFileSync(path.join(harness.cwd, "README.md"), "v2\n", "utf8");
518+
harness.provider.emit({
519+
type: "turn.completed",
520+
eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"),
521+
provider: "claudeCode",
522+
createdAt: new Date().toISOString(),
523+
threadId: ThreadId.makeUnsafe("thread-1"),
524+
turnId: asTurnId("turn-claude-1"),
525+
payload: { state: "completed" },
526+
});
527+
528+
await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed");
529+
const thread = await waitForThread(
530+
harness.engine,
531+
(entry) => entry.latestTurn?.turnId === "turn-claude-1" && entry.checkpoints.length === 1,
532+
);
533+
534+
expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1);
535+
expect(
536+
gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1)),
537+
).toBe(true);
538+
});
539+
478540
it("appends capture failure activity when turn diff summary cannot be derived", async () => {
479541
const harness = await createHarness({ seedFilesystemCheckpoints: false });
480542
const createdAt = new Date().toISOString();
@@ -790,6 +852,75 @@ describe("CheckpointReactor", () => {
790852
).toBe(false);
791853
});
792854

855+
it("executes provider revert and emits thread.reverted for claudeCode sessions", async () => {
856+
const harness = await createHarness({ providerName: "claudeCode" });
857+
const createdAt = new Date().toISOString();
858+
859+
await Effect.runPromise(
860+
harness.engine.dispatch({
861+
type: "thread.session.set",
862+
commandId: CommandId.makeUnsafe("cmd-session-set-claude"),
863+
threadId: ThreadId.makeUnsafe("thread-1"),
864+
session: {
865+
threadId: ThreadId.makeUnsafe("thread-1"),
866+
status: "ready",
867+
providerName: "claudeCode",
868+
runtimeMode: "approval-required",
869+
activeTurnId: null,
870+
lastError: null,
871+
updatedAt: createdAt,
872+
},
873+
createdAt,
874+
}),
875+
);
876+
877+
await Effect.runPromise(
878+
harness.engine.dispatch({
879+
type: "thread.turn.diff.complete",
880+
commandId: CommandId.makeUnsafe("cmd-diff-claude-1"),
881+
threadId: ThreadId.makeUnsafe("thread-1"),
882+
turnId: asTurnId("turn-claude-1"),
883+
completedAt: createdAt,
884+
checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1),
885+
status: "ready",
886+
files: [],
887+
checkpointTurnCount: 1,
888+
createdAt,
889+
}),
890+
);
891+
await Effect.runPromise(
892+
harness.engine.dispatch({
893+
type: "thread.turn.diff.complete",
894+
commandId: CommandId.makeUnsafe("cmd-diff-claude-2"),
895+
threadId: ThreadId.makeUnsafe("thread-1"),
896+
turnId: asTurnId("turn-claude-2"),
897+
completedAt: createdAt,
898+
checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 2),
899+
status: "ready",
900+
files: [],
901+
checkpointTurnCount: 2,
902+
createdAt,
903+
}),
904+
);
905+
906+
await Effect.runPromise(
907+
harness.engine.dispatch({
908+
type: "thread.checkpoint.revert",
909+
commandId: CommandId.makeUnsafe("cmd-revert-request-claude"),
910+
threadId: ThreadId.makeUnsafe("thread-1"),
911+
turnCount: 1,
912+
createdAt,
913+
}),
914+
);
915+
916+
await waitForEvent(harness.engine, (event) => event.type === "thread.reverted");
917+
expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1);
918+
expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({
919+
threadId: ThreadId.makeUnsafe("thread-1"),
920+
numTurns: 1,
921+
});
922+
});
923+
793924
it("processes consecutive revert requests with deterministic rollback sequencing", async () => {
794925
const harness = await createHarness();
795926
const createdAt = new Date().toISOString();

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ describe("ProviderCommandReactor", () => {
9494
input !== null &&
9595
"provider" in input &&
9696
(input.provider === "codex" ||
97+
input.provider === "claudeCode" ||
9798
input.provider === "cursor")
9899
? input.provider
99100
: "codex";
@@ -384,6 +385,43 @@ describe("ProviderCommandReactor", () => {
384385
});
385386
});
386387

388+
it("starts first turn with requested provider when provider is specified", async () => {
389+
const harness = await createHarness();
390+
const now = new Date().toISOString();
391+
392+
await Effect.runPromise(
393+
harness.engine.dispatch({
394+
type: "thread.turn.start",
395+
commandId: CommandId.makeUnsafe("cmd-turn-start-provider-first"),
396+
threadId: ThreadId.makeUnsafe("thread-1"),
397+
message: {
398+
messageId: asMessageId("user-message-provider-first"),
399+
role: "user",
400+
text: "hello claude",
401+
attachments: [],
402+
},
403+
provider: "claudeCode",
404+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
405+
runtimeMode: "approval-required",
406+
createdAt: now,
407+
}),
408+
);
409+
410+
await waitFor(() => harness.startSession.mock.calls.length === 1);
411+
await waitFor(() => harness.sendTurn.mock.calls.length === 1);
412+
expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
413+
provider: "claudeCode",
414+
cwd: "/tmp/provider-project",
415+
model: "gpt-5-codex",
416+
runtimeMode: "approval-required",
417+
});
418+
419+
const readModel = await Effect.runPromise(harness.engine.getReadModel());
420+
const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
421+
expect(thread?.session?.providerName).toBe("claudeCode");
422+
expect(thread?.session?.threadId).toBe("thread-1");
423+
});
424+
387425
it("starts first turn with cursor provider when provider is specified", async () => {
388426
const harness = await createHarness();
389427
const now = new Date().toISOString();
@@ -657,6 +695,66 @@ describe("ProviderCommandReactor", () => {
657695
expect(thread?.session?.runtimeMode).toBe("approval-required");
658696
});
659697

698+
it("switches provider by restarting the session when turn request provider changes", async () => {
699+
const harness = await createHarness();
700+
const now = new Date().toISOString();
701+
702+
await Effect.runPromise(
703+
harness.engine.dispatch({
704+
type: "thread.turn.start",
705+
commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-1"),
706+
threadId: ThreadId.makeUnsafe("thread-1"),
707+
message: {
708+
messageId: asMessageId("user-message-provider-switch-1"),
709+
role: "user",
710+
text: "first",
711+
attachments: [],
712+
},
713+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
714+
runtimeMode: "approval-required",
715+
createdAt: now,
716+
}),
717+
);
718+
719+
await waitFor(() => harness.startSession.mock.calls.length === 1);
720+
await waitFor(() => harness.sendTurn.mock.calls.length === 1);
721+
722+
await Effect.runPromise(
723+
harness.engine.dispatch({
724+
type: "thread.turn.start",
725+
commandId: CommandId.makeUnsafe("cmd-turn-start-provider-switch-2"),
726+
threadId: ThreadId.makeUnsafe("thread-1"),
727+
message: {
728+
messageId: asMessageId("user-message-provider-switch-2"),
729+
role: "user",
730+
text: "second",
731+
attachments: [],
732+
},
733+
provider: "claudeCode",
734+
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
735+
runtimeMode: "approval-required",
736+
createdAt: now,
737+
}),
738+
);
739+
740+
await waitFor(() => harness.startSession.mock.calls.length === 2);
741+
await waitFor(() => harness.sendTurn.mock.calls.length === 2);
742+
743+
expect(harness.stopSession.mock.calls.length).toBe(0);
744+
expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({
745+
threadId: ThreadId.makeUnsafe("thread-1"),
746+
provider: "claudeCode",
747+
runtimeMode: "approval-required",
748+
});
749+
expect(harness.startSession.mock.calls[1]?.[1]).not.toHaveProperty("resumeCursor");
750+
751+
const readModel = await Effect.runPromise(harness.engine.getReadModel());
752+
const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
753+
expect(thread?.session?.threadId).toBe("thread-1");
754+
expect(thread?.session?.providerName).toBe("claudeCode");
755+
expect(thread?.session?.runtimeMode).toBe("approval-required");
756+
});
757+
660758
it("does not stop the active session when restart fails before rebind", async () => {
661759
const harness = await createHarness();
662760
const now = new Date().toISOString();

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,60 @@ describe("ProviderRuntimeIngestion", () => {
384384
);
385385
});
386386

387+
it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => {
388+
const harness = await createHarness();
389+
const seededAt = new Date().toISOString();
390+
391+
await Effect.runPromise(
392+
harness.engine.dispatch({
393+
type: "thread.session.set",
394+
commandId: CommandId.makeUnsafe("cmd-session-seed-claude-placeholder"),
395+
threadId: ThreadId.makeUnsafe("thread-1"),
396+
session: {
397+
threadId: ThreadId.makeUnsafe("thread-1"),
398+
status: "ready",
399+
providerName: "claudeCode",
400+
runtimeMode: "approval-required",
401+
activeTurnId: null,
402+
updatedAt: seededAt,
403+
lastError: null,
404+
},
405+
createdAt: seededAt,
406+
}),
407+
);
408+
409+
harness.emit({
410+
type: "turn.started",
411+
eventId: asEventId("evt-turn-started-claude-placeholder"),
412+
provider: "claudeCode",
413+
createdAt: new Date().toISOString(),
414+
threadId: asThreadId("thread-1"),
415+
turnId: asTurnId("turn-claude-placeholder"),
416+
});
417+
418+
await waitForThread(
419+
harness.engine,
420+
(thread) =>
421+
thread.session?.status === "running" &&
422+
thread.session?.activeTurnId === "turn-claude-placeholder",
423+
);
424+
425+
harness.emit({
426+
type: "turn.completed",
427+
eventId: asEventId("evt-turn-completed-claude-placeholder"),
428+
provider: "claudeCode",
429+
createdAt: new Date().toISOString(),
430+
threadId: asThreadId("thread-1"),
431+
turnId: asTurnId("turn-claude-placeholder"),
432+
status: "completed",
433+
});
434+
435+
await waitForThread(
436+
harness.engine,
437+
(thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null,
438+
);
439+
});
440+
387441
it("ignores auxiliary turn completions from a different provider thread", async () => {
388442
const harness = await createHarness();
389443
const now = new Date().toISOString();

apps/server/src/provider/Layers/ProviderService.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,15 @@ const sleep = (ms: number) =>
216216

217217
function makeProviderServiceLayer() {
218218
const codex = makeFakeCodexAdapter();
219+
const claude = makeFakeCodexAdapter("claudeCode");
219220
const registry: typeof ProviderAdapterRegistry.Service = {
220221
getByProvider: (provider) =>
221222
provider === "codex"
222223
? Effect.succeed(codex.adapter)
224+
: provider === "claudeCode"
225+
? Effect.succeed(claude.adapter)
223226
: Effect.fail(new ProviderUnsupportedError({ provider })),
224-
listProviders: () => Effect.succeed(["codex"]),
227+
listProviders: () => Effect.succeed(["codex", "claudeCode"]),
225228
};
226229

227230
const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry);
@@ -244,6 +247,7 @@ function makeProviderServiceLayer() {
244247

245248
return {
246249
codex,
250+
claude,
247251
layer,
248252
};
249253
}
@@ -532,6 +536,29 @@ routing.layer("ProviderServiceLive routing", (it) => {
532536
}),
533537
);
534538

539+
it.effect("routes explicit claudeCode provider session starts to the claude adapter", () =>
540+
Effect.gen(function* () {
541+
const provider = yield* ProviderService;
542+
543+
const session = yield* provider.startSession(asThreadId("thread-claude"), {
544+
provider: "claudeCode",
545+
threadId: asThreadId("thread-claude"),
546+
cwd: "/tmp/project-claude",
547+
runtimeMode: "full-access",
548+
});
549+
550+
assert.equal(session.provider, "claudeCode");
551+
assert.equal(routing.claude.startSession.mock.calls.length, 1);
552+
const startInput = routing.claude.startSession.mock.calls[0]?.[0];
553+
assert.equal(typeof startInput === "object" && startInput !== null, true);
554+
if (startInput && typeof startInput === "object") {
555+
const startPayload = startInput as { provider?: string; cwd?: string };
556+
assert.equal(startPayload.provider, "claudeCode");
557+
assert.equal(startPayload.cwd, "/tmp/project-claude");
558+
}
559+
}),
560+
);
561+
535562
it.effect("recovers stale sessions for sendTurn using persisted cwd", () =>
536563
Effect.gen(function* () {
537564
const provider = yield* ProviderService;

0 commit comments

Comments
 (0)