# Testing, fixtures and the dev harness across the four apps Scope: vitest and playwright config, the `tests/` suites and their helpers, `src/dev/` (the browser-against-fake-data harness), Rust fixtures and `#[cfg(test)]` conventions, the tsconfig split. Not build tooling, not CI beyond how it invokes tests. Paths: margin `/Users/pj/Workspace/projects/python/margin`, margin-calendar `/Users/pj/Workspace/projects/python/margin-caledar`, margin-docs `/Users/pj/Workspace/projects/rust/margin-editor`, margin-mail `/Users/pj/Workspace/projects/rust/margin-mail`. Cites are relative to those roots. ## Findings first 1. **margin has no tests of any kind.** Confirmed plainly: `package.json:5-12` has `dev`, `build`, `preview`, `tauri`, `dmg`, `fonts:sync`, `fonts:check` and nothing else, and neither `vitest` nor `@playwright/test` is a dependency. No `tests/`, no `src/dev/`, zero `*.test.ts` under `src/`, zero `#[cfg(test)]` and zero `#[test]` under `src-tauri/src/`. Its `vite.config.ts` imports from `vite`, not `vitest/config`, so there is no `test` block to add to, and its `src/ipc.ts` (43 lines) calls `invoke` directly behind an `isDesktop` early-return, so there is no seam a fixture could plug into. Bringing margin into a shared harness is not a config change, it is building the dev fixture it never had. 2. **The three playwright configs are one file with the port swapped.** Diffing them leaves the port (1430/1440/1450), two rewritten comments, and two real differences: margin-docs adds `globalSetup: "./tests/identity.ts"` and drops `timezoneId`. Everything else is byte identical. 3. **margin-docs is the only app that checks the dev server is serving its own checkout**, and all three set `reuseExistingServer: true` on a fixed port. `tests/identity.ts` (106 lines) fetches five source files over `?raw` and byte-compares them against disk. Its own header says the four suites beside it "were happy to pass against anybody's copy". margin-calendar and margin-mail still are. 4. **The dev-mode invoke stub is the thing worth sharing and the three apps solved it three different ways.** All three branch identically in `src/ipc.ts`, but margin-calendar fakes no events at all, margin-mail fakes them as window `CustomEvent`s with an app-side bridge, and margin-docs hand-rolls 97 lines of `__TAURI_INTERNALS__` inside a test helper so the real `@tauri-apps/api` event plugin works in a plain tab. The third is the correct one and it is the one that is not reusable, because it lives in `tests/disk.ts` rather than in `src/dev/`. 5. **margin-docs has no shared spec helper at all.** 19 of 19 spec files define their own `async function open()` and inline the same `margindocs-recents` localStorage seed. margin-calendar and margin-mail both have `tests/app.ts`, and 33 of their 34 spec files import `openApp` from it. Between those two, `contrastOf` (60 lines), `clockAt`, `MIDDAY`, `settle`, `box` and `openDialog` are code identical and differ only in doc comments. ## What each app has | | margin | calendar | docs | mail | | --- | --- | --- | --- | --- | | `test` script | none | `vitest run` | `vitest run` | `vitest run` | | `test:ui` script | none | `playwright test` | `playwright test` | `playwright test` | | `vitest` dep | no | 3.2.4 | 3.2.4 | 3.2.4 | | `@playwright/test` dep | no | 1.62.1 | 1.62.1 | 1.62.1 | | `src/dev/` | absent | 356 lines | 1,040 lines | 3,341 lines | | spec files / source-level tests | 0 | 10 / 131 | 19 / 116 | 24 / 252 | | colocated `*.test.ts` / tests | 0 | 12 / 211 | 32 / 655 | 8 / 81 | | Rust `#[test]` | 0 | 79 | 0 (all integration) | 531 | | `src-tauri/tests/` | no | no | 8 files + support | no | | `src-tauri/fixtures/` | no | no | no | 76 files | Source-level counts are `test(` and `it(` at file scope; several files wrap tests in a `for (const theme of ["light","dark"])` loop, so the run counts are higher. CI runs `pnpm test` and `cargo test` in all three (calendar `ci.yml:31,70`, docs `ci.yml:34,58,78`, mail `ci.yml:49,92`) and Playwright nowhere. `just test` is `pnpm test` plus `cargo test` and `just test-ui` is `pnpm test:ui`, the three justfiles agreeing line for line on both. ## playwright.config.ts Shared by all three: `testDir: "./tests"`, `fullyParallel: true`, `retries: 0` (with the same comment in each saying a test that only passes on the second go is lying), `reporter: [["list"]]`, `outputDir: "node_modules/.cache/playwright"`, `timeout: 30_000`, `expect.timeout: 5_000`, `baseURL` off the port, `viewport: {1440, 900}`, `deviceScaleFactor: 1`, `locale: "en-GB"`, `trace: "retain-on-failure"`, `screenshot: "only-on-failure"`, a single project `{ name: "chromium", use: { browserName: "chromium" } }` with an identical comment explaining why it is not `devices["Desktop Chrome"]` (that device pins a Windows user agent and the keymap reads the platform off it), and a webServer block of `pnpm dev`, `reuseExistingServer: true`, `timeout: 60_000`, `stdout: "ignore"`, `stderr: "pipe"`. Differences, in full: port 1430 / 1440 / 1450; `timezoneId: "Asia/Kolkata"` in calendar (`playwright.config.ts:34`) and mail (`:31`) but not docs, because both anchor their fixture to the browser's local day; `globalSetup` in docs only (`:14`). No app configures `toHaveScreenshot`, `snapshotDir` or any visual-regression comparison, and there are zero snapshot baselines in the three. Every screenshot is a named PNG for a person or the docs. ## The tests directories margin-calendar, 10 specs plus `app.ts` (370 lines) and `tsconfig.json`. Geometry and layout (`grid` 26, `compact` 5, `views` 8), input (`keyboard` 14, `interaction` 12, `touch` 22), form factor (`phone` 10), design-system (`legibility` 10), overlays (7), regression (`bugs` 17). The two phone files use `test.use({ viewport, hasTouch, isMobile })` at file scope (`touch.spec.ts:19`, `phone.spec.ts:16`) rather than a Playwright project. margin-docs, 19 specs plus `caret.ts` (134), `disk.ts` (144), `saving.ts` (52), `identity.ts` (106). Six of the 19 are `_audit*.spec.ts`, headed "Temporary exploration harness. Deleted once the findings are written down" (`_audit.spec.ts:1`) and still present. The rest are bytes-on-disk claims (`bytes` 11, `color` 7, `export-writes-only-the-pdf` 2, `external-changes` 8), editing (`blocks`, `headings`, `tables`, `clipboard`, `shortcuts`), and smoke (9). margin-mail, 24 specs plus `app.ts` (401 lines) and `tsconfig.json`. Roughly one spec per screen, plus `kit.spec.ts` (design system), `keyboard.spec.ts`, `guide.spec.ts` and `guide-shots.spec.ts` (asset generation, not assertion). ### The helpers each invented `tests/app.ts` in calendar and mail is the same file with different measurement functions bolted on. Identical modulo doc comments: - `clockAt(hour, minute)` and `MIDDAY` (cal `:41-53`, mail `:46-58`), pinning the clock to the current day at a fixed hour in Asia/Kolkata. - `openApp(page, options)` (cal `:61-90`, mail `:66-90`). Same `addInitScript` body, same `__test-seeded` sentinel so a reload is not silently reset, same `page.clock.setFixedTime`. Only the seed keys differ. - `settle(page)`, two `requestAnimationFrame`s (cal `:105-112`, mail `:99-106`, third copy in docs `tests/caret.ts:46-53`), `box(target)` (cal `:114-124`, mail `:108-118`), and `openDialog(page)` (cal `:288-293`, mail `:188-193`). - `contrastOf(page, selector)` (cal `:306-366`, mail `:219-278`), which composites every translucent background between the element and the page through a 1x1 canvas so `oklch()` and `color-mix()` do not read as transparent. App-specific and correctly so: calendar's `gridFit`, `axis`, `blocks`, `headerDates`, `hourY`, `columnX`, `drag`; mail's `rows`, `groups`, `paneMessages`, `paletteRows`, `actionBar`, `checkedRows`, `bodyText`, `toast`, `listScroll`, `place`, `openRow`. Two mail helpers are generic and belong in a shared package despite being written for mail: `token(page, name)` (`app.ts:180-185`), reading a CSS custom property off the root, and `failCommands(page, commands)` (`app.ts:343-360`), which uses `page.route` to answer the request for `/src/dev/mockIpc.ts` with a shim that forwards to the real module (`?real`) and rejects the named commands. That is the only way to test a failure path when the backend is in the page rather than on the wire, and it is 18 lines that would work unchanged in any of the four. margin-docs' helpers have no sibling: `putCaret`/`caretIsIn` (`caret.ts`), whose 28-line header documents why nothing in the suite presses End to move a caret; `watchDirty`/`dirtyWasShown` (`saving.ts`), a MutationObserver installed before typing so the 500ms autosave cannot be raced; and `installTauriShim`/`change`/`ask` (`disk.ts`), covered below. ### Screenshot policy `page.screenshot` appears 33 times across 16 mail specs, 7 times across 4 docs specs (all in `_audit*`), and never in calendar. Mail writes into `screenshots/`, which is gitignored (`.gitignore:41`), so an ordinary run leaves the tree clean; the 10 pictures that ship inside the bundle go to the committed `public/guide/` and are gated by `test.skip(() => !process.env.GUIDE_SHOTS)` (`guide-shots.spec.ts:19-22`) behind `just guide-shots` (`justfile:28-30`). That two-tier rule is right and only mail has it. ### Design-system assertions `mail/tests/kit.spec.ts` (132 lines) is the only design-system suite. It renders a `#/kit` route in both palettes and shoots it, asserts fixed geometry read from custom properties (`--list-w` is `420px`, a row is 46px, an avatar 30x30), then runs three static source scans with `node:fs`: no hex literal in any stylesheet under `src/ui` or `src/screens`, every `` carries `NO_AUTOFILL` or an explicit `autoComplete`, and no file says "keychain". calendar's equivalent is runtime rather than static: `legibility.spec.ts` measures contrast, checks no block reads "Untitled" and checks every block has a non-empty accessible name, over both themes. docs' equivalent is a vitest, not a spec: `src/theme.test.ts` reads three stylesheets (including `node_modules/margin-shared/css/tokens.css`, `:18-22`) plus `index.html`'s pre-bundle boot script and asserts all of them declare the same variable set, because a missing dark variable falls back silently to the warm light value in `:root`. Nothing anywhere asserts anything about icons. ## src/dev: the invoke stub All three apps switch dev mode on the same way, in `src/ipc.ts`: ```ts export function call(command: string, args?: Record): Promise { if (import.meta.env.DEV && !isTauri) { return import("./dev/mockIpc").then((m) => m.mockCall(command, args)); } return invoke(command, args); } ``` calendar `ipc.ts:192-197`, docs `ipc.ts:288-292`, mail `ipc.ts:748-752`, byte identical bar the comment. `isTauri` is `"__TAURI_INTERNALS__" in window`, computed once at import time, and all three derive `isDesktop`, `isMacDesktop` and `live()` from it with the same comments verbatim (calendar `:9-40`, docs `:9-40`, mail `:10-34`). There is no environment variable and no dev-only build flag: the switch is "DEV build with no Tauri bridge", so `pnpm dev` in a browser and Playwright get the fixture and the packaged app cannot. `mockCall` is a `switch (command)` in all three, returning `as unknown as T` at every arm and throwing on an unknown command (calendar `mockIpc.ts:131-132`, "dev mock has no handler for"). There is no type link between a command name, its arguments and its return; calendar casts args to `Record` (`:41`) and then to the real type per field. Command counts: calendar 12 arms, docs 40, mail roughly 180. State is a mutable copy of the fixture taken at module load, so writes persist for the session and a reload resets (calendar `:11-14`, docs `:48-49`, mail `:72-84`). ### Fixtures `src/dev/fixture.ts` in each. calendar (222 lines) exports `devAccounts`, `devCalendars` and `devInstances(from, to)`, generated from a seed table and anchored to the current week; its header records that it is modelled on a real sync of 12,067 events where 90% came back with no summary. docs (344 lines) exports `devRoots`, `devEntries` (an in-memory folder with real base64 PNG, SVG and PDF bytes at `:208-273`) and path utilities the mock and the app both use. mail (1,572 lines) exports 20 symbols, 13 data tables (`devAccounts` through `devSyncStatus`) and 6 functions (`devDiscover`, `devCert`, `devFiles`, `devContacts`, `groupOf`, `categoryOf`). A fourth fixture source in docs has no sibling: `src/markdown/corpus/`, loaded through `import.meta.glob("./*/*.md", { query: "?raw", eager: true })` at `corpus/load.ts:20`. Its header records the bug that motivated the glob, that naming folders explicitly left twenty adversarial files inside the corpus but outside every gate reading it. That is the "add a file, get a test" pattern and it is worth generalising. ### Dev flags localStorage, read defensively inside try/catch. calendar has one, `margincal-dev-empty` (`mockIpc.ts:32-38`); docs has one, `margindocs-dev-no-writing-tools` (`:246-252`); mail has eight, `marginmail-dev-` plus `empty`, `crowd`, `notify`, `imap`, `bridge`, `pending`, `hydrate-fails`, `sync-fails`, read through `firstRun()` (`:220-230`) and a generic `flagged(key)` (`:262-268`). The naming is uniform (`-dev-`) and the reader is the same eight lines three times over. ### Faking events, three ways This is where the three diverge and where the shared design has to be decided. calendar does not fake events at all: `App.tsx:52` is `if (!isTauri) return;` before every `listen`, so in a browser `menu-action`, `auth`, `sync-progress` and `store-changed` never arrive, and anything they drive is unreachable from the Playwright suite. mail dispatches window `CustomEvent`s under the same names from inside the mock (`mockIpc.ts:237-241`), with an optional delay so a state that would otherwise last one frame is observable (`narrateFirstSync` at `:637-651` walks a five-step sync over 1,250ms). The app side is `onAppEvent` in `App.tsx:69-77`, seven lines picking `listen` or `window.addEventListener` off `isTauri`. Cheapest correct answer, and confined to mail. docs does neither. Its mock exports `external` (`mockIpc.ts:619-696`), a second surface that mutates the fixture the way another program would, behind the app's back, and returns the exact `WatchEvent[]` the Rust watcher would have emitted; putting them on the bus is the caller's job. That caller is `tests/disk.ts:37-133`, which installs a hand-rolled `__TAURI_INTERNALS__` at document start: `invoke`, `transformCallback`, `unregisterCallback`, `runCallback`, the `plugin:event|listen` and `|unlisten` commands, a listener map, `metadata`, `convertFileSrc` and `__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener`. Its comment at `:29-36` says this is deliberately not `@tauri-apps/api/mocks`, which cannot be reached from an init script, and deliberately written to the contract rather than to convenience. `emit` returns a delivery count per event so a test can tell a working subscription from a payload that fell on the floor (`:66-75`). `external` also carries `pauseWrites`/`resumeWrites` (`:679-695`) so a save can be held and a buffer kept dirty instead of racing the 500ms autosave. The docs approach is the strictly better one: the app runs the real `@tauri-apps/api`, so the `isTauri` branch that ships is the branch under test, where mail leaves `onAppEvent`'s Tauri arm never exercised in a browser. But it is 97 lines in a test helper, so a person running `pnpm dev` by hand gets no events at all. ## The Rust side margin-mail `src-tauri/fixtures/` is 36 `.eml` files (real messages as they come off the wire, CRLF throughout, half of them not UTF-8), 36 matching `golden/*.txt` and 4 `autoconfig/*.xml`, compiled into the test binary by a `corpus!` macro in `src/fixtures.rs:10-19` that emits one `include_bytes!` const per file plus an `all()` returning every pair, so a sweep over the corpus is one call. `fixtures.rs:58-102` is itself a test: every fixture has a header/body break, no bare LF, a plausible date and a parseable From. Nothing is generated at test time. Alongside it, `src/provider/fake.rs` (612 lines, `#![cfg(test)]` at `:15`) is an in-memory mailbox implementing the `Provider` trait: real ids, labels, dates and raw bytes, paging, a history log, and scripted failures via `fail_next` and `withhold_body`. `provider/mod.rs:1-14` records that this is the only reason the sync engine is testable without credentials. Nothing comparable exists elsewhere. margin-docs builds its Rust fixture at runtime instead: `src-tauri/tests/support/notes_repo.rs` (339 lines) creates a real git repository per test binary, copies 12 documents out of `src/markdown/corpus/real` so there is one corpus and not two, and generates 13,000 files under a vendored `node_modules` because several tests need a folder large enough that skipping it beats walking it. `git status` is the oracle. Conventions differ and both are defensible. calendar and mail put tests in `#[cfg(test)] mod tests` beside the code, 6 files / 79 tests and 54 files / 531 tests; fifteen of mail's are large enough to live in a sibling `/tests.rs` (backup, clips, contacts, decisions, drafts, exports, invites, notify, piles, screener, send, snooze, state, sync, unsubscribe). docs has zero `#[cfg(test)]` and eight integration binaries totalling 4,210 lines, one needing `--test-threads=1` and run as its own CI step (`ci.yml:78`). Eight mail tests carry `#[ignore]`, six of them "hits the network" in `imap/discover.rs`. ## tsconfig The app `tsconfig.json` is `"include": ["src"]` in all four, so `tests/` is excluded by omission rather than by an `exclude` entry, and the colocated `src/**/*.test.ts` files sit inside the app's own type check. `tests/tsconfig.json` exists in the three and is the same nine options each time (ES2022, bundler resolution, strict, noUnusedLocals, noUnusedParameters, skipLibCheck, noEmit) with `"include": [".", "../playwright.config.ts"]`. Mail's adds `"types": ["node"]`. Nothing runs `tsc -p tests/tsconfig.json` in any script or CI job, so the spec files are type checked only by an editor. Vitest config lives in `vite.config.ts` under `test:`, all three with `include: ["src/**/*.test.ts"]` and `environment: "node"`. No jsdom, no happy-dom, no `@testing-library/*` anywhere. margin-docs adds `maxWorkers: "50%"` and three 30-second timeouts with a 20-line comment explaining that a CPU-blocking markdown sweep cannot answer vitest's `onTaskUpdate` RPC while it runs. ## Colocated unit tests: what they are and are not They test pure functions in a node environment. No component is rendered, no store is mounted against a DOM, nothing goes near `mockIpc`. Calendar's convention is explicit: a component `X.tsx` gets a sibling `XModel.ts` holding the decisions and `XModel.test.ts` tests that module. `GridModel.test.ts` covers `busyHours`, `heldHours` and `bandAt` over synthetic `Placed` values with `instance: {} as Instance`, because only the times matter; `EventDetailsModel.test.ts` covers popover placement against a fixed `Bounds`; `overlayModel` and `QuickCreateModel` cover date arithmetic and draft construction; `AgendaModel` covers grouping, gap labels and search matching. The odd one out is `EventDetailsHtml.test.ts`, half readability and half hostile input, because the description string comes off the wire from whoever created the event. margin-mail's eight follow the same rule. `providers.test.ts` tests the routing decision for an address twice over, by domain and by what discovery found; `useSync.test.ts` the two pure decisions the header and toast make from a `SyncStatus`; `guide.test.ts` link-checks the article library without rendering it; the other five are table checks. margin-docs' two named ones are a different genre and the more interesting one. `theme.test.ts` and `width.test.ts` both read source with `node:fs` and assert that two tables not written in the same language agree: theme against three stylesheets and a boot script, width against the command table and the subscription joining them. The other 30 are the markdown bridge and are app-specific. None of them snapshot, mock `call()`, or configure coverage, and there is no property-based library (though `typst.test.ts` and the five `adversarial*.test.ts` files are hand-rolled corpus sweeps, which is the same idea). ## Known-failing specs Nothing in any repo records a known failure. The only markers are `guide-shots.spec.ts:19` (an intentional env gate, not a failure) and a `test.fail` at `external-changes.spec.ts:320`, kept so it would turn red the day the defect was fixed. `margin-editor/docs/conventions.md:74` is the standing rule: "Never weaken, skip or delete a test to reach green." The four margin-mail browser failures recorded in session memory are written down nowhere in the tree. If they are real that is the gap: either a per-app `tests/known-failures.md` or annotations on the specs, because right now the only record is a chat log. ## What a shared harness package would contain `margin-shared` already exists at `margin/shared` and is consumed by margin and margin-docs by relative `file:` path, and by margin-mail. margin-calendar does not depend on it at all. Adding a `./test` subpath export is the least-friction home; a separate package means a fourth `file:` edge. **1. A playwright config factory.** Everything in the three configs bar the port is a default, so the whole file becomes `export default marginPlaywrightConfig({ port: 1450, witnesses: ["src/ipc.ts", "src/dev/mockIpc.ts", "src/dev/fixture.ts"] })`. `witnesses` turns the docs identity check on for every app rather than one, with the factory supplying `globalSetup` so no app wires it. `timezoneId` defaults to `Asia/Kolkata` and docs passes null. Saves roughly 45 lines per app and makes "all three run at 1440x900 with retries 0" a fact rather than a coincidence. **2. The dev backend, with a typed command registry.** Replace `switch (command)` with a table keyed by command whose handlers are typed off the DTO types `src/ipc.ts` already declares: ```ts export const backend = defineBackend({ accounts_list: () => (firstRun() ? [] : accounts), thread_view: ({ key }: { key: string }): ThreadView => viewOf(byKey(key)), }); export type Command = keyof typeof backend; ``` `defineBackend` returns `{ mockCall, has, commands }`, with `mockCall` throwing the existing "no handler for" error on a miss. The win is that the arg cast at every arm and the `as unknown as T` at every return both disappear, and a command in `dto.rs` with no handler becomes checkable rather than a runtime surprise three screens later. Alongside it: `makeCall(loader)` producing the `call()` in `src/ipc.ts`, already identical in three places, and `devFlag(name)` replacing the three copies of the try/catch localStorage reader. **3. The Tauri shim, moved from `tests/` into the shared package and made the default.** Lift `installTauriShim` out of docs' `tests/disk.ts:37-133` unchanged, parameterise the backend module specifier, and expose it both as a Playwright init script (what docs does now) and as a dev entry point so `pnpm dev` in a browser gets real Tauri events too. Then `onAppEvent` (mail `App.tsx:69-77`) is unnecessary and the app runs one code path in both environments. This is the highest-value item in the audit: 97 lines that took real care to get right, written to a contract rather than to convenience, and two of the three apps are the poorer for not having it. With it, `emitEvent(page, name, payload)` and the delivery-count return become shared, and mail's `narrateFirstSync` style of timed multi-step event script becomes a shared `script([...])`. **4. Fixture loading.** Two patterns, both generalisable: docs' `import.meta.glob` corpus loader (`corpus/load.ts`) as `loadCorpus(glob)`, and mail's `corpus!` macro (`fixtures.rs:10-19`) as a shared Rust macro crate. Both encode the rule that adding a file puts it inside every sweep. Also shared: `mutableCopy(fixture)` for the clone-at-module-load idiom in all three mocks, and `undoLedger()` for mail's `undoable`/`runUndo` (`mockIpc.ts:292-314`), which any app with an undo toast will want. **5. Playwright helpers.** `settle`, `box`, `clockAt`, `MIDDAY`, `openDialog`, `token`, `failCommands`, plus `makeOpenApp({ theme: "marginmail-theme", pane: "marginmail-pane" })` returning an `openApp`, since only the seed keys differ between the two that have one. **6. Assertions on tokens and icons.** ```ts expectNoColourLiterals(root, ["src/ui", "src/screens"]); // mail kit.spec.ts:82-96 expectThemesAgree(sheets, bootScript); // docs theme.test.ts expectTokens(page, { "--list-w": "420px" }); // mail kit.spec.ts:62-67 expectContrast(page, selector, { min: 4.5 }); // cal legibility.spec.ts expectAccessibleNames(page, selector); // cal legibility.spec.ts expectIconsInSet(root, dirs); // does not exist yet ``` The first two are static scans over source and need no browser, so they can run under vitest in an app with no Playwright, which is how margin gets its first test. ## What stays per app Every measurement helper: calendar's `gridFit`, `axis`, `blocks`, `hourY`, `drag`; mail's `rows`, `groups`, `paneMessages`, `paletteRows`, `actionBar`, `toast`; docs' `putCaret`, `caretIsIn`, `watchDirty`. They read app-specific class names and encode app-specific timing, so sharing them would be indirection over one caller each. The fixtures themselves, and every spec file. A calendar of 12,067 events, a folder of markdown and a mailbox of threads have nothing in common but the loading mechanism. The Rust test convention. Mail's `#[cfg(test)] mod tests` beside the code and docs' integration binaries are both right for their shape, and forcing one on the other buys nothing. The shareable parts there are narrower: the `corpus!` macro and a `TempRepo` builder along the lines of `support/notes_repo.rs`.