← Blog
Note di lavoro

Test scritti dagli agenti e mutation testing: una tesi non (ancora) dimostrata

Ho fatto scrivere test e codice a due agenti separati e li ho messi alla prova con il mutation testing. La tesi del primo articolo — che separarli faccia emergere i buchi — non si dimostra. E il segnale più grosso non veniva dai test: una CI che non poteva fallire. Conclusioni parziali: servirebbero test più intensivi.

Un sigillo verde di CI con segno di spunta, incrinato, che lascia intravedere il rosso sotto; a sinistra una lista di test verdi che ne nasconde due falliti, con due agenti contrapposti e operatori di mutazione del codice.

Nel primo articolo di questa serie ho sostenuto che con gli agenti il costo dello sviluppo migra a monte (specificare) e a valle (verificare), e ho descritto uno schema: due agenti contrapposti, uno scrive i test dalla spec, l’altro il codice, con il mutation testing a fare da metro. Mancava la prova. Questo è il banco di prova.

Le conclusioni sono un po’ diverse da quelle che mi aspettavo. Tre cose della sezione 5 reggono — coverage al 100% che nasconde un test cieco, punteggio grezzo che inganna, il codice non richiesto come zona scoperta. Ma la tesi forte, che separare gli agenti faccia emergere i buchi, non si dimostra. E il segnale più importante non è arrivato affatto dai test.

Attenzione: questi risultati non sono conclusivi. Due feature piccole, spec precise, lo stesso modello per entrambi gli agenti, e classificazioni che sono giudizi di modelli e non di una persona. Servirebbero test più intensivi — più feature, più repliche, modelli di provider diversi, spec meno curate — prima di trarre conclusioni generali. Quello che segue è un esperimento onesto, non una dimostrazione.

Chi ha fatto cosa

Lo dico subito, perché cambia il peso di tutto il resto: tutto il lavoro tecnico è stato svolto da agenti AI in una sessione Claude Code (Taita, il mio side project: backend FastAPI + PostgreSQL, Python 3.11, ~2300 test; ottobre 2026). Io, come autore, non ho fatto controlli manuali: ho dato le direzioni e posto le domande critiche che trovate nel testo.

Ruolo Chi
Spec, orchestrazione, integrazione, classificazione dei mutanti, mutanti scritti a mano, analisi della CI, correzioni agente orchestratore (Claude Code)
Test, scritti solo dalla spec agente test, sotto-agente in worktree isolato
Codice, scritto solo dalla spec agente codice, sotto-agente in worktree isolato
Verifica indipendente dei test falliti revisore, sotto-agente con un modello diverso (Sonnet), senza conoscere le conclusioni dell’orchestratore

Classificazioni, diagnosi e giudizi citati sono di modelli, non di una persona. Anche le spec le ha scritte un agente, non uno sviluppatore.

Le mie direzioni e domande, nell’ordine in cui le ho poste:

  1. fare l’esperimento su Taita;
  2. “se pensi sia utile, un test con mock e DB, fallo”;
  3. far verificare da un altro modello che i test falliti non fossero problematici;
  4. preparare la PR con pipefail e sistemare i test rotti;
  5. “ma che senso ha fare test su SQLite?” / “perché si usa ancora SQLite?”;
  6. “se stai usando Postgres, ha senso rifare il mutation testing?”;
  7. “non è possibile lanciare in parallelo uno dei mutanti che va in timeout?”;
  8. “ha senso lanciare tutti i test?” → “fai la modifica a conftest” → “mi sembra che non sia migliorato molto”;
  9. “ha trovato un problema vero e dimostrato che gli agenti contrapposti aiutano?” → “allora proviamo a dimostrare la tesi”;
  10. dopo il 013: “e se usassi agenti con caratteristiche diverse più che modelli diversi?” → scelta della versione ridotta (014) per limiti di credito.

Negli esperimenti 012-014 spec, intenzione nascosta, implementazioni di riferimento, bug inseriti, prompt, regole di decisione e classificazioni sono tutti dell’orchestratore.

Parte 1 — I test scritti da un agente reggono al mutation testing?

