mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 14:52:18 +10:00
498 lines
15 KiB
TypeScript
498 lines
15 KiB
TypeScript
import type { PdfPreflightResult } from "@reactive-resume/pdf/server";
|
||
import type { CompileStylesheetResult } from "@reactive-resume/resume/stylesheet";
|
||
import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||
import type { SemanticStylesheet, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
|
||
import type { SemanticCssEventInput } from "./stylesheet-observability";
|
||
import type { StylesheetSnapshot } from "./stylesheet-service";
|
||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||
import { createStylesheetService } from "./stylesheet-service";
|
||
|
||
vi.mock("@reactive-resume/db/client", () => ({ db: {}, getPool: vi.fn() }));
|
||
|
||
const validSource = {
|
||
languageVersion: 1,
|
||
text: "@version 1;\nsection { color: #112233; }\n",
|
||
} satisfies StylesheetSource;
|
||
const invalidSource = {
|
||
languageVersion: 1,
|
||
text: "@version 1;\nsection {",
|
||
} satisfies StylesheetSource;
|
||
const previousSource = {
|
||
languageVersion: 1,
|
||
text: "@version 1;\nsection { color: #445566; }\n",
|
||
} satisfies StylesheetSource;
|
||
const previousApplied = {
|
||
languageVersion: 1,
|
||
text: "@version 1;\nsection { color: #778899; }\n",
|
||
} satisfies StylesheetSource;
|
||
const previousStylesheet = {
|
||
mode: "semantic",
|
||
source: previousSource,
|
||
applied: previousApplied,
|
||
} satisfies SemanticStylesheet;
|
||
const diagnostic = {
|
||
code: "PARSE_ERROR",
|
||
severity: "error" as const,
|
||
message: "Invalid stylesheet.",
|
||
range: {
|
||
start: { line: 1, column: 1, offset: 0 },
|
||
end: { line: 1, column: 1, offset: 0 },
|
||
},
|
||
};
|
||
const successfulCompile = {
|
||
program: { languageVersion: 1, rules: [] },
|
||
diagnostics: [],
|
||
} satisfies CompileStylesheetResult;
|
||
const failedCompile = { program: null, diagnostics: [diagnostic] } satisfies CompileStylesheetResult;
|
||
const successfulPreflight = {
|
||
ok: true,
|
||
pageCount: 1,
|
||
byteCount: 256,
|
||
diagnostics: [],
|
||
} satisfies PdfPreflightResult;
|
||
|
||
const commonMutationInput = {
|
||
id: "resume-1",
|
||
userId: "user-1",
|
||
expectedRevision: 3,
|
||
expectedRenderDataVersion: 8,
|
||
editGeneration: 11,
|
||
} as const;
|
||
|
||
const snapshot = (stylesheet: SemanticStylesheet = previousStylesheet): StylesheetSnapshot => {
|
||
const data: ResumeData = structuredClone(defaultResumeData);
|
||
data.metadata.stylesheet = stylesheet;
|
||
data.metadata.styleRules = [
|
||
{
|
||
id: "legacy-rule",
|
||
label: "Legacy",
|
||
enabled: true,
|
||
target: { scope: "global" },
|
||
slots: { heading: { color: "#123456" } },
|
||
},
|
||
];
|
||
|
||
return {
|
||
id: "resume-1",
|
||
userId: "user-1",
|
||
data,
|
||
isLocked: false,
|
||
stylesheetRevision: 3,
|
||
renderDataVersion: 8,
|
||
updatedAt: new Date("2026-07-28T12:00:00.000Z"),
|
||
};
|
||
};
|
||
|
||
type HarnessOptions = {
|
||
initial?: StylesheetSnapshot;
|
||
compile?: (source: StylesheetSource) => CompileStylesheetResult;
|
||
preflight?: ((input: { data: ResumeData; stylesheet: StylesheetSource }) => Promise<PdfPreflightResult>) | undefined;
|
||
locked?: StylesheetSnapshot;
|
||
useDefaultObserver?: boolean;
|
||
};
|
||
|
||
const createHarness = (options: HarnessOptions = {}) => {
|
||
const callOrder: string[] = [];
|
||
const events: SemanticCssEventInput[] = [];
|
||
let persisted = structuredClone(options.initial ?? snapshot());
|
||
const compile = vi.fn((source: StylesheetSource) => {
|
||
callOrder.push("compile");
|
||
return options.compile?.(source) ?? successfulCompile;
|
||
});
|
||
const preflight = options.preflight
|
||
? vi.fn(options.preflight)
|
||
: vi.fn(() => {
|
||
callOrder.push("preflight");
|
||
return Promise.resolve(successfulPreflight);
|
||
});
|
||
const publish = vi.fn(() => {
|
||
callOrder.push("publish");
|
||
return Promise.resolve();
|
||
});
|
||
|
||
const service = createStylesheetService({
|
||
readSnapshot: () => {
|
||
callOrder.push("readSnapshot");
|
||
return Promise.resolve(structuredClone(persisted));
|
||
},
|
||
convertLegacy: () => {
|
||
callOrder.push("convertLegacy");
|
||
return { languageVersion: 1, text: "@version 1;\n" };
|
||
},
|
||
compile,
|
||
preflight:
|
||
options.preflight === undefined
|
||
? preflight
|
||
: (input) => {
|
||
callOrder.push("preflight");
|
||
return preflight(input);
|
||
},
|
||
transaction: async (run) => {
|
||
callOrder.push("begin");
|
||
try {
|
||
const result = await run({
|
||
lock: () => {
|
||
callOrder.push("lock");
|
||
return Promise.resolve(structuredClone(options.locked ?? persisted));
|
||
},
|
||
update: ({ data }) => {
|
||
callOrder.push("update");
|
||
persisted = {
|
||
...persisted,
|
||
data,
|
||
stylesheetRevision: persisted.stylesheetRevision + 1,
|
||
};
|
||
return Promise.resolve(structuredClone(persisted));
|
||
},
|
||
});
|
||
callOrder.push("commit");
|
||
return result;
|
||
} catch (error) {
|
||
callOrder.push("rollback");
|
||
throw error;
|
||
}
|
||
},
|
||
compare: (locked, input) => {
|
||
callOrder.push("compare");
|
||
return (
|
||
locked.stylesheetRevision === input.expectedRevision &&
|
||
locked.renderDataVersion === input.expectedRenderDataVersion
|
||
);
|
||
},
|
||
publish,
|
||
...(options.useDefaultObserver ? {} : { observe: (event: SemanticCssEventInput) => events.push(event) }),
|
||
});
|
||
|
||
return { callOrder, compile, events, persisted: () => persisted, preflight, publish, service };
|
||
};
|
||
|
||
describe("stylesheet service", () => {
|
||
beforeEach(() => {
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
it("stores invalid editable source while preserving the last-valid applied source and render version", async () => {
|
||
const harness = createHarness({ compile: () => failedCompile });
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "edit_source",
|
||
source: invalidSource,
|
||
});
|
||
|
||
expect(result.stylesheet.source).toEqual(invalidSource);
|
||
expect(result.stylesheet.applied).toEqual(previousApplied);
|
||
expect(result.revision).toBe(4);
|
||
expect(result.renderDataVersion).toBe(8);
|
||
expect(result.editGeneration).toBe(11);
|
||
expect(result.diagnostics).toEqual([diagnostic]);
|
||
expect(harness.preflight).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("promotes a valid edit only in semantic mode after preflight outside the short transaction", async () => {
|
||
const harness = createHarness();
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "edit_source",
|
||
source: validSource,
|
||
});
|
||
|
||
expect(result.stylesheet.source).toEqual(validSource);
|
||
expect(result.stylesheet.applied).toEqual(validSource);
|
||
expect(harness.callOrder).toEqual([
|
||
"readSnapshot",
|
||
"compile",
|
||
"preflight",
|
||
"begin",
|
||
"lock",
|
||
"compare",
|
||
"update",
|
||
"commit",
|
||
"publish",
|
||
]);
|
||
});
|
||
|
||
it("keeps a valid legacy-mode edit dormant without preflight", async () => {
|
||
const initial = snapshot({ ...previousStylesheet, mode: "legacy" });
|
||
const harness = createHarness({ initial });
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "edit_source",
|
||
source: validSource,
|
||
});
|
||
|
||
expect(result.stylesheet).toEqual({
|
||
mode: "legacy",
|
||
source: validSource,
|
||
applied: previousApplied,
|
||
});
|
||
expect(harness.preflight).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("preserves editable source but not applied source when semantic render preflight fails", async () => {
|
||
const harness = createHarness({
|
||
preflight: async () => ({
|
||
ok: false,
|
||
code: "STYLESHEET_PREFLIGHT_RENDER_FAILED",
|
||
message: "PDF rendering failed.",
|
||
diagnostics: [],
|
||
}),
|
||
});
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "edit_source",
|
||
source: validSource,
|
||
});
|
||
|
||
expect(result.stylesheet.source).toEqual(validSource);
|
||
expect(result.stylesheet.applied).toEqual(previousApplied);
|
||
expect(result.diagnostics).toEqual([
|
||
expect.objectContaining({ code: "STYLESHEET_PREFLIGHT_RENDER_FAILED", severity: "error" }),
|
||
]);
|
||
});
|
||
|
||
it("activates an explicitly requested mode transition without checking legacy parity", async () => {
|
||
const initial = snapshot({ ...previousStylesheet, mode: "legacy" });
|
||
const harness = createHarness({ initial });
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "activate",
|
||
source: validSource,
|
||
});
|
||
|
||
expect(result.stylesheet).toEqual({ mode: "semantic", source: validSource, applied: validSource });
|
||
expect(harness.preflight).toHaveBeenCalledOnce();
|
||
expect(result.revision).toBe(4);
|
||
expect(result.renderDataVersion).toBe(8);
|
||
expect(harness.events).toContainEqual(
|
||
expect.objectContaining({
|
||
name: "semantic_css.activate",
|
||
durationMs: expect.any(Number),
|
||
pageCount: 1,
|
||
revision: 4,
|
||
success: true,
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("records a sanitized compile failure metric when the compiler throws", async () => {
|
||
const privateText = "private compiler failure john.doe@example.com";
|
||
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||
const harness = createHarness({
|
||
compile: () => {
|
||
throw new Error(privateText);
|
||
},
|
||
useDefaultObserver: true,
|
||
});
|
||
|
||
await expect(
|
||
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
|
||
).rejects.toThrow(privateText);
|
||
|
||
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
|
||
expect.objectContaining({
|
||
name: "semantic_css.compile",
|
||
durationMs: expect.any(Number),
|
||
diagnosticCodes: [],
|
||
success: false,
|
||
}),
|
||
);
|
||
const serialized = JSON.stringify(log.mock.calls);
|
||
expect(serialized).not.toContain(privateText);
|
||
expect(serialized).not.toContain(validSource.text);
|
||
expect(serialized).not.toContain("resume-1");
|
||
});
|
||
|
||
it("records a sanitized preflight failure metric when the runner throws", async () => {
|
||
const privateText = "private preflight failure john.doe@example.com";
|
||
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||
const harness = createHarness({
|
||
preflight: () => Promise.reject(new Error(privateText)),
|
||
useDefaultObserver: true,
|
||
});
|
||
|
||
await expect(
|
||
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
|
||
).rejects.toThrow(privateText);
|
||
|
||
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
|
||
expect.objectContaining({
|
||
name: "semantic_css.preflight",
|
||
durationMs: expect.any(Number),
|
||
diagnosticCodes: [],
|
||
success: false,
|
||
}),
|
||
);
|
||
const serialized = JSON.stringify(log.mock.calls);
|
||
expect(serialized).not.toContain(privateText);
|
||
expect(serialized).not.toContain(validSource.text);
|
||
expect(serialized).not.toContain("resume-1");
|
||
});
|
||
|
||
it("deactivates without compiling or deleting either source", async () => {
|
||
const harness = createHarness();
|
||
|
||
const result = await harness.service.mutate({ ...commonMutationInput, transition: "deactivate" });
|
||
|
||
expect(result.stylesheet).toEqual({ ...previousStylesheet, mode: "legacy" });
|
||
expect(harness.compile).not.toHaveBeenCalled();
|
||
expect(harness.preflight).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("does not rewrite invalid stored resume data during deactivation", async () => {
|
||
const initial = snapshot();
|
||
initial.data.customSections = [
|
||
{
|
||
id: "custom-experience",
|
||
type: "experience",
|
||
title: "Experience",
|
||
icon: "",
|
||
columns: 1,
|
||
hidden: false,
|
||
keepTogether: false,
|
||
startOnNewPage: false,
|
||
items: [{ id: "summary-item", hidden: false, content: "<p>Missing company</p>" }],
|
||
} as never,
|
||
];
|
||
const harness = createHarness({ initial });
|
||
|
||
const error = await harness.service
|
||
.mutate({ ...commonMutationInput, transition: "deactivate" })
|
||
.catch((caught: unknown) => caught);
|
||
|
||
expect(error).toMatchObject({ code: "INTERNAL_SERVER_ERROR", status: 500 });
|
||
expect(error).toHaveProperty("cause.issues.0.path", ["customSections", 0, "items", 0, "company"]);
|
||
expect(harness.callOrder).not.toContain("update");
|
||
expect(harness.publish).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("normalizes valid stored resume data during deactivation", async () => {
|
||
const initial = snapshot();
|
||
initial.data.customSections = [
|
||
{
|
||
id: "custom-experience",
|
||
type: "experience",
|
||
title: "Experience",
|
||
icon: "",
|
||
columns: 1,
|
||
hidden: false,
|
||
keepTogether: false,
|
||
startOnNewPage: false,
|
||
items: [
|
||
{
|
||
id: "experience-item",
|
||
hidden: false,
|
||
company: "Analytical Engines",
|
||
position: "Programmer",
|
||
location: "London",
|
||
period: "1842–1843",
|
||
description: "<p>Wrote the first algorithm.</p>",
|
||
content: "<p>Compatible overlap</p>",
|
||
},
|
||
],
|
||
} as never,
|
||
];
|
||
const harness = createHarness({ initial });
|
||
|
||
await harness.service.mutate({ ...commonMutationInput, transition: "deactivate" });
|
||
|
||
expect(harness.persisted().data.customSections[0]?.items[0]).toMatchObject({
|
||
content: "<p>Compatible overlap</p>",
|
||
roles: [],
|
||
website: { url: "", label: "", inlineLink: false },
|
||
});
|
||
});
|
||
|
||
it("validates and preflights restored applied source independently from invalid editable source", async () => {
|
||
const restoredApplied = { ...validSource, text: "@version 1;\nresume { color: #abcdef; }\n" };
|
||
const compile = vi.fn((source: StylesheetSource) =>
|
||
source === restoredApplied ? successfulCompile : failedCompile,
|
||
);
|
||
const harness = createHarness({ compile });
|
||
|
||
const result = await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "restore_history",
|
||
restore: {
|
||
mode: "semantic",
|
||
source: invalidSource,
|
||
applied: restoredApplied,
|
||
},
|
||
});
|
||
|
||
expect(compile).toHaveBeenCalledTimes(1);
|
||
expect(compile).toHaveBeenCalledWith(restoredApplied);
|
||
expect(harness.preflight).toHaveBeenCalledWith({
|
||
data: expect.any(Object),
|
||
stylesheet: restoredApplied,
|
||
});
|
||
expect(result.stylesheet).toEqual({
|
||
mode: "semantic",
|
||
source: invalidSource,
|
||
applied: restoredApplied,
|
||
});
|
||
});
|
||
|
||
it("rejects locked resumes before compiler or preflight work", async () => {
|
||
const initial = { ...snapshot(), isLocked: true };
|
||
const harness = createHarness({ initial });
|
||
|
||
await expect(
|
||
harness.service.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource }),
|
||
).rejects.toMatchObject({ code: "RESUME_LOCKED" });
|
||
expect(harness.compile).not.toHaveBeenCalled();
|
||
expect(harness.preflight).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("returns the locked canonical snapshot on a two-version conflict without writing", async () => {
|
||
const locked = {
|
||
...snapshot(),
|
||
stylesheetRevision: 4,
|
||
renderDataVersion: 9,
|
||
};
|
||
delete locked.data.metadata.stylesheet;
|
||
const harness = createHarness({ locked });
|
||
|
||
const error = await harness.service
|
||
.mutate({ ...commonMutationInput, transition: "edit_source", source: validSource })
|
||
.catch((caught: unknown) => caught);
|
||
|
||
expect(error).toMatchObject({
|
||
code: "STYLESHEET_REVISION_CONFLICT",
|
||
data: {
|
||
state: expect.objectContaining({
|
||
revision: 4,
|
||
renderDataVersion: 9,
|
||
stylesheet: expect.objectContaining({
|
||
mode: "legacy",
|
||
source: { languageVersion: 1, text: "@version 1;\n" },
|
||
}),
|
||
}),
|
||
},
|
||
});
|
||
expect(harness.callOrder).not.toContain("update");
|
||
expect(harness.publish).not.toHaveBeenCalled();
|
||
expect(harness.callOrder.indexOf("rollback")).toBeLessThan(harness.callOrder.indexOf("convertLegacy"));
|
||
});
|
||
|
||
it("reloads the exact invalid source, last-valid applied source, and untouched legacy rules", async () => {
|
||
const harness = createHarness({ compile: () => failedCompile });
|
||
await harness.service.mutate({
|
||
...commonMutationInput,
|
||
transition: "edit_source",
|
||
source: invalidSource,
|
||
});
|
||
|
||
const reloaded = await harness.service.getState({ id: "resume-1", userId: "user-1" });
|
||
|
||
expect(reloaded.stylesheet.source).toEqual(invalidSource);
|
||
expect(reloaded.stylesheet.applied).toEqual(previousApplied);
|
||
expect(harness.persisted().data.metadata.styleRules).toEqual(snapshot().data.metadata.styleRules);
|
||
});
|
||
});
|