--- name: codetrial-verify description: How a CodeTrial change is validated - scripts/test.sh as the credential-free gate, which generated artifacts have to be regenerated before it passes, the checks that need credentials or a browser and therefore sit outside it, the browser and mutation lanes, and how to bring the server up for a live look without taking the maintainer's port. Use before calling work done, when a gate fails on drift rather than on a bug, when adding a test, or when a fix needs to be seen working in the app. --- # Validating a CodeTrial change One gate needs no credentials and is the thing to run: ```sh ./scripts/test.sh # the gate CI's `check` job runs make check # the same, plus a live Gemini credential check ``` `scripts/test.sh` is a list of named gates that all run before it reports, so one failure does not hide the next; the summary line names every gate that failed. It covers `cargo fmt --check`, `clippy -D warnings`, `cargo test`, the Python unittest suites, the Node browser tests, ESLint, `ruff check`, `shellcheck`, the generated-artifact drift checks, the hook suite, `cargo-audit` and `actionlint`. It is not offline. The `fetch-vendor` gate downloads missing assets named by a `web/vendor/**/FETCH` manifest; it cannot restore committed vendor files. `actionlint` publishes itself as a container, so that lane may reach for docker. Both are checksum-pinned or version-pinned; neither needs a credential. A lane whose tool is absent skips instead of failing. Skips made directly by `scripts/test.sh` appear under its final `not checked by this run:` summary. Formatter skips do not: `scripts/indent.sh` prints them inline while the `indent` gate keeps running. Read both the final summary and the formatter output before claiming coverage; never treat the skip set or its size as fixed. The drift checks always run. That skipping is why a green local run is weaker evidence than a green CI run, and so is what this script leaves out: CI also holds a pull request's own commit messages to the rules, mutation-tests the diff in its own job, and builds release binaries for three targets. The `indent` gate is the one that surprises people. `scripts/indent.sh --check` copies the tree, runs the whole formatter chain over the copy, and diffs: comment reflow with `commentflow`, then `cargo fmt`, `ruff format` and `shfmt`. Checking the composition rather than each tool is not a flourish. commentflow puts a blank line before a comment inside a method chain and `cargo fmt` takes it straight back out, so `commentflow --check` alone can never be satisfied on Rust. `make indent` runs the same script with `--write`, so the fix for a failure is always that one command. Never pass `shfmt` a style flag; it reads `.editorconfig`. Prettier sits beside the chain rather than in it: it shares no file with the others, so the check runs it in place with `--cache`, alongside the copy. Its file set is in `scripts/indent.sh` and includes the issue forms, which ride along to be parsed. Without `npm ci` it skips with a note. ## Drift is the usual failure Several trees are generated, and the gate compares the committed bytes against what the generator would write now. A failure here is not a bug in your change; it means the source moved and the output did not: ```sh python3 scripts/gen-problems.py python3 scripts/gen-problem-cards.py # the problem cards in web/index.html node scripts/gen-wire-fixtures.mjs # browser/agent wire fixtures node scripts/gen-recording-fixtures.mjs python3 scripts/gen-calibration-fixtures.py ``` Each takes `--check`, which is the form the gate runs. Edit `problem-bank/`, never the files under `web/problems/` or `web/judges/`. `scripts/gen-problems.py --sync-study-plan` refuses to write while the plan and `problem-bank/` disagree and names what each side is missing, so port those first. ## What is deliberately outside the gate These exercise live server, external-service, or browser flows and are run on their own when the area they cover is touched: One command per line: two names on one line runs the first and passes the second as an argument it ignores. The entry requirements are: ```sh scripts/browser-check.sh # Playwright + Chromium; rust/dispatch also need LiveKit and Gemini credentials scripts/server-check.sh # cargo, node, curl; starts a server unless CODETRIAL_WEB_URL is set scripts/gemini-check.sh # GOOGLE_API_KEY(S) in the selected CodeTrial config scripts/parity-check.sh # the credentialed rust browser-check prerequisites scripts/report-parity-check.sh # the credentialed rust browser-check prerequisites scripts/visual-parity-check.sh # Playwright + Chromium; no service credentials scripts/recording-provision-check.sh # gcloud credentials and the CODETRIAL_RECORDING_* values checked at its start scripts/recording-integration.sh # --help lists per-phase credentials, tools and required --phase ``` Read the script's validation or usage block before a credentialed run; it is the source of truth for optional modes and the complete environment-variable list. CI additionally mutation-tests the diff: `plan-mutants` counts what the change is worth and `mutants` runs `cargo-mutants` over it. A surviving mutant means a line changed behavior with no test noticing, so the answer is a test, not a retry. ## The git hooks `make hooks` installs the fast half of the gate at commit time: `scripts/git-pre-commit.sh` runs `rustfmt`, ESLint, Prettier, `ruff` and `shellcheck` over a checkout of the index, so an unstaged edit neither fails a commit nor sneaks through one, plus `commentflow --check` and `shfmt -d` on staged shell. It does not build, test or check generated-artifact drift; that is what the gate is for. `scripts/git-commit-msg.sh` holds the message to the rules it prints with `--rules`, `scripts/git-prepare-commit-msg.sh` splices the template above a `commit -v` scissors line, and `scripts/git-pre-push.sh` replays the rules over commits a rebase or an amend rewrote after the fact. `make hooks` installs every `scripts/git-*.sh`, so adding one there installs itself. CI runs the same list over a pull request's own commits, so the rules bind someone who never installed the hooks as well. The hooks have their own suite. `scripts/test-git-hooks.sh` builds a scratch repository, installs the hooks into it and drives every case: the messages that must be rejected, the template splice above a `commit -v` scissors line, a staged file failing while the same edit unstaged does not, and a push carrying a commit that skipped the hook. It runs as the `git-hooks` gate, so editing a hook without running it is caught here. ## Writing a test No test code goes under `src/`; codetrial-conventions has that rule and the four things that bite when it is applied carelessly. Which of the two kinds you are writing follows from what the test needs to see: - Reaching a private or `pub(crate)` item makes it a unit test. It goes under `tests/unit/`, mirroring the path under `src/`, declared from the `src/` file with `#[cfg(test)] #[path = "..."] mod tests;`. - Reaching only the public API makes it an integration test, so it goes in `tests/*.rs` beside the suites already there. Browser tests go in `tests/browser/*.test.js` under `node --test`, Python tests in `tests/test_*.py` under `scripts/run-python-tests.py`, which runs a file's cases across a thread pool; a suite added there must keep every case owning its own sandbox, or it races. The golden fixtures in `tests/golden/` are the compatibility contract: a diff there is a claim that the observable output changed on purpose, and it belongs in the commit body. A test that passes without running anything is the failure mode this tree has already been bitten by, hence commits like "Prove an empty test run is not a pass". Assert on the count as well as the content when a suite discovers its own cases. The quieter version is a test that runs and cannot fail: a refusal asserted against a double that was scripted to refuse, or a hash compared against one the test computed with the function under test. The check that separates them is cheap and is the one to run before believing a new test: break the thing it names, watch it fail, put it back. ## Seeing it work in the app When a fix needs a live look, build the release binary and start the server yourself, then say it is ready to test. Do not hand over a command to run. ```sh make build ./target/release/codetrial web --web-addr 127.0.0.1:3100 ``` Port 3000 is the maintainer's own instance. Never bind, restart or kill it; pass `--web-addr` with another port and name that port in the report. Running from the checkout picks up `web/` edits with no environment variable set.