authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-06 16:50:57-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 16:50:57-07:00
logd3d4ea42ee569f20d8a79562974355f659dc46d9
tree65261ba136b70d1e2e1e721183d8be9b147f61dd
parent95139254ba98fb8ad12a001ebcbc9d4b833ead19
signature Signed by PGP key B5690EEEBB952194

fix(server): skip disabled settlement lookups (#10424)

Co-authored-by: adityagarud <garudaditya079@gmail.com>

2 files changed, 72 insertions(+), 7 deletions(-)

apps/server/src/orchestration/ThreadSettlementReactor.test.ts+68-7
......@@ -167,6 +167,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
167167 const snapshotReadCount = yield* Ref.make(0);
168168 const snapshotReads = yield* Queue.unbounded<number>();
169169 const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS);
170 const settingsReads = yield* Queue.unbounded<ServerSettings>();
170171 const settingsChanges = yield* PubSub.unbounded<ServerSettings>();
171172 const mergedPullRequests = yield* PubSub.unbounded<PullRequestMergeEvent>();
172173 const commands = yield* Ref.make<ReadonlyArray<AutoSettleCommand>>([]);
......@@ -226,7 +227,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
226227 const serverSettings = ServerSettingsService.of({
227228 start: Effect.void,
228229 ready: Effect.void,
229 getSettings: Ref.get(settings),
230 getSettings: Ref.get(settings).pipe(Effect.tap((value) => Queue.offer(settingsReads, value))),
230231 updateSettings,
231232 streamChanges: Stream.fromPubSub(settingsChanges),
232233 subscribeChanges: PubSub.subscribe(settingsChanges).pipe(
......@@ -271,6 +272,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
271272 snapshots,
272273 snapshotReadCount,
273274 snapshotReads,
275 settingsReads,
274276 commands,
275277 branchCalls,
276278 summaryCalls,
......@@ -376,6 +378,62 @@ describe("ThreadSettlementReactor", () => {
376378 ),
377379 );
378380
381 it.effect("skips PR work on startup, timer and merge sweeps when settlement is disabled", () =>
382 Effect.scoped(
383 Effect.gen(function* () {
384 yield* TestClock.setTime(Date.parse(NOW));
385 const fixture = yield* makeHarness({
386 snapshot: makeSnapshot([
387 makeThread("branch-thread", { branch: "feature" }),
388 makeThread("linked-thread", {
389 linkedPullRequest: {
390 projectId: PROJECT_ID,
391 repository: "owner/repository",
392 number: 42,
393 url: "https://example.test/owner/repository/pull/42",
394 },
395 }),
396 ]),
397 settings: {
398 ...DEFAULT_SERVER_SETTINGS,
399 sidebarAutoSettleAfterDays: null,
400 sidebarAutoSettleOnMerge: false,
401 },
402 });
403
404 yield* Effect.gen(function* () {
405 const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor;
406 yield* reactor.start();
407 yield* Queue.take(fixture.settingsReads);
408 yield* Deferred.succeed(fixture.activation, undefined);
409 yield* Queue.take(fixture.settingsReads);
410 yield* reactor.drain;
411 yield* TestClock.adjust("1 minute");
412 yield* Queue.take(fixture.settingsReads);
413 yield* reactor.drain;
414 yield* fixture.publishMerge;
415 yield* Queue.take(fixture.settingsReads);
416 yield* reactor.drain;
417
418 assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []);
419 assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []);
420 assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []);
421 assert.deepStrictEqual(yield* Ref.get(fixture.commands), []);
422 assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 0);
423
424 yield* fixture.updateSettings({ sidebarAutoSettleAfterDays: 1 });
425 yield* Queue.take(fixture.snapshotReads);
426 yield* reactor.drain;
427 // Inactive threads settle without a PR lookup, so only the dispatches prove work resumed.
428 assert.deepStrictEqual(
429 (yield* Ref.get(fixture.commands)).map((command) => command.threadId).toSorted(),
430 [ThreadId.make("branch-thread"), ThreadId.make("linked-thread")],
431 );
432 }).pipe(Effect.provide(fixture.layer));
433 }),
434 ),
435 );
436
379437 it.effect("starts without clients and skips protected threads before pull request lookup", () =>
380438 Effect.scoped(
381439 Effect.gen(function* () {
......@@ -694,14 +752,14 @@ describe("ThreadSettlementReactor", () => {
694752 Effect.tap((count) =>
695753 count === 1
696754 ? Deferred.succeed(firstLookupStarted, undefined)
697 : count === 3
755 : count === 2
698756 ? Deferred.succeed(laterLookupStarted, undefined)
699757 : Effect.void,
700758 ),
701759 Effect.tap((count) =>
702760 count === 1
703761 ? Deferred.await(releaseFirstLookup)
704 : count === 3
762 : count === 2
705763 ? Deferred.await(releaseLaterLookup)
706764 : Effect.void,
707765 ),
......@@ -716,13 +774,16 @@ describe("ThreadSettlementReactor", () => {
716774 yield* Deferred.succeed(fixture.activation, undefined);
717775 yield* Queue.take(fixture.snapshotReads);
718776 yield* Deferred.await(firstLookupStarted);
777 yield* Queue.clear(fixture.settingsReads);
719778
720779 yield* fixture.updateSettings({ sidebarAutoSettleOnMerge: false });
721780 yield* Deferred.succeed(releaseFirstLookup, undefined);
722 yield* Queue.take(fixture.snapshotReads);
781 // The in-flight decision and the newly queued sweep both read the disabled settings.
782 yield* Queue.take(fixture.settingsReads);
783 yield* Queue.take(fixture.settingsReads);
723784 yield* reactor.drain;
724785 assert.deepStrictEqual(yield* Ref.get(fixture.commands), []);
725 assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2);
786 assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 1);
726787
727788 yield* Ref.set(state, "closed");
728789 yield* fixture.updateSettings({ enableAgentBrowserAccess: false });
......@@ -731,8 +792,8 @@ describe("ThreadSettlementReactor", () => {
731792 yield* Deferred.succeed(releaseLaterLookup, undefined);
732793 yield* reactor.drain;
733794
734 assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 3);
735 assert.strictEqual(yield* Ref.get(lookupCount), 3);
795 assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2);
796 assert.strictEqual(yield* Ref.get(lookupCount), 2);
736797 assert.deepStrictEqual(
737798 (yield* Ref.get(fixture.commands)).map((command) => command.threadId),
738799 [ThreadId.make("settings-thread")],
apps/server/src/orchestration/ThreadSettlementReactor.ts+4
......@@ -44,6 +44,10 @@ export const make = Effect.gen(function* () {
4444 const sweep = Effect.fn("ThreadSettlementReactor.sweep")(function* (
4545 mergedPullRequest: PullRequestService.PullRequestMergeEvent | null,
4646 ) {
47 const settings = yield* settingsService.getSettings;
48 if (!settings.sidebarAutoSettleOnMerge && settings.sidebarAutoSettleAfterDays === null) {
49 return;
50 }
4751 const snapshot = yield* snapshots.getShellSnapshot();
4852 const now = DateTime.formatIso(yield* DateTime.now);
4953 const projects = new Map(snapshot.projects.map((project) => [project.id, project]));