Decisioni (strumenti, livelli, struttura): [ADR-0003](adr/0003-strategia-di-test.md). Qui: regola, flusso, soglie e **matrice requisito → test**, sorgente di verità per la copertura. Le spunte `[ ]` passano a `[x]` quando il test esiste e passa.
## Regola
**Nuova funzione, correzione o cambio di comportamento ⇒ test nello stesso commit.** Niente test, funzione non finita.
- Una nuova funzione aggiunge righe alla matrice qui sotto (con ID) prima di scrivere il codice.
- Una correzione di bug parte da un test che fallisce e lo lascia come regressione.
- Una modifica che cambia un requisito aggiorna prima il doc, poi la riga della matrice, poi i test, poi il codice.
- Una riga senza test non è copertura: va lasciata `[ ]` e conta come debito visibile.
## Flusso (estende il doc-first di [README.md](README.md))
Una cartella per livello, creata quando c'è il primo test; il livello si dichiara anche con `[Trait("Category", "Unit" | "Gui" | "Process" | "E2E")]`.
### FakeClaude
Piccolo eseguibile (`AssemblyName``claude`, nel PATH dei test) che prende il posto del vero motore. Un test lo configura con variabili d'ambiente del processo che avvia:
| Variabile | Effetto |
|---|---|
| `FAKE_CLAUDE_LOG` | file in cui accoda una riga JSON per invocazione: `{"args": [...], "cwd": "...", "stdin": "..."}` |
| `FAKE_CLAUDE_OUT` | testo scritto su stdout (anche per `--version`) |
| `FAKE_CLAUDE_DELAY_MS` | attesa prima di rispondere |
Legge stdin fino alla fine e lo registra: se il chiamante non lo chiude, il processo non termina (così CL-05 è verificabile). Il percorso del file compilato lo fornisce l'helper di test `FakeClaudePath`.
Esclusi dal calcolo: XAML generato, `Program.cs` (tre righe, coperte da E2E), `App.axaml.cs`. La soglia non sostituisce la matrice: copertura di righe alta con requisiti scoperti non basta.
## Matrice requisito → test
Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome del test è indicativo.
| CX-05 | Il parser legge `thread_id` da `thread.started` | U | `Codex_thread_id` | [ ] |
| CX-06 | La risposta è l'ultimo `agent_message`, anche con più messaggi | U | `Codex_ultimo_agent_message` | [ ] |
| CX-07 | `turn.failed`: messaggio interno di `error.message` estratto dal JSON annidato; exit ≠ 0 ⇒ errore | U | `Codex_errore_turno` | [ ] |
| CX-08 | Eventi `item.completed` di tipo `error` (avvisi) non fanno fallire la risposta | U | `Codex_avvisi_ignorati` | [ ] |
| CX-09 | Exit 0 senza `agent_message` ⇒ errore "nessuna risposta" | U | `Codex_nessuna_risposta` | [ ] |
| CX-10 | Righe non JSON o eventi sconosciuti sono ignorati senza eccezioni | U | `Codex_righe_non_json` | [ ] |
| CX-11 | In modalità CLI (output ereditato) non si usa `--json` | U | `Codex_cli_senza_json` | [ ] |
| CX-12 | Con un `FakeClaude` configurato come Codex il ciclo primo messaggio + ripresa funziona (id letto e riusato) | P | `Codex_ciclo_completo_con_finto` | [ ] |
| UP-07 | Metodo di installazione dedotto dal percorso reale (npm, Homebrew, WinGet, nativo) con collegamenti simbolici risolti | U | `Metodo_di_installazione` | [ ] |
| UP-08 | Comando manuale corretto per ogni coppia motore × metodo (tabella dell'ADR-0004) | U | `Comando_manuale` | [ ] |
| UP-09 | Un solo aggiornamento alla volta; durante una risposta in corso i pulsanti sono disabilitati e la chat non parte durante l'aggiornamento | U | `Aggiornamento_esclusivo` | [ ] |
| UP-10 | Dopo un aggiornamento riuscito le versioni mostrate si rileggono; la chat aperta resta invariata | U | `Versioni_rilette` | [ ] |
| UP-11 | Le righe di aggiornamento compaiono solo per i CLI installati | U/G | `Righe_solo_installati` | [ ] |
| UP-12 | Il pulsante Aggiorna del riquadro agisce solo sul CLI della sua riga | G | `Pulsante_per_riga` | [ ] |
| UP-13 | Nessun aggiornamento parte senza un clic | U | `Nessun_aggiornamento_automatico` | [ ] |
| UP-14 | Claude Code: l'ultima versione si legge dal testo di `…/claude-code-releases/latest` (spazi e a capo ripuliti) | U | `Ultima_versione_claude` | [ ] |
| UP-15 | Codex: l'ultima versione si legge da `tag_name` della release GitHub (`rust-v0.162.1` ⇒ 0.162.1); intestazione User-Agent inviata | U | `Ultima_versione_codex` | [ ] |
| UP-16 | Confronto: ultima > installata ⇒ disponibile; uguale o più nuova ⇒ aggiornato; versioni con testo attorno (`2.1.300 (Claude Code)`, `codex-cli 0.162.1`) lette bene | U | `Confronto_versioni` | [ ] |
| UP-17 | Rete assente, risposta non valida o timeout ⇒ stato "sconosciuto", nessuna eccezione, pulsante normale | U | `Controllo_senza_rete` | [ ] |
| UP-18 | Disponibile ⇒ riga con "Aggiornamento disponibile: X", pulsante evidenziato e puntino sul pulsante Impostazioni; aggiornato ⇒ nessuno dei tre | U/G | `Evidenziazione_aggiornamento` | [ ] |
| UP-19 | Il controllo parte all'avvio e all'apertura del riquadro solo se l'ultimo ha più di 5 minuti; dopo un aggiornamento riuscito si ripete; non blocca invio né interfaccia | U | `Quando_si_controlla` | [ ] |
| UP-20 | Il controllo non installa nulla e non modifica i CLI | P | `Controllo_in_sola_lettura` | [ ] |
| ST-01 | Salvataggio e rilettura: id, titolo, data, `started`, messaggi con ruolo e testo | U | `Salva_e_rileggi` | [ ] |
| ST-02 | Una chat senza messaggi non viene salvata | U | `Chat_vuota_non_salvata` | [ ] |
| ST-03 | Titolo = inizio del primo messaggio utente: 50 caratteri + "…", a capo sostituiti da spazi | U | `Titolo_troncato` | [ ] |
| ST-04 | L'id deve essere un Guid: un id con `..` o separatori non tocca il disco fuori dalla cartella | U | `Id_non_guid_rifiutato` | [ ] |
| ST-05 | L'elenco è ordinato dalla più recente | U | `Elenco_ordinato` | [ ] |
| ST-06 | File JSON corrotto ignorato senza eccezioni | U | `File_corrotto_ignorato` | [ ] |
| ST-07 | Eliminazione: rimuove solo il file di quella chat; id inesistente senza errori | U | `Elimina_chat` | [ ] |
| ST-08 | Compatibilità del formato: un JSON scritto dalla versione WinForms si legge | U | `Formato_storico_compatibile` | [ ] |
| ST-09 | Migrazione: la cartella di un nome precedente (`LlmRun`) si sposta nella nuova al primo avvio; se la nuova esiste non si tocca nulla | U | `Migrazione_da_nome_precedente` | [ ] |
| ST-10 | `AppInfo.Name` = `AssemblyName` del csproj | U | `Nome_da_assembly` | [ ] |
| ST-12 | Il JSON salva `Engine` e `SessionId`; un file senza questi campi si legge come chat di Claude Code con `SessionId` = id | U | `Motore_e_sessione_salvati_e_compatibilita` | [ ] |
| ST-11 | Errore di I/O nel salvataggio non perde la chat a schermo | U | vedi VM-12 | [ ] |
### Logica della chat — `Gui/MainViewModel.cs`
| ID | Requisito | Liv. | Test | |
|---|---|---|---|---|
| VM-01 | Senza chat salvate: schermata iniziale (`ShowEmpty`), elenco vuoto | U | `Avvio_senza_chat` | [ ] |
| VM-02 | All'avvio si riapre la chat più recente | U | `Avvio_riapre_ultima` | [ ] |
| VM-03 | `CanSend` falso se il testo è vuoto o solo spazi, o se occupato | U | `CanSend` | [ ] |
| VM-04 | Invio: il messaggio utente compare e viene **salvato prima** della risposta | U | `Messaggio_salvato_prima_della_risposta` | [ ] |
| VM-05 | Risposta riuscita: aggiunta come "ai", `started` diventa vero, il secondo invio usa `--resume` | U | `Risposta_e_ripresa` | [ ] |
| VM-06 | Risposta fallita (exit ≠ 0): messaggio "err" con codice e stderr; `started` resta falso e il tentativo successivo usa ancora `--session-id` | U | `Errore_non_avvia_la_sessione` | [ ] |
| VM-07 | `claude` assente: messaggio "'claude' non trovato nel PATH." | U | `Claude_assente_messaggio` | [ ] |
| VM-08 | Durante l'attesa: Invia e Nuova chat disabilitati; aprire/eliminare è ignorato | U | `Occupato_blocca_azioni` | [ ] |
| VM-20 | Valori iniziali: modello più piccolo del motore (Claude Code: Haiku 5.5; Codex: GPT-5.6 Luna) ed effort `low`; il primo invio passa `--model`/`--effort` (o `-m`/`-c model_reasoning_effort`) con quei valori | U | `Valori_iniziali` | [ ] |
| VM-14 | Eliminazione: con conferma rimuove; senza conferma non fa nulla; se è la chat aperta parte una nuova | U | `Elimina_con_e_senza_conferma` | [ ] |
| VM-16 | Barra laterale: il comando commuta `SidebarOpen` | U | `Barra_laterale` | [ ] |
| VM-17 | Copia: il comando passa il testo alla funzione degli appunti | U | `Copia_negli_appunti` | [ ] |
| VM-18 | Notifiche di proprietà: `HasMessages`, `ShowEmpty`, `CanSend`, `IsCustomModel` cambiano quando devono | U | `Notifiche_proprieta` | [ ] |
| VM-19 | Il titolo e la data dell'elenco riflettono l'ultimo scambio; la chat aperta è marcata attiva | U | `Elenco_chat_attiva` | [ ] |
### Renderer markdown — `Gui/MarkdownView.cs`
| ID | Requisito | Liv. | Test | |
|---|---|---|---|---|
| MD-01 | Sorgente vuota o nulla ⇒ nessun contenuto, senza errori | R | `Sorgente_vuota` | [ ] |
| MD-02 | Titoli h1–h6 con dimensioni decrescenti e peso semi-grassetto | R | `Titoli` | [ ] |
| MD-03 | Paragrafi; a capo morbido = spazio, a capo forzato = riga nuova | R | `A_capo` | [ ] |
| MD-04 | Grassetto, corsivo, barrato, anche annidati | R | `Enfasi` | [ ] |
| MD-05 | Codice inline in carattere monospazio con sfondo | R | `Codice_inline` | [ ] |
| MD-06 | Blocco di codice: monospazio, scorrimento orizzontale, pulsante Copia che copia solo il codice | R/G | `Blocco_codice_e_copia` | [ ] |
| MD-07 | Liste puntate e numerate (rispetta il numero iniziale), annidate | R | `Liste` | [ ] |
| MD-08 | Citazioni con barra laterale, anche annidate | R | `Citazioni` | [ ] |
| MD-09 | Tabelle: intestazione in evidenza, allineamenti sinistra/centro/destra, righe irregolari | R | `Tabelle` | [ ] |
| MD-10 | Linea orizzontale | R | `Linea_orizzontale` | [ ] |
| MD-11 | Link: colore d'accento e sottolineatura; il clic apre solo `http(s)` | R | `Link_http_https` | [ ] |
| MD-12 | Link: nessuna apertura se c'è una selezione in corso; cursore a mano sopra il link | R | `Link_con_selezione` | [ ] |
| MD-13 | Posizioni dei link corrette con enfasi, codice inline e a capo prima del link | R | `Intervalli_link` | [ ] |
| MD-14 | Immagini con URL `http(s)` o altro schema: si mostra il testo alternativo, nessuna richiesta di rete | R | `Immagini_remote_come_testo` | [x] |
| MD-19 | Immagine con percorso assoluto di un file png valido ⇒ controllo immagine con le dimensioni attese (ADR-0006) | R | `Immagine_percorso_assoluto` | [x] |
| MD-20 | Percorso relativo risolto rispetto a `AppContext.BaseDirectory`; anche URI `file://` | R | `Immagine_percorso_relativo_e_file_uri` | [x] |
| MD-21 | File mancante, illeggibile, corrotto o con estensione non ammessa ⇒ testo alternativo, nessuna eccezione | R | `Immagine_non_valida_ripiego` | [x] |
| MD-22 | File oltre il limite di 20 MB ⇒ testo alternativo, file non decodificato | R | `Immagine_oltre_limite` | [x] |
| MD-23 | Immagine più larga della colonna ⇒ ridotta in larghezza mantenendo le proporzioni; mai ingrandita oltre l'originale | R | `Immagine_larghezza_massima` | [x] |
| MD-24 | Il testo alternativo è nome accessibile e suggerimento dell'immagine | R | `Immagine_testo_alternativo_accessibile` | [x] |
| MD-25 | Immagine dentro un link, in una lista o in una tabella ⇒ resa senza rompere intervalli dei link (MD-13) né il layout | R | `Immagine_in_contesti_annidati` | [x] |
| MD-26 | Cambio di tema con immagini presenti ⇒ vista ricostruita, immagini ancora mostrate | R | `Immagine_cambio_tema` | [x] |
| MD-27 | Il caricamento è in background: subito dopo il rendering c'è il segnaposto `[alt]` e un'immagine in attesa; a fine decodifica compare l'immagine; un file corrotto lascia `[alt]` | R | `Immagine_caricamento_in_background` | [x] |
| MD-28 | Percorso relativo non presente nella cartella dell'exe ma in una cartella superiore (radice del progetto, es. `<cartella>/<file>` con l'exe in una sottocartella) ⇒ immagine mostrata | R | `Immagine_percorso_relativo_alla_radice` | [x] |
| MD-29 | Se il file esiste in più cartelle, vince la più vicina all'exe; se non esiste in nessuna ⇒ testo alternativo | R | `Immagine_cartella_piu_vicina` | [x] |
| MD-30 | Risposta con immagine e, sotto, didascalia e link in markdown: l'immagine compare e la didascalia resta testo; l'app non legge nessun catalogo | R | `Immagine_risposta_con_didascalia` | [x] |
| MD-31 | Formula inline `$…$` ⇒ controllo formula inserito nel testo; il testo prima e dopo resta intatto e non sparisce più (ADR-0008) | R | `Formula_inline` | [x] |
| MD-32 | Formula a blocco `$$…$$` ⇒ controllo formula (non un blocco di codice col sorgente) | R | `Formula_a_blocco` | [x] |
| MD-33 | LaTeX non valido (inline e a blocco) ⇒ sorgente come testo, nessuna eccezione | R | `Formula_non_valida_ripiego` | [x] |
| MD-34 | Costrutti LaTeX comuni (frazioni, apici e pedici, `\mathbf`, `\overline`, `\times`, lettere greche, `\left…\right`, `\sum`) ⇒ nessun errore | R | `Costrutti_latex_comuni` | [x] |
| MD-35 | Cambio di tema con formule presenti ⇒ vista ricostruita, formule ancora mostrate | R | `Formula_cambio_tema` | [x] |
| MD-36 | Formula dentro una lista, una tabella o un link ⇒ resa senza rompere il layout né gli intervalli dei link (MD-13) | R | `Formula_in_contesti_annidati` | [x] |
| MD-37 | Il nome accessibile della formula è il suo sorgente LaTeX | R | `Formula_nome_accessibile` | [x] |
| MD-38 | Simboli `$` isolati (es. prezzi «10 $ e 20 $») restano testo, nessuna formula | R | `Dollari_isolati_restano_testo` | [x] |
| UI-01 | La finestra si apre e mostra la schermata iniziale (logo, titolo, sottotitolo, nessun suggerimento) | G | `Apertura_schermata_iniziale` | [ ] |
| UI-16 | Il pulsante Impostazioni (in alto a destra) apre il riquadro con Modello ed Effort; `Esc` lo chiude | G | `Riquadro_impostazioni` | [ ] |
| UI-17 | Il composer non contiene selettori di modello ed effort | G | `Composer_senza_selettori` | [ ] |
| UI-18 | Le scelte fatte nel riquadro arrivano ai flag `--model`/`--effort` del messaggio successivo | G/U | `Impostazioni_applicate_all_invio` | [ ] |
| WIN-01 | L'eseguibile `win-x64` pubblicato ha sottosistema "Windows GUI" (intestazione PE); quello `linux-x64` non è interessato | E | `Sottosistema_gui` | [ ] |
| WIN-02 | CLI con output reindirizzato su file o pipe: l'output arriva, handle ereditati non toccati | E | `Cli_con_output_reindirizzato` | [ ] |
| WIN-03 | CLI da un terminale: output visibile (`AttachConsole` + `CONOUT$`); senza terminale padre l'attacco fallisce in silenzio e l'app prosegue | E (manuale su Windows nativo) | `Cli_con_terminale_padre` | [ ] |
| WIN-04 | Exit code corretto quando il chiamante attende il processo (`start /wait`, `Start-Process -Wait`) | E (manuale su Windows nativo) | `ExitCode_con_attesa` | [ ] |
| WIN-05 | Il codice di `ConsoleHost` non fa nulla su Linux/macOS | U | `ConsoleHost_non_windows` | [ ] |
| EN-11 | Cambiare CLI nelle Impostazioni apre una nuova chat con quel motore; modello ed effort tornano ai valori iniziali del nuovo motore (modello più piccolo, `low`); liste secondo il motore | U | [ ] |
| EN-12 | Motore non installato: etichetta "non installato", non selezionabile; il VM rifiuta la selezione | U/G | [ ] |
| EN-13 | L'interruttore CLI è disabilitato durante l'attesa | U/G | [ ] |
| EN-14 | Aprendo una chat salvata il selettore passa al motore della chat; sessione e `started` coerenti | U | [ ] |
| EN-15 | Etichetta autore, segnaposto, messaggio d'attesa ed elenco chat mostrano il nome del motore | U/G | [ ] |
| EN-16 | Dopo il primo invio con Codex il `SessionId` è quello letto dall'output e il secondo invio usa la ripresa; con errore al primo invio resta senza sessione | U | [ ] |
| EN-17 | CLI `--engine <id> "prompt"`; id sconosciuto o prompt mancante ⇒ uso su stderr ed exit 2; senza `--engine` ⇒ Claude Code | E | [ ] |
| EN-18 | Rilevamento nel PATH: nome esatto su Linux, `.exe` su Windows; cartella inesistente nel PATH ignorata | U | [ ] |
| FK-01 | Registra argomenti (anche con virgolette e spazi, uno per elemento), cartella di lavoro e stdin | P | `Registra_argomenti_cwd_e_stdin` | [x] |
| FK-02 | Scrive su stdout e stderr il testo configurato | P | `Output_e_stderr_configurati` | [x] |
| FK-03 | L'exit code configurato è quello del processo | P | `ExitCode_configurato` | [x] |
| FK-04 | Il ritardo configurato è rispettato | P | `Ritardo_configurato` | [x] |
| FK-05 | Termina solo dopo la chiusura di stdin | P | `Attende_la_chiusura_di_stdin` | [x] |