Mermaid validation harness: golden hostile-input tests + real parse check - #30
Conversation
Add four golden-file regression tests covering gaps the existing strings.Contains-based tests don't: a role packing every hostile character sanitize handles plus three it doesn't (<, >, %% — issue #29, left unfixed here on purpose), a very long unbroken-word role, an entity literally named "Self" with two self-relationships, and a relationship whose target entity is never declared. Each golden was verified by hand against the actual renderer output before being committed. Signed-off-by: Joe Beda <joe@stacklok.com>
Code review — round 1Three parallel angles: CLAUDE.md/rules compliance (Sonnet), shallow bug scan of the diff (Sonnet), adversarial run of the real binary + real Blocking
Not blocking / recorded as open questions, not required to fix
Clean
Fixing items 1–2 in a follow-up commit. |
… golden Round-1 review (PR #30) blocking findings: - hack/mermaid-check.sh could silently "pass" having checked zero Mermaid blocks. An unterminated ```mermaid fence dropped its block without ever checking or erroring on it; a fence-line format drift (e.g. a trailing space after ```mermaid) had the same silent effect. Now: an unterminated fence is its own hard error naming the file, and a zero total block count after scanning is a hard error in its own right, since the repo always has at least one generated Mermaid block to check. - TestERRole_HostileCharacters's comment overclaimed Mermaid directive-injection coverage (issue #29) that the fixture didn't actually exercise (only bare %%pct, which is inert). Extended the fixture with a %%{init: {'theme':'forest'}}%%-shaped substring — confirmed via a real mmdc render to silently retheme the diagram and drop the edge's label — and rewrote the comment to state precisely what the golden pins (the generated .mmd source text) versus what it does not (rendered-diagram behavior). Signed-off-by: Joe Beda <joe@stacklok.com>
Code review — round 2 (delta only)Reviewed only fix commit No issues found. Stopping the review loop here per |
GitHub-hosted ubuntu-latest runners disable unprivileged user namespaces
(AppArmor), which Chrome's sandbox requires. mmdc's bundled Chromium
fails to launch there with "No usable sandbox!" — this didn't reproduce
locally, only on the actual runner. Write a throwaway puppeteer config
with {"args": ["--no-sandbox"]} and pass it via -p to every mmdc
invocation. Safe here because this script only ever validates
repo-authored Mermaid (committed docs/examples or our own renderer's
golden fixtures), never untrusted/fetched content.
Signed-off-by: Joe Beda <joe@stacklok.com>
Post-review: CI environment fixAfter round 2 cleared, the real GitHub Actions run failed on every single Mermaid block (not just bad ones) — GitHub's |
docs/04-reading-the-diagrams.md is the page that teaches the notation, and its diagrams are written by hand rather than generated. Nothing regenerates them, so a broken block ships silently — the opposite of the committed golden output, which render-check already guards. All nine blocks parse today, so this is coverage rather than a fix. Raises the checked-block count from 7 to 16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Joe Beda <joe@stacklok.com>
|
Added That page's diagrams are hand-authored rather than generated, which makes them the most important to check, not the least: nothing regenerates them, so All nine blocks parse today, so this is coverage, not a fix. Checked-block count goes 7 → 16. Verified locally end to end:
The second open question — CI step living in Remaining before this is ready: the harness pins current |
… golden Round-1 review (PR #30) blocking findings: - hack/mermaid-check.sh could silently "pass" having checked zero Mermaid blocks. An unterminated ```mermaid fence dropped its block without ever checking or erroring on it; a fence-line format drift (e.g. a trailing space after ```mermaid) had the same silent effect. Now: an unterminated fence is its own hard error naming the file, and a zero total block count after scanning is a hard error in its own right, since the repo always has at least one generated Mermaid block to check. - TestERRole_HostileCharacters's comment overclaimed Mermaid directive-injection coverage (issue #29) that the fixture didn't actually exercise (only bare %%pct, which is inert). Extended the fixture with a %%{init: {'theme':'forest'}}%%-shaped substring — confirmed via a real mmdc render to silently retheme the diagram and drop the edge's label — and rewrote the comment to state precisely what the golden pins (the generated .mmd source text) versus what it does not (rendered-diagram behavior). Signed-off-by: Joe Beda <joe@stacklok.com>
Summary
Closes the gap where nothing in the repo verifies that generated Mermaid
actually parses. Two layers, per the plan:
Go golden tests (
internal/render/mermaid/mermaid_test.go+internal/render/mermaid/testdata/*.golden.mmd) covering four cases theexisting
strings.Contains-based tests don't:TestERRole_HostileCharacters— a role packing every hostile charactersanitizehandles (backticks, quotes, brackets, a literal newline)plus three it does not:
<,>, and%%. This pins the current(buggy) passthrough behavior on purpose — see Render: sanitize lets Mermaid directives and angle brackets through from role text #29 below — with a comment
pointing at the issue so a future fix updates this golden deliberately.
TestERRole_LongUnbrokenWord— a ~188-char single word with no spaces;there's no wrapping logic, so it passes through whole.
TestERSelfRow_EntityNamedSelfDoesNotCollide— an entity literally namedSelfwith two self-relationships; the self-row attribute names (self,self2) are fixed literals, not entity-name-derived, so no collision.TestER_UndeclaredRelationshipTarget— a relationship whose target entityhas no entry in
Entitiesat all.ER()doesn't validate references(that's
modelith lint's job), so it renders a well-formed edge withoutpanicking.
Deliberately not duplicated: backtick/bracket/newline/quote/backslash
sanitization, bounded/malformed cardinality glyph selection, self-row
numbering/dedup, and reciprocal-edge folding — all already covered by
TestERLabelSanitizesRole,TestADR_0008_SelfRowCarriesBothCardinalitySides,TestADR_0003_BoundedCardinalityRendersNearestGlyph,TestERCardinalityNotation,TestERMultipleSelfRelationships,TestERSelfRelationshipsDedupe, and the severalTestADR_0008_*tests.Real Mermaid parse check (
hack/mermaid-check.sh+task mermaid-check```mermaidfenced block fromexamples/*.mdanddocs/05-parking-garage/*.md, plus the four new raw.mmdgoldens above, and feeds each tonpx -y @mermaid-js/mermaid-cli@11(mmdc). Fails with a clear messagenaming the offending file/block on any parse error.
Related issue
References #29 (open, pre-existing bug:
sanitizelets<,>, and%%through unescaped, so a role can inject a Mermaid directive or lose text that
looks like markup). This PR documents the bug via
TestERRole_HostileCharacters's golden and comment; it does not fix it —out of scope per the task.
Wiring / non-negotiable choices
task mermaid-checkis its own standalone task, not added totask check'scmds— explicit product requirement, sotask checkstaysgreen for contributors without node/npm. This diverges from the
validate-pluginpattern (which IS incheck); the task'sdesc:says so.mmdcbinary (unlikethe plugin-validation step, which substitutes a lighter
jqcheck in CI).Pins
actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0with
node-version: "24"— reusing the exact SHA already pinned indeploy-docs.ymlfor the same version.Demonstrated failure, then reverted
Per the task's requirement, I temporarily added a broken fixture
(
internal/render/mermaid/testdata/zzz_temp_broken.mmdcontainingerDiagram\n A ||--o{ B :::: "broken"\n), ran the check, confirmed itfailed, then removed the fixture. Transcript:
After removing the temp fixture:
That fixture is not part of this PR's diff — it was created and deleted
entirely within the local worktree before committing.
Open questions / judgment calls
Since I couldn't ask before finishing, I picked the conservative default in
each case:
hack/mermaid-check.shdoes a simple line-oriented scan for```mermaid/
```on their own line. This matches every fence actually in the repotoday (confirmed: none nest fences inside fences). If docs ever start
nesting code fences, this scan would need to get smarter — noted in the
script's own comment.
.mmdvs re-wrapping in a fence. I fed the golden.mmdfiles tommdcdirectly rather than wrapping them in a throwaway fence, sincethey're already exactly what
mmdc -iexpects and wrapping-then-strippingwould add ceremony with no benefit. Documented in the script's top comment.
docs/05-parking-garage/index.mdalso matches thedocs/05-parking-garage/*.mdglob and contributes one of the 7 checked blocks (in addition to the
golden-fixture
garage.modelith.md). This seemed like desirable extracoverage (that page hand-embeds a diagram too) rather than scope creep, but
flagging it since the task only explicitly named the golden files.
layers landed in one commit.
Test plan
task test— all new and existing tests pass, including the four newgoldens (eyeballed each by hand against the real
mermaid.ER()outputbefore committing, per
testing.md).task check— fully green; does not invokemermaid-check.task mermaid-check— passes against real repo content (7 blocks).task mermaid-check's equivalent (hack/mermaid-check.sh)failing on injected bad input, then reverted (see transcript above).