Skip to content

Commit ea22f52

Browse files
d-csclaude
andcommitted
test(run-ops split): keyset-order hydrate + terminal-metadata read-seam regressions
Two regression tests for the write-path read seams: - runsRepository: paginating the full keyset over interleaved cuid/ksuid runs enumerates every id once, no empty page, in ClickHouse (created_at DESC, run_id DESC) order -- fails if hydration reverts to lexical id desc across the id-space seam. - runReader: a NEW-resident (ksuid) run's terminal metadata hydrates through the owning store, never a generic legacy replica. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c1371f2 commit ea22f52

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

apps/webapp/test/realtime/runReaderReadThrough.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,45 @@ describe("RunHydrator read-route through the runStore seam (legacy + new)", () =
310310
}
311311
);
312312

313+
// Terminal-metadata read-seam: a NEW-resident (ksuid) run's final metadata is hydrated through
314+
// the owning (NEW) store, not off a generic legacy replica. Asserts read-seam ROUTING for the
315+
// terminal read; it is not a hard ordering/consistency guarantee about when the terminal marker
316+
// and the row's terminal columns converge.
317+
heteroPostgresTest(
318+
"terminal hydrate reads a NEW-resident run's final metadata through the owning store",
319+
{ timeout: 60_000 },
320+
async ({ prisma14, prisma17 }) => {
321+
const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
322+
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
323+
const legacyFindRunSpy = vi.spyOn(legacyStore, "findRun");
324+
325+
const seed17 = await seedEnvironment(prisma17, "term17");
326+
const envId = seed17.environment.id;
327+
const terminalRunId = newId("terminal_run");
328+
329+
// A terminal run with its final metadata persisted on the NEW store only.
330+
await seedRun(prisma17, {
331+
runId: terminalRunId,
332+
organizationId: seed17.organization.id,
333+
projectId: seed17.project.id,
334+
runtimeEnvironmentId: envId,
335+
output: '{"result":"final"}',
336+
metadata: '{"done":true}',
337+
});
338+
339+
// A generic legacy replica would miss the NEW row entirely — the metadata must come off NEW.
340+
const runStore = makeRoutingShapedStore({ newStore, legacyStore });
341+
const hydrator = new RunHydrator({ replica: prisma14, runStore, cacheTtlMs: 0 });
342+
343+
const snapshot = await hydrator.getRunById(envId, terminalRunId);
344+
expect(snapshot?.id).toBe(terminalRunId);
345+
expect(snapshot?.metadata).toBe('{"done":true}');
346+
expect(snapshot?.output).toBe('{"result":"final"}');
347+
// The NEW-residency terminal read never touched the legacy slot.
348+
expect(legacyFindRunSpy).not.toHaveBeenCalled();
349+
}
350+
);
351+
313352
// A live-migrated run continues streaming across the seam crossing with no gap.
314353
heteroPostgresTest(
315354
"live-migrated run continues streaming across the seam crossing",

apps/webapp/test/runsRepository.readthrough.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ async function createRun(
9191
prisma: PrismaClient,
9292
ctx: SeedContext,
9393
run: {
94+
id?: string;
9495
friendlyId: string;
9596
taskIdentifier?: string;
9697
status?: any;
@@ -100,6 +101,7 @@ async function createRun(
100101
) {
101102
return prisma.taskRun.create({
102103
data: {
104+
...(run.id ? { id: run.id } : {}),
103105
friendlyId: run.friendlyId,
104106
taskIdentifier: run.taskIdentifier ?? "my-task",
105107
status: run.status ?? "PENDING",
@@ -349,4 +351,86 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)",
349351
}
350352
}
351353
);
354+
355+
// Full-keyset walk over interleaved cuid + ksuid ids: hydration must preserve the ClickHouse
356+
// (created_at DESC, run_id DESC) order across the id-space seam. A hydrate that reverts to lexical
357+
// `id desc` splits the two id-spaces into separate blocks, so it would fail this walk.
358+
replicationContainerTest(
359+
"paginating the full keyset enumerates every interleaved cuid/ksuid id once, in CH keyset order, with no empty page",
360+
async ({ clickhouseContainer, redisOptions, postgresContainer, prisma }) => {
361+
const { clickhouse } = await setupClickhouseReplication({
362+
prisma,
363+
databaseUrl: postgresContainer.getConnectionUri(),
364+
clickhouseUrl: clickhouseContainer.getConnectionUrl(),
365+
redisOptions,
366+
});
367+
368+
const ctx = await seedParents(prisma, "keysetwalk");
369+
370+
// cuid-shaped ids (25 chars, "c" prefix) and ksuid-shaped ids (27 chars, "2" prefix). Lexical
371+
// `id desc` groups all "c" ids ahead of all "2" ids; the created_at order below interleaves
372+
// them, so the two orders genuinely differ across the seam.
373+
const cuid = (n: number) => `c${String(n).padStart(24, "0")}`;
374+
const ksuid = (n: number) => `2${String(n).padStart(26, "0")}`;
375+
376+
// created_at DESC order (index 0 = most recent) interleaves the id-spaces: ksuid, cuid,
377+
// ksuid, cuid, ksuid, cuid.
378+
const now = Date.now();
379+
const seeds = [
380+
{ id: ksuid(6), friendlyId: "run_k6", createdAt: new Date(now - 0 * 60_000) },
381+
{ id: cuid(5), friendlyId: "run_c5", createdAt: new Date(now - 1 * 60_000) },
382+
{ id: ksuid(4), friendlyId: "run_k4", createdAt: new Date(now - 2 * 60_000) },
383+
{ id: cuid(3), friendlyId: "run_c3", createdAt: new Date(now - 3 * 60_000) },
384+
{ id: ksuid(2), friendlyId: "run_k2", createdAt: new Date(now - 4 * 60_000) },
385+
{ id: cuid(1), friendlyId: "run_c1", createdAt: new Date(now - 5 * 60_000) },
386+
];
387+
for (const s of seeds) {
388+
await createRun(prisma, ctx, s);
389+
}
390+
391+
await setTimeout(1500);
392+
393+
const runsRepository = new RunsRepository({ prisma, clickhouse });
394+
395+
// The authoritative order the hydrate must reproduce: exactly the CH keyset the id-list scan
396+
// returns (created_at DESC, run_id DESC). Lexical id-desc of the same ids differs from this.
397+
const chOrder = await runsRepository.listRunIds({
398+
page: { size: 100 },
399+
projectId: ctx.projectId,
400+
environmentId: ctx.environmentId,
401+
organizationId: ctx.organizationId,
402+
});
403+
const expectedOrder = chOrder.runIds;
404+
const lexicalIdDesc = [...expectedOrder].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
405+
expect(expectedOrder).not.toEqual(lexicalIdDesc); // the seam actually separates the two orders
406+
407+
// Walk the whole keyset a page at a time.
408+
const walked: string[] = [];
409+
let cursor: string | undefined;
410+
let pages = 0;
411+
while (true) {
412+
const { runs, pagination } = await runsRepository.listRuns({
413+
page: { size: 2, cursor },
414+
projectId: ctx.projectId,
415+
environmentId: ctx.environmentId,
416+
organizationId: ctx.organizationId,
417+
});
418+
pages++;
419+
expect(pages).toBeLessThan(20); // guard against a non-terminating walk
420+
421+
for (const r of runs) walked.push(r.id);
422+
423+
if (!pagination.nextCursor) break;
424+
// No empty page may be returned while more pages exist.
425+
expect(runs.length).toBeGreaterThan(0);
426+
cursor = pagination.nextCursor;
427+
}
428+
429+
// Every seeded id enumerated exactly once.
430+
expect(walked.slice().sort()).toEqual(seeds.map((s) => s.id).sort());
431+
expect(new Set(walked).size).toBe(seeds.length);
432+
// The emitted order equals the CH keyset order across the id-space seam.
433+
expect(walked).toEqual(expectedOrder);
434+
}
435+
);
352436
});

0 commit comments

Comments
 (0)