From b2798ae0e88db11c395b7b30e2ac217d9d4c731e Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 11 Oct 2026 12:08:36 +0700 Subject: [PATCH] up --- docs/agent/TASKS.md | 9 ++++++--- tests/venue/README.md | 9 +++++++++ tests/venue/flow.py | 26 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/agent/TASKS.md b/docs/agent/TASKS.md index 2ed4db0..490e42b 100644 --- a/docs/agent/TASKS.md +++ b/docs/agent/TASKS.md @@ -27,13 +27,13 @@ ### ED-029 — Автономная площадка и панель организатора -- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–7 завершены; автоматическая часть этапа 8 завершена, ожидает аппаратной приёмки. +- Статус: `IN_PROGRESS` — этап 3 принят пользователем без WAN; этапы 4–9 завершены; аппаратная приёмка этапа 8 подтверждена пользователем. - Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`. - Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт со статусом офлайн-режима; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки. - Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия. - Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без несогласованных новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy и автоматического переключения серверов; настройки роутера менять только по явному запросу пользователя; секреты и пакеты вне Git; commit/push выполняет пользователь. - Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15. -- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; автоматическая часть этапа 8 проверена на изолированном стенде; аппаратная приёмка ожидается, этапы 9–15 ещё не завершены. +- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Этап 3 принят пользователем после проверки без WAN; формат и код пакета игры (этап 4) реализованы и проверены. Экспорт со статусом офлайн-режима (этап 5) и локальный импорт (этап 6) реализованы и проверены. Этап 7 реализован и проверен; этап 8 проверен на изолированном стенде и принят пользователем на аппаратной контрольной точке; этап 9 реализован и проверен; этапы 10–15 ещё не завершены. - Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`. - Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke. - Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась. @@ -68,7 +68,10 @@ - Результат автоматической части этапа 8: добавлены повторяемые `tests/venue/flow.py` и `flow_browser.py`, команды и ручной контрольный список в README проверок. Актуальный код собирается в `/tmp`; стенд запускает два настоящих backend prod/venue с отдельными PostgreSQL/RustFS в Docker internal-сети без WAN, SMTP направлен в локальный sink. API проверяет оба формата, экспорт/импорт ZIP, исходные ссылки/ID/пароли команд и билетов, статусы участия, QR/PDF, обычный/повторный/конкурентный приём билетов, отказ без авторизации и неверные ссылки, локальные команды/билеты, старт/ходы/выдачу материалов/паузу/продолжение/завершение, ответы и баллы, результаты, перезапуск всех venue-сервисов и повторный импорт без потери прогресса. Проверена независимость источника и площадки; ограничения формата `offline` сохранены. После проверки удаляются только ресурсы стенда; ошибка очистки одного ресурса не останавливает попытки очистить остальные. - Исправления этапа 8: общий gameplay-сервис проверяет `run` до записи хода и подтверждения участия, одинаково для API/MCP; безопасная ошибка отображается как FailedPrecondition. Кнопка хода блокируется вне `run` и при отправке; отказ показывается пользователю и сохраняет код. Кнопкам lifecycle добавлены доступные имена. Конкурентный приём билета теперь перечитывает текущее состояние и сообщает «билет уже использован», а не ошибочно «игра уже завершена». PostgreSQL-тест конкурентности синхронизирует запросы блокировкой строки: до исправления воспроизводил ошибку, после исправления проходит. - Проверки этапа 8: `python3 tests/venue/flow.py --browser` прошёл с реальными API/хранилищем и отдельным браузерным стендом. Firefox проверил полный UI-flow на 1440 px (`online`) и 390 px (`offline`), реальное скачивание/загрузку ZIP, билеты, ходы и polling, паузу с введённым кодом, двойную отправку, отказ с сохранением ввода, выдачу материала, оценивание/результаты, завершённый экран игрока, неверный пароль и клавиатурный focus в модалке. Внутри стенда прошли PostgreSQL-регрессии gameplay/lifecycle, tickets, MCP и app; отдельно прошёл полный `go test ./...`. Frontend type-check, lint, lint:css и сборка в `/tmp` прошли; синтаксис Python, `gofmt`, diff-проверки, ручное ревью по Go/frontend-стандартам и независимое ревью затронутого flow выполнены. API/protobuf, миграции и зависимости не менялись. Работающие окружения и Cudy не изменялись; существующие пользовательские изменения бинарника и `prod` сохранены. -- Следующий шаг: ручная аппаратная приёмка этапа 8 по `tests/venue/README.md` — прежние ссылки, билеты/QR, игровой flow на Android/iPhone с отключёнными WAN и мобильными данными. После подтверждения — этап 9, пакет результатов. Обновление реальной площадки, камера/реальные телефоны, нагрузка на 100 клиентов, покрытие Wi-Fi, ZIP на 1 ГиБ и возврат результатов этим прогоном не проверялись. До реализации возврата использовать только тестовые игры. Общая задача остаётся `IN_PROGRESS` и не переносится в архив. +- Приёмка этапа 8: пользователь подтвердил «Этап 8 принят» при согласовании следующего этапа. Отдельный повтор аппаратной проверки агентом не выполнялся. +- Результат этапа 9: добавлен серверный сервис подготовки и проверки ZIP результатов версии 1, без новых API/UI, protobuf, миграций и зависимостей. Read-only PostgreSQL-снимок `REPEATABLE READ` связывает завершённую импортированную venue-игру с ID/SHA-256 исходного пакета. Сохраняются итоговые команды/участие, исходные и новые билеты, явные удаления, пароли QR/хеши билетов, порядок и время ходов/MCP, материалы и их выдача, ответы и nullable баллы. Исходные ссылки пользователей восстанавливаются из реестра; локальные исполнители вынесены в отдельное пространство. Материалы ссылаются на исходный ZIP, без локальных адресов RustFS. Изменённый игровой сценарий/формат и неизвестные материалы отклоняются. Повторная подготовка создаёт новый снимок с актуальными оценками и не меняет игру; кодек проверяет состав, версию, размеры/CRC/SHA-256, лимиты JSON/ZIP, уникальность и связи. Контракт описан в backend `docs/GAME_PACKAGE.md`. +- Проверки этапа 9: полный `go test ./...`, unit-тесты кодека/снимка и `python3 tests/venue/flow.py --results` прошли. Изолированный PostgreSQL/RustFS-стенд проверил обе игры, связь с исходным ZIP, новые локальные записи, прогресс/выдачу/ответы/баллы, свежий снимок после оценивания, конкурентное оценивание, сохранность предыдущего пакета, повтор после перезапуска и отсутствие изменений исходных игр. Unit-тесты проверяют удаления, пустые наборы, повторные/неизвестные ходы/MCP, исходных и локальных исполнителей, повреждённые ZIP и связи, лимиты, отмену и ошибки записи. `gofmt`, Python-синтаксис, diff-проверки, ручное ревью по Go-стандарту и независимое read-only ревью выполнены. Frontend-код не менялся; его штатная сборка в `/tmp` прошла в составе стенда. Тестовые пакеты/контейнеры удалены; работающие окружения и Cudy не менялись. Commit/push и развёртывание не выполнялись. +- Следующий шаг: этап 10 — безопасное применение результатов онлайн с проверкой исходного экспорта, конфликтами, соответствиями локальных ID и идемпотентностью возврата. API/UI передачи результатов остаются следующим этапам. ZIP на 1 ГиБ, нагрузка на 100 клиентов и покрытие Wi-Fi этим прогоном не проверялись. До завершения возврата использовать только тестовые игры. Общая задача остаётся `IN_PROGRESS` и не переносится в архив. ### ED-024 — Общая дизайн-система фронтенда diff --git a/tests/venue/README.md b/tests/venue/README.md index 9f6e102..8f6364f 100644 --- a/tests/venue/README.md +++ b/tests/venue/README.md @@ -33,6 +33,7 @@ python3 tests/venue/dns.py --server 192.168.10.155 ```sh python3 tests/venue/flow.py python3 tests/venue/flow.py --browser +python3 tests/venue/flow.py --results ``` `flow.py` собирает актуальный Go/Vue-код во временном каталоге, поднимает @@ -62,6 +63,14 @@ python3 tests/venue/flow.py --browser backend, имитируя смену DNS; API-ответы не подменяются. Тестовые ссылки, ZIP, профиль Firefox и сборки остаются во временном каталоге до конца прогона. +`--results` дополнительно проверяет этап 9: серверный сервис собирает ZIP +результатов обеих завершённых игр из реальной тестовой БД. Проверяются привязка +к исходному ZIP, прогресс/материалы/баллы, новый снимок после оценивания, +конкурентное оценивание, сохранность предыдущего пакета, повтор после +перезапуска и отсутствие изменений исходных игр. Локальные ссылки RustFS +в пакеты не попадают. ZIP находятся только во временном каталоге с правами +0600; новые API/UI для возврата и применение онлайн не выполняются. + ### Ручная контрольная точка этапа 8 Автоматический прогон не заменяет проверку телефонов и камеры. После отдельного diff --git a/tests/venue/flow.py b/tests/venue/flow.py index d7497bb..2860945 100644 --- a/tests/venue/flow.py +++ b/tests/venue/flow.py @@ -19,6 +19,7 @@ import urllib.error import urllib.parse import urllib.request import uuid +import zipfile sys.dont_write_bytecode = True ROOT = Path(__file__).resolve().parents[2] @@ -371,6 +372,7 @@ def exercise(stand, data): def main(): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--results', action='store_true', help='Also verify stage 9 result packages before/after restart') parser.add_argument('--browser', action='store_true', help='Also run Firefox against the real APIs (Firefox required)') args = parser.parse_args() with tempfile.TemporaryDirectory(prefix='ed029-flow-') as directory: @@ -389,6 +391,30 @@ def main(): assert imported['gameID'] == data['game'] and not imported['repeated'] for data in fixtures: exercise(stand, data) + if args.results: + source_before = [stand.apis['prod'].game(data['game']) for data in fixtures] + venue_before = [stand.apis['venue'].game(data['game']) for data in fixtures] + for attempt in range(2): + if attempt: + stand.restart() + for data in fixtures: + with zipfile.ZipFile(stand.directory / f'game-{data["game"]}.zip') as archive: + manifest = json.loads(archive.read('manifest.json')) + result = run('env', f'TEST_DATABASE_URL=host={pg_address} user=test password=test-only dbname=test sslmode=disable', + f'ED029_RESULTS_GAME_ID={data["game"]}', f'ED029_RESULTS_PACKAGE_ID={manifest["package_id"]}', + f'ED029_RESULTS_DIR={stand.directory}', 'go', 'test', './internal/services/game_results_package_service', + '-run', 'TestVenueFlowResults', '-count=1', '-v', cwd=ROOT / 'evening_detective_server') + assert '--- PASS: TestVenueFlowResults' in result + with zipfile.ZipFile(stand.directory / f'results-{data["game"]}.zip') as archive: + result_manifest = json.loads(archive.read('manifest.json')) + payload = archive.read('results.json') + assert set(archive.namelist()) == {'manifest.json', 'results.json'} + assert result_manifest['source_sha256'] == hashlib.sha256(data['package']).hexdigest() + assert result_manifest['sha256'] == hashlib.sha256(payload).hexdigest() + assert b'game-import-' not in payload + assert [stand.apis['prod'].game(data['game']) for data in fixtures] == source_before + assert [stand.apis['venue'].game(data['game']) for data in fixtures] == venue_before + print('PASS: stage 9 real result packages, fresh grading, concurrent snapshots, restart and unchanged games', flush=True) finally: stand.close() if args.browser: