The patch is small, the tests are green, and a booking for four seats goes through when only three are available. The remaining-seat count even looks sensible: zero. The problem becomes clear when you open the tests in the same diff. They used to require an error. Now they only require a nonnegative result.
Suppose AI is asked to simplify a seat-count helper without changing which requests it accepts. The patch and weakened tests below are written for this tutorial and run locally with Node’s built-in test runner.
Start with the rule the old tests protected
For this example, available has already been checked upstream: it’s a nonnegative safe integer. The helper takes a requested quantity, returns the exact number of seats left, or throws RangeError. It accepts only numeric safe integers from 1 through available. It doesn’t round, convert strings, or partially fulfill a booking.
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;
}
With three seats available, the examples are small enough to check by hand:
| Requested | Required behavior | Reason |
|---|---|---|
1, 2, 3 |
Return 2, 1, 0 respectively |
Accept the whole request |
0, -1, 1.5 |
Throw RangeError |
No zero, negative, or fractional bookings |
4 |
Throw RangeError |
Can’t fulfill the whole request |
'2', NaN, Infinity |
Throw RangeError |
Quantity must be a numeric safe integer |
When no seats are available, a request for one also throws. Returning zero is already a valid success result for an exact fit, so zero can’t double as an error signal.
In a real review, establish these rules from the requested behavior, existing callers, and the base revision’s tests. Old assertions can be wrong, but changing them needs an explanation of which behavior is changing. Here, the task explicitly asks to preserve rejection behavior.
Read the removed lines in both files
The proposed implementation replaces validation with clamping:
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;
}
For (3, 4), the local count becomes 3, and the return value is 0. For (3, 1.5), it becomes 1, and the function returns 2. The patch has changed which inputs succeed, even though ordinary requests still return the right answer.
Now read the test diff. Three tests follow this pattern:
- test('rejects more seats than available', () => {
- assert.throws(() => remainingSeats(3, 4), RangeError);
+ test('handles more seats than available', () => {
+ assert.ok(remainingSeats(3, 4) >= 0);
});
The input hasn’t changed. The required result has. The new assertion runs the helper directly, so it actually fails if the helper still throws. A version that silently accepts the oversized request now passes. Renaming “rejects” to “handles” makes that change less visible in the test report.
The zero and fractional cases lose their exception assertions too. The suite still contains five tests; the two ordinary success tests are unchanged. Test count doesn’t tell you which behaviors the suite still checks.
For a real patch, read the implementation and test changes together:
git diff BASE HEAD -- src/remaining-seats.mjs test/remaining-seats.test.mjs
git show BASE:test/remaining-seats.test.mjs
Replace the revisions and paths with your repository’s. Record the exact revisions you compare, especially if you’re also reading files from a working tree that has moved on. The download’s ai-style.patch lets you inspect this example without creating any Git history.
When you find a removed check, translate it into one input: removing requested > available gives you (3, 4). When you find a removed assertion, state what it used to require: this input had to throw before the caller could continue. That pairing is more useful than asking whether the new code looks defensive.
Open the caller that has no changed lines
The caller below is shared unchanged by both versions. The helper and a synchronous save function are passed in so the example can run entirely in memory:
export function createBooking(remainingSeats, save) {
return function book(available, requested) {
const remaining = remainingSeats(available, requested);
const reservation = { seats: requested, remaining };
save(reservation);
return reservation;
};
}
There are two quantities here. remaining comes from the helper; seats still uses the original requested. The old helper either accepted that exact quantity or threw before save. After the patch, the helper silently uses three while the caller records four.
Running the same caller with (3, 4) produces:
| Helper | Result seen by caller’s caller | Save calls |
|---|---|---|
| Original | RangeError |
None |
| Proposed | { seats: 4, remaining: 0 } |
One, with that same object |
This example observes an incorrect in-memory reservation and a save attempt. It doesn’t demonstrate a database transaction, a payment, or an actual customer booking.
Search beyond the changed-file list to find this dependency in your project:
rg -n 'remainingSeats|createBooking' src test
Follow imports to confirm which helper a caller uses. Then inspect what happens after the call: does it save the original input, build a response, or catch a particular error? If references aren’t easy to search by name, use your language server and check wrappers too. The code-reading guide explains how to combine those routes.
Keep the error check and observe the save separately
Restoring the original three rejection tests already catches this patch. Add a caller test to show why the rejection matters:
const writes = [];
const book = createBooking(remainingSeats, (row) => writes.push(row));
assert.throws(() => book(3, 4), RangeError);
assert.deepEqual(writes, []);
The selected helper is the real original or proposed function. Only save is replaced with an array recorder. Replacing the helper with a stub that always throws would bypass the change you’re reviewing.
That compact test stops at the first failed assertion. The downloadable suite also gives “no save attempt” its own test, so one run can report both failures:
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, []);
The empty catch is limited to this observation test; it lets the assertion inspect writes even when booking throws. It would be too weak on its own, because any error before saving could make it pass. The suite separately requires RangeError, verifies a valid reservation is saved exactly once with the right values, and verifies save failures reach the caller. Returning nothing or rejecting every request can’t satisfy those tests.
The verification suite has 15 tests: three exact success results, eight invalid inputs including a sold-out case, and four caller tests. The expected values come from the rules above. They aren’t calculated by copying the proposed clamp expression into the test.
Run the same expectations against both implementations
Download the complete example, extract it, and open a terminal in its review-ai-generated-tests folder. Use Node.js 22 or later; there’s no install step, network request, or provider configuration.
node run.mjs baseline
node run.mjs proposed-original
node run.mjs proposed-weak
node run.mjs verify-baseline
node run.mjs verify-proposed
Each command registers tests with node:test and exits with the test result. Run them separately: the second and fifth commands deliberately fail. Don’t put the sequence behind &&, which would stop at the first expected failure.
These are the observed results from Node.js v22.22.1 on macOS arm64:
| Mode | Implementation / assertions | Pass | Fail | Exit code |
|---|---|---|---|---|
baseline |
Original / original | 5 | 0 | 0 |
proposed-original |
Proposed / original | 2 | 3 | 1 |
proposed-weak |
Proposed / weakened | 5 | 0 | 0 |
verify-baseline |
Original / verification | 15 | 0 | 0 |
verify-proposed |
Proposed / verification | 5 | 10 | 1 |
The proposed implementation reports Missing expected exception (RangeError). The separate save test reports an actual array containing { seats: 4, remaining: 0 } where an empty array was required. node show-caller.mjs prints both caller outcomes without asserting either is correct.
For a green default check, run node --test run.mjs; it selects verify-baseline. This is different from a reproduction test that passes by asserting the broken outcome. Here, the verification tests require correct behavior, and the proposed implementation really fails them. The commands use no OS-specific shell features; this run was on macOS, not a Linux or Windows test matrix.
To verify AI code changes in your own project, preserve the relevant base assertions and run them against the proposed code, then add the smallest caller case the diff suggests. If the old and new implementations both pass your new test, inspect its inputs and assertions before treating it as regression coverage.
Turn the failure into a review decision
A review comment can stay concrete:
Replacing rejection with clamping makes
remainingSeats(3, 4)return zero. The unchangedcreateBookingthen saves{ seats: 4, remaining: 0 }. The three edited tests previously requiredRangeError; their new nonnegative assertions accept this behavior. Preserve the rejection checks and verify that an oversized booking never callssave. The original passes all 15 verification tests; this patch fails 10.
For the stated task, keep the original validation and restore the error assertions. If partial fulfillment is an intentional new requirement, define how the accepted quantity reaches the caller and what the user confirms, then change implementation, callers, and tests together. Silently changing the helper’s local quantity leaves those decisions unresolved.
The example covers synchronous validation and the call into a synchronous save function. It assumes a valid available count and doesn’t cover concurrent bookings, database atomicity, retries, or authorization. Those need tests at their actual integration points.
Yenpo can help you read the implementation, tests, and callers and compare committed versions. See the installation guide for current system requirements; the Node example and review method don’t depend on Yenpo. In a Git comparison, check the selected revisions, then ask:
Find the acceptance rules removed by this patch and the assertions changed alongside them. Follow unchanged callers that rely on those errors. Give a concrete input and expected result for each concern, with source references. Separate regressions from behavior already present in the base.