Skip to content

Commit c9d7f7e

Browse files
committed
test(webapp): add the split test files missing from the previous commit
The split of triggerTask.server.test.ts and triggerFailedTask.test.ts deleted the originals but the replacement files were never staged, so their 12 container tests silently stopped running.
1 parent 178c490 commit c9d7f7e

7 files changed

Lines changed: 1212 additions & 0 deletions
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Split from triggerTask.server.test.ts (combined parent + locked-worker read)
2+
// so CI's duration-based sharding can balance the container-heavy tests.
3+
import { describe, expect, vi } from "vitest";
4+
5+
// Mock the db prisma client. The service is constructed against a real
6+
// testcontainer prisma instead — these empty singletons only satisfy the
7+
// module-level imports of the production wiring (infrastructure boundary).
8+
vi.mock("~/db.server", () => ({
9+
prisma: {},
10+
$replica: {},
11+
runOpsNewPrisma: {},
12+
runOpsLegacyPrisma: {},
13+
runOpsNewReplica: {},
14+
runOpsLegacyReplica: {},
15+
}));
16+
// Inherited harness boilerplate. The parent read under test takes the
17+
// findRun(where, client) overload with this.prisma, so it does not consult this
18+
// flag; the mock only satisfies other wiring imported transitively.
19+
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
20+
21+
vi.mock("~/services/platform.v3.server", async (importOriginal) => {
22+
const actual = (await importOriginal()) as Record<string, unknown>;
23+
return {
24+
...actual,
25+
getEntitlement: vi.fn(),
26+
};
27+
});
28+
29+
import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests";
30+
import { assertNonNullable, containerTest } from "@internal/testcontainers";
31+
import { trace } from "@opentelemetry/api";
32+
import { IdempotencyKeyConcern } from "~/runEngine/concerns/idempotencyKeys.server";
33+
import { DefaultQueueManager } from "~/runEngine/concerns/queues.server";
34+
import { RunEngineTriggerTaskService } from "./triggerTask.server";
35+
import {
36+
buildEngine,
37+
CapturingParentRunValidator,
38+
MockPayloadProcessor,
39+
MockTraceEventConcern,
40+
} from "./triggerTask.server.test.helpers";
41+
42+
vi.setConfig({ testTimeout: 60_000 }); // 60 seconds timeout
43+
44+
describe("RunEngineTriggerTaskService combined parent + locked-worker reads", () => {
45+
containerTest(
46+
"issues two independent single-table reads when one call supplies both parentRunId and lockToVersion",
47+
async ({ prisma, redisOptions }) => {
48+
const engine = buildEngine(prisma, redisOptions);
49+
50+
try {
51+
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
52+
const taskIdentifier = "test-task";
53+
const { worker } = await setupBackgroundWorker(engine, environment, taskIdentifier);
54+
55+
const workerRow = await prisma.backgroundWorker.findUniqueOrThrow({
56+
where: { id: worker.id },
57+
});
58+
59+
// Count BOTH reads issued by the service on the control-plane client:
60+
// the parent read (runStore.findRun → taskRun.findFirst) and the
61+
// locked-worker read (backgroundWorker.findFirst). Capture every
62+
// findFirst arg so we can assert no read carries a cross-seam include.
63+
let taskRunFindFirstCalls = 0;
64+
let backgroundWorkerFindFirstCalls = 0;
65+
const findFirstArgs: any[] = [];
66+
const countingPrisma = new Proxy(prisma, {
67+
get(target, prop, receiver) {
68+
if (prop === "backgroundWorker") {
69+
const delegate = Reflect.get(target, prop, receiver);
70+
return new Proxy(delegate, {
71+
get(bwTarget, bwProp, bwReceiver) {
72+
if (bwProp === "findFirst") {
73+
return async (args: any) => {
74+
backgroundWorkerFindFirstCalls += 1;
75+
findFirstArgs.push(args);
76+
return (delegate as any).findFirst(args);
77+
};
78+
}
79+
const value = Reflect.get(bwTarget, bwProp, bwReceiver);
80+
return typeof value === "function" ? value.bind(bwTarget) : value;
81+
},
82+
});
83+
}
84+
if (prop === "taskRun") {
85+
const delegate = Reflect.get(target, prop, receiver);
86+
return new Proxy(delegate, {
87+
get(trTarget, trProp, trReceiver) {
88+
if (trProp === "findFirst") {
89+
return async (args: any) => {
90+
taskRunFindFirstCalls += 1;
91+
findFirstArgs.push(args);
92+
return (delegate as any).findFirst(args);
93+
};
94+
}
95+
const value = Reflect.get(trTarget, trProp, trReceiver);
96+
return typeof value === "function" ? value.bind(trTarget) : value;
97+
},
98+
});
99+
}
100+
const value = Reflect.get(target, prop, receiver);
101+
return typeof value === "function" ? value.bind(target) : value;
102+
},
103+
}) as typeof prisma;
104+
105+
const triggerTaskService = new RunEngineTriggerTaskService({
106+
engine,
107+
prisma: countingPrisma,
108+
payloadProcessor: new MockPayloadProcessor(),
109+
// queueConcern/idempotency get the real unproxied prisma so the
110+
// counting proxy only observes reads issued by the service itself.
111+
queueConcern: new DefaultQueueManager(prisma, engine),
112+
idempotencyKeyConcern: new IdempotencyKeyConcern(
113+
prisma,
114+
engine,
115+
new MockTraceEventConcern()
116+
),
117+
validator: new CapturingParentRunValidator(),
118+
traceEventConcern: new MockTraceEventConcern(),
119+
tracer: trace.getTracer("test", "0.0.0"),
120+
metadataMaximumSize: 1024 * 1024 * 1,
121+
});
122+
123+
// ROOT parent first (uses the unproxied prisma via a separate service so
124+
// its internal reads don't pollute the child's counts).
125+
const parentService = new RunEngineTriggerTaskService({
126+
engine,
127+
prisma,
128+
payloadProcessor: new MockPayloadProcessor(),
129+
queueConcern: new DefaultQueueManager(prisma, engine),
130+
idempotencyKeyConcern: new IdempotencyKeyConcern(
131+
prisma,
132+
engine,
133+
new MockTraceEventConcern()
134+
),
135+
validator: new CapturingParentRunValidator(),
136+
traceEventConcern: new MockTraceEventConcern(),
137+
tracer: trace.getTracer("test", "0.0.0"),
138+
metadataMaximumSize: 1024 * 1024 * 1,
139+
});
140+
const parentResult = await parentService.call({
141+
taskId: taskIdentifier,
142+
environment,
143+
body: { payload: { kind: "parent" } },
144+
});
145+
assertNonNullable(parentResult);
146+
147+
// CHILD supplying BOTH parentRunId AND lockToVersion in one call.
148+
const childResult = await triggerTaskService.call({
149+
taskId: taskIdentifier,
150+
environment,
151+
body: {
152+
payload: { kind: "child" },
153+
options: {
154+
parentRunId: parentResult.run.friendlyId,
155+
lockToVersion: workerRow.version,
156+
},
157+
},
158+
});
159+
assertNonNullable(childResult);
160+
161+
const parentRow = await prisma.taskRun.findUniqueOrThrow({
162+
where: { id: parentResult.run.id },
163+
});
164+
const childRow = await prisma.taskRun.findUniqueOrThrow({
165+
where: { id: childResult.run.id },
166+
});
167+
168+
// Child resolved the parent (single-table parent read).
169+
expect(childRow.parentTaskRunId).toBe(parentRow.id);
170+
expect(childRow.depth).toBe(parentRow.depth + 1);
171+
172+
// Child locked to the worker (single-table worker read).
173+
expect(childRow.lockedToVersionId).toBe(workerRow.id);
174+
expect(childRow.taskVersion).toBe(workerRow.version);
175+
176+
// Exactly one backgroundWorker.findFirst fired for the locked-worker read,
177+
// and at least one taskRun.findFirst fired for the parent read.
178+
expect(backgroundWorkerFindFirstCalls).toBe(1);
179+
expect(taskRunFindFirstCalls).toBeGreaterThanOrEqual(1);
180+
181+
// NO-JOIN proof: no captured read carried an `include` joining
182+
// taskRun <-> backgroundWorker. Every findFirst arg has include undefined.
183+
for (const args of findFirstArgs) {
184+
expect(args?.include).toBeUndefined();
185+
}
186+
} finally {
187+
await engine.quit();
188+
}
189+
}
190+
);
191+
});

0 commit comments

Comments
 (0)