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/<pid> 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
