Matteo
Vincenti
Menu

Code review con gli agenti AI: quando una PR è davvero pronta

Una PR piccola può passare i test e restare sbagliata. Un esempio Docker mostra come trovare il difetto, correggerlo e motivare l'accettazione.

Una lente esamina due tessere di prenotazione sovrapposte davanti al modellino di una sala riunioni.

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:

Il requisito, prima della soluzione
Prenotazione richiestaRisultato attesoMotivo
11:00–12:00ConsentitaComincia quando termina quella esistente.
10:00–11:00RifiutataOccupa esattamente lo stesso intervallo.
10:30–11:30RifiutataLa 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:

diff
 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) restituisce true. Il requisito richiede false, 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à:

javascript
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:

diff
-    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:

bash
mkdir pr-review-demo
cd pr-review-demo

In questa directory crea i sei file seguenti. Sono tutti file da scrivere; il test runner non ne genera altri.

txt
pr-review-demo/
  validation.mjs
  baseline.mjs
  proposed.mjs
  fixed.mjs
  proposed.test.mjs
  contract.test.mjs

validation.mjs applica il contratto degli input: minuti interi, inizio precedente alla fine, intervalli dentro la giornata.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

bash
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:

bash
review_test baseline proposed.test.mjs
review_test proposed proposed.test.mjs

La 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:

bash
review_test proposed contract.test.mjs
review_test fixed proposed.test.mjs
review_test fixed contract.test.mjs
Riprodurre il caso in Docker
ImplementazioneSuitePassatiFallitiExit code
baselineproposed211
proposedproposed300
proposedcontract651
fixedproposed300
fixedcontract1100

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:

  1. 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.
  2. 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:

txt
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.

Condividi l’articolo

Hai una domanda o un’osservazione sull’articolo?

Scrivimi su LinkedIn
Tutti gli articoli