From f64d02df7f090720feb58c2cacd70de8de5393a0 Mon Sep 17 00:00:00 2001 From: Santhi Prakash <38608178+santhiprakash@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:37:49 +0530 Subject: [PATCH] fix(pdf): ignore phantom gengar skill text nodes (#3289) * docs(agents): align Redis compose commands with development guide - Problem: AGENTS.md omitted Redis from dev infrastructure compose commands while docs/contributing/development.mdx starts redis for local dev. - Fix: document full postgres/redis/seaweedfs compose command and note that REDIS_URL and ENCRYPTION_SECRET are required for AI agent features. - Verification: preflight upstream fetch; manual diff against development.mdx and compose.dev.yml redis service; duplicate PR gate passed. * fix(pdf): ignore phantom gengar skill text nodes - Problem: gengar template resumes with skills keywords fail semantic CSS activation because the legacy renderer emits a harmless empty text node that parity treats as a mismatch. - Fix: treat the specific empty text artifact as presentation-neutral in the legacy parity comparator and add a regression test for the phantom fontSize 9 node. - Verification: pnpm test src/semantic/legacy-parity.test.ts in packages/pdf passed (31 tests). * docs: clarify host and container Redis URLs - Problem: the development guide only showed the Docker Redis hostname, which fails for host-run development.\n- Fix: document localhost for host execution and redis for Docker execution.\n- Verification: pnpm test src/semantic/legacy-parity.test.ts (31 passed). * fix(pdf): omit empty skill proficiency text * test(pdf): cover blank skill proficiency --------- Co-authored-by: Amruth Pillai --- .../pdf/src/semantic/legacy-parity.test.ts | 40 +++++++++++++++++++ .../pdf/src/templates/shared/sections.tsx | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/pdf/src/semantic/legacy-parity.test.ts b/packages/pdf/src/semantic/legacy-parity.test.ts index 6083f570c..2e243b6ad 100644 --- a/packages/pdf/src/semantic/legacy-parity.test.ts +++ b/packages/pdf/src/semantic/legacy-parity.test.ts @@ -3,9 +3,12 @@ import type { Template } from "@reactive-resume/schema/templates"; import type { LegacyParityHostNode } from "./legacy-parity"; import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { pdf } from "@react-pdf/renderer"; +import { createElement } from "react"; import { styleRulesSchema } from "@reactive-resume/schema/resume/data"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { sampleResumeData } from "@reactive-resume/schema/resume/sample"; +import { ResumeDocument } from "../document"; import { convertLegacyStyleRules } from "./legacy-converter"; import { compareLegacyParityHostNodes, compareLegacySemanticPresentation } from "./legacy-parity"; @@ -157,6 +160,27 @@ const buildFixture = (rules: StyleRule[]): ResumeData => { return data; }; +const nodeText = (node: LegacyParityHostNode): string => + node.value ?? (node.children ?? []).map((child) => nodeText(child)).join(""); + +const renderHostDocument = async (data: ResumeData): Promise => { + const element = createElement(ResumeDocument, { data, template: "gengar" }) as unknown as Parameters[0]; + const instance = pdf(element); + await expect.poll(() => instance.container.document).not.toBeNull(); + return instance.container.document as LegacyParityHostNode; +}; + +const findEmptyTextNodes = (node: LegacyParityHostNode): LegacyParityHostNode[] => [ + ...(node.type === "TEXT" && nodeText(node).trim() === "" ? [node] : []), + ...(node.children ?? []).flatMap(findEmptyTextNodes), +]; + +const hasFontSize = (node: LegacyParityHostNode, fontSize: number): boolean => + (Array.isArray(node.style) ? node.style : [node.style]).some( + (style) => + style !== null && typeof style === "object" && (style as Readonly>).fontSize === fontSize, + ); + describe("compareLegacySemanticPresentation", () => { it("renders the mandatory target shapes in the shared parity document", () => { const data = buildFixture([]); @@ -231,6 +255,22 @@ describe("compareLegacySemanticPresentation", () => { expect(comparison.mismatches).toEqual([]); }); + it("omits empty Gengar skill proficiency text while preserving populated text", async () => { + const data = buildFixture([]); + data.metadata.template = "gengar"; + data.metadata.layout.pages = [{ fullWidth: false, main: [], sidebar: ["skills"] }]; + data.metadata.typography.body.fontSize = 9; + const [skill] = data.sections.skills.items; + if (!skill) throw new Error("Expected Gengar skill fixture."); + for (const proficiency of ["", " \t"]) { + skill.proficiency = proficiency; + expect(findEmptyTextNodes(await renderHostDocument(data)).filter((node) => hasFontSize(node, 9))).toEqual([]); + } + + skill.proficiency = "Expert"; + expect(nodeText(await renderHostDocument(data))).toContain("Expert"); + }); + it.each(["onyx", "meowth"] as const)( "matches combined separator and box-style primitives on %s", async (template) => { diff --git a/packages/pdf/src/templates/shared/sections.tsx b/packages/pdf/src/templates/shared/sections.tsx index 3acb41442..cb844b9b0 100644 --- a/packages/pdf/src/templates/shared/sections.tsx +++ b/packages/pdf/src/templates/shared/sections.tsx @@ -1153,7 +1153,7 @@ const SkillsSection = ({ sectionId = "skills", sectionData }: ItemSectionProps - {item.proficiency} + {hasSplitRowText(item.proficiency) && {item.proficiency}} {item.keywords.join(", ")}