From c5e85dc8a6c20adac3349b0552b61ea469cab10b Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:39:33 +0000 Subject: [PATCH 1/5] docs: name the suites wt test doesn't run, and correct the dependency list --- CLAUDE.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 33b44564..d3fcd2d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,18 @@ wt test # run pytest in generator/ 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` covers `generator/` only. Two smaller suites live beside the code +they test and have CI jobs of their own — run them from their own directory: + +```bash +# proxy addon — pin mitmproxy the way CI does, to the version production runs +cd proxy && uv run --no-project --with pytest \ + --with "mitmproxy==$(yq -e '.inputs.mitmproxy_version.default' ../claude/action.yaml)" pytest +# install-tend OAuth wrapper +cd plugins/install-tend/skills/install-tend/scripts && uv run --no-project --with pytest pytest ``` ## Architecture @@ -106,8 +117,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 From 674b54dab2f1ac9310d89a252b11f2ade0835987 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:47:02 +0000 Subject: [PATCH 2/5] docs: keep the suite commands root-runnable and point at worker/ Subshells so both lines can be pasted from the repo root, and a pointer to worker/'s vitest suite so the count doesn't read as "nothing else to run". --- CLAUDE.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d3fcd2d2..6dc0225e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,17 +18,20 @@ uvx tend@latest check # verify branch protection, secrets, bot acce pre-commit run --all-files # lint: ruff, typos, actionlint, shellcheck, uv-lock ``` -`wt test` covers `generator/` only. Two smaller suites live beside the code +`wt test` covers `generator/` only. Two more pytest suites live beside the code they test and have CI jobs of their own — run them from their own directory: ```bash # proxy addon — pin mitmproxy the way CI does, to the version production runs -cd proxy && uv run --no-project --with pytest \ - --with "mitmproxy==$(yq -e '.inputs.mitmproxy_version.default' ../claude/action.yaml)" pytest +(cd proxy && uv run --no-project --with pytest \ + --with "mitmproxy==$(yq -e '.inputs.mitmproxy_version.default' ../claude/action.yaml)" pytest) # install-tend OAuth wrapper -cd plugins/install-tend/skills/install-tend/scripts && uv run --no-project --with pytest pytest +(cd plugins/install-tend/skills/install-tend/scripts && uv run --no-project --with pytest pytest) ``` +`worker/` is a third suite outside `wt test`, vitest rather than pytest, with +its own CI job — see [`worker/README.md`](worker/README.md). + ## Architecture Four pieces: From d8e888659e2e052676e9020cc77d96f4384fc4fa Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 06:51:31 +0000 Subject: [PATCH 3/5] docs: the suite commands run from the repo root, not their own directory --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6dc0225e..873fa014 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ pre-commit run --all-files # lint: ruff, typos, actionlint, shellcheck, ``` `wt test` covers `generator/` only. Two more pytest suites live beside the code -they test and have CI jobs of their own — run them from their own directory: +they test and have CI jobs of their own — run each from the repo root: ```bash # proxy addon — pin mitmproxy the way CI does, to the version production runs From cde97e1233724fd6a13ba096f2fbe0e46df8aa71 Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:47:23 +0000 Subject: [PATCH 4/5] test: wt test runs every suite, not just generator/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Point the alias at dev/test.sh, which runs generator/, proxy/ (pinned to the mitmproxy version production runs), the install-tend scripts, and worker/ — the same set ci.yaml covers. Arguments still forward to the pytest suites. CLAUDE.md drops the block of hand-run commands for the suites wt test missed, and shellcheck's file pattern picks up dev/. --- .config/wt.toml | 5 ++-- .pre-commit-config.yaml | 5 ++-- CLAUDE.md | 17 +++--------- dev/test.sh | 60 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 18 deletions(-) create mode 100755 dev/test.sh 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 f9f07421..2f1840e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,13 +30,14 @@ repos: # 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. + # 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|proxy/setup-sandbox\.sh)$ + files: ^(shared/steps/.*\.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 873fa014..15add2ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,26 +11,15 @@ 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, shellcheck, uv-lock ``` -`wt test` covers `generator/` only. Two more pytest suites live beside the code -they test and have CI jobs of their own — run each from the repo root: - -```bash -# proxy addon — pin mitmproxy the way CI does, to the version production runs -(cd proxy && uv run --no-project --with pytest \ - --with "mitmproxy==$(yq -e '.inputs.mitmproxy_version.default' ../claude/action.yaml)" pytest) -# install-tend OAuth wrapper -(cd plugins/install-tend/skills/install-tend/scripts && uv run --no-project --with pytest pytest) -``` - -`worker/` is a third suite outside `wt test`, vitest rather than pytest, with -its own CI job — see [`worker/README.md`](worker/README.md). +`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 diff --git a/dev/test.sh b/dev/test.sh new file mode 100755 index 00000000..bff60f6b --- /dev/null +++ b/dev/test.sh @@ -0,0 +1,60 @@ +#!/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=$? + # pytest exits 5 for "no tests collected" — what a -k aimed at one suite looks + # like from the others. Only a filtered run gets to treat that as a pass. + if [ "$rc" -eq 5 ] && [ ${#pytest_args[@]} -gt 0 ]; 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 only when the tree is missing, 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 ]; 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 From a4570515f600c691dac00505c679d1f82ae8ce6b Mon Sep 17 00:00:00 2001 From: tend-agent <270458913+tend-agent@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:56:48 +0000 Subject: [PATCH 5/5] test: tolerate pytest exit 4 outside generator/, reinstall worker/ on a stale lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wt test hit exit 4 ("file or directory not found") in the suites the path doesn't belong to; only 5 was tolerated, so a filtered run that passed in generator/ still failed. generator/ stays strict, so a bad flag — 4 in every suite — still fails. The worker/ install fired only on a missing node_modules, so a moved lockfile left tsc and vitest on a stale tree while CI reran npm ci. Compare mtimes against node_modules/.package-lock.json instead. --- dev/test.sh | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/dev/test.sh b/dev/test.sh index bff60f6b..5bd424fe 100755 --- a/dev/test.sh +++ b/dev/test.sh @@ -18,9 +18,13 @@ suite() { shift printf '\n==> %s: %s\n' "$dir" "$*" (cd "$dir" && "$@") || rc=$? - # pytest exits 5 for "no tests collected" — what a -k aimed at one suite looks - # like from the others. Only a filtered run gets to treat that as a pass. - if [ "$rc" -eq 5 ] && [ ${#pytest_args[@]} -gt 0 ]; then rc=0; fi + # 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 } @@ -43,11 +47,12 @@ suite plugins/install-tend/skills/install-tend/scripts \ uv run --no-project --with pytest pytest "${pytest_args[@]}" if [ ${#pytest_args[@]} -eq 0 ]; then - # Install only when the tree is missing, 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 ]; 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