| 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 | 57 | cwd: string, |
| 58 | 58 | hasSession = true, |
| 59 | 59 | sessionCwd = cwd, |
| 60 | providerName: "codex" | "claudeCode" = "codex", | |
| 60 | providerName: ProviderSession["provider"] = "codex", | |
| 61 | 61 | ) { |
| 62 | 62 | const now = new Date().toISOString(); |
| 63 | 63 | const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>()); |
| ... | ... | @@ -235,7 +235,6 @@ describe("CheckpointReactor", () => { |
| 235 | 235 | readonly projectWorkspaceRoot?: string; |
| 236 | 236 | readonly threadWorktreePath?: string | null; |
| 237 | 237 | readonly providerSessionCwd?: string; |
| 238 | readonly providerName?: "codex" | "claudeCode"; | |
| 239 | 238 | }) { |
| 240 | 239 | const cwd = createGitRepository(); |
| 241 | 240 | tempDirs.push(cwd); |
| ... | ... | @@ -243,7 +242,7 @@ describe("CheckpointReactor", () => { |
| 243 | 242 | cwd, |
| 244 | 243 | options?.hasSession ?? true, |
| 245 | 244 | options?.providerSessionCwd ?? cwd, |
| 246 | options?.providerName ?? "codex", | |
| 245 | "codex", | |
| 247 | 246 | ); |
| 248 | 247 | const orchestrationLayer = OrchestrationEngineLive.pipe( |
| 249 | 248 | Layer.provide(OrchestrationProjectionPipelineLive), |
| ... | ... | @@ -476,69 +475,6 @@ describe("CheckpointReactor", () => { |
| 476 | 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 | 478 | it("appends capture failure activity when turn diff summary cannot be derived", async () => { |
| 543 | 479 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 544 | 480 | const createdAt = new Date().toISOString(); |
| ... | ... | @@ -845,7 +781,7 @@ describe("CheckpointReactor", () => { |
| 845 | 781 | expect(thread.checkpoints[0]?.checkpointTurnCount).toBe(1); |
| 846 | 782 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1); |
| 847 | 783 | expect(harness.provider.rollbackConversation).toHaveBeenCalledWith({ |
| 848 | ||
| 784 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 849 | 785 | numTurns: 1, |
| 850 | 786 | }); |
| 851 | 787 | expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v2\n"); |
| ... | ... | @@ -854,75 +790,6 @@ describe("CheckpointReactor", () => { |
| 854 | 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 | 793 | it("processes consecutive revert requests with deterministic rollback sequencing", async () => { |
| 927 | 794 | const harness = await createHarness(); |
| 928 | 795 | const createdAt = new Date().toISOString(); |
| ... | ... | @@ -1008,11 +875,11 @@ describe("CheckpointReactor", () => { |
| 1008 | 875 | |
| 1009 | 876 | expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(2); |
| 1010 | 877 | expect(harness.provider.rollbackConversation.mock.calls[0]?.[0]).toEqual({ |
| 1011 | ||
| 878 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 1012 | 879 | numTurns: 1, |
| 1013 | 880 | }); |
| 1014 | 881 | expect(harness.provider.rollbackConversation.mock.calls[1]?.[0]).toEqual({ |
| 1015 | ||
| 882 | threadId: ThreadId.makeUnsafe("thread-1"), | |
| 1016 | 883 | numTurns: 1, |
| 1017 | 884 | }); |
| 1018 | 885 | }); |
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+10| ... | ... | @@ -102,6 +102,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 102 | 102 | projectId: ProjectId.makeUnsafe("project-1"), |
| 103 | 103 | title: "Thread 1", |
| 104 | 104 | model: "gpt-5-codex", |
| 105 | runtimeMode: "full-access", | |
| 105 | 106 | branch: null, |
| 106 | 107 | worktreePath: null, |
| 107 | 108 | createdAt: now, |
| ... | ... | @@ -372,6 +373,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 372 | 373 | projectId: ProjectId.makeUnsafe("project-clear-attachments"), |
| 373 | 374 | title: "Thread Clear Attachments", |
| 374 | 375 | model: "gpt-5-codex", |
| 376 | runtimeMode: "full-access", | |
| 375 | 377 | branch: null, |
| 376 | 378 | worktreePath: null, |
| 377 | 379 | createdAt: now, |
| ... | ... | @@ -498,6 +500,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 498 | 500 | projectId: ProjectId.makeUnsafe("project-overwrite"), |
| 499 | 501 | title: "Thread Overwrite", |
| 500 | 502 | model: "gpt-5-codex", |
| 503 | runtimeMode: "full-access", | |
| 501 | 504 | branch: null, |
| 502 | 505 | worktreePath: null, |
| 503 | 506 | createdAt: now, |
| ... | ... | @@ -648,6 +651,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 648 | 651 | projectId: ProjectId.makeUnsafe("project-rollback"), |
| 649 | 652 | title: "Thread Rollback", |
| 650 | 653 | model: "gpt-5-codex", |
| 654 | runtimeMode: "full-access", | |
| 651 | 655 | branch: null, |
| 652 | 656 | worktreePath: null, |
| 653 | 657 | createdAt: now, |
| ... | ... | @@ -778,6 +782,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 778 | 782 | projectId: ProjectId.makeUnsafe("project-revert-files"), |
| 779 | 783 | title: "Thread Revert Files", |
| 780 | 784 | model: "gpt-5-codex", |
| 785 | runtimeMode: "full-access", | |
| 781 | 786 | branch: null, |
| 782 | 787 | worktreePath: null, |
| 783 | 788 | createdAt: now, |
| ... | ... | @@ -992,6 +997,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 992 | 997 | projectId: ProjectId.makeUnsafe("project-delete-files"), |
| 993 | 998 | title: "Thread Delete Files", |
| 994 | 999 | model: "gpt-5-codex", |
| 1000 | runtimeMode: "full-access", | |
| 995 | 1001 | branch: null, |
| 996 | 1002 | worktreePath: null, |
| 997 | 1003 | createdAt: now, |
| ... | ... | @@ -1154,6 +1160,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1154 | 1160 | projectId: ProjectId.makeUnsafe("project-a"), |
| 1155 | 1161 | title: "Thread A", |
| 1156 | 1162 | model: "gpt-5-codex", |
| 1163 | runtimeMode: "full-access", | |
| 1157 | 1164 | branch: null, |
| 1158 | 1165 | worktreePath: null, |
| 1159 | 1166 | createdAt: now, |
| ... | ... | @@ -1277,6 +1284,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1277 | 1284 | projectId: ProjectId.makeUnsafe("project-empty"), |
| 1278 | 1285 | title: "Thread Empty", |
| 1279 | 1286 | model: "gpt-5-codex", |
| 1287 | runtimeMode: "full-access", | |
| 1280 | 1288 | branch: null, |
| 1281 | 1289 | worktreePath: null, |
| 1282 | 1290 | createdAt: now, |
| ... | ... | @@ -1413,6 +1421,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1413 | 1421 | projectId: ProjectId.makeUnsafe("project-conflict"), |
| 1414 | 1422 | title: "Thread Conflict", |
| 1415 | 1423 | model: "gpt-5-codex", |
| 1424 | runtimeMode: "full-access", | |
| 1416 | 1425 | branch: null, |
| 1417 | 1426 | worktreePath: null, |
| 1418 | 1427 | createdAt: "2026-02-26T13:00:01.000Z", |
| ... | ... | @@ -1553,6 +1562,7 @@ projectionLayer("OrchestrationProjectionPipeline", (it) => { |
| 1553 | 1562 | projectId: ProjectId.makeUnsafe("project-revert"), |
| 1554 | 1563 | title: "Thread Revert", |
| 1555 | 1564 | model: "gpt-5-codex", |
| 1565 | runtimeMode: "full-access", | |
| 1556 | 1566 | branch: null, |
| 1557 | 1567 | worktreePath: null, |
| 1558 | 1568 | createdAt: "2026-02-26T12:00:01.000Z", |
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts-98| ... | ... | @@ -94,7 +94,6 @@ describe("ProviderCommandReactor", () => { |
| 94 | 94 | input !== null && |
| 95 | 95 | "provider" in input && |
| 96 | 96 | (input.provider === "codex" || |
| 97 | input.provider === "claudeCode" || | |
| 98 | 97 | input.provider === "cursor") |
| 99 | 98 | ? input.provider |
| 100 | 99 | : "codex"; |
| ... | ... | @@ -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 | 387 | it("starts first turn with cursor provider when provider is specified", async () => { |
| 426 | 388 | const harness = await createHarness(); |
| 427 | 389 | const now = new Date().toISOString(); |
| ... | ... | @@ -695,66 +657,6 @@ describe("ProviderCommandReactor", () => { |
| 695 | 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 | 660 | it("does not stop the active session when restart fails before rebind", async () => { |
| 759 | 661 | const harness = await createHarness(); |
| 760 | 662 | const now = new Date().toISOString(); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts-54| ... | ... | @@ -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 | 440 | it("ignores non-active turn completion when runtime omits thread id", async () => { |
| 495 | 441 | const harness = await createHarness(); |
| 496 | 442 | const now = new Date().toISOString(); |
apps/server/src/orchestration/projector.test.ts+2| ... | ... | @@ -75,6 +75,7 @@ describe("orchestration projector", () => { |
| 75 | 75 | title: "demo", |
| 76 | 76 | model: "gpt-5-codex", |
| 77 | 77 | runtimeMode: "full-access", |
| 78 | interactionMode: "default", | |
| 78 | 79 | branch: null, |
| 79 | 80 | worktreePath: null, |
| 80 | 81 | latestTurn: null, |
| ... | ... | @@ -82,6 +83,7 @@ describe("orchestration projector", () => { |
| 82 | 83 | updatedAt: now, |
| 83 | 84 | deletedAt: null, |
| 84 | 85 | messages: [], |
| 86 | proposedPlans: [], | |
| 85 | 87 | activities: [], |
| 86 | 88 | checkpoints: [], |
| 87 | 89 | session: null, |
apps/server/src/provider/Layers/ProviderService.test.ts+2-29| ... | ... | @@ -216,15 +216,12 @@ const sleep = (ms: number) => |
| 216 | 216 | |
| 217 | 217 | function makeProviderServiceLayer() { |
| 218 | 218 | const codex = makeFakeCodexAdapter(); |
| 219 | const claude = makeFakeCodexAdapter("claudeCode"); | |
| 220 | 219 | const registry: typeof ProviderAdapterRegistry.Service = { |
| 221 | 220 | getByProvider: (provider) => |
| 222 | 221 | provider === "codex" |
| 223 | 222 | ? Effect.succeed(codex.adapter) |
| 224 | : provider === "claudeCode" | |
| 225 | ? Effect.succeed(claude.adapter) | |
| 226 | : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 227 | listProviders: () => Effect.succeed(["codex", "claudeCode"]), | |
| 223 | : Effect.fail(new ProviderUnsupportedError({ provider })), | |
| 224 | listProviders: () => Effect.succeed(["codex"]), | |
| 228 | 225 | }; |
| 229 | 226 | |
| 230 | 227 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| ... | ... | @@ -247,7 +244,6 @@ function makeProviderServiceLayer() { |
| 247 | 244 | |
| 248 | 245 | return { |
| 249 | 246 | codex, |
| 250 | claude, | |
| 251 | 247 | layer, |
| 252 | 248 | }; |
| 253 | 249 | } |
| ... | ... | @@ -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 | 495 | it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => |
| 523 | 496 | Effect.gen(function* () { |
| 524 | 497 | const provider = yield* ProviderService; |