Ir para o conteúdo

Blog

Agente de terminal

Um gerador que perdeu metade da CLI silenciosamente

A referência de comandos deste site é gerada automaticamente. A primeira versão do gerador produzia um arquivo JSON limpo, ordenado e inteiramente convincente, com 53 subcomandos faltando — e não dava erro nenhum.

A CLI do Chimera tem 109 invocações: 56 comandos raiz mais 53 subcomandos distribuídos em 11 grupos. A documentação cobre o caminho feliz de uns dezessete deles, escrita por uma pessoa, que é a maneira certa de escrever um guia de início rápido e a maneira errada de manter uma referência exaustiva. Uma lista de flags copiada à mão está correta exatamente uma vez: no dia em que foi digitada.

Por isso a referência deste site é gerada a partir da própria CLI, do mesmo jeito que os tipos TypeScript do app desktop são gerados a partir do schema da API, com o mesmo gate de desvio no CI: regenerar, e falhar se a cópia commitada for diferente.

O bug

A primeira versão percorria a árvore de comandos assim:

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")

Essa checagem falhou. Não o percurso — a checagem. typer.main.get_command() retornava um objeto que não era instância do click.Group instalado, porque o Typer 0.27 vendoriza sua própria cópia do Click sob typer._click. TyperGroup herda de typer._click.core.Command, e o click que o seu próprio código importa é um objeto de classe inteiramente diferente.

Ter lançado exceção foi sorte. A versão anterior fazia a coisa óbvia em vez disso — tratar qualquer coisa que não fosse um click.Group como um comando folha — e essa não lançava nada. Ela produzia um arquivo JSON bem formado, ordenado e determinístico descrevendo 56 comandos, com cada grupo achatado em uma única entrada e os 53 subcomandos todos sumidos.

Por que essa é a pior falha possível

Um gerador que trava te avisa que falhou. Um gerador que produz uma saída plausível não te avisa nada, e tudo que vem depois herda esse silêncio. A página de referência teria renderizado perfeitamente. chimera kanban teria sido listado com seu texto de ajuda e sem subcomandos, o que tem exatamente a cara de um comando que não tem subcomandos. A busca teria indexado isso. Ninguém olhando para a página conseguiria detectar a ausência, porque ausência é exatamente a cara que uma página completa também tem.

A falha foi pega contando, não olhando: a referência deveria mostrar 109 invocações e mostrava 56.

O conserto, e o teste que sobrevive a ele

O dumper agora usa duck typing — ele pergunta se um objeto tem um mapeamento commands em vez de perguntar de que classe ele é. Isso sobrevive a um Click vendorizado, a um upgrade do Typer e a uma mudança de versão major do Click, porque "tem subcomandos" é uma propriedade da coisa em si, e "é um click.Group" é uma propriedade do grafo de import.

Mais útil que o conserto é o formato do teste. A asserção óbvia é que o dump teve sucesso:

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

A asserção que pega essa falha é sobre o que foi encontrado:

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

Qualquer teste que só afirma que um gerador rodou é um teste que passa enquanto o gerador está errado. O que vale a pena escrever afirma o que ele deveria ter encontrado.

Onde olhar

https://chimeraagent.space