AIハーネス(Claude Code・Codex・Cursor・Grok)のコマンド実行フックに入り、テスト実行を記録し、段階1以降は変更に関わる分だけに絞る。設計はdocs/design.md、アダプタの契約は docs/adapter-contract.md。
testgate hook <harness> は、アダプタが扱えるテストコマンド(複合でない、time/timeout の包みが無い)を node <入口> run --cwd <cwd> -- <元のコマンド> に包み直して返す。席はその包んだコマンドを実行し、関所(run)がアダプタで変更に関わる分だけに絞って流し、合計行を落とし、長い出力を刈って返す。包んだ分は RTK の書き換えを通らない(関所の出力はもう短い)。
- 包めない時(テストでない、複合、アダプタが無い、Windows の PowerShell 以外のハーネス)は段階0と同じ: 記録して
rtk hook <harness>へ中継する。 TESTGATE_PHASE=0で段階0(記録だけ)へ戻せる。currentを v0.1.11 へ付け替えても戻る。- 子プロセスの起動は shell を通さない。Windows では実行ファイルを
PATH・PATHEXTで探し、.exeは直接起こす。.cmd/.bat(npm・npx など)だけcmd.exe /d /s /cに引数を引用して渡す(--filterの|や空白を含む道が壊れない)。 - 前置きの環境変数(
TESTGATE_FULL=1 npm test)は包んだ後も先頭に残るので効く。PowerShell は& 'node' ...の形で包む(Claude の tool_name=PowerShell の時だけ)。 - 全部通った記録が無く、変更も無い(commit 直後など)時、または変更が未追跡の資料などコードでない物だけでアダプタが「関わるテストが無い」と言う時は、一度全部流して「最後に全部通った commit」の基準を作る。失敗したら基準は作らず、通るまで同じ条件で全部流す。この全部流しには時間上限を掛けない。上限(
TESTGATE_FULL_LIMIT_SEC、既定 180 秒)が付くのはTESTGATE_FULL=1とアダプタが選んだ full だけ。アダプタが判断できなかった passthrough にも掛けない。 run側の select の待ち時間はTESTGATE_SELECT_TIMEOUT_MS(既定 30 秒。段階0の影の判断は 5 秒)。- 記録: hook が
phase:1, event:'rewrite'(元のコマンドと包んだコマンド)、runがphase:1, event:'run'(mode、理由、所要 ms、件数、終了コード)を1行ずつ。
ミューテーションテスト(Stryker)で「消してよい試験の候補」を出す。消すのは担当。候補には「殺す変異」と「同じ変異を殺す残す試験の名前」を付ける。
testgate prune [--cwd DIR] [--sample N] [--seed S] [--out FILE] [--tests GLOB]... [--exclude GLOB]... [--mutate GLOB]... [--jobs N] [--no-preflight] [--runner node-test|jest|vitest]
- 対象は JavaScript / TypeScript(node --test は専用の走者で試験1本単位、Jest・Vitest は Stryker 標準の走者。Jest・Vitest は未検証)。Python・Rust・Go・.NET は後の版。swift・gradle は対象外。
package.jsonのtestscript から、node の引数(--importなど)・試験ファイル・build の要否を読む。npm run Xは scripts の X へ展開し(3段まで)、tsx --testは node --test に--import tsxを付けた物として読む。vitest と node --test が混ざる script は node --test の段を使う。ファイル指定無しのnode --testは流さない。package.json や test script が無い時は--runner node-test --tests GLOBで指定する(v0.3.4 から)。tsconfig とbuildscript があれば src の.tsを変異して build し、無ければsrclibbinの.js/.mjs/.cjsを変異する。試験が--experimental-strip-typesで src の.tsを直接読む形(yui)は build せず、src の.tsを変異して注入する(v0.3.5 から)。--sample N(既定 200、0 で全部): 変異を seed 付きの乱択で N 件に絞る。1変異ごとに全試験を流すので、時間は「試験1回の時間 × N」。- 候補の決め方: 「自分しか殺さない変異」を持つ試験は必ず残し、残りは未検出の変異を一番多く殺す順に残す(貪欲な最小被覆)。残らない試験が「冗長」、変異を1つも殺さなかった試験が「検出0」。skip の試験は出さない。検出0 は標本に入らなかった所を守っている事があるので、冗長より弱い根拠。
- 道具(Stryker 一式 約120MB)は本体に入れず、初回に
~/.local/share/testgate/prune-tools/($TESTGATE_PRUNE_TOOLSか$XDG_DATA_HOMEで変更)へ npm で導入する。記録は~/.local/state/testgate/prune/<名前>/<日時>/(candidates.txt / candidates.json / mutation.json / stryker.log)。log.jsonlにもevent: "prune"を1行残す。 - 下見(v0.3.3 から): Stryker は初回の試験が1本でも落ちると止まるので、先に sandbox と同じ形の写し(.git 無し、node_modules は link)で試験を一度流し、落ちた試験ファイルを除外に足してから流す(
.gitや外の状態を読む試験が機械的に外れる。Aiterm と Throughline の product-ci-plan.test など)。除外した物は stderr と candidates.json のpreflightExcludedに出る。--no-preflightで飛ばせる。 - 候補の読み方(v0.3.2 から、エレグの注文): 「冗長」「残す」はこの標本での検出の重なり・被覆集合の事で、入力条件・分岐・断言まで代替できるかは担当が決める。そのために候補の変異ごとに、変異ID・file:始-終の位置・演算子・完全な replacement(短縮 label は識別子にしない)と、その試験が落ちた断言/例外の1行目(走者が
failures.jsonlに残す)を添える。試験の大半(max(10, 20%) 本より多く)が同時に落ちる変異は「広域」(共通の起動処理の失敗)として冗長の根拠にせず、それしか検出しない試験は「広域のみ」と出す。記録の candidates.json には commit・platform・標本の位置・除外した試験ファイル・変異したファイル一覧も入れる。生存した変異は未到達の OS 分岐や等価変異を含み得るので「試験の穴」と確定しない。 - 合算(v0.3.1 から): 同じ名前の過去の記録(
~/.local/state/testgate/prune/<名前>/*/mutation.json)が2回分以上あれば、試験は id、変異は元コードの hash・始終の位置・演算子・完全な replacement で束ねて killedBy を和集合にし、合算後の候補をcandidates-merged.txt / .jsonにも書く。1回の標本では「検出0」が標本に入らなかった所を守る試験で膨らむので、月次で seed を変えて積み上げ、合算の候補を担当へ出す。 - 変異は環境変数で届くが、試験が子プロセスへ自前の env を渡す時は届かない。そこで sandbox の製品コードの先頭に「env が無ければリポジトリ直下の
.active-mutantを読む」ヘッダを注入し、試験の起動時にその file へ変異番号を書く。この file は走者1つで使うので、Stryker の並列は 1、並列は試験ファイル単位(--jobs、既定は node --test の既定)で取る。 - 試験が終わっても子プロセス(hook など)が居残って node --test が終わらない事がある。走者は初回に分かった試験が全部報告されたら1秒待って process group を止め、結果を返す。
testgate hook <harness> は、stdin の hook 入力をそのまま rtk hook <harness> へ渡し、出力と終了コードを中継する。違いは、テスト実行だったら ~/.local/share/testgate/log.jsonl に一行記録すること。RTK が無ければ何も書き換えずに通す。
echo '{"tool_name":"Bash","tool_input":{"command":"pytest -q"}}' | node bin/testgate.mjs hook claude
node bin/testgate.mjs inspect "cd app && npm test"
記録の列: ts, host, bot, harness, tool, id(道具呼び出しの id), session(会話の id), transcript(会話記録の path。Claude のみ), cwd, runner, lang, compound, command(前置きの環境変数の値は *** に消す), phase, shadow(影の判断)。置き場は ~/.local/state/testgate/log.jsonl(TESTGATE_LOG か XDG_STATE_HOME で変更可)。
所要秒と成否は段階0では hook を足さず、会話記録(Claude の transcript、Codex の rollout)を id で突き合わせて後から出す(トロニーの判断、2026-10-13)。testgate post は実装してあるが、段階0では登録しない。
npm test
- 版ごとのフォルダに置き、
currentの付け替えで切り替える。差し替え中の半端な状態を動いている席が踏まないため。- BellTeam コンテナ:
/srv/bellteam/shared/tools/testgate/<tag>/と/srv/bellteam/shared/tools/testgate/current→ 登録するコマンドはnode /srv/bellteam/shared/tools/testgate/current/bin/testgate.mjs hook <harness> - 端末(rabbit・Mac・fox):
~/.local/lib/testgate/<tag>/とcurrent(Windows は%USERPROFILE%\.local\lib\testgate\)。ドリリーの RTK 配布手順に同乗。
- BellTeam コンテナ:
- 依存なしなので
npm installも setup.sh も要らない。git clone --branch <tag> --depth 1か tarball の展開だけ。 - 戻し方:
currentを前の版へ付け替える。登録の文字列は変えなくてよい。
- Claude の設定から
rtk hook claudeの文字列が消えると、RTK が1日1回「No hook installed — runrtk init -g」と出す。RTK_SUPPRESS_HOOK_WARNING=1で止まる(コンテナはトロニーが入れる。端末はドリリーの配布に含める)。 - 関所の項目だけがある所へ
rtk init -g --hook-only --auto-patchを流すと、rtk hook claudeが2つ目の項目として足され、二重に書き換えが走る。関所ごしの登録ではrtk initを呼ばない。 - Grok だけ、空の入力・JSON でない入力・極端に長いコマンドの3通りで終了コードが BellTeam の grok-hook.mjs(1)と関所(0)で違う。どちらも元のコマンドは通るので直さない。
- 正常系: stdout と終了コードは
rtk hook <harness>と完全に同じ。deny/ask の JSON もそのまま中継する(トロニーの確認: 4 CLI × 26 通りで stdout 一致、2026-10-13)。 - hook として呼ばれた時(
testgate hook …)は、知らない名前や内部の失敗でも exit 2 を返さない。1 か 0 で、元のコマンドを通す。 - rtk が無い(ENOENT): 何も書き換えず exit 0。stderr に一行。
- rtk hook の timeout やその他の spawn エラー: 何も書き換えず exit 1。stderr に理由。exit 2 は使わない(Claude では 2 がブロックになる)。
- 入力先頭の BOM は除去して判定する。
- ログの書き込みに失敗しても hook は通す(stderr に出すだけ)。
adapters/<id>/adapter.mjs が置いてあれば、段階0でも「段階1ならどう選んだか」を記録に残す(shadow 欄: adapter, mode, reason, argv, changedCount, base)。コマンドは変えない。
testgate run [--cwd DIR] -- <command...>: アダプタで選んだコマンドを関所が実行し、生出力から件数を読み、合計行を落として返す。全部流しはTESTGATE_FULL=1の時だけで、TESTGATE_FULL_LIMIT_SEC(既定 180)を超えると止める。通った全部流しは「最後に全部通った commit」として記録し、次回の差分の基準になる。testgate decide "<command>": 実行せずに判断だけ表示する(開発用)。testgate post <harness>: 実行後の hook。同じ tool_use_id の事前記録と突き合わせて所要秒と大まかな件数を足す。