Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Practice test-driven development for real logic: write the failing case, watch i

The specification of that boundary is the conformance corpus, not the Python tests. A change to what gets joined is a **corpus case first** — see [corpus/README.md](corpus/README.md) for the format, which is three files in a directory and needs no parser worth the name. `tests/test_unwrap.py` covers what a corpus cannot. Most of that is the surface no other implementation shares -- argument handling, file discovery, the transcript skip, encoding failures, and the exit codes those produce, which is the list its own module docstring gives. The rest is matcher-level pins that sit below the corpus's altitude, and those the Rust does share: it carries the same vectors as unit tests of its own, because a matcher is where a hand-written port is most likely to differ in a way no document would describe.

Both tiers explain how to add a case, and the CLI tier's answer keys are generated rather than written: `REGENERATE_CLI_CORPUS=1 uv run python -m pytest tests/test_cli_corpus.py -k <slug>` writes `expected/` and `stdout.txt` from what the reference run did, leaving `exit_code` as the one expectation you state rather than observe. There is no such path for the transform tier, so produce those two files by running the tool and reading the diff. Never edit an answer key to make a test pass; a key written by hand pins what its author believed, which is the one thing a conformance case must not do.
Both tiers explain how to add a case, and the CLI tier's answer keys are generated rather than written: `REGENERATE_CLI_CORPUS=1 uv run python -m pytest tests/test_cli_corpus.py -k <slug>` writes `expected/` and `stdout.txt` from what the reference run did, leaving `exit_code` as the one expectation you state rather than observe. There is no such path for the transform tier, so produce those two files by running the tool and reading the diff. Never edit an answer key to make a test pass; a key written by hand pins what its author believed, which is the one thing a conformance case must not do. A case that changes what gets joined reverses that order: write the intended key, watch it fail, change both implementations, then run the tool and confirm it produces what you wrote. The implementation moves to meet the key, not the key to meet the implementation.

Corpus cases are literal files because a GFM hard break *is* two trailing spaces and a CRLF case *is* `\r\n`, and any inline format puts both where a tidying hook eats them silently — leaving a case that passes while testing nothing. And `.pre-commit-config.yaml` carries `exclude: ^corpus/[^/]+/` because this repository runs its own unwrap hook over `types: [markdown]`; without it, one commit would rewrite every input into its own expected output and turn the suite green against nothing. That key does not reach every caller — `pre-commit try-repo` builds its config from the hook manifest it is pointed at, and the composite action sweeps with its own `git ls-files` — which is why exclusion also belongs to the tool. A `.unwrapignore` at the repository root names both corpus tiers, and because the tool reads it wherever it is invoked from, CI and `make hook-test` can both just say `--all-files`.

Expand Down
2 changes: 2 additions & 0 deletions corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,6 @@ Write the three files and it is picked up automatically; nothing registers cases

Produce `expected.md` by running the tool over `input.md` and reading the diff, rather than by writing it out, and never edit one to make a test pass: an answer key written by hand pins what its author believed, which is the one thing a conformance case must not do. This tier has no regeneration command — `REGENERATE_CLI_CORPUS` belongs to the CLI tier and does not reach it — so nothing but the discipline enforces that here, which is why it is worth stating in the tier that lacks the tooling rather than only in the one that has it.

A case that changes what gets joined reverses that order. The intended output is what the change is for, so write `expected.md` first, watch the case fail, change both implementations, then run the tool and confirm it produces what you wrote. What ships is still the tool's output. Editing a key to make a test pass moves the key to meet the implementation; this moves the implementation to meet the key.

A case whose expected output equals its input is not a wasted case. Most of this tool is the part that declines to act, and those are exactly the cases a change is most likely to break.
Loading