| author | |
| committer | |
| log | c78f05a45e1c2b274d8b0ab2b4d2d88b4767c968 |
| tree | df417f431353de58e2ff56433f0066e142021cf2 |
| parent | 4e89d74436a167c01f25a6a5283638843398ea3a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>2 files changed, 69 insertions(+), 7 deletions(-)
apps/server/src/orchestration/ThreadSettlementReactor.test.ts+42| ... | @@ -17,6 +17,7 @@ import { assert, describe, it } from "@effect/vitest"; | ... | @@ -17,6 +17,7 @@ import { assert, describe, it } from "@effect/vitest"; |
| 17 | import * as Crypto from "effect/Crypto"; | 17 | import * as Crypto from "effect/Crypto"; |
| 18 | import * as Deferred from "effect/Deferred"; | 18 | import * as Deferred from "effect/Deferred"; |
| 19 | import * as Effect from "effect/Effect"; | 19 | import * as Effect from "effect/Effect"; |
| 20 | import * as FileSystem from "effect/FileSystem"; | ||
| 20 | import * as Layer from "effect/Layer"; | 21 | import * as Layer from "effect/Layer"; |
| 21 | import * as PubSub from "effect/PubSub"; | 22 | import * as PubSub from "effect/PubSub"; |
| 22 | import * as Queue from "effect/Queue"; | 23 | import * as Queue from "effect/Queue"; |
| ... | @@ -134,6 +135,7 @@ interface HarnessOptions { | ... | @@ -134,6 +135,7 @@ interface HarnessOptions { |
| 134 | readonly settings?: ServerSettings; | 135 | readonly settings?: ServerSettings; |
| 135 | readonly branchPullRequest?: GitManager["Service"]["branchPullRequest"]; | 136 | readonly branchPullRequest?: GitManager["Service"]["branchPullRequest"]; |
| 136 | readonly pullRequestSummary?: PullRequestService["Service"]["summary"]; | 137 | readonly pullRequestSummary?: PullRequestService["Service"]["summary"]; |
| 138 | readonly existingWorktreePaths?: ReadonlyArray<string>; | ||
| 137 | readonly onDispatch?: ( | 139 | readonly onDispatch?: ( |
| 138 | command: AutoSettleCommand, | 140 | command: AutoSettleCommand, |
| 139 | ) => Effect.Effect<void, OrchestrationCommandInvariantError>; | 141 | ) => Effect.Effect<void, OrchestrationCommandInvariantError>; |
| ... | @@ -235,6 +237,9 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: | ... | @@ -235,6 +237,9 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options: |
| 235 | Layer.succeed(ServerSettingsService, serverSettings), | 237 | Layer.succeed(ServerSettingsService, serverSettings), |
| 236 | Layer.succeed(ServerActivation, Deferred.await(activation)), | 238 | Layer.succeed(ServerActivation, Deferred.await(activation)), |
| 237 | Layer.succeed(Crypto.Crypto, testCrypto), | 239 | Layer.succeed(Crypto.Crypto, testCrypto), |
| 240 | FileSystem.layerNoop({ | ||
| 241 | exists: (path) => Effect.succeed(options.existingWorktreePaths?.includes(path) ?? false), | ||
| 242 | }), | ||
| 238 | ); | 243 | ); |
| 239 | 244 | ||
| 240 | return { | 245 | return { |
| ... | @@ -647,6 +652,43 @@ describe("ThreadSettlementReactor", () => { | ... | @@ -647,6 +652,43 @@ describe("ThreadSettlementReactor", () => { |
| 647 | ), | 652 | ), |
| 648 | ); | 653 | ); |
| 649 | 654 | ||
| 655 | it.effect("looks up the branch pull request from a thread's live worktree", () => | ||
| 656 | Effect.scoped( | ||
| 657 | Effect.gen(function* () { | ||
| 658 | yield* TestClock.setTime(Date.parse(NOW)); | ||
| 659 | const fixture = yield* makeHarness({ | ||
| 660 | snapshot: makeSnapshot( | ||
| 661 | [ | ||
| 662 | makeThread("live-worktree", { | ||
| 663 | branch: "feature/live", | ||
| 664 | worktreePath: "/workspace/project-root/.worktrees/live", | ||
| 665 | }), | ||
| 666 | makeThread("deleted-worktree", { | ||
| 667 | branch: "feature/deleted", | ||
| 668 | worktreePath: "/workspace/project-root/.worktrees/deleted", | ||
| 669 | }), | ||
| 670 | ], | ||
| 671 | [makeProject(PROJECT_ID, "/workspace/project-root")], | ||
| 672 | ), | ||
| 673 | existingWorktreePaths: ["/workspace/project-root/.worktrees/live"], | ||
| 674 | }); | ||
| 675 | |||
| 676 | yield* Effect.gen(function* () { | ||
| 677 | const reactor = yield* ThreadSettlementReactor.ThreadSettlementReactor; | ||
| 678 | yield* startHarness(reactor, fixture.activation, fixture.snapshotReads); | ||
| 679 | |||
| 680 | assert.deepStrictEqual( | ||
| 681 | new Set(yield* Ref.get(fixture.branchCalls)), | ||
| 682 | new Set([ | ||
| 683 | { cwd: "/workspace/project-root/.worktrees/live", branch: "feature/live" }, | ||
| 684 | { cwd: "/workspace/project-root", branch: "feature/deleted" }, | ||
| 685 | ]), | ||
| 686 | ); | ||
| 687 | }).pipe(Effect.provide(fixture.layer)); | ||
| 688 | }), | ||
| 689 | ), | ||
| 690 | ); | ||
| 691 | |||
| 650 | it.effect("carries the snapshot guard and survives a stale dispatch rejection", () => | 692 | it.effect("carries the snapshot guard and survives a stale dispatch rejection", () => |
| 651 | Effect.scoped( | 693 | Effect.scoped( |
| 652 | Effect.gen(function* () { | 694 | Effect.gen(function* () { |
apps/server/src/orchestration/ThreadSettlementReactor.ts+27-7| ... | @@ -5,6 +5,7 @@ import * as Context from "effect/Context"; | ... | @@ -5,6 +5,7 @@ import * as Context from "effect/Context"; |
| 5 | import * as Crypto from "effect/Crypto"; | 5 | import * as Crypto from "effect/Crypto"; |
| 6 | import * as DateTime from "effect/DateTime"; | 6 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; | ||
| 8 | import * as Layer from "effect/Layer"; | 9 | import * as Layer from "effect/Layer"; |
| 9 | import * as Schedule from "effect/Schedule"; | 10 | import * as Schedule from "effect/Schedule"; |
| 10 | import type * as Scope from "effect/Scope"; | 11 | import type * as Scope from "effect/Scope"; |
| ... | @@ -37,6 +38,7 @@ export const make = Effect.gen(function* () { | ... | @@ -37,6 +38,7 @@ export const make = Effect.gen(function* () { |
| 37 | const git = yield* GitManager.GitManager; | 38 | const git = yield* GitManager.GitManager; |
| 38 | const pullRequests = yield* PullRequestService.PullRequestService; | 39 | const pullRequests = yield* PullRequestService.PullRequestService; |
| 39 | const crypto = yield* Crypto.Crypto; | 40 | const crypto = yield* Crypto.Crypto; |
| 41 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 40 | 42 | ||
| 41 | const sweep = Effect.fn("ThreadSettlementReactor.sweep")(function* ( | 43 | const sweep = Effect.fn("ThreadSettlementReactor.sweep")(function* ( |
| 42 | mergedPullRequest: PullRequestService.PullRequestMergeEvent | null, | 44 | mergedPullRequest: PullRequestService.PullRequestMergeEvent | null, |
| ... | @@ -54,6 +56,26 @@ export const make = Effect.gen(function* () { | ... | @@ -54,6 +56,26 @@ export const make = Effect.gen(function* () { |
| 54 | mergedPullRequest.repository.toLowerCase() && | 56 | mergedPullRequest.repository.toLowerCase() && |
| 55 | thread.linkedPullRequest.number === mergedPullRequest.number)), | 57 | thread.linkedPullRequest.number === mergedPullRequest.number)), |
| 56 | ); | 58 | ); |
| 59 | // Use the same cwd as the sidebar so both paths share GitManager's PR cache. | ||
| 60 | const lookupCwdByThreadId = new Map<string, string>(); | ||
| 61 | yield* Effect.forEach( | ||
| 62 | candidates, | ||
| 63 | (thread) => | ||
| 64 | Effect.gen(function* () { | ||
| 65 | const project = projects.get(thread.projectId); | ||
| 66 | if (project === undefined || thread.linkedPullRequest != null) return; | ||
| 67 | const worktreeExists = | ||
| 68 | thread.worktreePath !== null && | ||
| 69 | (yield* fileSystem.exists(thread.worktreePath).pipe(Effect.orElseSucceed(() => false))); | ||
| 70 | lookupCwdByThreadId.set( | ||
| 71 | thread.id, | ||
| 72 | worktreeExists && thread.worktreePath !== null | ||
| 73 | ? thread.worktreePath | ||
| 74 | : project.workspaceRoot, | ||
| 75 | ); | ||
| 76 | }), | ||
| 77 | { concurrency: 8, discard: true }, | ||
| 78 | ); | ||
| 57 | const lookupKey = (thread: (typeof candidates)[number]) => { | 79 | const lookupKey = (thread: (typeof candidates)[number]) => { |
| 58 | if (thread.linkedPullRequest != null) { | 80 | if (thread.linkedPullRequest != null) { |
| 59 | return JSON.stringify([ | 81 | return JSON.stringify([ |
| ... | @@ -64,11 +86,9 @@ export const make = Effect.gen(function* () { | ... | @@ -64,11 +86,9 @@ export const make = Effect.gen(function* () { |
| 64 | ]); | 86 | ]); |
| 65 | } | 87 | } |
| 66 | if (thread.branch === null) return JSON.stringify(["none", thread.id]); | 88 | if (thread.branch === null) return JSON.stringify(["none", thread.id]); |
| 67 | const project = projects.get(thread.projectId); | 89 | const cwd = lookupCwdByThreadId.get(thread.id); |
| 68 | return JSON.stringify( | 90 | return JSON.stringify( |
| 69 | project === undefined | 91 | cwd === undefined ? ["missing-project", thread.id] : ["branch", cwd, thread.branch], |
| 70 | ? ["missing-project", thread.id] | ||
| 71 | : ["branch", project.workspaceRoot, thread.branch], | ||
| 72 | ); | 92 | ); |
| 73 | }; | 93 | }; |
| 74 | const groups = Map.groupBy(candidates, lookupKey); | 94 | const groups = Map.groupBy(candidates, lookupKey); |
| ... | @@ -100,11 +120,11 @@ export const make = Effect.gen(function* () { | ... | @@ -100,11 +120,11 @@ export const make = Effect.gen(function* () { |
| 100 | } satisfies SettlementPullRequest; | 120 | } satisfies SettlementPullRequest; |
| 101 | } | 121 | } |
| 102 | if (thread.branch === null) return null; | 122 | if (thread.branch === null) return null; |
| 103 | const project = projects.get(thread.projectId); | 123 | const cwd = lookupCwdByThreadId.get(thread.id); |
| 104 | if (project === undefined) { | 124 | if (cwd === undefined) { |
| 105 | return yield* Effect.die(new Error("thread project not found")); | 125 | return yield* Effect.die(new Error("thread project not found")); |
| 106 | } | 126 | } |
| 107 | return yield* git.branchPullRequest({ cwd: project.workspaceRoot, branch: thread.branch }); | 127 | return yield* git.branchPullRequest({ cwd, branch: thread.branch }); |
| 108 | }); | 128 | }); |
| 109 | 129 | ||
| 110 | yield* Effect.forEach( | 130 | yield* Effect.forEach( |