Skip to content

Commit cb97148

Browse files
d-csclaude
andcommitted
fix(run-ops split): self-default resolveWaitpointThroughReadThrough to safe run-ops clients
The read-through concern defaulted both newClient and legacyReplica to $replica (control-plane), so a bare caller that omits `deps` — the waitpoints wait route — never queried the dedicated run-ops replica. A co-located, NEW-resident waitpoint minted by streams.input().wait() lives on the run-ops-new DB, so the read missed, returned null, and the route 404'd (re-serialized to 500). Match the deps the complete/callback routes pass: default newClient to runOpsNewReplica, legacyReplica to $replica, and splitEnabled to runOpsSplitReadEnabled — mirroring readThroughRun's own self-defaulting. This immunizes any bare caller (present or future) against the control-plane pin, without touching the wait route. The wait/complete/callback call sites live on a higher branch and are unchanged; complete/callback keep their explicit deps (now redundant but harmless). Adds a heteroRunOps regression case driving the concern with no `deps` via the `defaults` DI seam: proves the old $replica default misses a NEW-resident waitpoint (null) while the safe run-ops default finds it. No mocks; the fallback is exercised against real PG14/PG17 containers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 88359b2 commit cb97148

2 files changed

Lines changed: 68 additions & 4 deletions

File tree

apps/webapp/app/runEngine/concerns/resolveWaitpointThroughReadThrough.server.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { PrismaReplicaClient } from "~/db.server";
2-
import { $replica } from "~/db.server";
2+
import {
3+
$replica as defaultLegacyReplica,
4+
runOpsNewReplica as defaultNewClient,
5+
runOpsSplitReadEnabled as defaultSplitReadEnabled,
6+
} from "~/db.server";
37
import { readThroughRun } from "~/v3/runOpsMigration/readThrough.server";
48

59
type ResolveWaitpointDeps = {
@@ -9,21 +13,38 @@ type ResolveWaitpointDeps = {
913
isPastRetention?: (id: string) => boolean;
1014
};
1115

16+
// Safe defaults matching the deps `complete`/`callback` pass, so a bare caller still fans
17+
// out to the dedicated run-ops replica (NEW-resident waitpoints) before control-plane.
18+
export type ResolveWaitpointReadThroughDefaults = {
19+
newClient: PrismaReplicaClient;
20+
legacyReplica: PrismaReplicaClient;
21+
splitEnabled: boolean;
22+
};
23+
24+
const productionDefaults: ResolveWaitpointReadThroughDefaults = {
25+
newClient: defaultNewClient,
26+
legacyReplica: defaultLegacyReplica,
27+
splitEnabled: defaultSplitReadEnabled,
28+
};
29+
1230
export async function resolveWaitpointThroughReadThrough<T>(opts: {
1331
waitpointId: string;
1432
environmentId: string;
1533
read: (client: PrismaReplicaClient) => Promise<T | null>;
1634
deps?: ResolveWaitpointDeps;
35+
defaults?: ResolveWaitpointReadThroughDefaults;
1736
}): Promise<T | null> {
37+
const defaults = opts.defaults ?? productionDefaults;
38+
1839
const result = await readThroughRun({
1940
runId: opts.waitpointId,
2041
environmentId: opts.environmentId,
2142
readNew: (client) => opts.read(client),
2243
readLegacy: (replica) => opts.read(replica),
2344
deps: {
24-
splitEnabled: opts.deps?.splitEnabled,
25-
newClient: opts.deps?.newClient ?? $replica,
26-
legacyReplica: opts.deps?.legacyReplica ?? $replica,
45+
splitEnabled: opts.deps?.splitEnabled ?? defaults.splitEnabled,
46+
newClient: opts.deps?.newClient ?? defaults.newClient,
47+
legacyReplica: opts.deps?.legacyReplica ?? defaults.legacyReplica,
2748
isPastRetention: opts.deps?.isPastRetention,
2849
},
2950
});

apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,49 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run
151151
}
152152
);
153153

154+
heteroRunOpsPostgresTest(
155+
"bare caller (no deps) resolves a NEW-resident waitpoint via the safe run-ops defaults",
156+
async ({ prisma17, prisma14 }) => {
157+
// The bare wait route passes NO `deps`; the `defaults` DI seam models old vs new
158+
// fallback against containers, avoiding the real db.server topology.
159+
const id = generateKsuidId();
160+
expect(id.length).toBe(27);
161+
const environmentId = generateKsuidId();
162+
const projectId = generateKsuidId();
163+
const seeded = await seedWaitpoint(prisma17, id, { id: environmentId, projectId });
164+
165+
// FAIL-BEFORE: old default pinned newClient to control-plane ($replica ≈ prisma14) → miss.
166+
const oldDefaultResult = await resolveWaitpointThroughReadThrough({
167+
waitpointId: id,
168+
environmentId,
169+
read: read(id, environmentId),
170+
defaults: {
171+
newClient: prisma14 as unknown as PrismaReplicaClient,
172+
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
173+
splitEnabled: true,
174+
},
175+
});
176+
expect(oldDefaultResult).toBeNull();
177+
178+
// PASS-AFTER: safe default routes newClient to the run-ops replica (runOpsNewReplica ≈ prisma17).
179+
const safeDefaultResult = await resolveWaitpointThroughReadThrough({
180+
waitpointId: id,
181+
environmentId,
182+
read: read(id, environmentId),
183+
defaults: {
184+
newClient: prisma17 as unknown as PrismaReplicaClient,
185+
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
186+
splitEnabled: true,
187+
},
188+
});
189+
190+
expect(safeDefaultResult).not.toBeNull();
191+
expect(safeDefaultResult!.id).toBe(seeded.id);
192+
expect(safeDefaultResult!.projectId).toBe(projectId);
193+
expect(safeDefaultResult!.environmentId).toBe(environmentId);
194+
}
195+
);
196+
154197
heteroRunOpsPostgresTest("not-found maps to null (no throw)", async ({ prisma17, prisma14 }) => {
155198
const id = generateLegacyCuid();
156199
const { environment } = await seedOrgProjectEnv(prisma14, "nf");

0 commit comments

Comments
 (0)