CLAUDE.md@tests ยท diff
git:20261006.1ff74f0 to git:20261007.aaa7079
29 added, 84 removed. Audit A to A.
- # tests/ - dev-time regression suites for the plugin scripts
+ # tests - dev-time regression suites for the plugin scripts
- Node's native `node:test` runner over `.ts` files (type stripping), run by `.github/workflows/ci.yml`:
- Linux only on push/PR, the macOS + Windows legs only on a manual `workflow_dispatch`. A test that
- passes locally on one OS is not proven on the other two until that dispatch runs. How to run and how
- a file is shaped live in `.claude/rules/tests-running.md` and `tests-structure.md`.
+ Owns every automated check in the repository: the per-plugin suites, the `release.sh` suite and the repo-wide static sweeps. It sits outside every plugin and never ships; supercc and superbiz have no suite.
- ## Layout
+ ## Terms
- - `tests/<plugin>/` (and `tests/github/` for `.github/scripts/`) - one file per tested script,
- named after the script's basename. Six files test no script: `tests/viber/panel-run-state.unit.test.ts`,
- `panel-run-events.unit.test.ts` (the pure functions of `viber/hooks/panel/`) and
- `write-report-name.unit.test.ts` (those of `viber/hooks/write/`),
- `tests/superui/import-safety.unit.test.ts`
- (`check_contrast.ts` imports without firing its guarded `main()`), `tests/viber/profiler.test.ts`
- (runs the fenced git command lifted verbatim from `viber/agents/profiler.md`) and
- `tests/viber/help.unit.test.ts` (checks `skills/setup/assets/help.html` against `plugin.json`, the
- `SKILL.md` frontmatter and the `viber.yml` template).
- - `tests/harness/` - the shared helpers every script test uses; `tests/harness.test.ts` asserts
- `runScript`, `withTempDir`, `withGitRepo`, `withStub`, `forEachShell("posix")`,
- `denyRead`/`restoreRead` and `writePng`; `slash`, `canSymlinkDir`, `coreUtilsPath` and `test` (which every file runs through) have no
- test there.
- - `tests/portability.unit.test.ts`, `tests/orphan-tags.unit.test.ts` - static sweeps over the whole repo.
- Each rule is a pure function with a self-check test proving it fires on a synthetic bad sample;
- a new rule gets its self-check too, or its green run proves nothing.
+ - **Sweep**: a static, repo-wide test (`portability`, `orphan-tags`) that enumerates files from the git index and checks each against pure-function rules, instead of running a script.
+ - **SUT**: the shipped script a test file exercises, resolved from the test file with `path.resolve(import.meta.dirname, "../../<plugin>/...")`.
- ## Two tiers: local unit, CI integration
+ ## Relationships
- - **Unit** - `*.unit.test.ts`: in-process only, no `withTempDir`/`withGitRepo`/`withStub`, no
- spawned script (one read-only `git ls-files` of this repo is the limit). The whole tier runs
- locally in seconds and is the only suite a developer machine runs, on every change:
- `node --test --test-reporter=dot "tests/**/*.unit.test.ts"`. It must stay under one minute.
- - **Integration** - every other `*.test.ts`: a real subprocess of a shipped script, a temp-dir or
- git fixture, a stub. CI only: `ci.yml` runs `"tests/**/*.test.ts"`, which matches both tiers. A
- case spawning a script costs 1-2 s on Windows, so this tier never joins the local run; a new
- test needing a fixture or a subprocess is an integration test by definition.
- - Debugging one red CI file locally stays possible by naming it:
- `node --test tests/viber/config.test.ts`.
+ - Child nodes: `tests/harness/CLAUDE.md` (shared helpers), `tests/superui/CLAUDE.md` (superui's suite), `tests/viber/CLAUDE.md` (viber's suite).
+ - Which suite a change reaches:
+ - a viber or superui script, hook or `.ts` module -> its file under `tests/viber/` or `tests/superui/`;
+ - `.github/scripts/release.sh` -> `tests/github/release.test.ts`;
+ - any tracked `*.sh` / `*.ts` outside `tests/` and `docs/`, and every `!` preload in a `SKILL.md` -> `tests/portability.unit.test.ts` (shebang, no CRLF, `100755` exec bit when invoked without an interpreter word, quoted glob arguments in preloads, no bash-only syntax in `#!/bin/sh`, no unguarded GNU-only tools);
+ - any tracked file outside `docs/` -> `tests/orphan-tags.unit.test.ts` (a line that is only `</name>` with no `<name` opener in the same file);
+ - anything under `tests/harness/` -> `tests/harness.test.ts`, then every suite, since all of them import it.
- ## Harness contract
+ ## Contracts
- - `test(name, [options], fn)` (`harness/test.ts`) replaces node:test's: a file's cases run inside
- one suite named after the file, up to 4 at once (`P2P2_TEST_CONCURRENCY`), each still reported,
- skipped and matched by `--test-name-pattern` under its own name. Every helper below that
- spawns or wraps a fixture is async and awaited.
- - `runScript(script, args, opts)` (`harness/run.ts`) runs a shipped script as a real subprocess
- (a `.ts`/`.js` one through `process.execPath`) and resolves when it exits. The child env is sanitised to a fixed base list (PATH, HOME, TEMP, ...): every
- variable a script reads must be passed through `opts.env`. On win32 the script's shebang is read
- and its interpreter resolved from PATH; an argument holding `\n`/`\r` travels through the
- environment (`P2P2_ARGV<n>`) because no CreateProcess command line survives it - only for a
- bash/sh script, so such a case skips for any other interpreter. Default timeout 60 s on purpose
- (CI over-subscribes cores with `--test-concurrency=12`).
- - `forEachShell("bash" | "posix", fn)` (`harness/shells.ts`) runs a case under the first shell
- present locally, and under every shell present only when `CI` is `true`/`1` (`FULL_SHELL_MATRIX`;
- `ci.yml` sets it, `CI=true node --test ...` runs it locally): each distinct bash major (macOS 3.2
- vs 5.x) for a `#!/usr/bin/env bash` script; `/bin/sh`, `dash`, `busybox sh`, `bash --posix` for a
- `#!/bin/sh` one. An absent or unrun shell is returned as a `ShellSkip`, never a failure. A
- bash-only script is never run under `"posix"`.
- - `withTempDir` / `withGitRepo` (`harness/tmp.ts`) - every filesystem or git fixture lives in a temp
- dir removed on return or throw. `withGitRepo` copies a per-process template made by one real
- `git init` (no spawn per repo) and pins HOME/USERPROFILE, its own `GIT_CONFIG_GLOBAL`,
- `GIT_CONFIG_NOSYSTEM=1` and fixed author/committer/date: pass its `env` to every call so nothing
- reads the developer's real `~/.gitconfig`. Never run git against this repo: the commit and
- release scripts under test really commit, tag and push.
- - `withStub(name, body, fn)` + `opts.stubDirs` (`harness/stub.ts`) puts a fake `gh`/`npm`/... first
- on PATH; a test never lets a script reach a real network-facing tool. `stubDirs` only prepends,
- so asserting a tool is ABSENT needs `opts.env.PATH = coreUtilsPath()` first (grep's dir plus
- bash's) - it still carries `git` on POSIX, so a git-branching script must hold either way.
- - `writePng` (`harness/png.ts`) synthesises RGBA PNG fixtures; no image library.
+ - Every test file imports `test` from `harness/test.ts`, never from `node:test`: cases register synchronously at load (no top-level `await` before a `test()` call) and share no mutable state, because a file's cases run concurrently.
+ - Tier by filename: `*.unit.test.ts` runs no shipped script (at most a read-only git query); a test that spawns a SUT is integration and takes plain `*.test.ts`.
+ - Every sweep rule is a pure function with a self-check that proves it fires on a synthetic bad sample; a new rule comes with its self-check, so a green run is never vacuous.
+ - No test reaches the network or touches this repo's working tree: a `gh` is always a `withStub`, every push goes to a local `--bare` remote, every write lands in a temp dir.
+ - A test whose tool is missing on `PATH` skips, never fails (`release.test.ts` skips wholesale without `jq` or `bash`).
- ## Cross-platform traps
+ ## Commands
- - Compare a path a script printed through `slash()` (`harness/paths.ts`) on both sides: on Windows
- its stdout mixes native `\` with the script's own `/`.
- - Make a file unreadable with `denyRead()` / `restoreRead()` (`harness/perms.ts`), never `chmod`:
- mode bits gate nothing on Windows (it uses an `icacls /deny` ACE) and root ignores them on POSIX.
- Gate the case with `{ skip: canDenyRead() ? false : "<reason>" }` and restore in `finally`.
- - Gate any directory-symlink case on `canSymlinkDir()` (`harness/symlinks.ts`): a plain Windows
- account gets EPERM.
- - Capability is always probed by doing the real thing, never by reading `process.platform`; a
- platform check is only for a case whose meaning differs by OS (a backslash is a legal filename
- character off Windows), and it skips with a reason string.
- - A test depending on an optional tool (`jq` for `release.sh`) registers one skipped `test()` naming
- the reason instead of failing.
- - The static sweeps and the exec-bit checks read the git index (`git ls-files -s`), not the working
- tree: a new script is invisible to them, and its `100755` mode unchecked, until it is staged.
- - `portability.unit.test.ts`'s `bashismViolations` sweep is text matching, not syntax-aware: an awk
- `function name(...)` definition, or any bare `((` not preceded by `$` (even from nested parens
- in an `if`), reads as bash-only under a `#!/bin/sh` shebang. A POSIX-sh script embedding an awk
- block must avoid both.
- - `tests/viber/config.test.ts` reads `config.sh`'s dotted stdout by trailing slice
- (`printed.slice(-5, -1)`, `stdout.split("\n").slice(-2)`), not by key name: a group appended
- after `branching` shifts every such offset, and its own assertions must move with it.
+ - One sweep or the harness proof: `node --test tests/portability.unit.test.ts`, `node --test tests/harness.test.ts`.
+ - One case: add `--test-name-pattern "<case name>"`; each case keeps its own name inside its file's suite.
+ - Cases per file at once: `P2P2_TEST_CONCURRENCY` (default 4), multiplied by `--test-concurrency` (files at once).
+
+ ## Traps
+
+ - Sweeps read the git index: a new, untracked script or `SKILL.md` is not checked until it is `git add`ed.
+ - CI on push and pull request runs Linux only; macOS and Windows run only on a manual `workflow_dispatch` of the CI workflow, so a cross-OS regression stays green until someone dispatches it.
+ - `docs/` is outside both sweeps: the archived superdev sources there are never checked.