mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-19 13:01:44 +10:00
fix(stylesheet): skip parity on explicit activation (#3316)
This commit is contained in:
@@ -10921,46 +10921,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defined": {
|
||||
"const": true
|
||||
},
|
||||
"code": {
|
||||
"const": "STYLESHEET_PARITY_FAILED"
|
||||
},
|
||||
"status": {
|
||||
"const": 400
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"default": "The converted stylesheet does not preserve legacy PDF presentation."
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mismatches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"mismatches"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"defined",
|
||||
"code",
|
||||
"status",
|
||||
"message",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -202,9 +202,6 @@ export const resumeDto = {
|
||||
validation: z.strictObject({
|
||||
diagnostics: z.array(stylesheetDiagnosticSchema),
|
||||
}),
|
||||
parity: z.strictObject({
|
||||
mismatches: z.array(z.string()),
|
||||
}),
|
||||
revisionConflict: z.strictObject({
|
||||
state: stylesheetStateSchema,
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,6 @@ type SemanticCssEventName =
|
||||
| "semantic_css.compile"
|
||||
| "semantic_css.preflight"
|
||||
| "semantic_css.convert_legacy"
|
||||
| "semantic_css.parity_check"
|
||||
| "semantic_css.activate";
|
||||
|
||||
export type SemanticCssEventInput = {
|
||||
|
||||
@@ -3,17 +3,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import {
|
||||
checkLegacyStylesheetParity,
|
||||
prepareImportedResumeData,
|
||||
validateHistoricalStylesheet,
|
||||
} from "./stylesheet-preflight";
|
||||
|
||||
const semanticPdfMocks = vi.hoisted(() => ({
|
||||
compareLegacySemanticPresentation: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@reactive-resume/pdf/semantic", () => semanticPdfMocks);
|
||||
import { prepareImportedResumeData, validateHistoricalStylesheet } from "./stylesheet-preflight";
|
||||
|
||||
const validSource = {
|
||||
languageVersion: 1,
|
||||
@@ -47,7 +37,6 @@ const createRunner = () => {
|
||||
describe("stylesheet persistence preparation", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
semanticPdfMocks.compareLegacySemanticPresentation.mockReset();
|
||||
});
|
||||
|
||||
it("uses a valid imported source as applied only after PDF preflight", async () => {
|
||||
@@ -171,32 +160,4 @@ describe("stylesheet persistence preparation", () => {
|
||||
expect(serialized).not.toContain(validSource.text);
|
||||
expect(serialized).not.toContain("import-observed");
|
||||
});
|
||||
|
||||
it("records a sanitized parity failure metric when the comparator throws", async () => {
|
||||
const privateText = "private parity failure john.doe@example.com";
|
||||
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||||
semanticPdfMocks.compareLegacySemanticPresentation.mockRejectedValueOnce(new Error(privateText));
|
||||
|
||||
await expect(
|
||||
checkLegacyStylesheetParity({
|
||||
data: resumeData(),
|
||||
stylesheet: validSource,
|
||||
resumeId: "parity-observed",
|
||||
revision: 5,
|
||||
}),
|
||||
).rejects.toThrow(privateText);
|
||||
|
||||
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "semantic_css.parity_check",
|
||||
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("parity-observed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { StylesheetSnapshot } from "./stylesheet-service";
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import { compileStylesheet } from "@reactive-resume/resume/stylesheet";
|
||||
import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/stylesheet";
|
||||
import { templateSchema } from "@reactive-resume/schema/templates";
|
||||
import { recordSemanticCssEvent } from "./stylesheet-observability";
|
||||
|
||||
type PrepareImportedResumeDataInput = {
|
||||
@@ -191,47 +190,3 @@ export async function convertLegacyStylesheet(snapshot: StylesheetSnapshot): Pro
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkLegacyStylesheetParity(input: {
|
||||
data: ResumeData;
|
||||
stylesheet: StylesheetSource;
|
||||
resumeId: string;
|
||||
revision: number;
|
||||
}): Promise<{ mismatches: readonly string[] }> {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const { compareLegacySemanticPresentation } = await import("@reactive-resume/pdf/semantic");
|
||||
const result = await compareLegacySemanticPresentation({
|
||||
data: input.data,
|
||||
convertedSource: input.stylesheet,
|
||||
templates: templateSchema.options,
|
||||
});
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.parity_check",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: input.stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(input.stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: result.mismatches.length === 0,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
recordSemanticCssEvent({
|
||||
name: "semantic_css.parity_check",
|
||||
resumeId: input.resumeId,
|
||||
durationMs: performance.now() - startedAt,
|
||||
languageVersion: input.stylesheet.languageVersion,
|
||||
sourceBytes: byteCount(input.stylesheet),
|
||||
template: input.data.metadata.template,
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: input.revision,
|
||||
success: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,6 @@ describe("resume stylesheet route error contract", () => {
|
||||
}>();
|
||||
});
|
||||
|
||||
it("exposes strict parity mismatch data at runtime and in the inferred client error", () => {
|
||||
const schema = dataSchema("STYLESHEET_PARITY_FAILED");
|
||||
|
||||
expect(schema.safeParse({ mismatches: ["onyx: page 1"] }).success).toBe(true);
|
||||
expect(schema.safeParse({ mismatches: ["onyx: page 1"], source }).success).toBe(false);
|
||||
expectTypeOf<ErrorData<"STYLESHEET_PARITY_FAILED">>().toEqualTypeOf<{ mismatches: string[] }>();
|
||||
});
|
||||
|
||||
it("exposes strict canonical conflict state for type-safe client rebasing", () => {
|
||||
const schema = dataSchema("STYLESHEET_REVISION_CONFLICT");
|
||||
|
||||
|
||||
@@ -88,8 +88,6 @@ type HarnessOptions = {
|
||||
initial?: StylesheetSnapshot;
|
||||
compile?: (source: StylesheetSource) => CompileStylesheetResult;
|
||||
preflight?: ((input: { data: ResumeData; stylesheet: StylesheetSource }) => Promise<PdfPreflightResult>) | undefined;
|
||||
parityMismatches?: readonly string[];
|
||||
parity?: () => Promise<{ mismatches: readonly string[] }>;
|
||||
locked?: StylesheetSnapshot;
|
||||
useDefaultObserver?: boolean;
|
||||
};
|
||||
@@ -108,10 +106,6 @@ const createHarness = (options: HarnessOptions = {}) => {
|
||||
callOrder.push("preflight");
|
||||
return Promise.resolve(successfulPreflight);
|
||||
});
|
||||
const parity = vi.fn(() => {
|
||||
callOrder.push("parity");
|
||||
return options.parity?.() ?? Promise.resolve({ mismatches: options.parityMismatches ?? [] });
|
||||
});
|
||||
const publish = vi.fn(() => {
|
||||
callOrder.push("publish");
|
||||
return Promise.resolve();
|
||||
@@ -134,7 +128,6 @@ const createHarness = (options: HarnessOptions = {}) => {
|
||||
callOrder.push("preflight");
|
||||
return preflight(input);
|
||||
},
|
||||
parity,
|
||||
transaction: async (run) => {
|
||||
callOrder.push("begin");
|
||||
try {
|
||||
@@ -171,7 +164,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
||||
...(options.useDefaultObserver ? {} : { observe: (event: SemanticCssEventInput) => events.push(event) }),
|
||||
});
|
||||
|
||||
return { callOrder, compile, events, parity, persisted: () => persisted, preflight, publish, service };
|
||||
return { callOrder, compile, events, persisted: () => persisted, preflight, publish, service };
|
||||
};
|
||||
|
||||
describe("stylesheet service", () => {
|
||||
@@ -262,7 +255,7 @@ describe("stylesheet service", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires parity and preflight before explicit activation", async () => {
|
||||
it("activates an explicitly requested mode transition without checking legacy parity", async () => {
|
||||
const initial = snapshot({ ...previousStylesheet, mode: "legacy" });
|
||||
const harness = createHarness({ initial });
|
||||
|
||||
@@ -273,7 +266,6 @@ describe("stylesheet service", () => {
|
||||
});
|
||||
|
||||
expect(result.stylesheet).toEqual({ mode: "semantic", source: validSource, applied: validSource });
|
||||
expect(harness.parity).toHaveBeenCalledOnce();
|
||||
expect(harness.preflight).toHaveBeenCalledOnce();
|
||||
expect(result.revision).toBe(4);
|
||||
expect(result.renderDataVersion).toBe(8);
|
||||
@@ -342,35 +334,6 @@ describe("stylesheet service", () => {
|
||||
expect(serialized).not.toContain("resume-1");
|
||||
});
|
||||
|
||||
it("records a sanitized activation failure metric when parity throws", async () => {
|
||||
const privateText = "private parity failure john.doe@example.com";
|
||||
const log = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
||||
const harness = createHarness({
|
||||
initial: snapshot({ ...previousStylesheet, mode: "legacy" }),
|
||||
parity: () => Promise.reject(new Error(privateText)),
|
||||
useDefaultObserver: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
harness.service.mutate({ ...commonMutationInput, transition: "activate", source: validSource }),
|
||||
).rejects.toThrow(privateText);
|
||||
|
||||
expect(log.mock.calls.map(([event]) => event)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "semantic_css.activate",
|
||||
durationMs: expect.any(Number),
|
||||
diagnosticCodes: [],
|
||||
pageCount: null,
|
||||
revision: 3,
|
||||
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();
|
||||
|
||||
@@ -378,7 +341,6 @@ describe("stylesheet service", () => {
|
||||
|
||||
expect(result.stylesheet).toEqual({ ...previousStylesheet, mode: "legacy" });
|
||||
expect(harness.compile).not.toHaveBeenCalled();
|
||||
expect(harness.parity).not.toHaveBeenCalled();
|
||||
expect(harness.preflight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EMPTY_SEMANTIC_CSS_SOURCE } from "@reactive-resume/schema/resume/styles
|
||||
import { publishResumeUpdated } from "./events";
|
||||
import { parseStoredResumeData } from "./resume-data-validation";
|
||||
import { recordSemanticCssEvent } from "./stylesheet-observability";
|
||||
import { checkLegacyStylesheetParity, convertLegacyStylesheet } from "./stylesheet-preflight";
|
||||
import { convertLegacyStylesheet } from "./stylesheet-preflight";
|
||||
|
||||
export type StylesheetSnapshot = {
|
||||
id: string;
|
||||
@@ -68,12 +68,6 @@ type StylesheetServiceDependencies = {
|
||||
convertLegacy(snapshot: StylesheetSnapshot): StylesheetSource | Promise<StylesheetSource>;
|
||||
compile(source: StylesheetSource): CompileStylesheetResult;
|
||||
preflight?(input: { data: ResumeData; stylesheet: StylesheetSource }): Promise<PdfPreflightResult>;
|
||||
parity(input: {
|
||||
data: ResumeData;
|
||||
stylesheet: StylesheetSource;
|
||||
resumeId: string;
|
||||
revision: number;
|
||||
}): Promise<{ mismatches: readonly string[] }>;
|
||||
transaction<T>(run: (transaction: StylesheetTransaction) => Promise<T>): Promise<T>;
|
||||
compare(snapshot: StylesheetSnapshot, input: StylesheetMutationCommon): boolean | Promise<boolean>;
|
||||
publish(snapshot: StylesheetSnapshot): Promise<void>;
|
||||
@@ -262,20 +256,6 @@ export function createStylesheetService(dependencies: StylesheetServiceDependenc
|
||||
throw validationError("The stylesheet cannot be activated because it is invalid.", compiled.diagnostics);
|
||||
}
|
||||
|
||||
const parity = await dependencies.parity({
|
||||
data: snapshot.data,
|
||||
stylesheet: input.source,
|
||||
resumeId: snapshot.id,
|
||||
revision: snapshot.stylesheetRevision,
|
||||
});
|
||||
if (parity.mismatches.length > 0) {
|
||||
throw new ORPCError("STYLESHEET_PARITY_FAILED", {
|
||||
status: 400,
|
||||
message: "The converted stylesheet does not preserve the legacy PDF presentation.",
|
||||
data: { mismatches: parity.mismatches },
|
||||
});
|
||||
}
|
||||
|
||||
const preflight = await runPreflight(snapshot, input.source);
|
||||
didPreflight = true;
|
||||
activationPageCount = preflight.ok ? preflight.pageCount : null;
|
||||
@@ -397,7 +377,6 @@ export function createDatabaseStylesheetService(options: DatabaseStylesheetServi
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
parity: checkLegacyStylesheetParity,
|
||||
transaction: (run) =>
|
||||
database.transaction((transaction) =>
|
||||
run({
|
||||
|
||||
@@ -13,11 +13,6 @@ const errors = {
|
||||
status: 400,
|
||||
data: resumeDto.stylesheet.errors.validation,
|
||||
},
|
||||
STYLESHEET_PARITY_FAILED: {
|
||||
message: "The converted stylesheet does not preserve legacy PDF presentation.",
|
||||
status: 400,
|
||||
data: resumeDto.stylesheet.errors.parity,
|
||||
},
|
||||
STYLESHEET_REVISION_CONFLICT: {
|
||||
message: "The resume or stylesheet changed while the candidate was being validated.",
|
||||
status: 409,
|
||||
|
||||
Reference in New Issue
Block a user