Protocollo (identico per due feature)

  1. Spec scritta dall’orchestratore e committata prima di qualunque codice.
  2. Due sotto-agenti in worktree git separati, avviati in parallelo, senza contesto condiviso:
    • agente test: solo la spec (il codice non esiste ancora); prompt neutro (“suite completa, da merge”), nessuna menzione del mutation testing;
    • agente codice: solo la spec, con il divieto di scrivere test.
  3. Integrazione → coverage → mutmut 3.8.0 (mutanti solo sul modulo nuovo, solo la sua suite).
  4. Seconda iterazione: all’agente test va il report grezzo dei mutanti sopravvissuti, senza dire quali sono equivalenti, chiedendo di ucciderli solo se sono buchi reali rispetto alla spec e di motivare gli altri.

Feature 1 — funzione pura (giorni di scuola e “ponti”)

  • ~180 righe, nessun I/O. Test: 396 casi, tutti verdi al primo run contro il codice dell’altro agente. Coverage 100% righe e branch.
  • mutmut iter. 1: 87/93 uccisi (93,5%), ~17 s.
  • 6 sopravvissuti: 4 sul testo dei messaggi d’errore (non nel contratto), 1 equivalente (< → <= che riassegna lo stesso valore), 1 buco reale.
  • Il buco reale: un test chiamato test_default_max_length_is_one. Nome corretto, verde, coverage piena; ma nei dati scelti max_length=1 e max_length=2 davano lo stesso risultato, quindi non poteva accorgersi di un default sbagliato. Solo il mutante (default 1 → 2) l’ha rivelato.
  • Iter. 2: l’agente test uccide quel mutante, riconosce da solo l’equivalente, lascia vivi i 4 sui messaggi motivando. 88/93, 0 buchi rispetto alla spec.
  • Scelta spontanea dell’agente test: un “oracolo” (seconda implementazione dalla spec) confrontato con il codice su 40 anni scolastici casuali → ~930 righe di test per ~180 di codice.

Feature 2 — DB + mock (notifica dei ponti alla famiglia)

  • Legge Family e SchoolCalendarEvent via ORM, usa la feature 1, invia tramite un servizio di notifica asincrono mockato.
  • Test: 93 casi, 91 verdi al primo run. I 2 rossi erano falsi positivi dell’agente test: per verificare “una sola query sul DB” contavano anche la query che il test stesso provocava leggendo family.id dopo expire_all(). Corretti dall’orchestratore (unico intervento sui test). Il revisore indipendente ha confermato la causa e che la correzione non indebolisce il test: iniettando una query in più nel codice, i test corretti falliscono.
  • Coverage branch 97%: le righe scoperte erano codice non richiesto dalla spec, aggiunto di sua iniziativa dall’agente codice.
  • mutmut iter. 1 (su SQLite): 161/188 (85,6%), ~5,5 min. 27 sopravvissuti, classificati dall’orchestratore prima di vedere il giudizio dell’agente:
    • 13 testo/argomenti dei log (la spec vieta solo dati personali nei log);
    • 8 nel codice non richiesto dalla spec;
    • 6 equivalenti, di cui uno rivela un difetto della spec (un messaggio su più righe impossibile da ottenere);
    • 0 buchi reali rispetto alla spec.
  • Iter. 2: l’agente arriva allo stesso verdetto su tutti i 27; uccide 1 mutante (continue → break), dichiarando che il test fissa un comportamento non specificato. 164/188. 2 dei 3 uccisi in più sono morti per caso (formato del log invalido → errore di logging), non per un’asserzione.
Funzione pura DB + mock
Test verdi al primo run 396/396 91/93 (2 test sbagliati)
Coverage branch (iter. 1) 100% 97%
Mutation score iter. 1 93,5% (87/93) 85,6% (161/188)
Mutation score iter. 2 94,6% (88/93) 87,2% (164/188)
Buchi reali rispetto alla spec (iter. 1 → 2) 1 → 0 0 → 0

Parte 2 — La CI verde che non poteva fallire

