| author | |
| committer | |
| log | a1b75fe17fee89a45d2a4d3688ac2b52e1fdac92 |
| tree | fab7cb054108bcaa394cbf81f5e60080b2a3afb8 |
| parent | 2e5e75699ef57f93967dffa64be9a70a7eec63a4 |
Co-authored-by: codex <codex@users.noreply.github.com>6 files changed, 19 insertions(+), 319 deletions(-)
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+5-138| ... | @@ -57,7 +57,7 @@ function createProviderServiceHarness( | ... | @@ -57,7 +57,7 @@ function createProviderServiceHarness( |
| 57 | cwd: string, | 57 | cwd: string, |
| 58 | hasSession = true, | 58 | hasSession = true, |
| 59 | sessionCwd = cwd, | 59 | sessionCwd = cwd, |
| 60 | providerName: "codex" | "claudeCode" = "codex", | 60 | providerName: ProviderSession["provider"] = "codex", |
| 61 | ) { | 61 | ) { |
| 62 | const now = new Date().toISOString(); | 62 | const now = new Date().toISOString(); |
| 63 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); | 63 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| ... | @@ -235,7 +235,6 @@ describe("CheckpointReactor", () => { | ... | @@ -235,7 +235,6 @@ describe("CheckpointReactor", () => { |
| 235 | readonly projectWorkspaceRoot?: string; | 235 | readonly projectWorkspaceRoot?: string; |
| 236 | readonly threadWorktreePath?: string | null; | 236 | readonly threadWorktreePath?: string | null; |
| 237 | readonly providerSessionCwd?: string; | 237 | readonly providerSessionCwd?: string; |
| 238 | readonly providerName?: "codex" | "claudeCode"; | ||
| 239 | }) { | 238 | }) { |
| 240 | const cwd = createGitRepository(); | 239 | const cwd = createGitRepository(); |
| 241 | tempDirs.push(cwd); | 240 | tempDirs.push(cwd); |
| ... | @@ -243,7 +242,7 @@ describe("CheckpointReactor", () => { | ... | @@ -243,7 +242,7 @@ describe("CheckpointReactor", () => { |
| 243 | cwd, | 242 | cwd, |
| 244 | options?.hasSession ?? true, | 243 | options?.hasSession ?? true, |
| 245 | options?.providerSessionCwd ?? cwd, | 244 | options?.providerSessionCwd ?? cwd, |
| 246 | options?.providerName ?? "codex", | 245 | "codex", |
| 247 | ); | 246 | ); |
| 248 | const orchestrationLayer = OrchestrationEngineLive.pipe( | 247 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 249 | Layer.provide(OrchestrationProjectionPipelineLive), | 248 | Layer.provide(OrchestrationProjectionPipelineLive), |
| ... | @@ -476,69 +475,6 @@ describe("CheckpointReactor", () => { | ... | @@ -476,69 +475,6 @@ describe("CheckpointReactor", () => { |
| 476 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); | 475 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 477 | }); | 476 | }); |
| 478 | 477 | ||
| 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 | |||
| 509 | createdAt: new Date().toISOString(), | ||
| 510 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 511 | turnId: asTurnId("turn-claude-1"), | ||
| 512 | }); | ||
| 513 | await waitForGitRefExists( | ||
| 514 | harness.cwd, | ||
| 515 | checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 0), | ||
| 516 | ); | ||
| 517 | |||
| 518 | fs.writeFileSync(path.join(harness.cwd, "README.md"), "v2\n", "utf8"); | ||
| 519 | harness.provider.emit({ | ||
| 520 | type: "turn.completed", | ||
| 521 | eventId: EventId.makeUnsafe("evt-turn-completed-claude-1"), | ||
| 522 | provider: "claudeCode", | ||
| 523 | |||
| 524 | createdAt: new Date().toISOString(), | ||
| 525 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 526 | turnId: asTurnId("turn-claude-1"), | ||
| 527 | payload: { state: "completed" }, | ||
| 528 | }); | ||
| 529 | |||
| 530 | await waitForEvent(harness.engine, (event) => event.type === "thread.turn-diff-completed"); | ||
| 531 | const thread = await waitForThread( | ||
| 532 | harness.engine, | ||
| 533 | (entry) => entry.latestTurn?.turnId === "turn-claude-1" && entry.checkpoints.length === 1, | ||
| 534 | ); | ||
| 535 | |||
| 536 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); | ||
| 537 | expect( | ||
| 538 | gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1)), | ||
| 539 | ).toBe(true); | ||
| 540 | }); | ||
| 541 | |||
| 542 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { | 478 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { |
| 543 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); | 479 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 544 | const createdAt = new Date().toISOString(); | 480 | const createdAt = new Date().toISOString(); |
| ... | @@ -845,7 +781,7 @@ describe("CheckpointReactor", () => { | ... | @@ -845,7 +781,7 @@ describe("CheckpointReactor", () => { |
| 845 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); | 781 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 846 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); | 782 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); |
| 847 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ | 783 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ |
| 848 | 784 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 849 | numTurns: 1, | 785 | numTurns: 1, |
| 850 | }); | 786 | }); |
| 851 | expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); | 787 | expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); |
| ... | @@ -854,75 +790,6 @@ describe("CheckpointReactor", () => { | ... | @@ -854,75 +790,6 @@ describe("CheckpointReactor", () => { |
| 854 | ).toBe(false); | 790 | ).toBe(false); |
| 855 | }); | 791 | }); |
| 856 | 792 | ||
| 857 | it("executes provider revert and emits thread.reverted for claudeCode sessions", async () => { | ||
| 858 | const harness = await createHarness({ providerName: "claudeCode" }); | ||
| 859 | const createdAt = new Date().toISOString(); | ||
| 860 | |||
| 861 | await Effect.runPromise( | ||
| 862 | harness.engine.dispatch({ | ||
| 863 | type: "thread.session.set", | ||
| 864 | commandId: CommandId.makeUnsafe("cmd-session-set-claude"), | ||
| 865 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 866 | session: { | ||
| 867 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 868 | status: "ready", | ||
| 869 | providerName: "claudeCode", | ||
| 870 | runtimeMode: "approval-required", | ||
| 871 | activeTurnId: null, | ||
| 872 | lastError: null, | ||
| 873 | updatedAt: createdAt, | ||
| 874 | }, | ||
| 875 | createdAt, | ||
| 876 | }), | ||
| 877 | ); | ||
| 878 | |||
| 879 | await Effect.runPromise( | ||
| 880 | harness.engine.dispatch({ | ||
| 881 | type: "thread.turn.diff.complete", | ||
| 882 | commandId: CommandId.makeUnsafe("cmd-diff-claude-1"), | ||
| 883 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 884 | turnId: asTurnId("turn-claude-1"), | ||
| 885 | completedAt: createdAt, | ||
| 886 | checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 1), | ||
| 887 | status: "ready", | ||
| 888 | files: [], | ||
| 889 | checkpointTurnCount: 1, | ||
| 890 | createdAt, | ||
| 891 | }), | ||
| 892 | ); | ||
| 893 | await Effect.runPromise( | ||
| 894 | harness.engine.dispatch({ | ||
| 895 | type: "thread.turn.diff.complete", | ||
| 896 | commandId: CommandId.makeUnsafe("cmd-diff-claude-2"), | ||
| 897 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 898 | turnId: asTurnId("turn-claude-2"), | ||
| 899 | completedAt: createdAt, | ||
| 900 | checkpointRef: checkpointRefForThreadTurn(ThreadId.makeUnsafe("thread-1"), 2), | ||
| 901 | status: "ready", | ||
| 902 | files: [], | ||
| 903 | checkpointTurnCount: 2, | ||
| 904 | createdAt, | ||
| 905 | }), | ||
| 906 | ); | ||
| 907 | |||
| 908 | await Effect.runPromise( | ||
| 909 | harness.engine.dispatch({ | ||
| 910 | type: "thread.checkpoint.revert", | ||
| 911 | commandId: CommandId.makeUnsafe("cmd-revert-request-claude"), | ||
| 912 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 913 | turnCount: 1, | ||
| 914 | createdAt, | ||
| 915 | }), | ||
| 916 | ); | ||
| 917 | |||
| 918 | await waitForEvent(harness.engine, (event) => event.type === "thread.reverted"); | ||
| 919 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); | ||
| 920 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ | ||
| 921 | |||
| 922 | numTurns: 1, | ||
| 923 | }); | ||
| 924 | }); | ||
| 925 | |||
| 926 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { | 793 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { |
| 927 | const harness = await createHarness(); | 794 | const harness = await createHarness(); |
| 928 | const createdAt = new Date().toISOString(); | 795 | const createdAt = new Date().toISOString(); |
| ... | @@ -1008,11 +875,11 @@ describe("CheckpointReactor", () => { | ... | @@ -1008,11 +875,11 @@ describe("CheckpointReactor", () => { |
| 1008 | 875 | ||
| 1009 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); | 876 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); |
| 1010 | expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ | 877 | expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ |
| 1011 | 878 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 1012 | numTurns: 1, | 879 | numTurns: 1, |
| 1013 | }); | 880 | }); |
| 1014 | expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ | 881 | expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ |
| 1015 | 882 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 1016 | numTurns: 1, | 883 | numTurns: 1, |
| 1017 | }); | 884 | }); |
| 1018 | }); | 885 | }); |
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+10| ... | @@ -102,6 +102,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -102,6 +102,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 102 | projectId: ProjectId.makeUnsafe("project-1"), | 102 | projectId: ProjectId.makeUnsafe("project-1"), |
| 103 | title: "Thread 1", | 103 | title: "Thread 1", |
| 104 | model: "gpt-5-codex", | 104 | model: "gpt-5-codex", |
| 105 | runtimeMode: "full-access", | ||
| 105 | branch: null, | 106 | branch: null, |
| 106 | worktreePath: null, | 107 | worktreePath: null, |
| 107 | createdAt: now, | 108 | createdAt: now, |
| ... | @@ -372,6 +373,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -372,6 +373,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 372 | projectId: ProjectId.makeUnsafe("project-clear-attachments"), | 373 | projectId: ProjectId.makeUnsafe("project-clear-attachments"), |
| 373 | title: "Thread Clear Attachments", | 374 | title: "Thread Clear Attachments", |
| 374 | model: "gpt-5-codex", | 375 | model: "gpt-5-codex", |
| 376 | runtimeMode: "full-access", | ||
| 375 | branch: null, | 377 | branch: null, |
| 376 | worktreePath: null, | 378 | worktreePath: null, |
| 377 | createdAt: now, | 379 | createdAt: now, |
| ... | @@ -498,6 +500,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -498,6 +500,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 498 | projectId: ProjectId.makeUnsafe("project-overwrite"), | 500 | projectId: ProjectId.makeUnsafe("project-overwrite"), |
| 499 | title: "Thread Overwrite", | 501 | title: "Thread Overwrite", |
| 500 | model: "gpt-5-codex", | 502 | model: "gpt-5-codex", |
| 503 | runtimeMode: "full-access", | ||
| 501 | branch: null, | 504 | branch: null, |
| 502 | worktreePath: null, | 505 | worktreePath: null, |
| 503 | createdAt: now, | 506 | createdAt: now, |
| ... | @@ -648,6 +651,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -648,6 +651,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 648 | projectId: ProjectId.makeUnsafe("project-rollback"), | 651 | projectId: ProjectId.makeUnsafe("project-rollback"), |
| 649 | title: "Thread Rollback", | 652 | title: "Thread Rollback", |
| 650 | model: "gpt-5-codex", | 653 | model: "gpt-5-codex", |
| 654 | runtimeMode: "full-access", | ||
| 651 | branch: null, | 655 | branch: null, |
| 652 | worktreePath: null, | 656 | worktreePath: null, |
| 653 | createdAt: now, | 657 | createdAt: now, |
| ... | @@ -778,6 +782,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -778,6 +782,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 778 | projectId: ProjectId.makeUnsafe("project-revert-files"), | 782 | projectId: ProjectId.makeUnsafe("project-revert-files"), |
| 779 | title: "Thread Revert Files", | 783 | title: "Thread Revert Files", |
| 780 | model: "gpt-5-codex", | 784 | model: "gpt-5-codex", |
| 785 | runtimeMode: "full-access", | ||
| 781 | branch: null, | 786 | branch: null, |
| 782 | worktreePath: null, | 787 | worktreePath: null, |
| 783 | createdAt: now, | 788 | createdAt: now, |
| ... | @@ -992,6 +997,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -992,6 +997,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 992 | projectId: ProjectId.makeUnsafe("project-delete-files"), | 997 | projectId: ProjectId.makeUnsafe("project-delete-files"), |
| 993 | title: "Thread Delete Files", | 998 | title: "Thread Delete Files", |
| 994 | model: "gpt-5-codex", | 999 | model: "gpt-5-codex", |
| 1000 | runtimeMode: "full-access", | ||
| 995 | branch: null, | 1001 | branch: null, |
| 996 | worktreePath: null, | 1002 | worktreePath: null, |
| 997 | createdAt: now, | 1003 | createdAt: now, |
| ... | @@ -1154,6 +1160,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -1154,6 +1160,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1154 | projectId: ProjectId.makeUnsafe("project-a"), | 1160 | projectId: ProjectId.makeUnsafe("project-a"), |
| 1155 | title: "Thread A", | 1161 | title: "Thread A", |
| 1156 | model: "gpt-5-codex", | 1162 | model: "gpt-5-codex", |
| 1163 | runtimeMode: "full-access", | ||
| 1157 | branch: null, | 1164 | branch: null, |
| 1158 | worktreePath: null, | 1165 | worktreePath: null, |
| 1159 | createdAt: now, | 1166 | createdAt: now, |
| ... | @@ -1277,6 +1284,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -1277,6 +1284,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1277 | projectId: ProjectId.makeUnsafe("project-empty"), | 1284 | projectId: ProjectId.makeUnsafe("project-empty"), |
| 1278 | title: "Thread Empty", | 1285 | title: "Thread Empty", |
| 1279 | model: "gpt-5-codex", | 1286 | model: "gpt-5-codex", |
| 1287 | runtimeMode: "full-access", | ||
| 1280 | branch: null, | 1288 | branch: null, |
| 1281 | worktreePath: null, | 1289 | worktreePath: null, |
| 1282 | createdAt: now, | 1290 | createdAt: now, |
| ... | @@ -1413,6 +1421,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -1413,6 +1421,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1413 | projectId: ProjectId.makeUnsafe("project-conflict"), | 1421 | projectId: ProjectId.makeUnsafe("project-conflict"), |
| 1414 | title: "Thread Conflict", | 1422 | title: "Thread Conflict", |
| 1415 | model: "gpt-5-codex", | 1423 | model: "gpt-5-codex", |
| 1424 | runtimeMode: "full-access", | ||
| 1416 | branch: null, | 1425 | branch: null, |
| 1417 | worktreePath: null, | 1426 | worktreePath: null, |
| 1418 | createdAt: "2026-02-26T13:00:01.000Z", | 1427 | createdAt: "2026-02-26T13:00:01.000Z", |
| ... | @@ -1553,6 +1562,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { | ... | @@ -1553,6 +1562,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1553 | projectId: ProjectId.makeUnsafe("project-revert"), | 1562 | projectId: ProjectId.makeUnsafe("project-revert"), |
| 1554 | title: "Thread Revert", | 1563 | title: "Thread Revert", |
| 1555 | model: "gpt-5-codex", | 1564 | model: "gpt-5-codex", |
| 1565 | runtimeMode: "full-access", | ||
| 1556 | branch: null, | 1566 | branch: null, |
| 1557 | worktreePath: null, | 1567 | worktreePath: null, |
| 1558 | createdAt: "2026-02-26T12:00:01.000Z", | 1568 | createdAt: "2026-02-26T12:00:01.000Z", |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts-98| ... | @@ -94,7 +94,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -94,7 +94,6 @@ describe("ProviderCommandReactor", () => { |
| 94 | input !== null && | 94 | input !== null && |
| 95 | "provider" in input && | 95 | "provider" in input && |
| 96 | (input.provider === "codex" || | 96 | (input.provider === "codex" || |
| 97 | input.provider === "claudeCode" || | ||
| 98 | input.provider === "cursor") | 97 | input.provider === "cursor") |
| 99 | ? input.provider | 98 | ? input.provider |
| 100 | : "codex"; | 99 | : "codex"; |
| ... | @@ -385,43 +384,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -385,43 +384,6 @@ describe("ProviderCommandReactor", () => { |
| 385 | }); | 384 | }); |
| 386 | }); | 385 | }); |
| 387 | 386 | ||
| 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 | |||
| 425 | it("starts first turn with cursor provider when provider is specified", async () => { | 387 | it("starts first turn with cursor provider when provider is specified", async () => { |
| 426 | const harness = await createHarness(); | 388 | const harness = await createHarness(); |
| 427 | const now = new Date().toISOString(); | 389 | const now = new Date().toISOString(); |
| ... | @@ -695,66 +657,6 @@ describe("ProviderCommandReactor", () => { | ... | @@ -695,66 +657,6 @@ describe("ProviderCommandReactor", () => { |
| 695 | expect(thread?.session?.runtimeMode).toBe("approval-required"); | 657 | expect(thread?.session?.runtimeMode).toBe("approval-required"); |
| 696 | }); | 658 | }); |
| 697 | 659 | ||
| 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 | |||
| 758 | it("does not stop the active session when restart fails before rebind", async () => { | 660 | it("does not stop the active session when restart fails before rebind", async () => { |
| 759 | const harness = await createHarness(); | 661 | const harness = await createHarness(); |
| 760 | const now = new Date().toISOString(); | 662 | const now = new Date().toISOString(); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts-54| ... | @@ -437,60 +437,6 @@ describe("ProviderRuntimeIngestion", () => { | ... | @@ -437,60 +437,6 @@ describe("ProviderRuntimeIngestion", () => { |
| 437 | ); | 437 | ); |
| 438 | }); | 438 | }); |
| 439 | 439 | ||
| 440 | it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => { | ||
| 441 | const harness = await createHarness(); | ||
| 442 | const seededAt = new Date().toISOString(); | ||
| 443 | |||
| 444 | await Effect.runPromise( | ||
| 445 | harness.engine.dispatch({ | ||
| 446 | type: "thread.session.set", | ||
| 447 | commandId: CommandId.makeUnsafe("cmd-session-seed-claude-placeholder"), | ||
| 448 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 449 | session: { | ||
| 450 | threadId: ThreadId.makeUnsafe("thread-1"), | ||
| 451 | status: "ready", | ||
| 452 | providerName: "claudeCode", | ||
| 453 | runtimeMode: "approval-required", | ||
| 454 | activeTurnId: null, | ||
| 455 | updatedAt: seededAt, | ||
| 456 | lastError: null, | ||
| 457 | }, | ||
| 458 | createdAt: seededAt, | ||
| 459 | }), | ||
| 460 | ); | ||
| 461 | |||
| 462 | harness.emit({ | ||
| 463 | type: "turn.started", | ||
| 464 | eventId: asEventId("evt-turn-started-claude-placeholder"), | ||
| 465 | provider: "claudeCode", | ||
| 466 | createdAt: new Date().toISOString(), | ||
| 467 | threadId: asThreadId("thread-1"), | ||
| 468 | turnId: asTurnId("turn-claude-placeholder"), | ||
| 469 | }); | ||
| 470 | |||
| 471 | await waitForThread( | ||
| 472 | harness.engine, | ||
| 473 | (thread) => | ||
| 474 | thread.session?.status === "running" && | ||
| 475 | thread.session?.activeTurnId === "turn-claude-placeholder", | ||
| 476 | ); | ||
| 477 | |||
| 478 | harness.emit({ | ||
| 479 | type: "turn.completed", | ||
| 480 | eventId: asEventId("evt-turn-completed-claude-placeholder"), | ||
| 481 | provider: "claudeCode", | ||
| 482 | createdAt: new Date().toISOString(), | ||
| 483 | threadId: asThreadId("thread-1"), | ||
| 484 | turnId: asTurnId("turn-claude-placeholder"), | ||
| 485 | status: "completed", | ||
| 486 | }); | ||
| 487 | |||
| 488 | await waitForThread( | ||
| 489 | harness.engine, | ||
| 490 | (thread) => thread.session?.status === "ready" && thread.session?.activeTurnId === null, | ||
| 491 | ); | ||
| 492 | }); | ||
| 493 | |||
| 494 | it("ignores non-active turn completion when runtime omits thread id", async () => { | 440 | it("ignores non-active turn completion when runtime omits thread id", async () => { |
| 495 | const harness = await createHarness(); | 441 | const harness = await createHarness(); |
| 496 | const now = new Date().toISOString(); | 442 | const now = new Date().toISOString(); |
apps/server/src/orchestration/projector.test.ts+2| ... | @@ -75,6 +75,7 @@ describe("orchestration projector", () => { | ... | @@ -75,6 +75,7 @@ describe("orchestration projector", () => { |
| 75 | title: "demo", | 75 | title: "demo", |
| 76 | model: "gpt-5-codex", | 76 | model: "gpt-5-codex", |
| 77 | runtimeMode: "full-access", | 77 | runtimeMode: "full-access", |
| 78 | interactionMode: "default", | ||
| 78 | branch: null, | 79 | branch: null, |
| 79 | worktreePath: null, | 80 | worktreePath: null, |
| 80 | latestTurn: null, | 81 | latestTurn: null, |
| ... | @@ -82,6 +83,7 @@ describe("orchestration projector", () => { | ... | @@ -82,6 +83,7 @@ describe("orchestration projector", () => { |
| 82 | updatedAt: now, | 83 | updatedAt: now, |
| 83 | deletedAt: null, | 84 | deletedAt: null, |
| 84 | messages: [], | 85 | messages: [], |
| 86 | proposedPlans: [], | ||
| 85 | activities: [], | 87 | activities: [], |
| 86 | checkpoints: [], | 88 | checkpoints: [], |
| 87 | session: null, | 89 | session: null, |
apps/server/src/provider/Layers/ProviderService.test.ts+2-29| ... | @@ -216,15 +216,12 @@ const sleep = (ms: number) => | ... | @@ -216,15 +216,12 @@ const sleep = (ms: number) => |
| 216 | 216 | ||
| 217 | function makeProviderServiceLayer() { | 217 | function makeProviderServiceLayer() { |
| 218 | const codex = makeFakeCodexAdapter(); | 218 | const codex = makeFakeCodexAdapter(); |
| 219 | const claude = makeFakeCodexAdapter("claudeCode"); | ||
| 220 | const registry: typeof ProviderAdapterRegistry.Service = { | 219 | const registry: typeof ProviderAdapterRegistry.Service = { |
| 221 | getByProvider: (provider) => | 220 | getByProvider: (provider) => |
| 222 | provider === "codex" | 221 | provider === "codex" |
| 223 | ? Effect.succeed(codex.adapter) | 222 | ? Effect.succeed(codex.adapter) |
| 224 | : provider === "claudeCode" | 223 | : Effect.fail(new ProviderUnsupportedError({ provider })), |
| 225 | ? Effect.succeed(claude.adapter) | 224 | listProviders: () => Effect.succeed(["codex"]), |
| 226 | : Effect.fail(new ProviderUnsupportedError({ provider })), | ||
| 227 | listProviders: () => Effect.succeed(["codex", "claudeCode"]), | ||
| 228 | }; | 225 | }; |
| 229 | 226 | ||
| 230 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 227 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| ... | @@ -247,7 +244,6 @@ function makeProviderServiceLayer() { | ... | @@ -247,7 +244,6 @@ function makeProviderServiceLayer() { |
| 247 | 244 | ||
| 248 | return { | 245 | return { |
| 249 | codex, | 246 | codex, |
| 250 | claude, | ||
| 251 | layer, | 247 | layer, |
| 252 | }; | 248 | }; |
| 253 | } | 249 | } |
| ... | @@ -496,29 +492,6 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -496,29 +492,6 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 496 | }), | 492 | }), |
| 497 | ); | 493 | ); |
| 498 | 494 | ||
| 499 | it.effect("routes explicit claudeCode provider session starts to the claude adapter", () => | ||
| 500 | Effect.gen(function* () { | ||
| 501 | const provider = yield* ProviderService; | ||
| 502 | |||
| 503 | const session = yield* provider.startSession(asThreadId("thread-claude"), { | ||
| 504 | provider: "claudeCode", | ||
| 505 | threadId: asThreadId("thread-claude"), | ||
| 506 | cwd: "/tmp/project-claude", | ||
| 507 | runtimeMode: "full-access", | ||
| 508 | }); | ||
| 509 | |||
| 510 | assert.equal(session.provider, "claudeCode"); | ||
| 511 | assert.equal(routing.claude.startSession.mock.calls.length, 1); | ||
| 512 | const startInput = routing.claude.startSession.mock.calls[0]?.[0]; | ||
| 513 | assert.equal(typeof startInput === "object" && startInput !== null, true); | ||
| 514 | if (startInput && typeof startInput === "object") { | ||
| 515 | const startPayload = startInput as { provider?: string; cwd?: string }; | ||
| 516 | assert.equal(startPayload.provider, "claudeCode"); | ||
| 517 | assert.equal(startPayload.cwd, "/tmp/project-claude"); | ||
| 518 | } | ||
| 519 | }), | ||
| 520 | ); | ||
| 521 | |||
| 522 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => | 495 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => |
| 523 | Effect.gen(function* () { | 496 | Effect.gen(function* () { |
| 524 | const provider = yield* ProviderService; | 497 | const provider = yield* ProviderService; |