Перейти к содержимому

Блог

Агент в терминале

Генератор, который тихо потерял половину командной строки

Справочник команд на этом сайте генерируется. Первая версия генератора выдала аккуратный, отсортированный и совершенно убедительный JSON, в котором не хватало 53 подкоманд, — и не выдала ни одной ошибки.

У командной строки Chimera 109 вызовов: 56 корневых команд плюс 53 подкоманды в 11 группах. Документация описывает удачный путь примерно для семнадцати из них и написана человеком — это правильный способ писать руководство для начинающих и неправильный способ поддерживать исчерпывающий справочник. Список флагов, скопированный руками, верен ровно один раз — в день, когда его набрали.

Поэтому справочник на этом сайте генерируется из самой командной строки — так же, как типы TypeScript для приложения генерируются из схемы API, и с тем же заслоном от расхождения в CI: перегенерировать и упасть, если закоммиченная копия отличается.

Ошибка

Первая версия обходила дерево команд так:

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

Не прошла проверка. Не обход — именно проверка. typer.main.get_command() вернул объект, который не является экземпляром установленного click.Group, потому что Typer 0.27 везёт с собой собственную копию Click под именем typer._click. TyperGroup наследуется от typer._click.core.Command, а click, который импортирует ваш собственный код, — это совершенно другой объект-класс.

То, что она подняла исключение, было везением. Версия до неё поступала очевидным образом — считала листовой командой всё, что не является click.Group, — и вот та не падала. Она выдавала правильно оформленный, отсортированный, детерминированный JSON с описанием 56 команд, где каждая группа была сплющена в одну запись, а все 53 подкоманды исчезли.

Почему это худший из возможных отказов

Генератор, который падает, сообщает вам, что он не справился. Генератор, который выдаёт правдоподобный результат, не сообщает ничего, и всё, что стоит ниже по течению, наследует это молчание. Страница справочника отрисовалась бы прекрасно. chimera kanban значилась бы со своим текстом справки и без подкоманд, что читается ровно как команда, у которой подкоманд нет. Поиск бы её проиндексировал. Никто, глядя на страницу, не смог бы заметить отсутствие, потому что полная страница выглядит точно так же.

Отказ поймали счётом, а не взглядом: справочник должен показывать 109 вызовов, а показывал 56.

Исправление и тест, который его переживёт

Теперь выгрузчик работает по утиной типизации — он спрашивает, есть ли у объекта отображение commands, а не какого он класса. Это переживёт и вложенный Click, и обновление Typer, и смену мажорной версии Click, потому что «есть подкоманды» — свойство самой вещи, а «является click.Group» — свойство графа импортов.

Полезнее самого исправления — форма теста. Очевидное утверждение состоит в том, что выгрузка удалась:

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

А утверждение, которое ловит этот отказ, говорит о том, что было найдено:

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

Любой тест, утверждающий, что генератор отработал, — это тест, который проходит, пока генератор ошибается. Писать стоит тот, который утверждает, что именно генератор должен был найти.

Куда смотреть

https://chimeraagent.space