È qui il colpo di scena, ed è il motivo per cui questo articolo non si chiama più “coverage e test ciechi”. Il revisore indipendente aveva concluso che un test preesistente (test_reactivation) “probabilmente fallisce anche in CI”, ma la CI era verde. Per risolvere la contraddizione l’orchestratore ha letto i log della CI invece di fidarsi del semaforo.

  • Il passo pytest era python -m pytest ... 2>&1 | tee pytest-output.txt, senza pipefail. GitHub Actions, senza shell: bash esplicito, esegue gli script con bash -e: l’exit code di una pipe è quello dell’ultimo comando, tee, che riesce sempre. Nessun test fallito poteva far fallire il job. Stesso difetto nel passo mypy (che non avrebbe mai potuto bloccare, nemmeno con mypy-blocking: true).
  • La riga esisteva almeno dal 7 agosto 2026 (storia git).
  • Log dell’ultima CI “verde” su main, run Postgres: 5 failed, 1735 passed, 28 skipped, 16 deselected, 83 errors.
  • Il job di deploy del backend chiama gli stessi controlli: anche i deploy passavano con test rossi.
  • L’AI stessa si era fidata del verde: l’orchestratore aveva riferito a me che “la CI è verde, i test passano anche su Postgres”, deducendolo dal semaforo. Affermazione non verificata, corretta dopo aver letto i log.

Cosa nascondeva

  • 83 errori, una sola causa: 13 file di test creavano un proprio DB SQLite in memoria, e la fixture condivisa client era sempre SQLite, anche nella run “Postgres”. Quei test non avevano mai girato su Postgres.
  • 5 fallimenti, tutti bug dei test, nessuno nel codice di produzione:
    • chiavi esterne violate (promemoria, membri della famiglia, integrazioni Gmail): SQLite di default non le controlla;
    • SQL scritto a mano senza schema;
    • un’asserzione sull’orario vera solo su SQLite (che salva l’ora locale senza fuso, mentre Postgres restituisce timestamptz in UTC);
    • un test che non simulava il ruolo DB dello scheduler: il codice si rifiutava correttamente di fare query cross-family senza quel ruolo.
  • Un sesto test (test_reactivation) falliva solo in locale: chiamava il vero servizio email fuori dal mock. Diagnosi del revisore indipendente.

La correzione

Prima scelta dell’orchestratore: spostare i 13 file nella lista “solo SQLite” del repository. Rendeva la CI onesta ma lasciava il punto cieco. Dopo la mia domanda (“che senso ha fare test su SQLite?”) li ha invece portati su Postgres e ha reso Postgres-aware la fixture client.

Run (CI) Prima Dopo
Postgres 5 failed, 83 errors, 1735 passed — job verde 1823 passed, 0 failed — job verde con pipefail
SQLite (lista storica) 466 passed 466 passed

Nessuna modifica al codice di produzione: tutti i problemi erano nei test o nella CI.

Perché si usava ancora SQLite

Per inerzia: è il default locale (nessun DB da avviare), una lista di ~30 file non è mai stata migrata, ed è veloce e parallelizzabile. Ma qui SQLite ha nascosto esattamente i bug trovati: chiavi esterne, fusi orari, schemi. E non può verificare RLS, BYPASSRLS o l’ordine delle righe, cioè le garanzie di sicurezza principali del progetto.

Parte 3 — Lo stesso mutation testing su Postgres

Mia domanda: “se stai usando Postgres, ha senso rifare il mutation testing?”. Sì per la feature 2 (quella con il DB), no per la 1 (funzione pura).

Stessi test e stesso codice; unica differenza il database. Su Postgres mutmut gira con un solo processo: con 8 processi i test condividerebbero lo stesso database e si disturberebbero, producendo mutanti “uccisi” per interferenza e non per merito dei test.

Prima run: i numeri ingannano

SQLite (8 processi) Postgres (1 processo)
Uccisi 164 152
Sopravvissuti 24 0
Timeout 0 36
Durata 5 min 92 min

Letto in superficie, Postgres dice “0 sopravvissuti”: test perfetti. È falso.

