Una pull request, cioè una proposta di modifica al codice, può risolvere il problema assegnato e introdurne un altro nello stesso punto. Se anche i test scritti per accompagnarla passano, chi la deve approvare riceve una risposta rassicurante alla domanda sbagliata.
Con gli agenti AI questo passaggio merita attenzione: l'agente può preparare sia l'implementazione sia le prove con cui la presenta. Il lavoro del reviewer è ricostruire il comportamento richiesto e cercare un caso capace di mettere in difficoltà la soluzione. Un secondo agente può aiutare a cercarlo; la decisione resta da motivare sul codice e sul risultato.
Per rendere visibile il ragionamento uso una piccola PR didattica: la disponibilità di una sala riunioni. Il codice, il difetto e i test sono costruiti per questo esempio. La dimostrazione è riproducibile in Docker, senza chiamate a modelli o servizi esterni.
Il requisito, prima della soluzione
La sala è prenotata dalle 10:00 alle 11:00. Il sistema deve permettere una prenotazione dalle 11:00 alle 12:00 e rifiutare qualunque intervallo che occupi anche solo una parte dello stesso orario. La versione attuale rifiuta erroneamente anche le prenotazioni consecutive.
Rappresentiamo gli orari come minuti dalla mezzanotte: 600 significa 10:00, 660 significa 11:00. Ogni intervallo include l'inizio ed esclude la fine, secondo la notazione [start, end). Per questo due prenotazioni possono incontrarsi alle 11:00 senza sovrapporsi.
Prima di leggere la modifica, scriverei questi tre casi:
| Prenotazione richiesta | Risultato atteso | Motivo |
|---|---|---|
| 11:00–12:00 | Consentita | Comincia quando termina quella esistente. |
| 10:00–11:00 | Rifiutata | Occupa esattamente lo stesso intervallo. |
| 10:30–11:30 | Rifiutata | La sala è già occupata fra le 10:30 e le 11:00. |
Il terzo caso è quello che rende interessante la review. Nasce dalla regola sulle sovrapposizioni; nessuna riga dell'implementazione è necessaria per stabilire il risultato atteso.
La modifica passa tre test
La funzione canBook riceve la nuova prenotazione e quelle esistenti. Restituisce true quando l'intervallo è disponibile. Il controllo iniziale applica la stessa validazione degli input a tutte le versioni. Nel diff seguente cambia soltanto il criterio con cui si riconosce un conflitto:
export function canBook(candidate, existing) {
validate(candidate, existing);
return !existing.some((slot) =>
- candidate.start <= slot.end && candidate.end >= slot.start);
+ candidate.start === slot.start && candidate.end === slot.end);
}La prima espressione considera in conflitto anche due intervalli adiacenti. La nuova confronta soltanto due prenotazioni identiche. Il caso delle 11:00 ora funziona, ma una prenotazione dalle 10:30 alle 11:30 viene accettata.
La suite che accompagna la proposta controlla un duplicato esatto, una prenotazione consecutiva e un orario libero. I tre test passano davvero: entrano nella funzione modificata e verificano risultati. È il comportamento delle sovrapposizioni parziali a essere rimasto fuori.
Questo dettaglio cambia il tipo di domanda da fare. «Hai eseguito i test?» qui riceve un sì corretto. La domanda utile è: «Quale test impedirebbe a questa funzione di accettare due prenotazioni che si sovrappongono?». La documentazione delle pratiche di review di Google propone proprio di verificare se i test fallirebbero davanti al codice rotto.
Nemmeno scoprire che il file è stato importato chiude la questione. Serve leggere l'asserzione: il codice può essere eseguito senza che il test distingua il risultato voluto da quello sbagliato. Nelle discussioni sugli agenti e i test eseguiti, questa distinzione emerge anche come obiezione ai controlli basati sul solo percorso dei file.
Il test che cambia il verdetto
Per la proposta attuale chiederei modifiche. Il rilievo può essere molto breve:
Con una prenotazione esistente 10:00–11:00,canBook({ start: 630, end: 690 }, existing)restituiscetrue. Il requisito richiedefalse, perché i due intervalli occupano la sala contemporaneamente per mezz'ora. Il confronto di uguaglianza introdotto nel diff intercetta solo i duplicati esatti.
Un test riproducibile rende il rilievo più utile di un giudizio generale sulla qualità:
test('rejects an overlapping booking', () => {
assert.equal(canBook(
{ start: 630, end: 690 },
[{ start: 600, end: 660 }],
), false);
});Sulla proposta questo test fallisce: il risultato osservato è true. La correzione deve riconoscere un'intersezione con durata positiva. La nuova prenotazione inizia prima che finisca quella esistente, e termina dopo che quella esistente è iniziata:
- candidate.start === slot.start && candidate.end === slot.end);
+ candidate.start < slot.end && candidate.end > slot.start);Le disuguaglianze strette permettono gli intervalli adiacenti. Per controllare il resto della regola aggiungo sovrapposizione dall'altro lato, contenimento, calendario vuoto e conflitto con la seconda prenotazione nell'elenco. Con la correzione passano sia i tre test iniziali sia gli undici della suite estesa.
Qui il controllo ha prodotto una decisione: respingere la prima modifica e accettare la correzione del predicato insieme ai test. L'accettazione riguarda questa funzione in memoria. Se due utenti possono prenotare nello stesso istante, la verifica e il salvataggio vanno coordinati anche nel database; una lettura seguita da una scrittura separate può ancora assegnare la sala due volte.
Riprodurre il caso in Docker
Servono Docker avviato, un terminale con una shell compatibile con sh e un editor. Il primo avvio scarica l'immagine Node ufficiale se manca; i test girano poi senza rete. Non occorrono Node sul computer, dipendenze npm, account AI o chiavi API.
Crea una directory vuota:
mkdir pr-review-demo
cd pr-review-demoIn questa directory crea i sei file seguenti. Sono tutti file da scrivere; il test runner non ne genera altri.
pr-review-demo/
validation.mjs
baseline.mjs
proposed.mjs
fixed.mjs
proposed.test.mjs
contract.test.mjsvalidation.mjs applica il contratto degli input: minuti interi, inizio precedente alla fine, intervalli dentro la giornata.
export function validate(candidate, existing) {
const valid = (slot) => slot !== null && typeof slot === 'object'
&& Number.isInteger(slot.start) && Number.isInteger(slot.end)
&& slot.start >= 0 && slot.start < slot.end && slot.end <= 1440;
if (!valid(candidate) || !Array.isArray(existing) || !existing.every(valid)) {
throw new TypeError('Expected slots with integer minutes: 0 <= start < end <= 1440');
}
}baseline.mjs conserva il comportamento di partenza, incluso il difetto sugli intervalli adiacenti.
import { validate } from './validation.mjs';
export function canBook(candidate, existing) {
validate(candidate, existing);
return !existing.some((slot) =>
candidate.start <= slot.end && candidate.end >= slot.start);
}proposed.mjs contiene la modifica sottoposta alla review.
import { validate } from './validation.mjs';
export function canBook(candidate, existing) {
validate(candidate, existing);
return !existing.some((slot) =>
candidate.start === slot.start && candidate.end === slot.end);
}fixed.mjs contiene la correzione.
import { validate } from './validation.mjs';
export function canBook(candidate, existing) {
validate(candidate, existing);
return !existing.some((slot) =>
candidate.start < slot.end && candidate.end > slot.start);
}proposed.test.mjs è la suite iniziale. La variabile VERSION sceglie quale implementazione caricare, lasciando identiche le prove.
import test from 'node:test';
import assert from 'node:assert/strict';
const { canBook } = await import(`./${process.env.VERSION ?? 'proposed'}.mjs`);
const existing = [{ start: 600, end: 660 }];
test('rejects an identical booking', () => {
assert.equal(canBook({ start: 600, end: 660 }, existing), false);
});
test('allows a booking immediately after the existing one', () => {
assert.equal(canBook({ start: 660, end: 720 }, existing), true);
});
test('allows a booking in a free interval', () => {
assert.equal(canBook({ start: 780, end: 840 }, existing), true);
});contract.test.mjs controlla i casi ricavati dal requisito e la validazione degli intervalli.
import test from 'node:test';
import assert from 'node:assert/strict';
const { canBook } = await import(`./${process.env.VERSION ?? 'proposed'}.mjs`);
const existing = [{ start: 600, end: 660 }];
for (const [name, candidate, expected] of [
['identical interval', { start: 600, end: 660 }, false],
['overlap on the right', { start: 630, end: 690 }, false],
['overlap on the left', { start: 570, end: 630 }, false],
['contained interval', { start: 615, end: 645 }, false],
['containing interval', { start: 570, end: 690 }, false],
['adjacent on the right', { start: 660, end: 720 }, true],
['adjacent on the left', { start: 540, end: 600 }, true],
['separate interval', { start: 780, end: 840 }, true],
]) {
test(name, () => assert.equal(canBook(candidate, existing), expected));
}
test('allows a booking in an empty calendar', () => {
assert.equal(canBook({ start: 600, end: 660 }, []), true);
});
test('checks all existing bookings', () => {
assert.equal(canBook({ start: 810, end: 870 }, [
...existing, { start: 780, end: 840 },
]), false);
});
test('rejects invalid intervals', () => {
for (const candidate of [null, { start: 600, end: 600 },
{ start: -1, end: 20 }, { start: 1430, end: 1441 },
{ start: 600.5, end: 660 }]) {
assert.throws(() => canBook(candidate, existing), TypeError);
}
});Nel terminale, restando nella directory con i sei file, imposta l'immagine e la funzione che esegue una suite. Il digest identifica un indice OCI multipiattaforma. Con la configurazione predefinita e senza --platform, Docker seleziona automaticamente la variante nativa; non occorre forzare arm64 o impostare emulazione. La prova del 24 ottobre 2026 riportata qui usa Node 22.23.4 su Linux arm64. Il comando monta soltanto questa directory in lettura; non espone porte e rimuove il container all'uscita.
export REVIEW_IMAGE='node@sha256:de170b39439eb0e5a3444ad7d60b015614a381e10fecae2e30ec9e735122f38d'
review_test() {
docker run --rm --network none --read-only --cap-drop ALL \
--security-opt no-new-privileges --pids-limit 64 --memory 128m --cpus 1 \
--user node --mount "type=bind,source=$PWD,target=/work,readonly" \
--workdir /work -e VERSION="$1" "$REVIEW_IMAGE" \
node --test "$2"
}Esegui prima le prove che accompagnano la proposta:
review_test baseline proposed.test.mjs
review_test proposed proposed.test.mjsLa baseline mostra 2 test passati e 1 fallito: è il caso dell'adiacenza da correggere. La proposta mostra 3 passati e 0 falliti. Ora esegui la suite estesa, poi le due suite sulla correzione:
review_test proposed contract.test.mjs
review_test fixed proposed.test.mjs
review_test fixed contract.test.mjs| Implementazione | Suite | Passati | Falliti | Exit code |
|---|---|---|---|---|
| baseline | proposed | 2 | 1 | 1 |
| proposed | proposed | 3 | 0 | 0 |
| proposed | contract | 6 | 5 | 1 |
| fixed | proposed | 3 | 0 | 0 |
| fixed | contract | 11 | 0 | 0 |
L'exit code 1 è atteso nei due casi che espongono un difetto. Il test runner di Node lo usa per segnalare test falliti. Per questo un comando con esito negativo fa parte della dimostrazione: permette di vedere quale implementazione il test riesce a respingere.
Quando hai finito, puoi eliminare la directory dell'esempio. L'immagine rimane nella cache Docker; rimuovila solo se l'hai scaricata per questa prova e nessun altro progetto la usa.
Dove aiuta un agente reviewer
Affidare a un agente una seconda lettura ha senso se gli si assegna un lavoro controllabile. Su questa PR gli chiederei di derivare esempi dal requisito, cercare un comportamento incompatibile e preparare un test. Per separare la scelta dei casi dalla soluzione proposta, divido la richiesta in due passaggi:
- Uso una sessione separata, con il solo requisito e senza accesso al diff, e chiedo casi con il risultato atteso, compresi quelli che dovrebbero far respingere una soluzione.
- Poi fornisco versione base, proposta e test, identificando i commit, e chiedo di verificare quei casi e motivare i rilievi.
Per il secondo passaggio uso questa richiesta:
Review this pull request against the acceptance requirement and the base version.
Do not edit files or approve the pull request.
Use the requirement-derived cases supplied separately; add missing cases with a requirement-based rationale.
For each material finding, report:
- input and relevant preconditions;
- expected behavior and its requirement;
- observed behavior, or "not executed";
- the relevant file and changed expression;
- a test that fails on the defect and passes on the correction.
Separate a confirmed defect from a hypothesis and from a style preference.
If execution is unavailable, provide the test and leave the result unverified.Il rapporto deve portare a una riga del diff e a un risultato che un'altra persona possa controllare. Quando l'agente può eseguire il test, servono comando, versione del codice e output; quando può solo leggerlo, l'osservazione resta un'ipotesi da provare. Anche la documentazione GitHub sulla code review AI richiede di validarne i rilievi e affiancarli alla review umana.
Un finding fondato può essere accolto anche se il resto della review contiene rumore. Al contrario, tre modelli d'accordo non chiariscono quale requisito abbiano controllato. Chiedere indiscriminatamente altre review aumenta il materiale da leggere; il controesempio permette di chiudere una questione specifica.
Quanto codice deve arrivare alla review
Le PR molto grandi rendono difficile ricostruire le intenzioni e separare i cambiamenti. Nelle discussioni di chi le riceve dagli agenti c'è chi vuole leggere tutto e chi preferisce concentrare l'attenzione sulle conseguenze. Il conflitto riguarda tempo disponibile e responsabilità, prima ancora della scelta del reviewer.
Qui la modifica occupa una sola espressione ed è comunque sbagliata. Mettere un limite di righe avrebbe lasciato passare il problema. È più utile chiedere una PR con un obiettivo riconoscibile: correzione del conflitto fra intervalli e relativi test, lasciando un eventuale refactor del calendario a un'altra modifica. Le indicazioni di Google sui piccoli cambi insistono sulla stessa completezza concettuale, includendo le prove pertinenti.
Il rischio determina anche quanta attenzione serve. Cambiare una spiegazione interna e cambiare una regola che assegna risorse ai clienti meritano percorsi diversi. La ricognizione di Gergely Orosz sulle code review descrive approcci che vanno dalla lettura manuale al triage per impatto: è un confronto più utile della scelta assoluta fra leggere tutto e fidarsi dell'agente.
Per decidere l'adozione nel team, il passo successivo è osservare il lavoro necessario ad accettare le modifiche: ne parlo nell'articolo su qualità, costi e responsabilità degli agenti AI. Per questa PR, invece, il punto di arrivo è più circoscritto.
La decisione da lasciare nella PR
Un commento di accettazione può conservare il ragionamento senza diventare un verbale:
Accetto la correzione del controllo degli intervalli: mantiene le prenotazioni consecutive e rifiuta sovrapposizioni e contenimenti. Le due suite passano sulla versione corretta: 3/3 e 11/11. La precedente proposta passa la suite iniziale ma fallisce cinque casi della suite estesa. Il salvataggio concorrente resta fuori da questa modifica.
Nel lavoro reale aggiungerei il commit verificato e il collegamento al run. Quando la PR cambia, riesegui i test sul nuovo commit e aggiorna il commento di accettazione indicando il commit e il run verificati. Conserva i risultati precedenti nella cronologia. Le protezioni dei branch possono richiedere nuovi controlli e approvazioni quando cambia il diff.

