| ... | @@ -161,6 +161,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: | ... | @@ -161,6 +161,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: |
| 161 | }> | 161 | }> |
| 162 | >([]); | 162 | >([]); |
| 163 | const summaryRecovery = yield* Ref.make<ReadonlyArray<boolean | undefined>>([]); | 163 | const summaryRecovery = yield* Ref.make<ReadonlyArray<boolean | undefined>>([]); |
| | 164 | const invalidatedCwds = yield* Ref.make<ReadonlyArray<string>>([]); |
| 164 | | 165 | |
| 165 | const updateSettings = (patch: ServerSettingsPatch) => | 166 | const updateSettings = (patch: ServerSettingsPatch) => |
| 166 | Effect.gen(function* () { | 167 | Effect.gen(function* () { |
| ... | @@ -221,7 +222,10 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: | ... | @@ -221,7 +222,10 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: |
| 221 | Effect.andThen(Ref.get(snapshots)), | 222 | Effect.andThen(Ref.get(snapshots)), |
| 222 | ), | 223 | ), |
| 223 | }), | 224 | }), |
| 224 | Layer.mock(GitManager)({ branchPullRequest }), | 225 | Layer.mock(GitManager)({ |
| | 226 | branchPullRequest, |
| | 227 | invalidateStatus: (cwd) => Ref.update(invalidatedCwds, (cwds) => [...cwds, cwd]), |
| | 228 | }), |
| 225 | Layer.mock(PullRequestService)({ | 229 | Layer.mock(PullRequestService)({ |
| 226 | summary: pullRequestSummary, | 230 | summary: pullRequestSummary, |
| 227 | subscribeMerges: PubSub.subscribe(mergedPullRequests).pipe( | 231 | subscribeMerges: PubSub.subscribe(mergedPullRequests).pipe( |
| ... | @@ -251,6 +255,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: | ... | @@ -251,6 +255,7 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: |
| 251 | branchCalls, | 255 | branchCalls, |
| 252 | summaryCalls, | 256 | summaryCalls, |
| 253 | summaryRecovery, | 257 | summaryRecovery, |
| | 258 | invalidatedCwds, |
| 254 | updateSettings, | 259 | updateSettings, |
| 255 | publishMerge: PubSub.publish(mergedPullRequests, { | 260 | publishMerge: PubSub.publish(mergedPullRequests, { |
| 256 | projectId: PROJECT_ID, | 261 | projectId: PROJECT_ID, |
| ... | @@ -446,6 +451,113 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -446,6 +451,113 @@ describe("ThreadSettlementReactor", () => { |
| 446 | ), | 451 | ), |
| 447 | ); | 452 | ); |
| 448 | | 453 | |
| | 454 | it.effect( |
| | 455 | "settles branch threads on a pull request merge without waiting for the next sweep", |
| | 456 | () => |
| | 457 | Effect.scoped( |
| | 458 | Effect.gen(function* () { |
| | 459 | yield* TestClock.setTime(Date.parse(NOW)); |
| | 460 | const state = yield* Ref.make<"open" | "merged">("open"); |
| | 461 | const mergedThreadSettled = yield* Deferred.make<void>(); |
| | 462 | const fixture = yield* makeHarness({ |
| | 463 | snapshot: makeSnapshot([makeThread("branch-thread", { branch: "saved-feature" })]), |
| | 464 | branchPullRequest: () => |
| | 465 | Ref.get(state).pipe( |
| | 466 | Effect.map((pullRequestState) => ({ state: pullRequestState, updatedAt: NOW })), |
| | 467 | ), |
| | 468 | onDispatch: () => Deferred.succeed(mergedThreadSettled, undefined), |
| | 469 | }); |
| | 470 | |
| | 471 | yield* Effect.gen(function* () { |
| | 472 | const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; |
| | 473 | yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); |
| | 474 | assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); |
| | 475 | assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), []); |
| | 476 | |
| | 477 | yield* Ref.set(state, "merged"); |
| | 478 | yield* fixture.publishMerge; |
| | 479 | yield* Deferred.await(mergedThreadSettled); |
| | 480 | |
| | 481 | assert.deepStrictEqual( |
| | 482 | (yield* Ref.get(fixture.commands)).map((command) => command.threadId), |
| | 483 | [ThreadId.make("branch-thread")], |
| | 484 | ); |
| | 485 | assert.deepStrictEqual(yield* Ref.get(fixture.invalidatedCwds), ["/workspace/project"]); |
| | 486 | yield* reactor.drain; |
| | 487 | }).pipe(Effect.provide(fixture.layer)); |
| | 488 | }), |
| | 489 | ), |
| | 490 | ); |
| | 491 | |
| | 492 | it.effect("a merge does not settle threads linked to an unrelated pull request", () => |
| | 493 | Effect.scoped( |
| | 494 | Effect.gen(function* () { |
| | 495 | yield* TestClock.setTime(Date.parse(NOW)); |
| | 496 | const mergedThreadSettled = yield* Deferred.make<void>(); |
| | 497 | const mergeLookupStarted = yield* Deferred.make<void>(); |
| | 498 | const releaseMergeLookup = yield* Deferred.make<void>(); |
| | 499 | const lookupCount = yield* Ref.make(0); |
| | 500 | const fixture = yield* makeHarness({ |
| | 501 | snapshot: makeSnapshot([ |
| | 502 | makeThread("merged-in-app", { |
| | 503 | linkedPullRequest: { |
| | 504 | projectId: PROJECT_ID, |
| | 505 | repository: "owner/repository", |
| | 506 | number: 42, |
| | 507 | url: "https://example.test/owner/repository/pull/42", |
| | 508 | }, |
| | 509 | }), |
| | 510 | makeThread("unrelated-linked", { |
| | 511 | linkedPullRequest: { |
| | 512 | projectId: PROJECT_ID, |
| | 513 | repository: "owner/repository", |
| | 514 | number: 99, |
| | 515 | url: "https://example.test/owner/repository/pull/99", |
| | 516 | }, |
| | 517 | }), |
| | 518 | ]), |
| | 519 | pullRequestSummary: (input) => |
| | 520 | Ref.updateAndGet(lookupCount, (count) => count + 1).pipe( |
| | 521 | // The initial sweep looks up both linked threads; the merge |
| | 522 | // sweep only looks up the unrelated one, since the merged |
| | 523 | // thread settles from the event itself. |
| | 524 | Effect.tap((count) => |
| | 525 | count === 3 ? Deferred.succeed(mergeLookupStarted, undefined) : Effect.void, |
| | 526 | ), |
| | 527 | Effect.tap((count) => |
| | 528 | count === 3 ? Deferred.await(releaseMergeLookup) : Effect.void, |
| | 529 | ), |
| | 530 | Effect.map(() => makePullRequestSummary({ ...input, state: "open" })), |
| | 531 | ), |
| | 532 | onDispatch: () => Deferred.succeed(mergedThreadSettled, undefined), |
| | 533 | }); |
| | 534 | |
| | 535 | yield* Effect.gen(function* () { |
| | 536 | const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; |
| | 537 | yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); |
| | 538 | assert.deepStrictEqual(yield* Ref.get(fixture.commands), []); |
| | 539 | |
| | 540 | yield* fixture.publishMerge; |
| | 541 | yield* Deferred.await(mergeLookupStarted); |
| | 542 | yield* Deferred.await(mergedThreadSettled); |
| | 543 | yield* Deferred.succeed(releaseMergeLookup, undefined); |
| | 544 | yield* reactor.drain; |
| | 545 | |
| | 546 | assert.deepStrictEqual( |
| | 547 | (yield* Ref.get(fixture.commands)).map((command) => command.threadId), |
| | 548 | [ThreadId.make("merged-in-app")], |
| | 549 | ); |
| | 550 | assert.deepStrictEqual( |
| | 551 | (yield* Ref.get(fixture.summaryCalls)) |
| | 552 | .map((call) => call.number) |
| | 553 | .toSorted((left, right) => left - right), |
| | 554 | [42, 99, 99], |
| | 555 | ); |
| | 556 | }).pipe(Effect.provide(fixture.layer)); |
| | 557 | }), |
| | 558 | ), |
| | 559 | ); |
| | 560 | |
| 449 | it.effect("uses fresh settlement settings after lookup and ignores unrelated changes", () => | 561 | it.effect("uses fresh settlement settings after lookup and ignores unrelated changes", () => |
| 450 | Effect.scoped( | 562 | Effect.scoped( |
| 451 | Effect.gen(function* () { | 563 | Effect.gen(function* () { |