diff --git a/.config/wt.toml b/.config/wt.toml index a8fe743b..9465379b 100644 --- a/.config/wt.toml +++ b/.config/wt.toml @@ -32,6 +32,7 @@ url = "http://localhost:{{ branch | hash_port }}" [[pre-remove]] server = "lsof -ti :{{ branch | hash_port }} -sTCP:LISTEN | xargs kill 2>/dev/null || true" -# The Python package lives under generator/; this alias runs pytest from the right cwd. +# Tests live in four places (generator/, proxy/, the install-tend scripts, +# worker/); dev/test.sh runs the lot, mirroring ci.yaml's test jobs. [aliases] -test = "cd generator && uv run pytest {{ args }}" +test = "dev/test.sh {{ args }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d32bfd81..a797b8c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,15 +29,16 @@ repos: hooks: - id: actionlint # The composite actions' shared step bodies live in standalone scripts - # (shared/steps/, proxy/) rather than inline `run:` blocks, so - # actionlint's built-in shellcheck doesn't see them. Run shellcheck directly - # to keep that coverage. -S warning matches actionlint's default severity. + # (shared/steps/, proxy/, generator/src/tend/templates/) rather than inline + # `run:` blocks, so actionlint's built-in shellcheck doesn't see them. Run + # shellcheck directly to keep that coverage — and over dev/, which actionlint + # never sees at all. -S warning matches actionlint's default severity. - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.11.0.1 hooks: - id: shellcheck args: ["-S", "warning"] - files: ^(shared/steps/.*\.sh|generator/src/tend/templates/.*\.sh|proxy/setup-sandbox\.sh)$ + files: ^(shared/steps/.*\.sh|generator/src/tend/templates/.*\.sh|proxy/setup-sandbox\.sh|dev/.*\.sh)$ - repo: local hooks: # The Claude Code slash-command preprocessor treats a backticked token diff --git a/CLAUDE.md b/CLAUDE.md index 0a053d4e..e64fdc04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,13 +11,16 @@ completely — old formats should fail with a clear error, not silently parse. ## Commands ```bash -wt test # run pytest in generator/ +wt test # every suite (generator/, proxy/, install-tend scripts, worker/) uvx tend@latest init # regenerate workflows from .config/tend.yaml uvx tend@latest init --dry-run # preview without writing uvx tend@latest check # verify branch protection, secrets, bot access -pre-commit run --all-files # lint: ruff, typos, actionlint, uv-lock +pre-commit run --all-files # lint: ruff, typos, actionlint, shellcheck, uv-lock ``` +`wt test` runs [`dev/test.sh`](dev/test.sh), which mirrors the test jobs in +`ci.yaml`; arguments go to the pytest suites (`wt test -k render`). + ## Architecture Four pieces: @@ -106,8 +109,8 @@ from `.config/tend.yaml`. Edit the generator or config, not the workflow files directly. The generator is a Python package under `generator/` — uses the uv_build -backend, requires Python 3.11+. Runtime dependencies: click, ruamel.yaml. -Dev dependencies: pytest, pytest-regtest. +backend, requires Python 3.11+. Runtime dependencies: click, jinja2, +ruamel.yaml. Dev dependencies: pytest, pytest-regtest. Consuming repos regenerate their `tend-*.yaml` workflows nightly (tend itself included — it dogfoods its own workflows). Changes to the generator do not diff --git a/dev/test.sh b/dev/test.sh new file mode 100755 index 00000000..5bd424fe --- /dev/null +++ b/dev/test.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Every test suite in the repo, mirroring the test jobs in +# .github/workflows/ci.yaml. `wt test` runs this (see .config/wt.toml), so one +# command covers generator/, proxy/, the install-tend scripts, and worker/. +# +# Arguments are forwarded to the pytest suites (`wt test -k render`); a filtered +# run skips worker/, whose vitest CLI takes different flags. Every suite runs +# even if an earlier one fails, and the failures are listed at the end. +set -o pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 1 + +failed=() + +# suite ... +suite() { + local dir=$1 rc=0 + shift + printf '\n==> %s: %s\n' "$dir" "$*" + (cd "$dir" && "$@") || rc=$? + # A filtered run gets slack: pytest exits 5 for "no tests collected" and 4 for + # a path that only exists in another suite. generator/ stays strict, so a bad + # flag — 4 everywhere — still fails the run. + if [ ${#pytest_args[@]} -gt 0 ] && + { [ "$rc" -eq 5 ] || { [ "$rc" -eq 4 ] && [ "$dir" != generator ]; }; }; then + rc=0 + fi + if [ "$rc" -ne 0 ]; then failed+=("$dir"); fi +} + +pytest_args=("$@") + +suite generator uv run pytest "${pytest_args[@]}" + +# The proxy addon isn't part of the generator package, and it imports +# mitmproxy.test, so it runs standalone against the version production runs +# rather than whatever mitmproxy is latest. +if mitmproxy_version=$(yq -e '.inputs.mitmproxy_version.default' claude/action.yaml); then + suite proxy uv run --no-project --with pytest \ + --with "mitmproxy==$mitmproxy_version" pytest "${pytest_args[@]}" +else + echo "==> proxy: cannot read mitmproxy_version from claude/action.yaml (yq installed?)" >&2 + failed+=(proxy) +fi + +suite plugins/install-tend/skills/install-tend/scripts \ + uv run --no-project --with pytest pytest "${pytest_args[@]}" + +if [ ${#pytest_args[@]} -eq 0 ]; then + # Install when the tree is missing or older than the lockfile (`npm ci` writes + # node_modules/.package-lock.json), and with `npm ci` rather than `npm install` + # — an older local npm reruns resolution and rewrites package-lock.json, + # leaving churn in the diff that has nothing to do with the change under test. + if [ ! -d worker/node_modules ] || + [ worker/package-lock.json -nt worker/node_modules/.package-lock.json ]; then + suite worker npm ci --prefer-offline --no-audit --no-fund + fi + suite worker npm run typecheck + suite worker npm test +fi + +if [ ${#failed[@]} -gt 0 ]; then + printf '\nfailed: %s\n' "${failed[*]}" >&2 + exit 1 +fi