小さなパッチのテストは成功していますが、残席 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 にあった動作と回帰を分けてください。