| ... | ... | @@ -305,7 +305,10 @@ describe("ThreadSettlementReactor", () => { |
| 305 | 305 | snapshot: makeSnapshot( |
| 306 | 306 | [ |
| 307 | 307 | makeThread("inactive", { branch: "inactive-feature" }), |
| 308 | | makeThread("closed-pr", { linkedPullRequest }), |
| 308 | makeThread("closed-pr", { |
| 309 | linkedPullRequest, |
| 310 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 311 | }), |
| 309 | 312 | ...skipped, |
| 310 | 313 | ], |
| 311 | 314 | [makeProject(), makeProject(LINKED_PROJECT_ID, "/workspace/linked")], |
| ... | ... | @@ -337,7 +340,7 @@ describe("ThreadSettlementReactor", () => { |
| 337 | 340 | { |
| 338 | 341 | threadId: ThreadId.make("closed-pr"), |
| 339 | 342 | snapshotSequence: 1, |
| 340 | | settledAt: "2026-08-20T00:00:00.000Z", |
| 343 | settledAt: "2026-08-27T00:00:00.000Z", |
| 341 | 344 | }, |
| 342 | 345 | { |
| 343 | 346 | threadId: ThreadId.make("inactive"), |
| ... | ... | @@ -346,9 +349,7 @@ describe("ThreadSettlementReactor", () => { |
| 346 | 349 | }, |
| 347 | 350 | ], |
| 348 | 351 | ); |
| 349 | | assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), [ |
| 350 | | { cwd: "/workspace/project", branch: "inactive-feature" }, |
| 351 | | ]); |
| 352 | assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); |
| 352 | 353 | assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), [ |
| 353 | 354 | { projectId: LINKED_PROJECT_ID, repository: "owner/repository", number: 42 }, |
| 354 | 355 | ]); |
| ... | ... | @@ -589,7 +590,12 @@ describe("ThreadSettlementReactor", () => { |
| 589 | 590 | const releaseLaterLookup = yield* Deferred.make<void>(); |
| 590 | 591 | const lookupCount = yield* Ref.make(0); |
| 591 | 592 | const fixture = yield* makeHarness({ |
| 592 | | snapshot: makeSnapshot([makeThread("settings-thread", { branch: "saved-feature" })]), |
| 593 | snapshot: makeSnapshot([ |
| 594 | makeThread("settings-thread", { |
| 595 | branch: "saved-feature", |
| 596 | latestUserMessageAt: "2026-08-28T00:00:00.000Z", |
| 597 | }), |
| 598 | ]), |
| 593 | 599 | settings: { |
| 594 | 600 | ...DEFAULT_SERVER_SETTINGS, |
| 595 | 601 | sidebarAutoSettleAfterDays: null, |
| ... | ... | @@ -661,6 +667,7 @@ describe("ThreadSettlementReactor", () => { |
| 661 | 667 | snapshot: makeSnapshot( |
| 662 | 668 | [ |
| 663 | 669 | makeThread("lookup-failed", { |
| 670 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 664 | 671 | linkedPullRequest: { |
| 665 | 672 | projectId: LINKED_PROJECT_ID, |
| 666 | 673 | repository: "owner/repository", |
| ... | ... | @@ -695,6 +702,121 @@ describe("ThreadSettlementReactor", () => { |
| 695 | 702 | ), |
| 696 | 703 | ); |
| 697 | 704 | |
| 705 | it.effect("settles inactive linked and branch threads without reading an unavailable host", () => |
| 706 | Effect.scoped( |
| 707 | Effect.gen(function* () { |
| 708 | yield* TestClock.setTime(Date.parse(NOW)); |
| 709 | const fixture = yield* makeHarness({ |
| 710 | snapshot: makeSnapshot([ |
| 711 | makeThread("inactive-linked", { |
| 712 | linkedPullRequest: { |
| 713 | projectId: PROJECT_ID, |
| 714 | repository: "owner/repository", |
| 715 | number: 42, |
| 716 | url: "https://example.test/owner/repository/pull/42", |
| 717 | }, |
| 718 | }), |
| 719 | makeThread("inactive-branch", { |
| 720 | branch: "saved-feature", |
| 721 | latestUserMessageAt: "2026-08-21T00:00:00.000Z", |
| 722 | }), |
| 723 | ]), |
| 724 | branchPullRequest: () => Effect.die(new Error("host unavailable")), |
| 725 | pullRequestSummary: () => |
| 726 | Effect.fail( |
| 727 | new PullRequestOperationError({ |
| 728 | operation: "summary", |
| 729 | detail: "host unavailable", |
| 730 | }), |
| 731 | ), |
| 732 | }); |
| 733 | |
| 734 | yield* Effect.gen(function* () { |
| 735 | const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; |
| 736 | yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); |
| 737 | |
| 738 | assert.deepStrictEqual( |
| 739 | (yield* Ref.get(fixture.commands)) |
| 740 | .map(({ threadId, snapshotSequence, settledAt }) => ({ |
| 741 | threadId, |
| 742 | snapshotSequence, |
| 743 | settledAt, |
| 744 | })) |
| 745 | .sort((left, right) => left.threadId.localeCompare(right.threadId)), |
| 746 | [ |
| 747 | { |
| 748 | threadId: ThreadId.make("inactive-branch"), |
| 749 | snapshotSequence: 1, |
| 750 | settledAt: "2026-08-21T00:00:00.000Z", |
| 751 | }, |
| 752 | { |
| 753 | threadId: ThreadId.make("inactive-linked"), |
| 754 | snapshotSequence: 1, |
| 755 | settledAt: "2026-08-20T00:00:00.000Z", |
| 756 | }, |
| 757 | ], |
| 758 | ); |
| 759 | assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); |
| 760 | assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); |
| 761 | }).pipe(Effect.provide(fixture.layer)); |
| 762 | }), |
| 763 | ), |
| 764 | ); |
| 765 | |
| 766 | it.effect("settles an inactive thread before its shared pull request lookup completes", () => |
| 767 | Effect.scoped( |
| 768 | Effect.gen(function* () { |
| 769 | yield* TestClock.setTime(Date.parse(NOW)); |
| 770 | const lookupStarted = yield* Deferred.make<void>(); |
| 771 | const releaseLookup = yield* Deferred.make<void>(); |
| 772 | const linkedPullRequest = { |
| 773 | projectId: PROJECT_ID, |
| 774 | repository: "owner/repository", |
| 775 | number: 42, |
| 776 | url: "https://example.test/owner/repository/pull/42", |
| 777 | } as const; |
| 778 | const fixture = yield* makeHarness({ |
| 779 | snapshot: makeSnapshot([ |
| 780 | makeThread("recent-linked", { |
| 781 | linkedPullRequest, |
| 782 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 783 | }), |
| 784 | makeThread("inactive-linked", { linkedPullRequest }), |
| 785 | ]), |
| 786 | pullRequestSummary: () => |
| 787 | Deferred.succeed(lookupStarted, undefined).pipe( |
| 788 | Effect.andThen(Deferred.await(releaseLookup)), |
| 789 | Effect.andThen( |
| 790 | Effect.fail( |
| 791 | new PullRequestOperationError({ |
| 792 | operation: "summary", |
| 793 | detail: "host unavailable", |
| 794 | }), |
| 795 | ), |
| 796 | ), |
| 797 | ), |
| 798 | }); |
| 799 | |
| 800 | yield* Effect.gen(function* () { |
| 801 | const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; |
| 802 | yield* reactor.start(); |
| 803 | yield* Deferred.succeed(fixture.activation, undefined); |
| 804 | yield* Deferred.await(lookupStarted); |
| 805 | |
| 806 | assert.deepStrictEqual( |
| 807 | (yield* Ref.get(fixture.commands)).map(({ threadId }) => threadId), |
| 808 | [ThreadId.make("inactive-linked")], |
| 809 | ); |
| 810 | |
| 811 | yield* Deferred.succeed(releaseLookup, undefined); |
| 812 | yield* reactor.drain; |
| 813 | assert.strictEqual((yield* Ref.get(fixture.commands)).length, 1); |
| 814 | assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); |
| 815 | }).pipe(Effect.provide(fixture.layer)); |
| 816 | }), |
| 817 | ), |
| 818 | ); |
| 819 | |
| 698 | 820 | it.effect("keeps threads active when their pull request project is unavailable", () => |
| 699 | 821 | Effect.scoped( |
| 700 | 822 | Effect.gen(function* () { |
| ... | ... | @@ -712,7 +834,10 @@ describe("ThreadSettlementReactor", () => { |
| 712 | 834 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 713 | 835 | linkedPullRequest, |
| 714 | 836 | }), |
| 715 | | makeThread("missing-branch-project", { branch: "saved-feature" }), |
| 837 | makeThread("missing-branch-project", { |
| 838 | branch: "saved-feature", |
| 839 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 840 | }), |
| 716 | 841 | ], |
| 717 | 842 | [makeProject(LINKED_PROJECT_ID, "/workspace/linked")], |
| 718 | 843 | ), |
| ... | ... | @@ -750,13 +875,21 @@ describe("ThreadSettlementReactor", () => { |
| 750 | 875 | makeThread("branch-one", { |
| 751 | 876 | branch: "saved-feature", |
| 752 | 877 | worktreePath: "/deleted/worktree-one", |
| 878 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 753 | 879 | }), |
| 754 | 880 | makeThread("branch-two", { |
| 755 | 881 | branch: "saved-feature", |
| 756 | 882 | worktreePath: "/deleted/worktree-two", |
| 883 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 884 | }), |
| 885 | makeThread("linked-one", { |
| 886 | linkedPullRequest, |
| 887 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 888 | }), |
| 889 | makeThread("linked-two", { |
| 890 | linkedPullRequest, |
| 891 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 757 | 892 | }), |
| 758 | | makeThread("linked-one", { linkedPullRequest }), |
| 759 | | makeThread("linked-two", { linkedPullRequest }), |
| 760 | 893 | ], |
| 761 | 894 | [ |
| 762 | 895 | makeProject(PROJECT_ID, "/workspace/project-root"), |
| ... | ... | @@ -803,10 +936,12 @@ describe("ThreadSettlementReactor", () => { |
| 803 | 936 | makeThread("live-worktree", { |
| 804 | 937 | branch: "feature/live", |
| 805 | 938 | worktreePath: "/workspace/project-root/.worktrees/live", |
| 939 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 806 | 940 | }), |
| 807 | 941 | makeThread("deleted-worktree", { |
| 808 | 942 | branch: "feature/deleted", |
| 809 | 943 | worktreePath: "/workspace/project-root/.worktrees/deleted", |
| 944 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 810 | 945 | }), |
| 811 | 946 | ], |
| 812 | 947 | [makeProject(PROJECT_ID, "/workspace/project-root")], |