Skip to content

Commit 9b4287b

Browse files
d-csclaude
andcommitted
fix(webapp): validate mollifier snapshot dates before use
`new Date("not-a-date")` returns a truthy Invalid Date object, which would mis-classify the run as CANCELED in the read-fallback synthesised shape. Add an `asDate` helper that rejects NaN-valued parses and use it for `cancelledAt` and `delayUntil`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9246ca1 commit 9b4287b

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

apps/webapp/app/v3/mollifier/readFallback.server.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ function asStringArray(value: unknown): string[] {
102102
return Array.isArray(value) && value.every((v) => typeof v === "string") ? (value as string[]) : [];
103103
}
104104

105+
function asDate(value: unknown): Date | undefined {
106+
const raw = asString(value);
107+
if (!raw) return undefined;
108+
const parsed = new Date(raw);
109+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
110+
}
111+
105112
export async function findRunByIdWithMollifierFallback(
106113
input: ReadFallbackInput,
107114
deps: ReadFallbackDeps = {},
@@ -134,17 +141,15 @@ export async function findRunByIdWithMollifierFallback(
134141
? (snapshot.environment as Record<string, unknown>)
135142
: undefined;
136143

137-
const cancelledAtRaw = asString(snapshot.cancelledAt);
138-
const cancelledAt = cancelledAtRaw ? new Date(cancelledAtRaw) : undefined;
144+
const cancelledAt = asDate(snapshot.cancelledAt);
139145
const cancelReason = asString(snapshot.cancelReason);
140146
let status: SyntheticRun["status"] = "QUEUED";
141147
if (cancelledAt) {
142148
status = "CANCELED";
143149
} else if (entry.status === "FAILED") {
144150
status = "FAILED";
145151
}
146-
const delayUntilRaw = asString(snapshot.delayUntil);
147-
const delayUntil = delayUntilRaw ? new Date(delayUntilRaw) : undefined;
152+
const delayUntil = asDate(snapshot.delayUntil);
148153

149154
return {
150155
id: RunId.fromFriendlyId(entry.runId),

apps/webapp/test/mollifierReadFallback.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,58 @@ describe("findRunByIdWithMollifierFallback", () => {
257257
expect(result!.runTags).toEqual(["t1", "t2"]);
258258
});
259259

260+
it("treats invalid date strings as undefined and does not mis-classify status as CANCELED", async () => {
261+
const entry: BufferEntry = {
262+
runId: "run_1",
263+
envId: "env_a",
264+
orgId: "org_1",
265+
payload: JSON.stringify({
266+
taskIdentifier: "t",
267+
cancelledAt: "not-a-date",
268+
cancelReason: "user requested",
269+
delayUntil: "also-not-a-date",
270+
}),
271+
status: "QUEUED",
272+
attempts: 0,
273+
createdAt: NOW,
274+
};
275+
const result = await findRunByIdWithMollifierFallback(
276+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
277+
{ getBuffer: () => fakeBuffer(entry) },
278+
);
279+
expect(result).not.toBeNull();
280+
expect(result!.status).toBe("QUEUED");
281+
expect(result!.cancelledAt).toBeUndefined();
282+
expect(result!.delayUntil).toBeUndefined();
283+
});
284+
285+
it("parses valid ISO date strings on cancelledAt and delayUntil", async () => {
286+
const cancelledAtIso = "2026-05-11T13:00:00.000Z";
287+
const delayUntilIso = "2026-05-11T14:00:00.000Z";
288+
const entry: BufferEntry = {
289+
runId: "run_1",
290+
envId: "env_a",
291+
orgId: "org_1",
292+
payload: JSON.stringify({
293+
taskIdentifier: "t",
294+
cancelledAt: cancelledAtIso,
295+
cancelReason: "user requested",
296+
delayUntil: delayUntilIso,
297+
}),
298+
status: "QUEUED",
299+
attempts: 0,
300+
createdAt: NOW,
301+
};
302+
const result = await findRunByIdWithMollifierFallback(
303+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
304+
{ getBuffer: () => fakeBuffer(entry) },
305+
);
306+
expect(result!.status).toBe("CANCELED");
307+
expect(result!.cancelledAt).toEqual(new Date(cancelledAtIso));
308+
expect(result!.cancelReason).toBe("user requested");
309+
expect(result!.delayUntil).toEqual(new Date(delayUntilIso));
310+
});
311+
260312
it("falls back to entry.envId for runtimeEnvironmentId when snapshot lacks environment.id", async () => {
261313
const entry: BufferEntry = {
262314
runId: "run_1",

0 commit comments

Comments
 (0)