Skip to content

Commit b320ba5

Browse files
d-csclaude
andcommitted
fix(run-ops split): drop dead connectedRuns relation from ApiWaitpointPresenter hydrate select
GET /api/v1/waitpoints/tokens/:id returned HTTP 500 under split routing. The hydrate() select in ApiWaitpointPresenter included the connectedRuns RELATION, but it is never referenced in the returned object (all scalars) — dead code left over from the presenters de-join pass (48ae62b70), which stripped relations from WaitpointListPresenter but missed this one. Under split routing a standalone cuid token classifies LEGACY, so readThroughRun probes the scalar-only run-ops NEW client FIRST. The dedicated run-ops Waitpoint model is scalar-only (relations de-normalized), so Prisma threw PrismaClientValidationError: Unknown field connectedRuns before the legacy fallback could run. Removing the block makes the select all-scalar and valid against both the control-plane client and the scalar-only run-ops-new client. No return-shape, type, or SDK change. Regression test: extends apiWaitpointPresenter.readthrough.test.ts with a heteroRunOpsPostgresTest case that uses the REAL scalar-only run-ops client (prisma17) as the split-mode NEW client and seeds a cuid token on the legacy side. Fails before the fix (Unknown field connectedRuns), passes after (resolves via the legacy fallback). Not caught by the existing heteroPostgresTest cases because those run the full control-plane schema on both sides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8f287d3 commit b320ba5

2 files changed

Lines changed: 46 additions & 8 deletions

File tree

apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,6 @@ export class ApiWaitpointPresenter extends BasePresenter {
6565
completedAfter: true,
6666
completedAt: true,
6767
createdAt: true,
68-
connectedRuns: {
69-
select: {
70-
friendlyId: true,
71-
},
72-
take: 5,
73-
},
7468
tags: true,
7569
},
7670
});

apps/webapp/test/apiWaitpointPresenter.readthrough.test.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
// The DB is never mocked: reads hit the two real containers. Only pure boundaries
33
// (splitEnabled, isPastRetention) and recording client wrappers are
44
// injected. heteroPostgresTest runs the legacy and new databases on different major versions.
5-
import { heteroPostgresTest, postgresTest } from "@internal/testcontainers";
5+
import {
6+
heteroPostgresTest,
7+
heteroRunOpsPostgresTest,
8+
postgresTest,
9+
} from "@internal/testcontainers";
10+
import type { RunOpsPrismaClient } from "@internal/run-ops-database";
611
import type { PrismaClient, WaitpointType } from "@trigger.dev/database";
712
import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic";
813
import { describe, expect, vi } from "vitest";
@@ -22,7 +27,7 @@ function generateLegacyCuid() {
2227

2328
// A read client whose waitpoint.findFirst is recorded; throws if used after being marked
2429
// forbidden, so we can prove a store was NEVER read.
25-
function recording(client: PrismaClient, opts: { forbidden?: boolean } = {}) {
30+
function recording(client: PrismaClient | RunOpsPrismaClient, opts: { forbidden?: boolean } = {}) {
2631
const calls: unknown[] = [];
2732
const waitpoint = {
2833
findFirst: (args: unknown) => {
@@ -253,6 +258,45 @@ describe("ApiWaitpointPresenter read-through (heterogeneous legacy + new Postgre
253258
);
254259
});
255260

261+
// Regression: the split-mode NEW client is the REAL scalar-only run-ops client (prisma17). A cuid
262+
// classifies LEGACY, so readThroughRun probes NEW first — a relation in hydrate() (connectedRuns)
263+
// throws PrismaClientValidationError there (the 500) before the legacy fallback runs.
264+
describe("ApiWaitpointPresenter read-through (dedicated scalar-only run-ops NEW client)", () => {
265+
heteroRunOpsPostgresTest(
266+
"cuid token: hydrate() select is valid against the scalar-only run-ops client, resolves via legacy",
267+
async ({ prisma17, prisma14 }) => {
268+
const id = generateLegacyCuid();
269+
expect(id.length).toBe(25);
270+
271+
const { project, environment } = await seedOrgProjectEnv(prisma14, "scalar-legacy");
272+
const seeded = await seedWaitpoint(
273+
prisma14,
274+
id,
275+
{ id: environment.id, projectId: project.id },
276+
{ tags: ["p", "q"], output: JSON.stringify({ ok: true }) }
277+
);
278+
279+
const newClient = recording(prisma17);
280+
const legacy = recording(prisma14);
281+
282+
const presenter = new ApiWaitpointPresenter(undefined, undefined, {
283+
splitEnabled: true,
284+
newClient: newClient.handle,
285+
legacyReplica: legacy.handle,
286+
});
287+
288+
// Must NOT throw PrismaClientValidationError; resolves the token off the legacy side.
289+
const result = await presenter.call(environmentArg(environment), id);
290+
291+
expect(result.id).toBe(seeded.friendlyId);
292+
expect(result.tags).toEqual(["p", "q"]);
293+
expect(result.output).toBe(JSON.stringify({ ok: true }));
294+
expect(newClient.calls.length).toBe(1);
295+
expect(legacy.calls.length).toBe(1);
296+
}
297+
);
298+
});
299+
256300
describe("ApiWaitpointPresenter passthrough (single-DB)", () => {
257301
postgresTest(
258302
"no read-through deps → one plain replica read; legacy never touched",

0 commit comments

Comments
 (0)