work_item: ORCH-099 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 title: "FND/F1a — лёгкий read-only /metrics: стадии/очередь/agent-liveness/cost" framework: pytest scope: > Покрывается: структура ответа GET /metrics (4 раздела + конверт), never-raise по полям, read-only инвариант, agent-liveness сырьё (pid/runtime/cpu-тики), агрегаты cost/токенов, аддитивность (не сломаны /health//status//queue). Вне покрытия: сам sidecar (F1b), хостовые/контейнерные метрики, пороги/алерты/Telegram. Полный регресс tests/ остаётся зелёным. notes: > Тесты идут в новый tests/test_metrics.py. Используется существующий паттерн conftest.py (autouse fresh_db на tmp_path + init_db, monkeypatch send_telegram). Эндпоинт зовётся как корутина через asyncio.run(main.metrics()) по образцу tests/test_queue_endpoint.py (asyncio.run(main.queue())). Read-only проверяется сравнением снимка БД до/после вызова. Never-raise — monkeypatch источника (worker / helper БД / чтения /proc) на бросающий стаб. tests: - id: TC-01 type: unit description: "build_metrics() возвращает dict с ключами schema_version, generated_at, stages, queue, agents, cost (конверт FR-5)." module: tests/test_metrics.py expected: PASS - id: TC-02 type: unit description: "Раздел stages: для задачи со stage!=done/cancelled элемент содержит work_item, stage, age_in_stage_s (int), repo; терминальные задачи (done/cancelled) исключены." module: tests/test_metrics.py expected: PASS - id: TC-03 type: unit description: "Раздел queue: counts (queued/running/failed), max_concurrency, сырьё ретраев и breaker-снимок (state/consecutive_transient/pause_remaining_s) присутствуют." module: tests/test_metrics.py expected: PASS - id: TC-04 type: unit description: "Раздел agents: по running-job отдаются agent, run_id, job_id, pid, runtime_s и поле CPU-liveness сырья (cpu_ticks или эквивалент)." module: tests/test_metrics.py expected: PASS - id: TC-05 type: unit description: "agent-liveness never-raise: при pid=None или отсутствующем /proc/ CPU-поле = null, остальные поля агента и весь ответ целы (без исключения)." module: tests/test_metrics.py expected: PASS - id: TC-06 type: unit description: "Раздел cost.aggregate: суммы cost_usd/input_tokens/output_tokens/cache_read_tokens/cache_creation_tokens из agent_runs; пустая таблица -> нули, не ошибка." module: tests/test_metrics.py expected: PASS - id: TC-07 type: unit description: "Never-raise по разделу: если источник раздела (напр. job_status_counts/worker.status) бросает, раздел получает null/дефолт, build_metrics() не пробрасывает исключение." module: tests/test_metrics.py expected: PASS - id: TC-08 type: integration description: "GET /metrics через ASGI/обработчик возвращает 200 и валидный JSON со всеми разделами на засеянной БД (задача + running-job + agent_run)." module: tests/test_metrics.py expected: PASS - id: TC-09 type: integration description: "Read-only: снимок всех таблиц БД (tasks/jobs/agent_runs) до и после вызова /metrics идентичен; повторный вызов не меняет состояние." module: tests/test_metrics.py expected: PASS - id: TC-10 type: integration description: "Аддитивность: GET /health, /status, /queue сохраняют прежний контракт (ключи на месте) при наличии /metrics; существующие тесты эндпоинтов зелёные." module: tests/test_metrics.py expected: PASS - id: TC-11 type: unit description: "Пустое состояние: при отсутствии активных задач/running-jobs/agent_runs ответ валиден — stages=[], agents=[], cost.aggregate=нули, queue.counts с нулями; 200/без исключений." module: tests/test_metrics.py expected: PASS