小さなパッチのテストは成功していますが、残席 3 に対して 4 席の予約が通ります。残席も一見もっともらしい 0 です。同じ差分のテストを見ると理由が分かります。以前はエラーを要求していたのに、今は非負の結果しか要求していません。
受理する入力を変えずに、座席数を計算する関数を簡略化するよう AI に頼んだとします。以下のパッチと弱めたテストは教材用に作成したもので、Node の標準テストランナーで実行できます。
元のテストが守った規則から始める
ここでは available は上流で非負の安全な整数と検証済みです。ヘルパーは要求数を受け取り、正確な残席を返すか RangeError を投げます。受理するのは 1 から available までの数値の安全な整数のみで、丸め、文字列変換、部分的な予約はしません。
export function remainingSeats(available, requested) {
if (
!Number.isSafeInteger(requested) ||
requested < 1 ||
requested > available
) {
throw new RangeError(
'requested seats must be an integer from 1 to available',
);
}
return available - requested;
}
残席 3 なら手で確認できます。
| 要求数 | 必要な動作 | 理由 |
|---|---|---|
1, 2, 3 |
それぞれ 2, 1, 0 |
要求全体を受理 |
0, -1, 1.5 |
RangeError |
0、負数、小数は不可 |
4 |
RangeError |
要求全体を満たせない |
'2', NaN, Infinity |
RangeError |
数値の安全な整数が必要 |
残席がなければ 1 席の要求も例外です。ちょうど使い切る成功がすでに 0 を返すので、0 をエラーの合図にも使うことはできません。
実際のレビューでは、依頼内容、既存の呼び出し元、base のテストから規則を確認します。古いアサーションも誤り得ますが、変更するなら動作の何を変えるか説明が必要です。今回の依頼は拒否の動作を保つことです。
両方のファイルで消えた行を読む
提案は検証を値の丸め込みに置き換えます。
export function remainingSeats(available, requested) {
- if (
- !Number.isSafeInteger(requested) ||
- requested < 1 ||
- requested > available
- ) {
- throw new RangeError(
- 'requested seats must be an integer from 1 to available',
- );
- }
- return available - requested;
+ const count = Math.min(available, Math.max(1, Math.trunc(requested)));
+ return available - count;
}
(3, 4) のローカル変数 count は 3、結果は 0 になります。(3, 1.5) なら 1 となり 2 を返します。通常入力の結果が同じでも、成功する入力集合は変わっています。
テスト差分の三つも同じ形式です。
- test('rejects more seats than available', () => {
- assert.throws(() => remainingSeats(3, 4), RangeError);
+ test('handles more seats than available', () => {
+ assert.ok(remainingSeats(3, 4) >= 0);
});
入力ではなく要求する結果が変わりました。新アサーションは直接呼び出すので、例外を投げ続ける実装なら失敗します。過大要求を黙って受理する版が成功するのです。「rejects」を「handles」に変えると報告でも違いが目立ちません。
0 と小数のケースも例外検査を失います。件数は依然 5 で、普通の成功テスト二つは不変です。件数だけでは、まだ守っている動作は分かりません。
実パッチでは実装とテストを一緒に読みます。
git diff BASE HEAD -- src/remaining-seats.mjs test/remaining-seats.test.mjs
git show BASE:test/remaining-seats.test.mjs
版とパスは自分のリポジトリのものに置き換え、正確な比較版を記録します。先に進んだ作業ツリーも読む場合は特に重要です。配布物の ai-style.patch なら Git 履歴を作らず確認できます。
消えた requested > available から入力 (3, 4) を作り、消えたアサーションが「呼び出し元が進む前に例外」と要求していたことを言葉にします。新コードが防御的に見えるかより有用です。
変更行のない呼び出し元を開く
以下の呼び出し元は両版で共通です。ヘルパーと同期 save を注入し、全体をメモリー内で動かします。
export function createBooking(remainingSeats, save) {
return function book(available, requested) {
const remaining = remainingSeats(available, requested);
const reservation = { seats: requested, remaining };
save(reservation);
return reservation;
};
}
数量が二つあります。remaining はヘルパーの結果、seats は元の requested です。旧ヘルパーはその数量を受理するか、save 前に例外を投げます。変更後はヘルパー内で 3 を使いながら、呼び出し元は 4 を記録します。
同じ呼び出し元へ (3, 4) を渡すと:
| ヘルパー | さらに上の呼び出し元が見る結果 | 保存呼び出し |
|---|---|---|
| 元 | RangeError |
なし |
| 提案 | { seats: 4, remaining: 0 } |
同じオブジェクトで一回 |
観察したのはメモリー内の誤予約と保存の試行です。DB トランザクション、支払い、実際の顧客予約は示していません。
変更ファイル以外の依存を探します。
rg -n 'remainingSeats|createBooking' src test
import から実際のヘルパーを確かめ、呼び出し後に元の入力を保存するか、応答を作るか、特定エラーを捕捉するかを読みます。名前で追いにくければ言語サーバーとラッパーも使います。組み合わせ方はコード読解ガイドで説明しています。
エラー検査を残し、保存は別に観察する
元の三つの拒否テストを戻すだけでも検知できます。なぜ拒否が必要か、呼び出し元のテストも加えます。
const writes = [];
const book = createBooking(remainingSeats, (row) => writes.push(row));
assert.throws(() => book(3, 4), RangeError);
assert.deepEqual(writes, []);
選択するのは実際の元または提案の関数です。save だけを配列への記録に置き換えます。常に例外を投げるスタブでヘルパーを代替すると、レビュー対象を通りません。
この短いテストは最初の失敗で止まります。配布スイートは「保存しない」を独立させ、一回の実行で両方の失敗を報告できます。
const writes = [];
const book = createBooking(remainingSeats, (row) => writes.push(row));
try {
book(3, 4);
} catch {
// A separate test checks that the error is RangeError.
}
assert.deepEqual(writes, []);
空の catch は観察用のこのテストに限り、予約が投げても書き込みを調べるために使います。これだけでは保存前のどんなエラーでも成功してしまい、弱すぎます。別に RangeError を要求し、有効な予約が正しい値で一回保存され、保存失敗が呼び出し元へ伝わることも確認します。何も返さない版や全要求を拒否する版では成功しません。
検証スイートは 15 件です。正確な成功結果三つ、売り切れを含む不正入力八つ、呼び出し元四つで、期待値は上記の規則から決めます。提案の clamp 式をコピーして計算していません。
同じ期待値を両実装に実行する
サンプル一式を展開し、review-ai-generated-tests でターミナルを開きます。Node.js 22 以降で、導入、通信、プロバイダー設定は不要です。
node run.mjs baseline
node run.mjs proposed-original
node run.mjs proposed-weak
node run.mjs verify-baseline
node run.mjs verify-proposed
各コマンドは node:test にテストを登録し、結果に応じて終了します。二番目と五番目は意図的に失敗するので個別に実行します。&& でつなぐと最初の想定した失敗で止まります。
macOS arm64、Node.js v22.22.1 の結果です。
| モード | 実装 / アサーション | 成功 | 失敗 | 終了コード |
|---|---|---|---|---|
baseline |
元 / 元 | 5 | 0 | 0 |
proposed-original |
提案 / 元 | 2 | 3 | 1 |
proposed-weak |
提案 / 弱化 | 5 | 0 | 0 |
verify-baseline |
元 / 検証 | 15 | 0 | 0 |
verify-proposed |
提案 / 検証 | 5 | 10 | 1 |
提案は Missing expected exception (RangeError). と報告します。別の保存テストは、空の配列を期待したのに { seats: 4, remaining: 0 } を含むことを示します。node show-caller.mjs は正否を判定せず両結果を表示します。
成功する既定の確認は node --test run.mjs で、verify-baseline を選びます。壊れた結果を期待して成功する再現テストとは異なり、この検証は正しい動作を要求して提案が実際に失敗します。OS 固有のシェル構文は使いませんが、記録したのは macOS で、Linux・Windows の検証ではありません。
自分のプロジェクトでは、関連する base のアサーションを提案に実行し、差分から考えられる最小の呼び出し元ケースを加えます。両版が新テストに成功するなら、回帰を守れたと判断する前に入力とアサーションを見直してください。
失敗をレビューの判断につなげる
具体的なコメント例です。
拒否を clamp に置き換えたため
remainingSeats(3, 4)が 0 を返し、未変更のcreateBookingが{ seats: 4, remaining: 0 }を保存します。編集された三つのテストは以前RangeErrorを要求していましたが、非負のアサーションでは通ります。拒否検査を保ち、過大予約がsaveを呼ばないことを確認してください。元は 15 件成功し、提案は 10 件失敗します。
今回の要件なら元の検証を残し、例外アサーションを戻します。部分的な予約が新要件なら、受理した数量を呼び出し元へどう伝え、ユーザーが何を確認するかを定め、実装・利用側・テストを一緒に変えます。関数内だけで黙って数量を変えても、その判断は解決しません。
対象は同期検証と同期保存の呼び出しです。有効な available を前提とし、同時予約、DB の原子性、再試行、認可は含みません。実際の結合箇所で確認が必要です。
Yenpo で実装、テスト、呼び出し元を読み、コミット済み版を比較できます。動作環境はインストールガイドを参照してください。Node の例とレビュー方法は Yenpo に依存しません。Git 比較で版を確認してから、次のように質問できます。
このパッチで消えた受理条件と、同時に変わったアサーションを探してください。その例外に依存する未変更の呼び出し元を追い、各懸念に具体的な入力、期待結果、ソース参照を示してください。base にあった動作と回帰を分けてください。