From fb594e45c36a101ec7eee560df1385c0290c607c Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Tue, 7 Jul 2026 00:11:16 -0700 Subject: [PATCH 1/2] README: short, human, indexed; deep dives move to docs/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README 320 → 114 lines with an Index section. Keeps: what it is, demo strip, install (two paths, brief), first video, Let Claude direct, pipeline diagram, template browsing, and a Going-deeper doc table. - Cut from README (single-source rule): full script.md grammar and the scene-template key table (owned by `videoeditor guide` and the `templates`/`preview` catalog), the crate map (owned by CLAUDE.md), and all nix depth. - New docs/nix.md owns the nix story: determinism/browser pinning rationale, every run variant, dev shell + CDP-client sharing, packaging & releases. - CLAUDE.md stays the agent/contributor ops manual; now points at docs/nix.md for depth. No content shared between README and CLAUDE.md. Co-Authored-By: Claude Fable 5 --- README.md | 348 +++++++++++----------------------------------------- docs/nix.md | 61 +++++++++ 2 files changed, 131 insertions(+), 278 deletions(-) create mode 100644 docs/nix.md diff --git a/README.md b/README.md index 2e51b1b..95eb558 100644 --- a/README.md +++ b/README.md @@ -4,317 +4,109 @@ Scripted short-video renderer for developers who'd rather write markdown than open DaVinci. `script.md` in → rendered vertical video out. ![five scenes from the bundled hello-bench example](docs/demo.jpg) -*The bundled `examples/hello-bench` episode — five scenes, rendered entirely -from markdown + SVG placeholders. This strip is the actual output.* +*The bundled `examples/hello-bench` episode — five scenes rendered entirely +from markdown + SVG placeholders. This strip is actual output.* - **Rust** orchestrates everything (`videoeditor`, one binary). -- **Web tech** does animation & composition: scenes are HTML/CSS/JS templates, - rendered frame-by-frame by headless Chrome as pure functions of `(data, t)` — - deterministic, no flaky screen recording. -- **ffmpeg** does the heavy lifting: scene encodes, concat, audio mix. -- **ElevenLabs** voices the narration and transcribes reference videos. +- **Web tech** does the animation: scenes are HTML templates rendered + frame-by-frame by headless Chrome as pure functions of `(data, t)`. +- **ffmpeg** does the heavy lifting; **ElevenLabs** voices the narration. - **Built to be driven by [Claude Code](https://claude.com/claude-code)** — - the repo ships the production rulebook an AI director follows (see below). + scaffolds wire the session up automatically. -## Bootstrap +## Index -### 1. Install nix (once) +- [Install](#install) +- [Your first video](#your-first-video) +- [Let Claude direct](#let-claude-direct) +- [How it works](#how-it-works) +- [Templates](#templates) +- [Going deeper](#going-deeper) -Nix is the preferred path: one command builds the pinned binary AND its whole -media toolchain deterministically from the committed `flake.lock` — the same -rustc, ffmpeg, and browser on every machine. Install it with the -[Determinate Systems installer](https://install.determinate.systems): +## Install -```bash -curl -fsSL https://install.determinate.systems/nix | sh -s -- install -``` - -### 2. Install videoeditor - -```bash -nix profile install github:security-union/videoeditor # install the CLI -# …or try it without installing: -nix run github:security-union/videoeditor -- --help - -# working from a local clone? build THAT tree instead of fetching GitHub: -nix build .#videoeditor && ./result/bin/videoeditor --help -``` - -The nix-built binary is wrapped so **every** runtime dependency is pinned — -ffmpeg on all systems, nixpkgs chromium on Linux, and on macOS the -free-licensed **Chrome for Testing** from the playwright browser bundle -(nixpkgs' `chromium` is Linux-only and `google-chrome` is unfree; `CHROME_BIN` -still overrides if you prefer your own). Nothing to apt/brew install, no -version drift: the same flake.lock renders the same pixels everywhere. The -only thing outside the pin is your **`ELEVENLABS_API_KEY`** -([elevenlabs.io](https://elevenlabs.io) → profile → API keys; free tier is -enough for shorts), needed only by `tts` and `analyze` — `parse`, `new`, -`render`, and `assemble` of already-voiced episodes run keyless. - -Contributors: `nix develop` (or `direnv allow` — `.envrc` is committed) drops -you in the pinned dev shell with rustc, ffmpeg, just, rust-analyzer — **and a -`videoeditor` command** that builds and runs the checkout you're in via -`cargo run` (first call compiles; always matches your edits, never a stale -store binary). `just --list` shows the task recipes. - -Just want the binary in a shell without the dev tooling? - -```bash -nix shell .#videoeditor # binary on PATH for this shell (or github:security-union/videoeditor) -nix run . -- build my-short # one-off invocation (apps.default) -nix run .#videoeditor -- --help # same, addressing the package explicitly -nix profile install . # permanent install -``` - -The pinned browser is an ordinary Chrome for Testing binary and `$CHROME_BIN` -points at it in the dev shell — any CDP client can reuse it. If you script -against the templates with puppeteer or playwright, aim them at the same -binary (`PUPPETEER_SKIP_DOWNLOAD=1` + -`puppeteer.launch({ executablePath: process.env.CHROME_BIN })`, or -`PLAYWRIGHT_BROWSERS_PATH`) instead of letting them download their own — -one browser build, identical pixels everywhere. - -### Without nix - -`videoeditor` is a single Rust binary that orchestrates tools on your PATH -(the yt-dlp model — templates and formats are embedded and self-extract to -`~/.cache/videoeditor//`, so the crate is a complete install; ffmpeg -and Chrome you bring yourself): +With [nix](https://install.determinate.systems) (preferred — pins the binary, +ffmpeg, and the render browser from the committed lockfile; details and every +run variant in [docs/nix.md](docs/nix.md)): ```bash -cargo install videoeditor # or: cargo install --path crates/videoeditor +curl -fsSL https://install.determinate.systems/nix | sh -s -- install # once +nix profile install github:security-union/videoeditor ``` -| Dependency | Needed for | Check | Get it | -|---|---|---|---| -| ffmpeg + ffprobe | `render`, `assemble`, `analyze` | `ffmpeg -version` | `brew install ffmpeg` · `apt install ffmpeg` · `dnf install ffmpeg` | -| Chrome / Chromium | `render` (web scenes), `grab` | `"$CHROME_BIN" --version` or a normal install | [google.com/chrome](https://www.google.com/chrome/) — auto-detected on macOS and Linux; `CHROME_BIN=/path/to/chrome` to override | -| `ELEVENLABS_API_KEY` | `tts`, `analyze` only | `echo $ELEVENLABS_API_KEY` | [elevenlabs.io](https://elevenlabs.io) | +Without nix: `cargo install videoeditor`, then bring ffmpeg +(`brew/apt/dnf install ffmpeg`) and Chrome (system install is auto-detected; +`CHROME_BIN` overrides). macOS and Linux; on Windows use WSL. -macOS and Linux are supported; on Windows use WSL. +Voicing needs an `ELEVENLABS_API_KEY` +([elevenlabs.io](https://elevenlabs.io) → profile → API keys; free tier is +plenty). Everything except `tts`/`analyze` runs keyless. -### 3. Your first video +## Your first video ```bash -export ELEVENLABS_API_KEY=... # https://elevenlabs.io → profile → API keys - -videoeditor new my-first-short # runnable scaffold: placeholder SVGs + code included -videoeditor build my-first-short # tts → render → assemble -open my-first-short/build/final.mp4 +videoeditor new my-first-short # runnable scaffold, placeholder assets included +cd my-first-short && claude # then type /direct ``` -The scaffold renders as-is — replace the placeholder narration, code panels, -and SVG memes as your episode takes shape. (The bundled -`examples/hello-bench` needs a source checkout: `git clone` this repo, then -`videoeditor build examples/hello-bench`.) - -Iterating: `videoeditor tts ` re-voices only missing/changed clips -(`--clip name --force` to re-roll one take) and prints ⚠ fit-check warnings -when narration overlaps; `videoeditor render --scene name` re-renders a -single scene; `videoeditor assemble ` re-mixes in seconds. Hacking on the -HTML templates? Point `VIDEOEDITOR_ROOT` at your checkout and edits apply -without rebuilding. - -### 4. Let Claude direct +No Claude? The manual loop: edit `script.md`, then +`videoeditor build .` (tts → render → assemble) and open `build/final.mp4`. +Iterate one piece at a time: `tts . --clip name --force` re-rolls one voice +take, `render . --scene name` re-renders one scene, `assemble .` re-mixes in +seconds. `videoeditor guide` prints the full rulebook. -This tool is designed to pair with **[Claude Code](https://claude.com/claude-code)** -as the director, and it wires the session up by itself: the binary embeds -the canonical rulebook (`videoeditor guide` prints it — pipeline, grammar, -the director loop, craft rules), `videoeditor -h` points agents at it, -`videoeditor new` drops a thin CLAUDE.md into every episode that defers to -it, and `pack init` writes the template-authoring contract to the pack's -`templates/CLAUDE.md`. Claude Code only loads the context of the directory -it starts in, so cd first: +## Let Claude direct -```bash -videoeditor new my-first-short -cd my-first-short && claude -``` - -then type **`/direct`** — a wizard command the scaffold drops into -`.claude/commands/` that interviews you (topic, receipts, tone, assets, -voice) and then drives script → voice → render → final.mp4 with approval -checkpoints before anything costs money. Or just ask in your own words: +The tool wires Claude Code up by itself: every scaffold carries a `CLAUDE.md`, +`videoeditor -h` points agents at the embedded rulebook (`videoeditor guide`), +and `/direct` — dropped into every episode — runs the wizard: it interviews +you (format, topic, real data for the receipts, tone, assets, voice), then +drives script → voice → render → final.mp4 with approval checkpoints before +anything costs money. Or skip the wizard and ask in your own words: > "Make a 25-second short: Rust vs Go parsing a 1GB JSON file. Run a real > benchmark first, then script it, voice it, render it, and QA the frames." -Claude runs the benchmark, writes `script.md`, calls `videoeditor tts`, -recomputes timings from the fit-check warnings, renders scene by scene, and -inspects extracted frames — the same loop a human editor would run, minus -the human. - -## Pipeline +## How it works ``` script.md ──parse──► timeline plan │ - ├─ videoeditor tts [CLIP:] → ElevenLabs → audio/clips/__.mp3 + audio/clips.json - ├─ videoeditor render [SCENE:] → Chrome frames → ffmpeg → build/scenes/NN_name.mp4 - └─ videoeditor assemble concat + narration@offsets + clip audio + music → build/final.mp4 -``` - -`videoeditor build ` runs all three. Extras: `analyze` (transcript + -scene-cut timing table of a reference video), `new` (scaffold an episode), -`grab` (fetch a URL through your own logged-in Chrome), `parse` (dump the -resolved plan as JSON). - -## Workspace layout (ffmpeg-style: thin CLI over focused libraries) - -| Crate | Role | Analogy | -|-------|------|---------| -| [`videoeditor`](crates/videoeditor) | CLI + scene orchestration + embedded templates | `ffmpeg` the binary | -| [`videoeditor-timeline`](crates/videoeditor-timeline) | `script.md` parser → typed timeline model | `libavformat` | -| [`videoeditor-chrome`](crates/videoeditor-chrome) | headless-Chrome CDP driver (frame capture, page grab) | `libavdevice` | -| [`videoeditor-media`](crates/videoeditor-media) | everything that shells out to ffmpeg: encodes, concat, audio mix, scene cuts | `libavcodec` | -| [`videoeditor-voice`](crates/videoeditor-voice) | ElevenLabs TTS + Scribe STT | — | - -## script.md grammar - -```markdown ---- -title: My Short # metadata frontmatter -fps: 30 -width: 1080 -height: 1920 -voice_id: # e.g. pNInz6obpgDQGcFmaJgB ("Adam", a public preset) -music: assets/music/bed.mp3 -music_gain_db: -20 ---- - -[SCENE: name | template=code-meme duration=6.42] -[DATA: code=assets/code/threads.rs meme=assets/memes/happy.svg pointer=true] -[DATA: bench="Execution time:|μ: 150µS|σ: 50µS" bench_at=5.8] -[CLIP: explain | at=0.19] -Narration text until the next marker. `at` = seconds from scene start -(omit to auto-place after the previous clip). `tempo=1.05` speeds the clip. - -[SCENE: outro | template=video-clip duration=2.2] -[DATA: src=assets/clips/punchline.mp4 seek=0] # keeps native audio; audio=false to mute -``` - -- Scene `duration` is authoritative — narration is placed inside it. This is - how you clone a reference video's timing exactly. -- All paths are relative to the episode directory. -- Unknown `[MARKERS:]` and `` are ignored (annotate freely). - -## Scene templates (`templates/scenes/`) - -A template is one HTML file. Contract: the renderer loads the page, injects the -merged `[DATA:]` map via CDP, then per frame calls `__sceneSeek(t)` and -screenshots. Everything visible must derive from `SCENE.d` (data keys, asset -paths already inlined as data: URIs, plus `codeText`, `duration`, `width`, -`height`) and `SCENE.t`. No CSS animations, no timers — pure state. - -| Template | Purpose | Data keys | -|----------|---------|-----------| -| `title-card` | X-vs-Y hook: logos, VS, flame, popping title | `left`, `right`, `left_label`, `right_label`, `title`, `*_at` (ms) | -| `code-meme` | top: highlighted code, bottom: meme + popping benchmark | `code`, `lang`, `code_size` (px), `full` (code takes the whole frame), `meme`, `badge`, `label`, `pointer`, `pointer_from/to` (s), `bench` (lines split by `\|`), `bench_at` (s), `typing` | -| `duel-table` | two-column concept duel (X is for…, Y is for…) | `title`, `left/right(+_label)`, `rows="a:b\|…"`, `row_pops="l:r,…"` (s) | -| `stat-card` | one big number: headline enters, stat counts up + lands, verdict slams | `title`, `value`, `decimals`, `prefix/suffix`, `verdict`, `*_at` (ms) | -| `scoreboard` | final ranking, winner green / loser red | `title`, `rows="name value\|…"` | -| `video-clip` | not HTML — ffmpeg passthrough of `src` (trim/scale/native audio) | `src`, `seek`, `audio`, `crop_top`, `caption` | - -Add a template = drop an HTML file in `templates/scenes/`. Zero engine changes. -Templates compose the **animation building blocks** in `_lib/scene.js` — pop, -enter, slam, shake, pulse, count-up, word-pop, typewriter, Ken Burns, easing — -all pure functions of t, tuned on real episodes (`stat-card` is the reference -composition). Every pack vendors this library at `pack init`. - -**Browsing the repertoire**: `videoeditor templates` lists every scene -template visible from where you stand (episode-local, packs, built-ins) with -its description and data keys; `videoeditor preview` renders each one's -bundled demo data into a five-frame contact sheet PNG so you can SEE the -animation before using it (`videoeditor preview code-meme` for just one, -`--episode ` to browse what a specific episode resolves). Templates -self-describe via an embedded `template-info` JSON block — packs that ship -one show up in the catalog like the built-ins do. - -## Template packs — your look, outside the engine - -The built-ins are a starting point, not your brand. A **pack** is a -self-contained directory of scene templates a creator owns and versions -independently (a git repo of its own, shared between channels, whatever): - -```bash -videoeditor pack init creator-a # scaffold: example template + vendored scene runtime + ├─ videoeditor tts [CLIP:] → ElevenLabs → audio/clips/ + audio/clips.json + ├─ videoeditor render [SCENE:] → Chrome frames → ffmpeg → build/scenes/ + └─ videoeditor assemble concat + narration@offsets + music → build/final.mp4 ``` -```markdown ---- -title: My Episode -packs: ../creator-a, ../shared-lower-thirds # comma-separated, episode-relative ---- - -[SCENE: intro | template=my-scene duration=2.5] -``` - -Resolution is layered, most specific wins — so creator A and creator B render -the same `script.md` grammar through entirely different visual identities: - -1. the episode's own `templates/scenes/` (one-off scenes) -2. frontmatter `packs:` in order -3. `$VIDEOEDITOR_PACK_PATH` (colon-separated, machine-level) -4. the engine built-ins - -A pack file named like a built-in **overrides** it. `videoeditor pack list -` prints the layers and exactly which file every scene resolves to; -renders log the source when a template comes from a pack. A single video gets -unique templates the same way — its `templates/scenes/` is the most specific -layer (`videoeditor pack init ` scaffolds it). Full guide with a -worked channel layout: [docs/templates.md](docs/templates.md). +A scene names a template and a duration; `[DATA:]` feeds the template; +`[CLIP:]` blocks carry narration placed at exact offsets. The engine warns +when narration overlaps (fit-check) or a template clips its content — and +the whole render is deterministic: same inputs, same pixels. -**Don't hand-write frame-by-frame animation.** `pack init` drops a `CLAUDE.md` -into the pack that turns Claude Code into that pack's template engineer — it -knows the `(data, t)` contract, the runtime helpers, and the render-one-scene → -inspect-frames QA loop. You describe the look ("neon terminal, CRT flicker, -title slams in when the voice says the name"); Claude writes the template, -renders it, reads the frames, and iterates with you. - -## Formats (`formats//`) - -A format is a narrative spine over the same machinery: `spec.md` (the rules) + -`skeleton.md` + starter assets (what `videoeditor new` copies). Built-ins: -**meme-benchmark** — the viral "X vs Y with receipts" ~20-second shape — and -**blank** (`videoeditor new my-video --format blank`) for ideas that aren't a -duel: announcement, tip, story. - -Production craft — how to make these videos actually good (real benchmark -receipts, congruence between audio and screen, pacing, review ritual) — is -embedded in the binary: `videoeditor guide` prints the canonical rulebook -(source: [crates/videoeditor/guide.md](crates/videoeditor/guide.md)). - -## Episode layout - -``` -my-short/ -├── script.md # source of truth -├── assets/{code,memes,clips,logos,music}/ -├── audio/clips/ # generated narration (name-keyed) — regenerable -└── build/ # frames/, scenes/, final.mp4 — disposable -``` +It's a five-crate Rust workspace, ffmpeg-style (thin CLI over focused +libraries) — the crate map lives in [CLAUDE.md](CLAUDE.md). -## Packaging & releases +## Templates -Two distribution channels, both fed from the same tag: +Browse the repertoire: `videoeditor templates` lists every scene template +with its data keys; `videoeditor preview` renders each one's demo into a +contact-sheet PNG so you can see the motion before using it. -- **nix flake (preferred)** — `nix profile install github:security-union/videoeditor` - builds from the committed `flake.lock`: pinned rustc, pinned ffmpeg, pinned - browser (chromium on Linux, playwright's Chrome for Testing on macOS), - binary wrapped so those exact versions are found at runtime. Fully - deterministic; CI builds the flake on every PR. -- **crates.io** — released with [release-plz](https://release-plz.dev): - merging the release PR bumps versions, updates changelogs, tags, and - publishes all five crates. The crate is a complete install (templates - embedded), but ffmpeg/Chrome come from your system: ffmpeg's GPL-licensed - builds can't ship inside an MIT binary, and staying a thin orchestrator - keeps the binary a few megabytes instead of a few hundred. +Your look doesn't live in the engine: each episode can carry its own +templates, and shared **packs** carry a channel's identity across videos — +layered resolution, most specific wins. `videoeditor pack init` scaffolds +one, including the authoring contract Claude follows (compose the proven +animation blocks in `_lib/scene.js`; never hand-roll curves). Full guide: +[docs/templates.md](docs/templates.md). -Developing: `nix develop` (or bring your own rustc + ffmpeg), then -`just --list` for the task recipes — CI runs the same `just check`, -`just test`, `just build` you run locally. +## Going deeper -## License +| Where | What | +|---|---| +| `videoeditor guide` | the embedded rulebook: grammar, director loop, craft rules | +| [docs/templates.md](docs/templates.md) | organizing templates: per-video, packs, machine-wide | +| [docs/nix.md](docs/nix.md) | deterministic installs, dev shell, releases | +| [CLAUDE.md](CLAUDE.md) | contributor + agent operations: crate map, commands, invariants | +| [examples/hello-bench](examples/hello-bench) | the end-to-end example episode | -MIT. Syntax highlighting via [highlight.js](https://highlightjs.org) -(BSD-3-Clause, vendored in `templates/scenes/_vendor/`). +MIT licensed. Syntax highlighting via [highlight.js](https://highlightjs.org) +(BSD-3-Clause, vendored). diff --git a/docs/nix.md b/docs/nix.md new file mode 100644 index 0000000..d1219f0 --- /dev/null +++ b/docs/nix.md @@ -0,0 +1,61 @@ +# Nix: deterministic installs, every way to run, releases + +## Why nix is the preferred path + +The flake builds the binary AND its whole media toolchain from the committed +`flake.lock`: the same rustc, the same ffmpeg, the same browser on every +machine — the same lockfile renders the same pixels. The installed binary is +wrapped so its pinned dependencies are always found; nothing to `apt`/`brew` +install, no version drift. + +The pinned browser: nixpkgs `chromium` on Linux; on macOS, the free-licensed +**Chrome for Testing** from the playwright browser bundle (nixpkgs' `chromium` +is Linux-only and `google-chrome` is unfree). `CHROME_BIN` overrides +everywhere if you prefer your own. Templates and formats ship inside the +package as a store path (`VIDEOEDITOR_ROOT` baked into the wrapper), so they +can never go stale. + +## Every way to run it + +```bash +nix profile install github:security-union/videoeditor # permanent, on PATH +nix profile install . # same, from a local clone +nix profile upgrade videoeditor # refresh after changes +nix shell .#videoeditor # binary on PATH for this shell only +nix run . -- build my-short # one-off (apps.default) +nix run .#videoeditor -- --help # same, addressing the package +nix build .#videoeditor && ./result/bin/videoeditor # build without installing +``` + +## The dev shell + +`nix develop` (or `direnv allow` — `.envrc` is committed) provides rustc, +cargo, clippy, rustfmt, rust-analyzer, ffmpeg, just — **and a `videoeditor` +command** that execs `cargo run --release` against the checkout you entered +from. It is always source-fresh: no stale store binary shadowing your edits; +the first call compiles, cargo caches the rest. `just --list` shows the task +recipes; CI runs the same ones. + +The dev shell exports `CHROME_BIN` pointing at the pinned browser. It's an +ordinary Chrome for Testing binary, so any CDP client can share it — if you +script against templates with puppeteer or playwright, aim them at the same +binary instead of letting them download their own +(`PUPPETEER_SKIP_DOWNLOAD=1` + +`puppeteer.launch({ executablePath: process.env.CHROME_BIN })`, or +`PLAYWRIGHT_BROWSERS_PATH`): one browser build, identical pixels everywhere. + +## Packaging & releases + +Two distribution channels, fed from the same tag: + +- **The flake** (preferred): fully pinned as above; CI builds it sandboxed on + ubuntu + macos for every PR. +- **crates.io**: releases are automated with + [release-plz](https://release-plz.dev) — merging the release PR bumps + versions, updates changelogs, tags, and publishes all five crates. The + crate is a complete install (templates embedded, self-extracted to + `~/.cache/videoeditor//`), but ffmpeg and Chrome come from your + system: ffmpeg's GPL-licensed builds can't ship inside an MIT binary, and + staying a thin orchestrator keeps the binary megabytes instead of hundreds. + +Keep `flake.lock` committed; `nix flake update` only as a deliberate PR. From 152544997d32d79cbb77eff6a7fad7570dce295a Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Tue, 7 Jul 2026 00:11:54 -0700 Subject: [PATCH 2/2] CLAUDE.md: point at docs/nix.md for nix depth Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b9554ec..ca7d0b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,10 @@ vertical video out via headless Chrome + ffmpeg + ElevenLabs). ## Commands -- Nix is the preferred dependency path: `nix develop` (dev shell) / - `nix build .#videoeditor` (the install artifact CI gates on). Keep - `flake.lock` committed; `nix flake update` only as a deliberate PR. +- Nix is the preferred dependency path: `nix develop` (dev shell; puts a + source-fresh `videoeditor` on PATH) / `nix build .#videoeditor` (the + install artifact CI gates on). Details: docs/nix.md. Keep `flake.lock` + committed; `nix flake update` only as a deliberate PR. - `just check` — clippy -D warnings + fmt --check (CI runs the same recipe). - `just test` / `cargo test --workspace` — parser + fit-check tests live in