From 0a3ca8fceb0b582281c2d772fc2c627e896ca5ac Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sun, 6 Sep 2026 05:39:26 +0000 Subject: [PATCH] fix(server): skip disabled settlement snapshots --- .../ThreadSettlementReactor.test.ts | 75 +++++++++++++++++-- .../orchestration/ThreadSettlementReactor.ts | 4 + 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts index f2038bfe689d..dbb3c5f9f755 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts @@ -167,6 +167,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: const snapshotReadCount = yield* Ref.make(0); const snapshotReads = yield* Queue.unbounded(); const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS); + const settingsReads = yield* Queue.unbounded(); const settingsChanges = yield* PubSub.unbounded(); const mergedPullRequests = yield* PubSub.unbounded(); const commands = yield* Ref.make>([]); @@ -226,7 +227,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: const serverSettings = ServerSettingsService.of({ start: Effect.void, ready: Effect.void, - getSettings: Ref.get(settings), + getSettings: Ref.get(settings).pipe(Effect.tap((value) => Queue.offer(settingsReads, value))), updateSettings, streamChanges: Stream.fromPubSub(settingsChanges), subscribeChanges: PubSub.subscribe(settingsChanges).pipe( @@ -271,6 +272,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: snapshots, snapshotReadCount, snapshotReads, + settingsReads, commands, branchCalls, summaryCalls, @@ -376,6 +378,62 @@ describe("ThreadSettlementReactor", () => { ), ); + it.effect("skips PR work on startup, timer and merge sweeps when settlement is disabled", () => + Effect.scoped( + Effect.gen(function* () { + yield* TestClock.setTime(Date.parse(NOW)); + const fixture = yield* makeHarness({ + snapshot: makeSnapshot([ + makeThread("branch-thread", { branch: "feature" }), + makeThread("linked-thread", { + linkedPullRequest: { + projectId: PROJECT_ID, + repository: "owner/repository", + number: 42, + url: "https://example.test/owner/repository/pull/42", + }, + }), + ]), + settings: { + ...DEFAULT_SERVER_SETTINGS, + sidebarAutoSettleAfterDays: null, + sidebarAutoSettleOnMerge: false, + }, + }); + + yield* Effect.gen(function* () { + const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; + yield* reactor.start(); + yield* Queue.take(fixture.settingsReads); + yield* Deferred.succeed(fixture.activation, undefined); + yield* Queue.take(fixture.settingsReads); + yield* reactor.drain; + yield* TestClock.adjust("1 minute"); + yield* Queue.take(fixture.settingsReads); + yield* reactor.drain; + yield* fixture.publishMerge; + yield* Queue.take(fixture.settingsReads); + yield* reactor.drain; + + assert.deepStrictEqual(yield* Ref.get(fixture.branchCalls), []); + assert.deepStrictEqual(yield* Ref.get(fixture.summaryCalls), []); + assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []); + assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 0); + + yield* fixture.updateSettings({ sidebarAutoSettleAfterDays: 1 }); + yield* Queue.take(fixture.snapshotReads); + yield* reactor.drain; + // Inactive threads settle without a PR lookup, so only the dispatches prove work resumed. + assert.deepStrictEqual( + (yield* Ref.get(fixture.commands)).map((command) => command.threadId).toSorted(), + [ThreadId.make("branch-thread"), ThreadId.make("linked-thread")], + ); + }).pipe(Effect.provide(fixture.layer)); + }), + ), + ); + it.effect("starts without clients and skips protected threads before pull request lookup", () => Effect.scoped( Effect.gen(function* () { @@ -694,14 +752,14 @@ describe("ThreadSettlementReactor", () => { Effect.tap((count) => count === 1 ? Deferred.succeed(firstLookupStarted, undefined) - : count === 3 + : count === 2 ? Deferred.succeed(laterLookupStarted, undefined) : Effect.void, ), Effect.tap((count) => count === 1 ? Deferred.await(releaseFirstLookup) - : count === 3 + : count === 2 ? Deferred.await(releaseLaterLookup) : Effect.void, ), @@ -716,13 +774,16 @@ describe("ThreadSettlementReactor", () => { yield* Deferred.succeed(fixture.activation, undefined); yield* Queue.take(fixture.snapshotReads); yield* Deferred.await(firstLookupStarted); + yield* Queue.clear(fixture.settingsReads); yield* fixture.updateSettings({ sidebarAutoSettleOnMerge: false }); yield* Deferred.succeed(releaseFirstLookup, undefined); - yield* Queue.take(fixture.snapshotReads); + // The in-flight decision and the newly queued sweep both read the disabled settings. + yield* Queue.take(fixture.settingsReads); + yield* Queue.take(fixture.settingsReads); yield* reactor.drain; assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); - assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 1); yield* Ref.set(state, "closed"); yield* fixture.updateSettings({ enableAgentBrowserAccess: false }); @@ -731,8 +792,8 @@ describe("ThreadSettlementReactor", () => { yield* Deferred.succeed(releaseLaterLookup, undefined); yield* reactor.drain; - assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 3); - assert.strictEqual(yield* Ref.get(lookupCount), 3); + assert.strictEqual(yield* Ref.get(fixture.snapshotReadCount), 2); + assert.strictEqual(yield* Ref.get(lookupCount), 2); assert.deepStrictEqual( (yield* Ref.get(fixture.commands)).map((command) => command.threadId), [ThreadId.make("settings-thread")], diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.ts b/apps/server/src/orchestration/ThreadSettlementReactor.ts index 994830b30a47..4830ec9d80e5 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.ts @@ -44,6 +44,10 @@ export const make = Effect.gen(function* () { const sweep = Effect.fn("ThreadSettlementReactor.sweep")(function* ( mergedPullRequest: PullRequestService.PullRequestMergeEvent | null, ) { + const settings = yield* settingsService.getSettings; + if (!settings.sidebarAutoSettleOnMerge && settings.sidebarAutoSettleAfterDays === null) { + return; + } const snapshot = yield* snapshots.getShellSnapshot(); const now = DateTime.formatIso(yield* DateTime.now); const projects = new Map(snapshot.projects.map((project) => [project.id, project]));