authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-09-10 02:54:25-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 02:54:25-03:00
logd29c56a5c404cb0f58d3b2ac41762fa0d0ac28d4
tree6056ee76648e1cb353a46a1cfd0230f63e45325c
parentbb5e824c9fcbd76c93ef15b304f89f8b6999f32c
signature Signed by PGP key B5690EEEBB952194

fix(server): resolve project identity before legacy pr relinks (#11045)


2 files changed, 30 insertions(+), 1 deletions(-)

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+7-1
......@@ -27,7 +27,7 @@ import * as Option from "effect/Option";
2727import * as Queue from "effect/Queue";
2828import * as Stream from "effect/Stream";
2929import { TestClock } from "effect/testing";
30import { describe, expect, it } from "vite-plus/test";
30import { describe, expect, it, vi } from "vite-plus/test";
3131
3232import { PersistenceSqlError } from "../../persistence/Errors.ts";
3333import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
......@@ -1039,6 +1039,8 @@ describe("OrchestrationEngine", () => {
10391039 },
10401040 }),
10411041 });
1042 // Same-tick links must replace the old PR, not rely on timestamp ordering.
1043 const clock = vi.spyOn(Date, "now").mockReturnValue(Date.parse(now()));
10421044 try {
10431045 const projectId = ProjectId.make("pr-race-project");
10441046 const threadId = ThreadId.make("pr-race-thread");
......@@ -1141,6 +1143,9 @@ describe("OrchestrationEngine", () => {
11411143 if (change === "delete") return;
11421144 const current = (await system.readModel()).threads[0];
11431145 expect(current?.branchPullRequest ?? null).toBeNull();
1146 expect(current?.pullRequests.map((link) => link.number)).toEqual(
1147 change === "unlink" ? [] : change === "relink" ? [3] : [1],
1148 );
11441149 expect(current?.linkedPullRequest ?? null).toEqual(
11451150 change === "unlink"
11461151 ? null
......@@ -1149,6 +1154,7 @@ describe("OrchestrationEngine", () => {
11491154 : previous,
11501155 );
11511156 } finally {
1157 clock.mockRestore();
11521158 await system.dispose();
11531159 }
11541160 },
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+23
......@@ -212,6 +212,29 @@ const makeOrchestrationEngine = Effect.gen(function* () {
212212 });
213213 }
214214
215 // New and moved projects do not carry a resolved identity in the event-derived
216 // command model. Legacy PR edits need it to identify the link they replace.
217 if (
218 envelope.command.type === "thread.meta.update" &&
219 envelope.command.linkedPullRequest !== undefined
220 ) {
221 const threadId = envelope.command.threadId;
222 const thread = commandReadModel.threads.find((thread) => thread.id === threadId);
223 if (thread !== undefined) {
224 const project = yield* projectionSnapshotQuery.getProjectShellById(thread.projectId);
225 if (Option.isSome(project)) {
226 commandReadModel = {
227 ...commandReadModel,
228 projects: commandReadModel.projects.map((entry) =>
229 entry.id === thread.projectId
230 ? { ...entry, repositoryIdentity: project.value.repositoryIdentity }
231 : entry,
232 ),
233 };
234 }
235 }
236 }
237
215238 // Command snapshots omit activities at startup and cap them while running.
216239 // Read this request's durable state before deciding how to send the answer.
217240 const userInputActivity =