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";...@@ -27,7 +27,7 @@ import * as Option from "effect/Option";
27import * as Queue from "effect/Queue";27import * as Queue from "effect/Queue";
28import * as Stream from "effect/Stream";28import * as Stream from "effect/Stream";
29import { TestClock } from "effect/testing";29import { TestClock } from "effect/testing";
30import { describe, expect, it } from "vite-plus/test";30import { describe, expect, it, vi } from "vite-plus/test";
3131
32import { PersistenceSqlError } from "../../persistence/Errors.ts";32import { PersistenceSqlError } from "../../persistence/Errors.ts";
33import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";33import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
...@@ -1039,6 +1039,8 @@ describe("OrchestrationEngine", () => {...@@ -1039,6 +1039,8 @@ describe("OrchestrationEngine", () => {
1039 },1039 },
1040 }),1040 }),
1041 });1041 });
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()));
1042 try {1044 try {
1043 const projectId = ProjectId.make("pr-race-project");1045 const projectId = ProjectId.make("pr-race-project");
1044 const threadId = ThreadId.make("pr-race-thread");1046 const threadId = ThreadId.make("pr-race-thread");
...@@ -1141,6 +1143,9 @@ describe("OrchestrationEngine", () => {...@@ -1141,6 +1143,9 @@ describe("OrchestrationEngine", () => {
1141 if (change === "delete") return;1143 if (change === "delete") return;
1142 const current = (await system.readModel()).threads[0];1144 const current = (await system.readModel()).threads[0];
1143 expect(current?.branchPullRequest ?? null).toBeNull();1145 expect(current?.branchPullRequest ?? null).toBeNull();
1146 expect(current?.pullRequests.map((link) => link.number)).toEqual(
1147 change === "unlink" ? [] : change === "relink" ? [3] : [1],
1148 );
1144 expect(current?.linkedPullRequest ?? null).toEqual(1149 expect(current?.linkedPullRequest ?? null).toEqual(
1145 change === "unlink"1150 change === "unlink"
1146 ? null1151 ? null
...@@ -1149,6 +1154,7 @@ describe("OrchestrationEngine", () => {...@@ -1149,6 +1154,7 @@ describe("OrchestrationEngine", () => {
1149 : previous,1154 : previous,
1150 );1155 );
1151 } finally {1156 } finally {
1157 clock.mockRestore();
1152 await system.dispose();1158 await system.dispose();
1153 }1159 }
1154 },1160 },
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+23
...@@ -212,6 +212,29 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -212,6 +212,29 @@ const makeOrchestrationEngine = Effect.gen(function* () {
212 });212 });
213 }213 }
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
215 // Command snapshots omit activities at startup and cap them while running.238 // Command snapshots omit activities at startup and cap them while running.
216 // Read this request's durable state before deciding how to send the answer.239 // Read this request's durable state before deciding how to send the answer.
217 const userInputActivity =240 const userInputActivity =