fix(webapp): cancel API findResource must return non-null for buffered runs

The route builder treats a null `findResource` result as a 404 BEFORE the
action handler runs (`apiBuilder.server.ts:321`). My C1 commit had
`findResource: async () => null`, which meant every cancel call —
including for valid PG-row runs — was 404'd by the builder before the
mutateWithFallback flow could resolve anything.

Fixes by mirroring the Phase A discriminated-union pattern: findResource
checks PG first, falls back to the buffer with env+org auth, returns
`null` only when neither store has the run. The action handler still
uses mutateWithFallback (slightly redundant lookup) so the wait-and-
bounce path stays intact.

Found while running the Phase F challenge suite — cancel was 404'ing
on a confirmed-buffered runId.
This commit is contained in:
Dan Sutton
2026-05-21 11:03:26 +01:00
parent 469dd3af47
commit b490afe239
@@ -1,14 +1,20 @@
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { $replica } from "~/db.server";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
import { mutateWithFallback } from "~/v3/mollifier/mutateWithFallback.server";
import { getMollifierBuffer } from "~/v3/mollifier/mollifierBuffer.server";
const ParamsSchema = z.object({
runParam: z.string(),
});
type ResolvedCancelTarget =
| { source: "pg"; friendlyId: string }
| { source: "buffer"; friendlyId: string };
const { action } = createActionApiRoute(
{
params: ParamsSchema,
@@ -18,12 +24,29 @@ const { action } = createActionApiRoute(
action: "write",
resource: (params) => ({ type: "runs", id: params.runParam }),
},
// PG-side authorisation is performed inside mutateWithFallback. Routing
// the resource through findResource (which would require a PG-or-buffer
// resolved discriminated union here) would duplicate the resolution
// mutateWithFallback already does, so we pass `null` to signal "open"
// and let the helper do the lookup atomically with the mutation.
findResource: async () => null,
// Mirror the Phase A read-fallback discriminated-union pattern. The
// route builder 404s if findResource returns null
// (`apiBuilder.server.ts:321`), so we must check both stores here.
// The action then re-resolves via mutateWithFallback (PG-first →
// buffer patch → wait-and-bounce) — slightly redundant lookup but
// keeps the helper's atomicity intact.
findResource: async (params, auth): Promise<ResolvedCancelTarget | null> => {
const pgRun = await $replica.taskRun.findFirst({
where: { friendlyId: params.runParam, runtimeEnvironmentId: auth.environment.id },
select: { friendlyId: true },
});
if (pgRun) return { source: "pg", friendlyId: pgRun.friendlyId };
const buffer = getMollifierBuffer();
const entry = buffer ? await buffer.getEntry(params.runParam) : null;
if (
entry &&
entry.envId === auth.environment.id &&
entry.orgId === auth.environment.organizationId
) {
return { source: "buffer", friendlyId: params.runParam };
}
return null;
},
},
async ({ params, authentication }) => {
const runId = params.runParam;