From 98a299b61a9ba7d2a26961f08a0de701214fe7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ademir=20Jos=C3=A9=20Ferreira=20J=C3=BAnior?= Date: Sun, 16 Aug 2026 04:34:33 -0300 Subject: [PATCH] Assert corpus round-trip convergence for supported syntax Twelve of the sixteen scoped files are rewritten on first open, so byte identity is not assertable today. The dominant cause is the serializer escaping characters that would not parse as syntax in place, which is the open question in #245; the remainder is delimiter and structural form the document model does not retain. Convergence is an interim property chosen for that reason. It holds the editor to a stable document without blessing any particular normalization, and it is what catches the structural loss fixed in #247. Byte identity stays the target, and this assertion should tighten toward it as the escaping question is settled. The other corpus subtrees stay manual. The byte fixtures exist to pin CR, CRLF, BOM, NUL, and a missing final newline, which is precisely what reading and serializing normalizes away; the environment and extension trees assert behavior this test cannot reach. --- CONTRIBUTING.md | 2 +- .../editor/tests/corpusRoundTrip.test.tsx | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/features/editor/tests/corpusRoundTrip.test.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e12412..7aaadce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,7 +131,7 @@ Pull request requirements: After submission, CI runs the automated checks. Maintainers apply a type label, assign the pull request's owner, and review the scope, implementation, and verification evidence; priority and Project status stay on the issue. Contributors should address review feedback or explain unresolved trade-offs. Maintainers squash merge accepted pull requests using the pull request title as the commit title on `main`. The pull request body becomes that commit's body and is the permanent record of the change; intermediate commits do not survive the merge. That is why the pull request body carries verification evidence and intermediate commit messages do not. -Verify changes locally before merging. Use `pnpm check:frontend` for frontend-only work, `pnpm check:backend` for Rust/Tauri-only work, and `pnpm check` for cross-cutting updates. For manual testing of Markdown and the article navigator, open the committed `corpus/` directory or one of its focused scenario directories in the app. +Verify changes locally before merging. Use `pnpm check:frontend` for frontend-only work, `pnpm check:backend` for Rust/Tauri-only work, and `pnpm check` for cross-cutting updates. For manual testing of Markdown and the article navigator, open the committed `corpus/` directory or one of its focused scenario directories in the app. Markdown round-trip convergence over `corpus/commonmark/`, `corpus/gfm/`, and `corpus/isolated/end-of-file/` is asserted automatically by `src/features/editor/tests/corpusRoundTrip.test.tsx`, so the manual pass over those directories covers rendering, interaction, and navigator behavior; the remaining subtrees are verified only by hand. Frontend checks enforce a coverage floor. It is a ratchet set just below the measured numbers rather than a target: a change that falls below it needs tests, not a lower floor, and the floor is raised when the measured numbers move up. diff --git a/src/features/editor/tests/corpusRoundTrip.test.tsx b/src/features/editor/tests/corpusRoundTrip.test.tsx new file mode 100644 index 0000000..e5953a8 --- /dev/null +++ b/src/features/editor/tests/corpusRoundTrip.test.tsx @@ -0,0 +1,51 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { createMarkdownReferenceContext } from "@/test/factories/editor"; +import { setupMilkdownEditorMount } from "@/test/utils/milkdown"; +import { mockTauriApiCommand } from "@/test/utils/tauriApi"; + +const mountEditor = setupMilkdownEditorMount(createMarkdownReferenceContext()); + +const corpusFiles = [ + "commonmark/blocks.md", + "commonmark/code.md", + "commonmark/emphasis.md", + "commonmark/html.md", + "commonmark/links-and-images.md", + "commonmark/lists-and-blockquotes.md", + "commonmark/text-and-breaks.md", + "gfm/autolinks.md", + "gfm/strikethrough.md", + "gfm/tables.md", + "gfm/tagfilter.md", + "gfm/task-lists.md", + "isolated/end-of-file/incomplete-html-comment.md", + "isolated/end-of-file/unclosed-code-fence.md", + "isolated/end-of-file/unclosed-directive.md", + "isolated/end-of-file/unclosed-html-block.md", +]; + +const readCorpusFile = (relativePath: string) => + readFileSync(resolve(process.cwd(), "corpus", relativePath), "utf8"); + +// The editor is allowed to normalize on first open, so the baseline is the first +// serialization rather than the corpus file. +describe("Corpus round trip", () => { + beforeEach(() => { + mockTauriApiCommand("resolveMarkdownImageTarget", ({ target }) => ({ + kind: "renderable", + path: `C:/Notes/${target}`, + })); + }); + + it.each(corpusFiles)("converges on a stable serialization for %s", async (relativePath) => { + const source = readCorpusFile(relativePath); + + const first = (await mountEditor(source)).getMarkdown(); + const second = (await mountEditor(first)).getMarkdown(); + + expect(second).toBe(first); + }); +});