mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-20 21:41:43 +10:00
fix(stylesheet): harden PDF preflight and surface worker failures (#3284)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
4a8f87ab8f
commit
ba8e1be2ab
@@ -55,4 +55,42 @@ describe("stylesheet preflight worker", () => {
|
||||
cause: { name: "ZodError", message: "Invalid resume data", issues },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes a sanitized cause when PDF preflight throws an unexpected error", async () => {
|
||||
let handler: ((event: MessageEvent<PreflightWorkerRequest>) => Promise<void>) | undefined;
|
||||
const postMessage = vi.fn();
|
||||
vi.stubGlobal("self", {
|
||||
postMessage,
|
||||
addEventListener: vi.fn((_type, listener) => {
|
||||
handler = listener as typeof handler;
|
||||
}),
|
||||
});
|
||||
mocks.renderPreflightPdf.mockRejectedValueOnce(new Error("Canvas is already closed"));
|
||||
vi.resetModules();
|
||||
await import("./preflight.worker");
|
||||
|
||||
await handler?.({
|
||||
data: {
|
||||
type: "preflight",
|
||||
requestId: 8,
|
||||
editGeneration: 4,
|
||||
input: {} as never,
|
||||
limits: {} as never,
|
||||
},
|
||||
} as unknown as MessageEvent<PreflightWorkerRequest>);
|
||||
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "preflight_result",
|
||||
requestId: 8,
|
||||
editGeneration: 4,
|
||||
result: expect.objectContaining({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: expect.stringContaining("Canvas is already closed"),
|
||||
diagnostics: [],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,13 @@ const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflig
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const sanitizeWorkerCause = (cause: unknown): string => {
|
||||
if (!(cause instanceof Error)) return "The PDF preflight worker failed.";
|
||||
const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
if (!detail) return "The PDF preflight worker failed.";
|
||||
return `The PDF preflight worker failed. (${cause.name}: ${detail})`;
|
||||
};
|
||||
|
||||
const serializeZodCause = (cause: unknown): SerializedPreflightCause | undefined => {
|
||||
if (!(cause instanceof Error) || cause.name !== "ZodError" || !("issues" in cause) || !Array.isArray(cause.issues)) {
|
||||
return;
|
||||
@@ -49,7 +56,7 @@ self.addEventListener("message", async ({ data }: MessageEvent<PreflightWorkerRe
|
||||
self.postMessage(response);
|
||||
return;
|
||||
}
|
||||
const result = failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed.");
|
||||
const result = failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", sanitizeWorkerCause(cause));
|
||||
const response: PreflightWorkerResponse = {
|
||||
type: "preflight_result",
|
||||
requestId: data.requestId,
|
||||
|
||||
@@ -759,6 +759,66 @@ describe("stylesheet store runtime", () => {
|
||||
expect(runtime.store.getState().status).toBe("error");
|
||||
});
|
||||
|
||||
it("surfaces browser preflight failures as diagnostics when the worker returns an empty list", async () => {
|
||||
let resolveMutate!: (value: MutationResult & { diagnostics: never[] }) => void;
|
||||
const mutate = vi.fn(
|
||||
() =>
|
||||
new Promise<MutationResult & { diagnostics: never[] }>((resolve) => {
|
||||
resolveMutate = resolve;
|
||||
}),
|
||||
);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: async ({ editGeneration }) => ({
|
||||
type: "compile_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
}),
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed.",
|
||||
diagnostics: [],
|
||||
},
|
||||
}),
|
||||
mutate,
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("@version 1;\nsection { color: teal; }");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().diagnostics).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
severity: "error",
|
||||
message: "The PDF preflight worker failed.",
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(runtime.store.getState().diagnostics.some(({ severity }) => severity === "error")).toBe(true);
|
||||
expect(mutate).toHaveBeenCalled();
|
||||
resolveMutate({
|
||||
stylesheet: stylesheet("@version 1;\n"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration: 1,
|
||||
diagnostics: [],
|
||||
});
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
it("finishes an edit when refreshIntelligence interleaves through the shared compile client", async () => {
|
||||
const { createCompileWorkerClient } = await import("./worker-client");
|
||||
const listeners = new Map<string, Set<EventListener>>();
|
||||
|
||||
@@ -113,6 +113,18 @@ const emptySemanticTree = (): SemanticNode => ({
|
||||
const HISTORY_COALESCE_MS = 500;
|
||||
const MAX_HISTORY_ENTRIES = 50;
|
||||
|
||||
const preflightFailureDiagnostic = (
|
||||
result: Extract<PreflightWorkerResponse["result"], { ok: false }>,
|
||||
): SemanticCssDiagnostic => ({
|
||||
code: result.code,
|
||||
severity: "error",
|
||||
message: result.message,
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 1, offset: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
const inactiveState = (): Omit<
|
||||
StylesheetStoreState,
|
||||
"setSourceText" | "setFocused" | "activate" | "deactivate" | "undo" | "redo" | "refreshIntelligence"
|
||||
@@ -389,7 +401,14 @@ export function createStylesheetStoreRuntime(options: CreateStylesheetStoreRunti
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (destroyed || preflight.editGeneration !== store.getState().editGeneration) return;
|
||||
if (!preflight.result.ok) {
|
||||
patch({ diagnostics: [...compiled.diagnostics, ...preflight.result.diagnostics], status: "error" });
|
||||
patch({
|
||||
diagnostics: [
|
||||
...compiled.diagnostics,
|
||||
...preflight.result.diagnostics,
|
||||
preflightFailureDiagnostic(preflight.result),
|
||||
],
|
||||
status: "error",
|
||||
});
|
||||
if (candidate.transition !== "edit_source") return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,20 @@ describe("stylesheet worker clients", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("rejects pending preflight work when the worker emits an error event", async () => {
|
||||
const fake = worker();
|
||||
const client = createPreflightWorkerClient(() => fake, 5_000);
|
||||
const pending = client.preflight({ editGeneration: 1 } as never);
|
||||
const outcome = pending.catch((error: unknown) => error);
|
||||
|
||||
fake.emit({ type: "preflight_ready" });
|
||||
await Promise.resolve();
|
||||
fake.emitError({ message: "Worker crashed" } as ErrorEvent);
|
||||
|
||||
expect(await outcome).toMatchObject({ message: "Worker crashed" });
|
||||
expect(fake.terminate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects structured resume-data failures without waiting for the timeout", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fake = worker();
|
||||
|
||||
@@ -128,8 +128,18 @@ export function createPreflightWorkerClient(
|
||||
pending.delete(response.requestId);
|
||||
request.resolve(response);
|
||||
};
|
||||
const onError: WorkerErrorListener = () => {
|
||||
const failPending = (error: Error) => {
|
||||
for (const request of pending.values()) {
|
||||
if (request.timer) clearTimeout(request.timer);
|
||||
request.reject(error);
|
||||
}
|
||||
pending.clear();
|
||||
};
|
||||
|
||||
const onError: WorkerErrorListener = (event) => {
|
||||
const error = new Error(event.message || "Stylesheet preflight worker failed.");
|
||||
terminate();
|
||||
failPending(error);
|
||||
};
|
||||
|
||||
const terminate = () => {
|
||||
|
||||
Reference in New Issue
Block a user