Vai al contenuto

Blog

Agente da terminale

Un generatore che ha silenziosamente perso metà della CLI

Il riferimento dei comandi su questo sito è generato. La prima versione del generatore produceva un file JSON pulito, ordinato, del tutto convincente, a cui mancavano 53 sottocomandi — e non generava alcun errore.

La CLI di Chimera ha 109 invocazioni: 56 comandi radice più 53 sottocomandi distribuiti in 11 gruppi. La documentazione copre il percorso felice per una diciassettina di essi, scritta da un umano, che è il modo giusto di scrivere una guida introduttiva e il modo sbagliato di mantenere un riferimento esaustivo. Un elenco di flag copiato a mano è corretto esattamente una volta — il giorno in cui è stato scritto.

Perciò il riferimento su questo sito viene generato direttamente dalla CLI, allo stesso modo in cui i tipi TypeScript dell'app desktop vengono generati dallo schema dell'API, con lo stesso gate anti-drift in CI: rigenera, e fallisci se la copia committata differisce.

Il bug

La prima versione attraversava l'albero dei comandi così:

root = typer.main.get_command(cli_app)
if not isinstance(root, click.Group):
    raise TypeError("the Chimera CLI is expected to be a command group")

Quel controllo falliva. Non l'attraversamento — il controllo. typer.main.get_command() restituiva un oggetto che non era un'istanza del click.Group installato, perché Typer 0.27 include una propria copia vendorizzata di Click sotto typer._click. TyperGroup eredita da typer._click.core.Command, e il click importato dal tuo stesso codice è un oggetto classe completamente diverso.

Sollevare un'eccezione è stata fortuna. La versione precedente faceva invece la cosa ovvia — trattare tutto ciò che non è un click.Group come un comando foglia — e quella non sollevava nulla. Produceva un file JSON ben formato, ordinato, deterministico, che descriveva 56 comandi, con ogni gruppo appiattito in una singola voce e tutti i 53 sottocomandi scomparsi.

Perché questo è il peggior fallimento possibile

Un generatore che va in crash ti dice che ha fallito. Un generatore che produce un output plausibile non ti dice niente, e tutto ciò che viene dopo eredita quel silenzio. La pagina di riferimento si sarebbe renderizzata splendidamente. chimera kanban sarebbe stato elencato con il suo testo di help e nessun sottocomando, il che si legge esattamente come un comando che non ha sottocomandi. La ricerca lo avrebbe indicizzato. Nessuno guardando la pagina avrebbe potuto rilevare l'assenza, perché l'assenza è anche l'aspetto di una pagina completa.

Il fallimento è stato scoperto contando, non guardando: il riferimento dovrebbe mostrare 109 invocazioni e ne mostrava 56.

La correzione, e il test che le sopravvive

Il dumper ora è duck-typed — chiede se un oggetto ha una mappa commands, non a quale classe appartiene. Questo sopravvive a un Click vendorizzato, a un aggiornamento di Typer e a una major version di Click, perché "ha sottocomandi" è una proprietà della cosa, mentre "è un click.Group" è una proprietà del grafo degli import.

Più utile della correzione è la forma del test. L'asserzione ovvia è che il dump sia riuscito:

def test_dump_works():
    assert build()  # passes with 53 subcommands missing

L'asserzione che intercetta questo fallimento riguarda cosa è stato trovato:

def test_finds_the_groups_and_not_just_the_leaves():
    groups = [c for c in build()["commands"] if "commands" in c]
    assert len(groups) >= 10

Qualsiasi test che si limita ad asserire che un generatore sia stato eseguito è un test che passa anche quando il generatore è sbagliato. Quello che vale la pena scrivere asserisce cosa doveva trovare.

Dove guardare

https://chimeraagent.space