diff --git a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts index eefc18f7b461..fcba210cf12b 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.test.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.test.ts @@ -149,6 +149,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>([]); @@ -208,7 +209,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( @@ -253,6 +254,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: snapshots, snapshotReadCount, snapshotReads, + settingsReads, commands, branchCalls, summaryCalls, @@ -281,6 +283,63 @@ const startHarness = Effect.fn("startThreadSettlementHarness")(function* ( }); 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; + assert.strictEqual((yield* Ref.get(fixture.branchCalls)).length, 1); + assert.strictEqual((yield* Ref.get(fixture.summaryCalls)).length, 1); + 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* () { @@ -600,14 +659,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, ), @@ -627,13 +686,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 }); @@ -642,8 +704,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 70de3c41d7e9..48c72a6a43c1 100644 --- a/apps/server/src/orchestration/ThreadSettlementReactor.ts +++ b/apps/server/src/orchestration/ThreadSettlementReactor.ts @@ -43,6 +43,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]));