Perché i timeout

  • mutmut assegna a ogni mutante un tempo massimo di (tempo stimato dei test + 1) × 15. Per i 95 test della feature stimava 4,2 secondi → limite di 78 secondi.
  • La run reale del file su Postgres durava circa 220 secondi: ogni test creava e distruggeva le tabelle nelle fixture. Dai numeri risulta che la stima di mutmut non include questo costo (deduzione dai dati, non verificata nel codice di mutmut).
  • mutmut esegue i test in ordine di durata stimata, dal più breve (verificato nel codice e nelle statistiche salvate). Il test che uccide diversi mutanti, test_scenario1..., era il 94° su 95 in quell’ordine; nell’ordine normale di pytest è il primo.
  • Risultato: vanno in timeout sia i sopravvissuti veri (devono eseguire tutta la suite) sia uccisi veri il cui test killer arriva in fondo.

Verifica dei 36 timeout

Rilanciati dall’orchestratore senza limite di tempo, eseguendo i test direttamente sui mutanti di mutmut (variabile MUTANT_UNDER_TEST), 4 in parallelo, ciascuno su un proprio database Postgres clonato (mia idea).

36 su 36 verificati → 23 sopravvissuti veri, 13 uccisi veri. Nessuno con un esito diverso da SQLite.

  • Sopravvissuto mascherato: mutante 20 (nome della chiusura → None): 95 test passati in 227 s; per mutmut era “timeout”.
  • Ucciso mascherato: mutante 50 (max_length 1 → 2): 1 test fallisce, ma dopo ~225 s; per mutmut era “timeout”.
  • 6 mutanti muoiono in 11 secondi al primo test nell’ordine di pytest, ma in mutmut erano andati in timeout per l’ordinamento.
  • Un falso “ucciso”: il mutante 11 (argomento rimosso da un logger.warning) per mutmut su Postgres era “killed”; rilanciato a mano sopravvive, sia nell’ordine di pytest sia nell’ordine di mutmut (95 passati in 247 s). Causa non accertata.

Risultato reale su Postgres dopo la verifica: 164 uccisi, 24 sopravvissuti, identico a SQLite. Postgres non ha cambiato quali mutanti i test vedono; ha cambiato solo il modo in cui lo strumento li riporta.

La causa vera: fixture lente

Mia domanda: “ha senso lanciare tutti i test?”. Giravano già solo i 95 test della feature, e tutti passano dall’unico punto d’ingresso, quindi servono tutti. Il costo non era il numero di test ma la fixture test_db, che creava e distruggeva ~110 tabelle a ogni test (~2 s per test su Postgres).

Modifica (chiesta da me, fatta dall’orchestratore):

  • primo tentativo: schema creato una volta + TRUNCATE a fine test → solo da ~220 s a ~130 s per il file. Io: “mi sembra che non sia migliorato molto”. Profilando, il TRUNCATE di ~110 tabelle costava ancora ~1,5 s a test (costo fisso per tabella);
  • secondo tentativo: DELETE per tabella con i vincoli FK sospesi nella transazione + reset delle sequenze → da ~220 s a 23 s per il file della feature.

Effetto sull’intera suite Postgres: in locale da 759 s a 146 s (1827 test, tutti verdi); nella CI di GitHub il passo pytest su Postgres da 562 s a 116 s (1823 test, tutti verdi).

Seconda run di mutmut su Postgres, con le fixture veloci

Stesse impostazioni di default, un solo processo, nessuna verifica a mano.

mutmut, feature 2 SQLite Postgres, fixture lente Postgres, fixture veloci
Uccisi 164 152 (di cui 1 falso) 164
Sopravvissuti 24 0 24 — gli stessi 24 di SQLite, nome per nome
Timeout 0 36 0
Durata 5 min (8 processi) 92 min (1 processo) 19 min (1 processo)

Con le fixture veloci mutmut riporta su Postgres esattamente il risultato vero. Anche il mutante 11 (il falso “ucciso” della prima run) risulta sopravvissuto: l’anomalia era legata alla run lenta. Il problema non era Postgres, ma il costo delle fixture combinato con le stime e l’ordinamento di mutmut.

Lezione

