Skip to content

Commit 9535ae6

Browse files
d-csclaude
andcommitted
fix(run-ops split): resolve parent run through an injectable run store in TriggerFailedTaskService
TriggerFailedTaskService read the parent run via the ambient module-singleton store while the engine wrote the run through its own store, so a ksuid parent's row was not found and parentTaskRunId came back null. Add an optional injected runStore (defaults to the shared singleton, preserving production behaviour) and resolve the parent through it at both call sites, mirroring triggerTask.server.ts. Align the three affected webapp tests to read through the same store the engine wrote to: triggerFailedTask.test.ts passes engine.runStore; performTaskRunAlerts routing passes a passthrough store over the seeded container; triggerTask.test.ts stubs the run-ops db handles and pins split mode off so the idempotency dedup uses the container client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 641b55c commit 9535ae6

4 files changed

Lines changed: 34 additions & 4 deletions

File tree

apps/webapp/app/runEngine/services/triggerFailedTask.server.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import { getEventRepository } from "~/v3/eventRepository/index.server";
1313
import { PerformTaskRunAlertsService } from "~/v3/services/alerts/performTaskRunAlerts.server";
1414
import { DefaultQueueManager } from "../concerns/queues.server";
1515
import type { TriggerTaskRequest } from "../types";
16-
import { runStore } from "~/v3/runStore.server";
16+
import { runStore as defaultRunStore } from "~/v3/runStore.server";
17+
import type { RunStore } from "@internal/run-store";
1718

1819
export type TriggerFailedTaskRequest = {
1920
/** The task identifier (e.g. "my-task") */
@@ -65,19 +66,25 @@ export class TriggerFailedTaskService {
6566
// Injected so the migrated-marker read stays off the hot path when split is off
6667
// (same guard as RunEngineTriggerTaskService); defaults to the live resolver.
6768
private readonly isSplitEnabled: () => Promise<boolean>;
69+
// Resolves the parent run for depth/root/parent linkage. Defaults to the shared
70+
// singleton (in production the same store the engine writes through). Injected in
71+
// tests so the read resolves on the same store the engine wrote to.
72+
private readonly runStore: RunStore;
6873

6974
constructor(opts: {
7075
prisma: PrismaClientOrTransaction;
7176
engine: RunEngine;
7277
replicaPrisma?: PrismaClientOrTransaction;
7378
isKnownMigrated?: (runId: string) => Promise<boolean>;
7479
isSplitEnabled?: () => Promise<boolean>;
80+
runStore?: RunStore;
7581
}) {
7682
this.prisma = opts.prisma;
7783
this.replicaPrisma = opts.replicaPrisma ?? opts.prisma;
7884
this.engine = opts.engine;
7985
this.isKnownMigrated = opts.isKnownMigrated ?? defaultIsKnownMigrated;
8086
this.isSplitEnabled = opts.isSplitEnabled ?? defaultIsSplitEnabled;
87+
this.runStore = opts.runStore ?? defaultRunStore;
8188
}
8289

8390
// Mint a failed run's friendlyId. The id-kind decides which store the run is
@@ -135,7 +142,7 @@ export class TriggerFailedTaskService {
135142

136143
// Resolve parent run for rootTaskRunId and depth (same as triggerTask.server.ts)
137144
const parentRun = request.parentRunId
138-
? await runStore.findRun(
145+
? await this.runStore.findRun(
139146
{
140147
id: RunId.fromFriendlyId(request.parentRunId),
141148
runtimeEnvironmentId: request.environment.id,
@@ -341,7 +348,7 @@ export class TriggerFailedTaskService {
341348
let depth = 0;
342349

343350
if (opts.parentRunId) {
344-
const parentRun = await runStore.findRun(
351+
const parentRun = await this.runStore.findRun(
345352
{
346353
id: RunId.fromFriendlyId(opts.parentRunId),
347354
runtimeEnvironmentId: opts.environmentId,

apps/webapp/test/engine/triggerFailedTask.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
4343
const service = new TriggerFailedTaskService({
4444
prisma,
4545
engine,
46+
// Read the parent through the same store the engine wrote it to.
47+
runStore: engine.runStore,
4648
isKnownMigrated: async () => false,
4749
});
4850

@@ -97,6 +99,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
9799
const service = new TriggerFailedTaskService({
98100
prisma,
99101
engine,
102+
// Read the parent through the same store the engine wrote it to.
103+
runStore: engine.runStore,
100104
isKnownMigrated: async () => false,
101105
});
102106

@@ -154,6 +158,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
154158
const service = new TriggerFailedTaskService({
155159
prisma,
156160
engine,
161+
// Read the parent through the same store the engine wrote it to.
162+
runStore: engine.runStore,
157163
isKnownMigrated: async () => false,
158164
});
159165

@@ -201,6 +207,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
201207
const service = new TriggerFailedTaskService({
202208
prisma,
203209
engine,
210+
// Read the parent through the same store the engine wrote it to.
211+
runStore: engine.runStore,
204212
isKnownMigrated: async (id: string) => id === parentFriendlyId,
205213
});
206214

@@ -247,6 +255,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
247255
const service = new TriggerFailedTaskService({
248256
prisma,
249257
engine,
258+
// Read the parent through the same store the engine wrote it to.
259+
runStore: engine.runStore,
250260
isKnownMigrated: async () => false,
251261
});
252262

@@ -278,6 +288,8 @@ describe("TriggerFailedTaskService — failed run residency", () => {
278288
const service = new TriggerFailedTaskService({
279289
prisma,
280290
engine,
291+
// Read the parent through the same store the engine wrote it to.
292+
runStore: engine.runStore,
281293
isKnownMigrated: async () => false,
282294
});
283295

apps/webapp/test/engine/triggerTask.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { describe, expect, vi } from "vitest";
22

3-
// Mock the db prisma client
3+
// Mock the db prisma client. The run-ops handles are stubbed so the idempotency
4+
// dedup import resolves; with split off (below) they are never used — the concern's
5+
// constructor prisma is passed through to every store call.
46
vi.mock("~/db.server", () => ({
57
prisma: {},
68
$replica: {},
9+
runOpsNewPrisma: {},
10+
runOpsLegacyPrisma: {},
711
}));
812

13+
// Keep split off so resolveIdempotencyDedupClient returns the passed container client.
14+
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
15+
916
vi.mock("~/services/platform.v3.server", async (importOriginal) => {
1017
const actual = (await importOriginal()) as Record<string, unknown>;
1118
return {

apps/webapp/test/performTaskRunAlertsStoreRouting.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ describe("PerformTaskRunAlertsService passthrough (single-DB)", () => {
338338

339339
const service = new PerformTaskRunAlertsService({
340340
prisma,
341+
// The single-DB default store: a passthrough PostgresRunStore over the one
342+
// container. Injected explicitly so the read resolves on the container the run
343+
// was seeded into, not the ambient module singleton.
344+
runStore: new PostgresRunStore({ prisma, readOnlyPrisma: prisma }),
341345
controlPlaneResolver: buildControlPlaneResolver(prisma),
342346
});
343347
await service.call(id).catch(() => {});

0 commit comments

Comments
 (0)