AIが書いたregression test、本当にbugを検出している? Timewitnessでfix前後を確かめる
Claude CodeやCodexのようなAI agentにbug修正を頼むと、fixと一緒にregression testも書いてくれることが増えた。testがPASSすると、ひとまず安心する。
しかし、それだけでは弱い。
何が問題なのか
AIがfixとtestを同時に書いたとき、今のtreeでtestがPASSするのは当然といえば当然である。fixもtestも、同じ変更セットの中に含まれているからだ。
問題は、そのtestが「誰が実装してもとりあえずPASSしてしまう弱いtest」ではないという保証がどこにもないこと。assert.ok(true)に毛が生えた程度のtestでも、現在のtreeではPASSしてしまう。
本当に確かめたいのは、
修正前の実装 + 追加されたtest → FAIL
になっていたかどうかである。これが確認できて初めて、そのtestは「修正前後を実際に区別できるtest」だと言える。
欲しい証拠
図にすると次のようになる。
new regression test
|
+--> before world (old implementation) --> FAIL
|
+--> after world (current implementation) --> PASS
|
v
PROVEN
Before世界とAfter世界の両方で同じtestを実行し、FAIL→PASSの遷移が実際に起きたことを実行で確認する。これがTimewitnessの中心にある考え方である。
Timewitness
Timewitnessは、追加した回帰テストが「修正前にはFAILし、修正後にはPASSする」ことを、dirty working treeのまま実行して確かめるローカルCLIツール。
元のGit working treeをreset・checkout・restore・stash・commitすることは一切しない。armした時点のsource一式をisolated worldとして別途コピーし、そのコピーの中でbefore/after両方のtestを実行する。commitもstashも不要で、既存のdirty stateを壊さない。
使い方
node timewitness.cjs arm
# 普段どおりコードを修正し、regression testを追加する
node timewitness.cjs prove --test test/foo.test.js
armで現在のworking filesを保存し、修正とtest追加を終えたらproveで証明を実行する。対象はWindows / Git / Node.js 24以上 / node:test。
出力
TIMEWITNESS: PROVEN
Before: FAIL 2/2
After : PASS 2/2
判定は3種類。
- PROVEN — 同じtest・command・cwd・環境で、同じassertion失敗から成功への遷移が反復して一致した
- NOT_PROVEN — beforeもPASSしていた、testが違う、比較の証拠がない、等
- INCONCLUSIVE — skip/todoがある、timeout、環境差、実行の揺れ、等
実在bugで試した
合成デモだけでなく、自分の別プロジェクトで実際に過去に直したbugでも試した。
対象はFix latest Layer 1 run selectionというcommit。2つのrunのうち「新しいが未評価のrun」ではなく「古いが評価済みのrun」を優先してしまうロジックのバグで、修正前の実装に対して新しい回帰テスト3ファイルを3回反復で実行した結果は次のとおり。
Before: FAIL 3/3
After : PASS 3/3
TIMEWITNESS: PROVEN
同じ環境で、修正と無関係な合成のnegative control(assert.equal(1 + 1, 2))も実行した。
Before: PASS 2/2
After : PASS 2/2
TIMEWITNESS: NOT_PROVEN
2回の実行を通じてfalse PROVEN(誤ってPROVENと判定したケース)は0件だった。何でもPROVENを返すツールではなく、無関係な変更にはきちんとNOT_PROVENを返す、という点を実測で確認できたのは大きかった。
dirty treeをどう扱うか
Timewitnessが実際にしていないことを明示しておく。
- 元のGit working treeへのreset / checkout / restore / stashはしない
- commitもしない
- 元のfileへの書き込みもしない
before/afterはそれぞれ独立したisolated worldとして、arm時点のcopyから実行する。作業中のdirty stateをそのまま保ったまま検証できる。
PROVENが意味しないこと
PROVENは強い言葉に見えるが、証明している範囲は限定的である。
- fix全体が正しいことの証明ではない
- bugが完全に直ったことの証明ではない
- test coverageが十分であることの証明ではない
- scope外のhidden dependencyまでは証明しない
PROVENが示しているのは、「選んだtestが、保存したbefore worldと現在のafter worldを、実行によって識別した」という限定的な実行証拠にすぎない。
v0.1の制約
- 対象はWindows / Git / Node.js 24以上 /
node:testのみ - npm workspaces / pnpm / yarn workspacesの一般対応はない
- 依存の再現は既存node_modulesのbyte一致確認で、lockfileからのinstallは行わない
- 環境やdependencyに差があれば、黙って通さずINCONCLUSIVEになる
現時点ではまだ小さなNode製CLIプロジェクト向けの道具である。
作って分かったこと
作りながら気づいたのは、自分が本当に確かめたいのは「testが通ること」ではなく「testが修正を区別できること」だという点だった。
AI agentがコードとtestを大量に生成できるようになるほど、この区別の価値は上がっていくかもしれない。ただしこれは今回の限定的な検証から得た感触であり、断定できることではない。
GitHub
ソースコード・test・実在bugでの検証結果は以下で公開している。