Su un DB reale, con fixture lente e le impostazioni di default di mutmut, “timeout” non porta informazione in nessuna direzione: contarli come “rilevati” (convenzione comune) gonfia il punteggio fino al 100%, contarli come falliti lo sgonfia. Rendendo veloci le fixture il problema sparisce del tutto (0 timeout, risultato identico a SQLite, 19 minuti invece di 92). Il costo e l’affidabilità del mutation testing su un DB reale dipendono soprattutto da come sono scritte le fixture.

Parte 4 — Agenti contrapposti: la separazione aiuta davvero?

Mia domanda: i risultati precedenti dimostrano che un agente test separato dall’agente codice trova problemi che l’agente unico nasconderebbe? Risposta onesta: no, non li dimostravano. Da qui tre esperimenti, tutti pre-registrati: protocollo, regola di decisione, intenzione nascosta e implementazione di riferimento fissati dall’orchestratore e committati come SHA-256 prima di lanciare qualunque agente. Stesso modello per tutti gli agenti salvo dove indicato; 3 repliche per condizione.

012 — spec precisa (rotazione dei turni di accompagnamento)

Condizioni: A agente test + agente codice separati; B un agente scrive entrambi; C come A ma l’agente test usa un modello diverso (Sonnet). Misura: 15 bug inseriti dall’orchestratore (verificati non equivalenti) + bug reali nel codice degli agenti.

Suite Bug inseriti trovati Falsi positivi
A1/A2/A3 15/15/15 0
B1/B2/B3 15/14/15 0
C1/C2/C3 15/15/15 1/0/0

Le 6 implementazioni: 0 differenze dal riferimento su 6000 casi casuali ciascuna. Esito: non concludente (effetto soffitto). Con una spec precisa tutti scrivono codice corretto e test che trovano tutto; 8 suite su 9 contengono una seconda implementazione della spec usata come oracolo, anche quelle scritte dall’agente che scrive il codice.

013 — spec vaga (divisione delle spese di famiglia)

Spec volutamente sintetica (“divisa in modo equo”, “pochi trasferimenti”); le decisioni del product owner (10 punti) restano nascoste. Condizioni A e B come sopra.

  • Tutte e 6 le implementazioni deviano dall’intenzione (niente soffitto): 4 in modo lieve (chiudono prima le coppie di importo uguale → ordine dei trasferimenti diverso), 2 in modo marcato (minimo esatto dei trasferimenti; una anche pareggi dei centesimi per ordine di inserimento invece che alfabetico).
  • Coppie A (test separati × codice separato): la deviazione emerge in 1 su 3 (servivano ≥ 2). Implementazioni B con deviazioni non viste dai propri test: 3 su 3.
  • Esito: non concludente.

Perché: (1) i test separati verificano proprietà (somma, ±1 centesimo, al massimo n−1 trasferimenti) e lasciano esplicitamente aperti i punti ambigui, quindi non possono fallire su di essi; (2) le interpretazioni sono correlate: 5 implementazioni su 6 scelgono la stessa lettura sbagliata di “pochi trasferimenti” — stesso modello, stessa lettura; la separazione non rende indipendenti gli errori di interpretazione; (3) i test scritti insieme al codice sono accoppiati al codice (una suite importa una costante interna e non gira su nessun’altra implementazione).

014 — cambia il ruolo, non il modello (versione ridotta)

Mia idea: agenti con caratteristiche diverse invece di modelli diversi. Stessa spec del 013, stesse 6 implementazioni come bersaglio, due ruoli con 3 agenti ciascuno:

  • R — revisore di spec: elenca ambiguità e domande per il product owner, niente codice.
  • Q — tester avversariale: per ogni punto aperto sceglie la lettura più plausibile, la dichiara e la fissa con un test esatto (vietati i test “accetta uno qualsiasi dei comportamenti”).

Le tre deviazioni note del 013: D1 pareggi dei centesimi, D2 strategia dei trasferimenti, D3 ordine dei trasferimenti.

Revisori — regola: utili se ≥ 2 su 3 segnalano D1, D2 e D3.

Revisore Domande D1 D2 D3 Risposta proposta su D2
R1 11 ✓ ✓ ✓ prima le coppie uguali (≠ intenzione)
R2 12 ✓ ✓ ✓ prima le coppie uguali (≠ intenzione)
R3 13 ✓ ✓ ✓ prima le coppie uguali (≠ intenzione)

