| ... | @@ -305,7 +305,10 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -305,7 +305,10 @@ describe("ThreadSettlementReactor", () => { |
| 305 | snapshot: makeSnapshot( | 305 | snapshot: makeSnapshot( |
| 306 | [ | 306 | [ |
| 307 | makeThread("inactive", { branch: "inactive-feature" }), | 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 | ...skipped, | 312 | ...skipped, |
| 310 | ], | 313 | ], |
| 311 | [makeProject(), makeProject(LINKED_PROJECT_ID, "/workspace/linked")], | 314 | [makeProject(), makeProject(LINKED_PROJECT_ID, "/workspace/linked")], |
| ... | @@ -337,7 +340,7 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -337,7 +340,7 @@ describe("ThreadSettlementReactor", () => { |
| 337 | { | 340 | { |
| 338 | threadId: ThreadId.make("closed-pr"), | 341 | threadId: ThreadId.make("closed-pr"), |
| 339 | snapshotSequence: 1, | 342 | snapshotSequence: 1, |
| 340 | settledAt: "2026-08-20T00:00:00.000Z", | 343 | settledAt: "2026-08-27T00:00:00.000Z", |
| 341 | }, | 344 | }, |
| 342 | { | 345 | { |
| 343 | threadId: ThreadId.make("inactive"), | 346 | threadId: ThreadId.make("inactive"), |
| ... | @@ -346,9 +349,7 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -346,9 +349,7 @@ describe("ThreadSettlementReactor", () => { |
| 346 | }, | 349 | }, |
| 347 | ], | 350 | ], |
| 348 | ); | 351 | ); |
| 349 | assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), [ | 352 | assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); |
| 350 | { cwd: "/workspace/project", branch: "inactive-feature" }, | | |
| 351 | ]); | | |
| 352 | assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), [ | 353 | assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), [ |
| 353 | { projectId: LINKED_PROJECT_ID, repository: "owner/repository", number: 42 }, | 354 | { projectId: LINKED_PROJECT_ID, repository: "owner/repository", number: 42 }, |
| 354 | ]); | 355 | ]); |
| ... | @@ -589,7 +590,12 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -589,7 +590,12 @@ describe("ThreadSettlementReactor", () => { |
| 589 | const releaseLaterLookup = yield* Deferred.make<void>(); | 590 | const releaseLaterLookup = yield* Deferred.make<void>(); |
| 590 | const lookupCount = yield* Ref.make(0); | 591 | const lookupCount = yield* Ref.make(0); |
| 591 | const fixture = yield* makeHarness({ | 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 | settings: { | 599 | settings: { |
| 594 | ...DEFAULT_SERVER_SETTINGS, | 600 | ...DEFAULT_SERVER_SETTINGS, |
| 595 | sidebarAutoSettleAfterDays: null, | 601 | sidebarAutoSettleAfterDays: null, |
| ... | @@ -661,6 +667,7 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -661,6 +667,7 @@ describe("ThreadSettlementReactor", () => { |
| 661 | snapshot: makeSnapshot( | 667 | snapshot: makeSnapshot( |
| 662 | [ | 668 | [ |
| 663 | makeThread("lookup-failed", { | 669 | makeThread("lookup-failed", { |
| | 670 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 664 | linkedPullRequest: { | 671 | linkedPullRequest: { |
| 665 | projectId: LINKED_PROJECT_ID, | 672 | projectId: LINKED_PROJECT_ID, |
| 666 | repository: "owner/repository", | 673 | repository: "owner/repository", |
| ... | @@ -695,6 +702,121 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -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 | it.effect("keeps threads active when their pull request project is unavailable", () => | 820 | it.effect("keeps threads active when their pull request project is unavailable", () => |
| 699 | Effect.scoped( | 821 | Effect.scoped( |
| 700 | Effect.gen(function* () { | 822 | Effect.gen(function* () { |
| ... | @@ -712,7 +834,10 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -712,7 +834,10 @@ describe("ThreadSettlementReactor", () => { |
| 712 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", | 834 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 713 | linkedPullRequest, | 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 | [makeProject(LINKED_PROJECT_ID, "/workspace/linked")], | 842 | [makeProject(LINKED_PROJECT_ID, "/workspace/linked")], |
| 718 | ), | 843 | ), |
| ... | @@ -750,13 +875,21 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -750,13 +875,21 @@ describe("ThreadSettlementReactor", () => { |
| 750 | makeThread("branch-one", { | 875 | makeThread("branch-one", { |
| 751 | branch: "saved-feature", | 876 | branch: "saved-feature", |
| 752 | worktreePath: "/deleted/worktree-one", | 877 | worktreePath: "/deleted/worktree-one", |
| | 878 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 753 | }), | 879 | }), |
| 754 | makeThread("branch-two", { | 880 | makeThread("branch-two", { |
| 755 | branch: "saved-feature", | 881 | branch: "saved-feature", |
| 756 | worktreePath: "/deleted/worktree-two", | 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 | makeProject(PROJECT_ID, "/workspace/project-root"), | 895 | makeProject(PROJECT_ID, "/workspace/project-root"), |
| ... | @@ -803,10 +936,12 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -803,10 +936,12 @@ describe("ThreadSettlementReactor", () => { |
| 803 | makeThread("live-worktree", { | 936 | makeThread("live-worktree", { |
| 804 | branch: "feature/live", | 937 | branch: "feature/live", |
| 805 | worktreePath: "/workspace/project-root/.worktrees/live", | 938 | worktreePath: "/workspace/project-root/.worktrees/live", |
| | 939 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 806 | }), | 940 | }), |
| 807 | makeThread("deleted-worktree", { | 941 | makeThread("deleted-worktree", { |
| 808 | branch: "feature/deleted", | 942 | branch: "feature/deleted", |
| 809 | worktreePath: "/workspace/project-root/.worktrees/deleted", | 943 | worktreePath: "/workspace/project-root/.worktrees/deleted", |
| | 944 | latestUserMessageAt: "2026-08-27T00:00:00.000Z", |
| 810 | }), | 945 | }), |
| 811 | ], | 946 | ], |
| 812 | [makeProject(PROJECT_ID, "/workspace/project-root")], | 947 | [makeProject(PROJECT_ID, "/workspace/project-root")], |