lct-42: стейл-ответ SELECT, пришедший после конкурентного invalidate_login
на том же узле, откатывал поколение назад и снова принимал отозванную
cookie. После ожидания перепроверяем кэш и не перезаписываем более свежее
значение. Детерминированный тест гонки; SCALE-OUT.md отделяет старые
замеры от непроверенного кластерным смоуком изменения.
lct-42: узел кластера принимал вход другого узла первую секунду за отзыв
и рвал сокет FORBIDDEN. Неизвестный логин теперь проверяется разовым
SELECT auth_version, результат кэшируется, параллельные запросы одного
логина дедуплицируются; логина нет в users или БД недоступна — как раньше.
Продукт сместился на упражнение ДДС с готовой карточкой, а часть
документов описывала голосовой тренажёр, LLM-судью и несуществующие
модули. Всё сверено с кодом.
- tasks/README.md: статусы карточек по коду, lct-13 снята
- arch: дерево модулей BACKEND/FRONTEND по коду, STT по умолчанию
Whisper small, события и маршруты CONTRACT по events.py и API
- product: разбор, методика, режимы и звонящий без судьи и
нереализованных функций (автопроигрывание эталона, ЭРА-ГЛОНАСС)
- spec, hackathon: нормативы настраиваемые, формат сценария по схеме,
питч под ДДС, исторические решения помечены датой
- удалены посторонний 1.md и отработанные гайды lct-01 из корня
Перенесены все 32 билета учебного центра, по три вызова в каждом. Это тот
материал, по которому заказчик учит операторов сегодня, — библиотека больше
не состоит из придуманных нами ситуаций.
Состав: медицина 23, пожары 19, полиция 16, ЖКХ 5, прочее 33. Исходы:
карточка 86, передача в другой регион 8, справка 2. С уточнением адреса —
29 вызовов. Уровни L1/L2/L3 — 11/38/47.
Написаны чек-листы, которых не было: medical, police, utility.
Признаки ЕКП подбирались под каждый вызов и проверены загрузчиком: комбинация,
которой нет в классификаторе, роняет загрузку. Несколько раз это и случилось,
и ошибка называла, чего не хватает.
Эталонных ответов в сценариях намеренно нет: в билетах их нет тоже, их знает
преподаватель, а наш эталон выводится кодом из фактов. Факты каждого вызова —
ровно то, что названо в строке билета.
193 теста зелёных, make seed заливает 97 сценариев.
Целый раздел ТЗ, от которого не было ни одной функции.
- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
классификатора, число живых занятий. Отдельной строкой — секрет сессии:
значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
а если его нет — из контейнера базы; если нет ни того ни другого,
администратор видит, чего не хватает, а кнопка не молчит.
Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.
Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.
193 теста зелёных (12 новых).
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
Девять вызовов из билетов учебного центра в scenarios/tickets/. Библиотека
стала вложенной: загрузчик ходит по подкаталогам, checklists/ и pregenerated/
сценариями не считаются. Поля ticket и position задаются вместе.
Написаны чек-листы, которых не было: medical, police, utility. Без них
переносить было не на что — в библиотеке существовал только пожарный.
Девять вызовов закрывают все приёмы, ради которых билеты и брались: адрес
по ориентиру, адрес из другого региона, линейный объект (съезд с МКАД),
непрофильный вызов, профильный но не экстренный, массовая драка, где список
оповещения поднимает и полицию, и скорую.
Подводный камень записан в SCENARIO-FORMAT: значения признаков ЕКП бывают
с запятыми («Человек в воде, на льдине»), и в короткой записи YAML разрывает
такое значение на два.
Осталось семь билетов — работа механическая, рецепт отработан на трёх.
161 тест зелёный.