3 su 3: regola soddisfatta. D1-D3 sono sempre tra le prime 4 domande. Ma le risposte proposte ripetono le letture sbagliate degli implementatori (R3 propone anche i pareggi per ordine di inserimento, la deviazione di A3): il revisore vede l’ambiguità ma la risolverebbe come gli altri agenti.

Tester avversariali — regola: meglio del neutro se ≥ 2 suite su 3 fanno emergere deviazioni su ≥ 2 implementazioni. Riferimento: le suite neutre del 013 (al massimo 1).

Suite Implementazioni con deviazione emersa Falsi allarmi sul codice corretto
Q1 2 su 6 16 (rumorosa)
Q2 6 su 6 8
Q3 6 su 6 4
neutre 013 1 su 6 1-2

3 su 3: regola soddisfatta. Q2 e Q3 trovano anche la deviazione lieve che nessuna suite del 013 aveva visto, con un caso scritto a mano in cui un debito e un credito coincidono. Il prezzo: ogni lettura sbagliata del tester diventa un test rosso sul codice corretto. Q1 sceglie male due punti (pareggi, tipo di errore) → 16 falsi allarmi, e su 4 implementazioni su 6 i suoi test sull’algoritmo falliscono già per altro, coprendo la deviazione vera. Un test rosso dice “le due letture divergono”, non chi ha ragione.

Lezione

Separare gli agenti non basta: con lo stesso modello e lo stesso ruolo condividono le stesse letture della spec (013). Cambiare il ruolo cambia il risultato (014): il revisore fa emergere le domande giuste prima del codice, il tester avversariale le rende verificabili. Nessuno dei due però sa la risposta giusta: senza una persona che decida, ripropongono la lettura più comune. Il valore dell’agente contrapposto è trasformare un’ambiguità silenziosa in una domanda esplicita o in un test rosso da arbitrare.

Conclusioni che i dati sostengono

Lo ripeto perché conta: sono conclusioni su un campione piccolo, non leggi generali.

  1. Coverage 100% ≠ test che verificano: il test con il nome giusto e i dati sbagliati.
  2. Il mutation score grezzo inganna in entrambe le direzioni: il calo da 93,5% a 85,6% è rumore (log, equivalenti, codice non richiesto); su Postgres con fixture lente il “100% rilevato” era un artefatto dei timeout. Dopo la verifica (e con fixture veloci) i risultati su SQLite e Postgres coincidono: 164 uccisi, gli stessi 24 sopravvissuti, 0 buchi reali rispetto alla spec.
  3. Con DB/ORM l’agente test sbaglia in modo diverso: test troppo severi per il motivo sbagliato, non troppo deboli. Li trova l’integrazione, non il mutation testing.
  4. La zona più scoperta è il codice che nessuno ha chiesto: problema di processo.
  5. Dato il feedback, l’agente test non ha “giocato” la metrica.
  6. Il segnale più importante non veniva dai test ma dalla pipeline: una CI che non può fallire rende inutile qualunque qualità dei test, e nessuna metrica sui test l’avrebbe scoperto. Serve leggere i log.
  7. Il verde va verificato anche dall’AI: l’orchestratore stesso ha riportato un risultato non verificato fidandosi del semaforo.
  8. SQLite nei test nasconde proprio i bug che contano (FK, fusi orari, schemi, RLS), e la sua comodità era servita a nascondere test che non giravano mai sul DB vero.
  9. Le domande dell’autore hanno cambiato le scelte: “che senso ha SQLite?” ha evitato un rimedio di comodo; “non è migliorato molto” ha portato da 130 s a 23 s per il file, e la suite Postgres della CI da ~9 a ~2 minuti.
  10. Con una spec precisa la separazione degli agenti non si misura (012: soffitto).
  11. Con una spec vaga la sola separazione non basta (013): stesso modello, stesse letture sbagliate; i test separati verificano proprietà e lasciano aperti i punti ambigui.
  12. Il ruolo conta più della separazione (014): revisore di spec e tester avversariale fanno emergere le ambiguità che avevano prodotto le deviazioni, al costo di falsi allarmi e senza saper scegliere la risposta giusta. Servono come generatori di domande per chi decide, non come arbitri.

