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での検証結果は以下で公開している。

https://github.com/iwadjp/timewitness