Skip to content

Commit cc74842

Browse files
authored
test(webapp): replace slow metadata replica guard with unit test (#4312)
1 parent 1cbe25b commit cc74842

1 file changed

Lines changed: 75 additions & 218 deletions

File tree

Lines changed: 75 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,17 @@
1-
// Property: the metadata GET loader resolves a live run on a replica+buffer double-miss via its primary
2-
// fallback, returning 200 + the run's metadata. Drives the REAL exported `loader` end-to-end:
3-
// 1. runStore.findRun(..., $replica) → REPLICA (lagging) → null
4-
// 2. findRunByIdWithMollifierFallback(...) → buffer MISS (null)
5-
// 3. runStore.findRunOnPrimary(...) → owning PRIMARY → HIT
6-
// Store is REAL: a split RoutingRunStore over two testcontainer Postgres DBs, the owning
7-
// (legacy/control-plane) REPLICA frozen behind the shared laggingReplica. Only the loader's webapp
8-
// singletons (auth, $replica brand, mollifier buffer, route builder, logging) are stubbed.
9-
10-
import { describe, expect, vi } from "vitest";
11-
import { heteroRunOpsPostgresTest, laggingReplica } from "@internal/testcontainers";
12-
import type { PrismaClient } from "@trigger.dev/database";
13-
import type { RunOpsPrismaClient } from "@internal/run-ops-database";
14-
import { PostgresRunStore, RoutingRunStore } from "@internal/run-store";
15-
import type { CreateRunInput } from "@internal/run-store";
16-
17-
// vi.mock factories are hoisted above the imports, so anything they reference must be created inside
18-
// vi.hoisted. `holder.store` is filled in per-test with the REAL split router; the mocked
19-
// `~/v3/runStore.server` export is a stable Proxy that forwards every property access to it. The
20-
// branded `$replica` marker uses the global-registry symbol the run-store brands replicas with
21-
// (readReplicaClient.ts) — a branded client makes the routing store keep the read on the owning
22-
// REPLICA (no primary escalation), which under lag is exactly the miss the primary fallback recovers.
23-
const { holder } = vi.hoisted(() => {
24-
const REPLICA_BRAND = Symbol.for("trigger.dev/run-store/read-replica");
25-
return {
26-
holder: {
27-
store: undefined as unknown,
28-
brandedReplica: { [REPLICA_BRAND]: true } as object,
29-
environment: undefined as unknown,
30-
bufferResult: null as unknown,
31-
},
32-
};
33-
});
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const { mocks } = vi.hoisted(() => ({
4+
mocks: {
5+
replica: {},
6+
environment: { id: "env_meta", organizationId: "org_meta" },
7+
authenticateApiRequest: vi.fn(),
8+
findRun: vi.fn(),
9+
findRunOnPrimary: vi.fn(),
10+
findRunByIdWithMollifierFallback: vi.fn(),
11+
},
12+
}));
3413

35-
vi.mock("~/db.server", () => ({ prisma: {}, $replica: holder.brandedReplica }));
14+
vi.mock("~/db.server", () => ({ prisma: {}, $replica: mocks.replica }));
3615
vi.mock("~/env.server", () => ({
3716
env: {
3817
TASK_RUN_METADATA_MAXIMUM_SIZE: 256 * 1024,
@@ -41,42 +20,30 @@ vi.mock("~/env.server", () => ({
4120
TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS: 10,
4221
},
4322
}));
44-
// The route module builds its action at import time via createActionApiRoute; stub the builder so the
45-
// heavy platform/auth middleware graph never evaluates. We drive the exported `loader` directly.
4623
vi.mock("~/services/routeBuilders/apiBuilder.server", () => ({
4724
createActionApiRoute: () => ({ action: vi.fn() }),
4825
}));
4926
vi.mock("~/services/apiAuth.server", () => ({
50-
authenticateApiRequest: vi.fn(async () => ({ environment: holder.environment })),
27+
authenticateApiRequest: mocks.authenticateApiRequest,
5128
}));
52-
// Inject the REAL split router. A stable Proxy keeps the named import binding constant while
53-
// forwarding every method to the per-test router set in holder.store.
5429
vi.mock("~/v3/runStore.server", () => ({
55-
runStore: new Proxy(
56-
{},
57-
{
58-
get(_target, prop) {
59-
const store = holder.store as Record<string | symbol, unknown>;
60-
if (!store) throw new Error("test bug: holder.store not initialised before loader ran");
61-
const value = store[prop];
62-
return typeof value === "function"
63-
? (value as (...a: unknown[]) => unknown).bind(store)
64-
: value;
65-
},
66-
}
67-
),
30+
runStore: {
31+
findRun: mocks.findRun,
32+
findRunOnPrimary: mocks.findRunOnPrimary,
33+
},
6834
}));
69-
// Buffer fallback returns whatever the test set (null = buffer miss, the double-miss scenario).
7035
vi.mock("~/v3/mollifier/readFallback.server", () => ({
71-
findRunByIdWithMollifierFallback: vi.fn(async () => holder.bufferResult),
36+
findRunByIdWithMollifierFallback: mocks.findRunByIdWithMollifierFallback,
7237
}));
73-
// Action-only leaf; stub to keep the import graph light.
7438
vi.mock("~/v3/mollifier/applyMetadataMutation.server", () => ({
7539
applyMetadataMutationToBufferedRun: vi.fn(),
7640
}));
7741
vi.mock("~/services/metadata/updateMetadataInstance.server", () => ({
7842
updateMetadataService: { call: vi.fn(async () => undefined) },
7943
}));
44+
vi.mock("~/services/realtime/runChangeNotifierInstance.server", () => ({
45+
publishChangeRecord: vi.fn(),
46+
}));
8047
vi.mock("~/v3/services/common.server", () => ({
8148
ServiceValidationError: class extends Error {},
8249
}));
@@ -86,176 +53,66 @@ vi.mock("~/services/logger.server", () => ({
8653

8754
import { loader } from "~/routes/api.v1.runs.$runId.metadata";
8855

89-
// A cuid (25 chars after `run_`) classifies LEGACY, so both the create and the friendlyId-keyed reads
90-
// route to the legacy (control-plane) store — the store that owns this run.
91-
const CUID_25 = "c".repeat(25);
56+
const friendlyId = "run_meta_live";
57+
const runWhere = { friendlyId, runtimeEnvironmentId: mocks.environment.id };
58+
const metadataSelect = { select: { metadata: true, metadataType: true } };
9259

93-
async function seedEnvironmentLegacy(prisma: PrismaClient, suffix: string) {
94-
const organization = await prisma.organization.create({
95-
data: { title: `Org ${suffix}`, slug: `org-${suffix}` },
96-
});
97-
const project = await prisma.project.create({
98-
data: {
99-
name: `Project ${suffix}`,
100-
slug: `project-${suffix}`,
101-
externalRef: `proj_${suffix}`,
102-
organizationId: organization.id,
103-
},
104-
});
105-
const environment = await prisma.runtimeEnvironment.create({
106-
data: {
107-
type: "DEVELOPMENT",
108-
slug: "dev",
109-
projectId: project.id,
110-
organizationId: organization.id,
111-
apiKey: `tr_dev_${suffix}`,
112-
pkApiKey: `pk_dev_${suffix}`,
113-
shortcode: `short_${suffix}`,
114-
},
115-
});
116-
return { organization, project, environment };
117-
}
118-
119-
function buildCreateRunInput(params: {
120-
runId: string;
121-
friendlyId: string;
122-
organizationId: string;
123-
projectId: string;
124-
runtimeEnvironmentId: string;
125-
metadata?: string;
126-
metadataType?: string;
127-
}): CreateRunInput {
128-
return {
129-
data: {
130-
id: params.runId,
131-
engine: "V2",
132-
status: "PENDING",
133-
friendlyId: params.friendlyId,
134-
runtimeEnvironmentId: params.runtimeEnvironmentId,
135-
environmentType: "DEVELOPMENT",
136-
organizationId: params.organizationId,
137-
projectId: params.projectId,
138-
taskIdentifier: "my-task",
139-
payload: '{"hello":"world"}',
140-
payloadType: "application/json",
141-
metadata: params.metadata,
142-
metadataType: params.metadataType,
143-
context: { foo: "bar" },
144-
traceContext: { trace: "ctx" },
145-
traceId: "trace_1",
146-
spanId: "span_1",
147-
runTags: ["alpha"],
148-
queue: "task/my-task",
149-
isTest: false,
150-
taskEventStore: "taskEvent",
151-
depth: 0,
152-
createdAt: new Date("2024-01-01T00:00:00.000Z"),
153-
},
154-
snapshot: {
155-
engine: "V2",
156-
executionStatus: "RUN_CREATED",
157-
description: "Run was created",
158-
runStatus: "PENDING",
159-
environmentId: params.runtimeEnvironmentId,
160-
environmentType: "DEVELOPMENT",
161-
projectId: params.projectId,
162-
organizationId: params.organizationId,
163-
},
164-
};
165-
}
166-
167-
function buildRouterWithLaggingLegacyReplica(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) {
168-
const legacyReplica = laggingReplica(prisma14, [{ model: "taskRun", mode: "missing" }]);
169-
const legacyStore = new PostgresRunStore({
170-
prisma: prisma14,
171-
readOnlyPrisma: legacyReplica.client,
172-
schemaVariant: "legacy",
173-
});
174-
const newStore = new PostgresRunStore({
175-
prisma: prisma17 as never,
176-
readOnlyPrisma: prisma17 as never,
177-
schemaVariant: "dedicated",
178-
});
179-
const router = new RoutingRunStore({ new: newStore, legacy: legacyStore });
180-
return { router, legacyStore, legacyReplica };
181-
}
182-
183-
async function callLoader(friendlyId: string) {
184-
const response = await loader({
185-
request: new Request("https://api.trigger.dev/api/v1/runs/" + friendlyId + "/metadata", {
60+
async function callLoader(runId = friendlyId) {
61+
return (await loader({
62+
request: new Request(`https://example.com/api/v1/runs/${runId}/metadata`, {
18663
headers: { Authorization: "Bearer tr_dev_meta" },
18764
}),
188-
params: { runId: friendlyId },
65+
params: { runId },
18966
context: {} as never,
190-
});
191-
return response as Response;
67+
})) as Response;
19268
}
19369

194-
describe("metadata GET loader under replica lag", () => {
195-
heteroRunOpsPostgresTest(
196-
"metadata GET loader resolves a live run on a replica and buffer double-miss",
197-
async ({ prisma14, prisma17 }) => {
198-
const { router, legacyStore, legacyReplica } = buildRouterWithLaggingLegacyReplica(
199-
prisma14,
200-
prisma17
201-
);
202-
203-
const seed = await seedEnvironmentLegacy(prisma14, "meta");
204-
const runId = `run_${CUID_25}`; // cuid → LEGACY-owned
205-
const friendlyId = "run_meta_live";
206-
const metadata = '{"phase":"one"}';
207-
await legacyStore.createRun(
208-
buildCreateRunInput({
209-
runId,
210-
friendlyId,
211-
organizationId: seed.organization.id,
212-
projectId: seed.project.id,
213-
runtimeEnvironmentId: seed.environment.id,
214-
metadata,
215-
metadataType: "application/json",
216-
})
217-
);
218-
219-
// Wire the loader's module singletons: the real split router, the authenticated env, and a
220-
// buffer that MISSES (the run has drained from the buffer to the primary but the replica has
221-
// not caught up — the exact double-miss under test).
222-
holder.store = router;
223-
holder.environment = { id: seed.environment.id, organizationId: seed.organization.id };
224-
holder.bufferResult = null;
225-
226-
const response = await callLoader(friendlyId);
227-
const body = (await response.json()) as {
228-
metadata?: unknown;
229-
metadataType?: unknown;
230-
error?: string;
231-
};
232-
233-
// The replica WAS consulted (and, being frozen, missed) — proving the read genuinely went
234-
// through the lagging replica and the recovery is the primary fallback, not a lucky replica hit.
235-
expect(legacyReplica.wasHit()).toBe(true);
236-
237-
// The property: the loader re-reads the owning primary and returns the live run's metadata.
238-
expect(response.status).toBe(200);
239-
expect(body.metadata).toBe(metadata);
240-
expect(body.metadataType).toBe("application/json");
241-
}
242-
);
70+
beforeEach(() => {
71+
vi.clearAllMocks();
72+
mocks.authenticateApiRequest.mockResolvedValue({ environment: mocks.environment });
73+
mocks.findRun.mockResolvedValue(null);
74+
mocks.findRunByIdWithMollifierFallback.mockResolvedValue(null);
75+
mocks.findRunOnPrimary.mockResolvedValue(null);
76+
});
24377

244-
// Negative control: when the run truly does not exist anywhere (replica miss + buffer miss +
245-
// primary miss), the loader must still 404. This pins the behavior to "recover a LIVE run" rather
246-
// than "never 404".
247-
heteroRunOpsPostgresTest(
248-
"metadata GET loader 404s when the run is absent on the primary too",
249-
async ({ prisma14, prisma17 }) => {
250-
const { router } = buildRouterWithLaggingLegacyReplica(prisma14, prisma17);
251-
const seed = await seedEnvironmentLegacy(prisma14, "meta_absent");
78+
describe("metadata GET loader under replica lag", () => {
79+
it("resolves a live run after a replica and buffer double-miss", async () => {
80+
const metadata = '{"phase":"one"}';
81+
mocks.findRunOnPrimary.mockResolvedValue({
82+
metadata,
83+
metadataType: "application/json",
84+
});
85+
86+
const response = await callLoader();
87+
88+
expect(mocks.findRun).toHaveBeenCalledWith(runWhere, metadataSelect, mocks.replica);
89+
expect(mocks.findRunByIdWithMollifierFallback).toHaveBeenCalledWith({
90+
runId: friendlyId,
91+
environmentId: mocks.environment.id,
92+
organizationId: mocks.environment.organizationId,
93+
});
94+
expect(mocks.findRunOnPrimary).toHaveBeenCalledWith(runWhere, metadataSelect);
95+
expect(mocks.findRun.mock.invocationCallOrder[0]).toBeLessThan(
96+
mocks.findRunByIdWithMollifierFallback.mock.invocationCallOrder[0]
97+
);
98+
expect(mocks.findRunByIdWithMollifierFallback.mock.invocationCallOrder[0]).toBeLessThan(
99+
mocks.findRunOnPrimary.mock.invocationCallOrder[0]
100+
);
101+
await expect(response.json()).resolves.toEqual({
102+
metadata,
103+
metadataType: "application/json",
104+
});
105+
expect(response.status).toBe(200);
106+
});
252107

253-
holder.store = router;
254-
holder.environment = { id: seed.environment.id, organizationId: seed.organization.id };
255-
holder.bufferResult = null;
108+
it("returns 404 when the run is absent from the primary too", async () => {
109+
const response = await callLoader("run_does_not_exist");
256110

257-
const response = await callLoader("run_does_not_exist");
258-
expect(response.status).toBe(404);
259-
}
260-
);
111+
expect(mocks.findRunOnPrimary).toHaveBeenCalledWith(
112+
{ friendlyId: "run_does_not_exist", runtimeEnvironmentId: mocks.environment.id },
113+
metadataSelect
114+
);
115+
await expect(response.json()).resolves.toEqual({ error: "Run not found" });
116+
expect(response.status).toBe(404);
117+
});
261118
});

0 commit comments

Comments
 (0)