test: pin the render protocol between Rust and the preview - #378
Merged
Conversation
The Rust renderer's output is a behavioural contract the frontend depends on: task markers and their source positions, the raw text kept inside display math, heading ids and the fold anchors keyed to them, and the distinction between a Markdown task and a hand-written HTML checkbox. All four have regressed before. The existing suite mostly matches source text with regular expressions, which fails on rewrites that change nothing and stays green on behaviour changes that break users. Today alone, replacing `lock().unwrap()` with a helper and moving a `localStorage.setItem` into a persistence table each turned an assertion red without altering behaviour. These tests run `processMarkdownHtml` for real against 29 documents whose HTML is genuine `convert_markdown` output, then query the result structurally rather than by string. Node has no DOM and adding one would touch the lockfile, so the harness ships a deliberately narrow parser, serializer, selector engine and TreeWalker — validated by self-tests and by a differential run against Python's `html.parser`, which produces identical trees for all 29 fixtures. The renderer HTML is captured, not called: `convert_markdown` is private and unreachable from an integration test. It was generated by a throwaway crate carrying verbatim copies of the pre-passes and the exact comrak options, and validated by reproducing byte-for-byte the HTML literals that lib.rs's own tests already assert. So these fixtures are inputs — they cannot detect Rust drift on their own, and the file header says so along with how to regenerate them. Failability was checked empirically, not argued: sixteen mutations of the frontend and of simulated renderer drift each turn the suite red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
force-pushed
the
test/render-protocol-fixture
branch
from
August 2, 2026 19:58
a62d47a to
136b836
Compare
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Rust renderer's output is a behavioural contract the frontend depends on. Four parts of it have regressed before:
$$…$$so KaTeX still sees its own sourcemarkpad提升.mdlists this fixture as the first item of the structural queue and as the safety net a later PreviewRenderer extraction would need. This is that net; it changes no implementation code.Why not more source-regex tests
Most of
scripts/matches source text with regular expressions. That fails on rewrites which change nothing and stays green on behaviour changes that break users. Today alone: replacinglock().unwrap()with a recovering helper, and moving alocalStorage.setIteminto a persistence table, each turned an assertion red without altering behaviour.These run
processMarkdownHtmlfor real against 29 documents whose HTML is genuineconvert_markdownoutput, then re-parse the result and query it structurally — so nothing depends on the harness's own serialisation conventions.The two halves, and their honest status
Frontend: genuinely executed. No DOM shim precedent existed in
scripts/and no DOM library is innode_modules(adding one touchespackage.jsonand the lockfile, andnode_modulesis shared). So the harness ships a deliberately narrow parser, serializer, selector engine and TreeWalker. It is validated two ways: eight self-tests including parse→serialize→parse stability over all 29 fixtures, and a differential run against Python'shtml.parser, which yields identical trees for every fixture. That validates the tokenizer and tree builder — not browser DOM semantics, which only the targeted self-tests cover. The file is documented as "must not grow into a general parser".Renderer HTML: captured, not called.
convert_markdownis a privatefn;src-tauri/tests/cannot reach it. The HTML was not hand-written — it came from a throwaway crate carrying comrak 0.18, verbatim copies ofprotect_display_math_underscores,annotate_task_checkboxes, both task regexps and the exactComrakOptions, validated by reproducing byte-for-byte the HTML literals thatlib.rs's own passing tests assert.So these fixtures are inputs. If comrak or
convert_markdowndrifts, nothing here goes red until someone regenerates them. Closing that gap needs the assertions to live inlib.rs'smod tests, where several already do. The fixture file header states this and how to regenerate.Coverage
> - [ ],1. [ ]and1) [ ],*/+, prose either side, CRLF, loose lists, multi-line continuation. Expected lines are derived from the Markdown by theTASK_SOURCE_RErule, then compared with what the viewer's own read (closest('li')→/^(\d+):/) produces\_, inside a list item, two blocks on one line, math beside real emphasis. Assertsdata-math-sourceequals the Markdown between delimiters, underscore count preserved, zero<em>, and that emphasis outside math still worksa.anchor, no duplicate ids, chevron ↔ wrapper pairing, nesting. The sharpest: three identical# Titles withcollapsedHeaders = {'title-1'}— only the second collapses, which fails outright if #371's id keying regresses- <input type="checkbox" disabled="" />, byte-identical to a real task item and separated only by source line; a raw<ul>whoselihas nodata-sourceposat all; a checkbox inside a<p>Failability, checked rather than argued
Sixteen mutations — frontend and simulated renderer drift — each turn the suite red: leaving checkboxes disabled, dropping id promotion, keying folds by text, dropping
data-math-source, treating raw checkboxes as tasks,lilosingdata-sourcepos,1.-概述anchorization,<em>inside$$, duplicate headings sharing an id, an escaped underscore being eaten, and so on. Three initially came out green; all three were bugs in the mutation script (first-occurrence-only replace, or mutating markdown and HTML together) and went red once corrected.Not covered
documentSession.toggleTaskCheckboxis not executed — the regex that rewrites the source line lives in a closure inside a runes module that cannot be imported undertsx. The tests stop at "a click resolves to line N"; that the rewrite of line N is correct is not covered. Prerequisite: extract that rewrite into a plain exported function.MarkdownViewer.svelte's handlers are not executed either. Itsdata-sourceposparse rule is restated here as the documented contract, so a change there will not red this suite.localhostandfile://are both blocked by the browser pane in this environment.Validation
npm test— 234/234 (29 new)npm run check— 0 errors, 0 warningsgit status— only the four new files underscripts/Worth knowing:
scripts/sits outside svelte-check's tsconfig include, as the whole existing suite does, so these files are not type-checked by CI.