Limiti da dichiarare

Questa è la parte che rende l’esperimento onesto — e il motivo per cui non lo chiamo una dimostrazione.

  • Due feature piccole, con spec precise e con esempi (scritte da un agente).
  • Agente test e agente codice usano lo stesso modello: possono fraintendere la spec nello stesso modo, e allora oracolo e codice concordano sull’errore.
  • Classificazioni e diagnosi sono giudizi di modelli (orchestratore + revisore con modello diverso), non verificati da una persona.
  • Postgres in locale era la 16, la CI usa la 15.
  • La causa precisa del falso “ucciso” (mutante 11) nella run lenta non è stata accertata; nella run con fixture veloci il mutante risulta correttamente sopravvissuto.
  • 012-014: una feature ciascuno, logica pura, n = 3 per condizione, un solo modello (salvo il revisore Sonnet in C del 012); la classificazione delle domande dei revisori del 014 è dell’orchestratore.
  • 014 è la versione ridotta: riusa le implementazioni del 013, note all’orchestratore che ha scritto i prompt; nessuna replica nuova della condizione neutra.
  • 014, contaminazione possibile: l’intenzione nascosta del 013 era già pubblica nel repository. Ai tester è stato detto di non leggerlo e i loro comandi registrati non leggono file del repository, ma il controllo non è crittografico come in 012/013. Q3 coincide quasi del tutto con l’intenzione: compatibile con il caso, non escludibile.
  • Non testato: modelli di provider diversi (bloccato dalla configurazione della rete della sessione).

In breve: servirebbero test più intensivi — più feature, più repliche, modelli davvero indipendenti, spec meno curate — prima di dire che la tesi del primo articolo è confermata o smentita. Per ora ho un indizio forte (il ruolo conta più della separazione) e una lezione che non cercavo (il segnale vero era nella pipeline).

Poscritto. Quella lezione che non cercavo l’ho ritrovata subito dopo, in un contesto del tutto diverso: un altro agente (Codex), un altro strumento e una migrazione legacy PHP→Laravel, con di nuovo un gate che diceva «fatto» senza esserlo. Se il verde falso ti interessa più del mutation testing, il caso è qui: Dal PHP legacy alla parità verificabile.

Note pratiche sugli strumenti

  • mutmut 3.x: paths_to_mutate non limita i file mutati (alias deprecato di source_paths): il primo tentativo ha generato mutanti per tutta l’app (763 MB). Serve only_mutate.
  • mutmut 3 non muta le costanti a livello di modulo. Verificato con 8 mutanti scritti a mano dall’orchestratore: 8/8 uccisi, ma lo strumento da solo non l’avrebbe visto.
  • mutmut 3 su DB reale: timeout = (stima + 1) × 15, con una stima che (dai dati) non include le fixture; test ordinati per durata stimata; più processi richiedono un database per processo. Con fixture veloci il problema sparisce.
  • GitHub Actions: senza shell: bash esplicito lo script gira con bash -e senza pipefail; cmd | tee file nasconde il fallimento di cmd.

Citazioni (dall’agente test, tradotte dall’inglese)

  • “La spec richiede solo che sia sollevato ValueError, non il testo.”
  • “Con break una riga d’import malformata fa perdere silenziosamente tutti gli eventi successivi.”
  • “Verificare la formulazione dei messaggi sarebbe fragile e non proteggerebbe nulla su cui gli utenti fanno affidamento.”

Fonti nel repository

  • PR MartoEporedia/taita#679 (unita): feature, test, specs/010-school-days-mutation/ e specs/011-bridge-notifications-mutation/ con experiment.md.
  • PR MartoEporedia/taita#680 (unita): pipefail, test portati su Postgres, fixture veloci.
  • PR MartoEporedia/taita#681 (unita): specs/012-carpool-rotation/ e specs/013-expense-split/ con experiment.md.
  • PR MartoEporedia/taita#682 (aperta): specs/014-agent-roles/ con protocollo, domande dei revisori, suite, risultati.
#AI#agenti#testing#mutation testing#CI