$ uv run tools/flow.py push answers/79-lab01-challenge-health.json
! unpublished '79 Ops facts (reference)' (w91aa244110abf96): it used the same webhook path
✔ pushed '79 Ops facts + health (answer)' (id wed38f213f1753a6)
   POST http://localhost:5678/webhook/my-ops-facts

$ uv run tools/flow.py test cases/lab01-challenge.json -v
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.1s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
    → request  POST http://localhost:8800/ops-reset
      {}
    ← response HTTP 200
      {
        "seeded_at": "2026-11-04T08:30:00",
        "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
        "counts": {
          "alerts": 6,
          "dlq": 3,
          "quality_log": 4,
          "quarantine": 1,
          "usage": 192
        }
      }
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
✔ 지난주 영역별 상태 · 1.1s
    → request  POST /webhook/my-ops-facts
      {}
    ← response HTTP 200
      {
        "period": {
          "from": "2026-10-26",
          "to": "2026-10-30"
        },
        "runs": {
          "total": 357,
          "errors": 21,
          "error_rate": 0.0588,
          "p95_ms": 2598,
          "unanswered_rate": 0.0476,
          "top_failure": {
            "node": "Search FAQ",
            "message": "timeout of 1000ms exceeded",
            "count": 16
          }
        },
        "alerts": {
          "count": 6,
          "high": 3,
          "by_reason": {
            "timeout": 1,
            "rate_limit": 1,
            "batch_rejected": 1,
            "low_quality": 2,
            "api_error": 1
          }
        },
        "dlq": {
          "size": 3,
          "parked": 2,
          "waiting": 1
        },
        "quality": {
          "batches": 4,
          "avg_score": 0.6583,
          "last_score": 1,
          "rejected_batches": 1,
          "quarantine": 1
        },
        "cost": {
          "requests": 156,
          "calls": 192,
          "cost_usd": 0.073604,
          "daily_usd": 0.08,
          "used_ratio": 0.9201,
          "top_user": {
            "user": "test-bot",
            "cost_usd": 0.027228,
            "share": 0.3699
          }
        },
        "release": {
          "active": "1.1.0",
          "canary_release": "1.2.0",
          "canary_percent": 20,
          "active_repeat_rate": 0.0773,
          "canary_repeat_rate": 0.625
        },
        "health": {
          "runs": "red",
          "alerts": "yellow",
          "dlq": "red",
          "quality": "yellow",
          "cost": "yellow",
          "release": "red"
        },
        "red": [
          "runs",
          "dlq",
          "release"
        ],
        "yellow": [
          "alerts",
          "quality",
          "cost"
        ],
        "overall": "red"
      }
✔ 빨강 · 노랑 목록 (정해진 순서) · 0.3s
    → request  POST /webhook/my-ops-facts
      {}
    ← response HTTP 200
      {
        "period": {
          "from": "2026-10-26",
          "to": "2026-10-30"
        },
        "runs": {
          "total": 357,
          "errors": 21,
          "error_rate": 0.0588,
          "p95_ms": 2598,
          "unanswered_rate": 0.0476,
          "top_failure": {
            "node": "Search FAQ",
            "message": "timeout of 1000ms exceeded",
            "count": 16
          }
        },
        "alerts": {
          "count": 6,
          "high": 3,
          "by_reason": {
            "timeout": 1,
            "rate_limit": 1,
            "batch_rejected": 1,
            "low_quality": 2,
            "api_error": 1
          }
        },
        "dlq": {
          "size": 3,
          "parked": 2,
          "waiting": 1
        },
        "quality": {
          "batches": 4,
          "avg_score": 0.6583,
          "last_score": 1,
          "rejected_batches": 1,
          "quarantine": 1
        },
        "cost": {
          "requests": 156,
          "calls": 192,
          "cost_usd": 0.073604,
          "daily_usd": 0.08,
          "used_ratio": 0.9201,
          "top_user": {
            "user": "test-bot",
            "cost_usd": 0.027228,
            "share": 0.3699
          }
        },
        "release": {
          "active": "1.1.0",
          "canary_release": "1.2.0",
          "canary_percent": 20,
          "active_repeat_rate": 0.0773,
          "canary_repeat_rate": 0.625
        },
        "health": {
          "runs": "red",
          "alerts": "yellow",
          "dlq": "red",
          "quality": "yellow",
          "cost": "yellow",
          "release": "red"
        },
        "red": [
          "runs",
          "dlq",
          "release"
        ],
        "yellow": [
          "alerts",
          "quality",
          "cost"
        ],
        "overall": "red"
      }
✔ 첫 주: 오류는 없지만 답 못 함 · 카나리 비교 불가 · 0.1s
    → request  POST /webhook/my-ops-facts
      {
        "from": "2026-10-19",
        "to": "2026-10-23"
      }
    ← response HTTP 200
      {
        "period": {
          "from": "2026-10-19",
          "to": "2026-10-23"
        },
        "runs": {
          "total": 309,
          "errors": 0,
          "error_rate": 0,
          "p95_ms": 2525,
          "unanswered_rate": 0.055,
          "top_failure": null
        },
        "alerts": {
          "count": 6,
          "high": 3,
          "by_reason": {
            "timeout": 1,
            "rate_limit": 1,
            "batch_rejected": 1,
            "low_quality": 2,
            "api_error": 1
          }
        },
        "dlq": {
          "size": 3,
          "parked": 2,
          "waiting": 1
        },
        "quality": {
          "batches": 4,
          "avg_score": 0.6583,
          "last_score": 1,
          "rejected_batches": 1,
          "quarantine": 1
        },
        "cost": {
          "requests": 156,
          "calls": 192,
          "cost_usd": 0.073604,
          "daily_usd": 0.08,
          "used_ratio": 0.9201,
          "top_user": {
            "user": "test-bot",
            "cost_usd": 0.027228,
            "share": 0.3699
          }
        },
        "release": {
          "active": "1.1.0",
          "canary_release": "1.2.0",
          "canary_percent": 20,
          "active_repeat_rate": null,
          "canary_repeat_rate": null
        },
        "health": {
          "runs": "yellow",
          "alerts": "yellow",
          "dlq": "red",
          "quality": "yellow",
          "cost": "yellow",
          "release": "yellow"
        },
        "red": [
          "dlq"
        ],
        "yellow": [
          "runs",
          "alerts",
          "quality",
          "cost",
          "release"
        ],
        "overall": "red"
      }
✔ before: POST http://localhost:8800/budget · HTTP 200 · 0.0s · {"daily_usd": 1.0, "user_limit_per_min": 20, "max_question_chars": 500, "used_usd": 0.073604, "remaining_usd": 0.926396,
    → request  POST http://localhost:8800/budget
      {
        "daily_usd": 1
      }
    ← response HTTP 200
      {
        "daily_usd": 1.0,
        "user_limit_per_min": 20,
        "max_question_chars": 500,
        "used_usd": 0.073604,
        "remaining_usd": 0.926396,
        "over": false
      }
✔ 예산을 늘리면 비용은 초록 · 0.1s
    → request  POST /webhook/my-ops-facts
      {}
    ← response HTTP 200
      {
        "period": {
          "from": "2026-10-26",
          "to": "2026-10-30"
        },
        "runs": {
          "total": 357,
          "errors": 21,
          "error_rate": 0.0588,
          "p95_ms": 2598,
          "unanswered_rate": 0.0476,
          "top_failure": {
            "node": "Search FAQ",
            "message": "timeout of 1000ms exceeded",
            "count": 16
          }
        },
        "alerts": {
          "count": 6,
          "high": 3,
          "by_reason": {
            "timeout": 1,
            "rate_limit": 1,
            "batch_rejected": 1,
            "low_quality": 2,
            "api_error": 1
          }
        },
        "dlq": {
          "size": 3,
          "parked": 2,
          "waiting": 1
        },
        "quality": {
          "batches": 4,
          "avg_score": 0.6583,
          "last_score": 1,
          "rejected_batches": 1,
          "quarantine": 1
        },
        "cost": {
          "requests": 156,
          "calls": 192,
          "cost_usd": 0.073604,
          "daily_usd": 1,
          "used_ratio": 0.0736,
          "top_user": {
            "user": "test-bot",
            "cost_usd": 0.027228,
            "share": 0.3699
          }
        },
        "release": {
          "active": "1.1.0",
          "canary_release": "1.2.0",
          "canary_percent": 20,
          "active_repeat_rate": 0.0773,
          "canary_repeat_rate": 0.625
        },
        "health": {
          "runs": "red",
          "alerts": "yellow",
          "dlq": "red",
          "quality": "yellow",
          "cost": "green",
          "release": "red"
        },
        "red": [
          "runs",
          "dlq",
          "release"
        ],
        "yellow": [
          "alerts",
          "quality"
        ],
        "overall": "red"
      }
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
    → request  POST http://localhost:8800/dlq/update
      {
        "dlq_id": 1,
        "remove": true
      }
    ← response HTTP 200
      {
        "removed": true,
        "size": 2
      }
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 1}
    → request  POST http://localhost:8800/dlq/update
      {
        "dlq_id": 2,
        "remove": true
      }
    ← response HTTP 200
      {
        "removed": true,
        "size": 1
      }
✔ parked를 처리하면 DLQ는 노랑 · 0.1s
    → request  POST /webhook/my-ops-facts
      {}
    ← response HTTP 200
      {
        "period": {
          "from": "2026-10-26",
          "to": "2026-10-30"
        },
        "runs": {
          "total": 357,
          "errors": 21,
          "error_rate": 0.0588,
          "p95_ms": 2598,
          "unanswered_rate": 0.0476,
          "top_failure": {
            "node": "Search FAQ",
            "message": "timeout of 1000ms exceeded",
            "count": 16
          }
        },
        "alerts": {
          "count": 6,
          "high": 3,
          "by_reason": {
            "timeout": 1,
            "rate_limit": 1,
            "batch_rejected": 1,
            "low_quality": 2,
            "api_error": 1
          }
        },
        "dlq": {
          "size": 1,
          "parked": 0,
          "waiting": 1
        },
        "quality": {
          "batches": 4,
          "avg_score": 0.6583,
          "last_score": 1,
          "rejected_batches": 1,
          "quarantine": 1
        },
        "cost": {
          "requests": 156,
          "calls": 192,
          "cost_usd": 0.073604,
          "daily_usd": 1,
          "used_ratio": 0.0736,
          "top_user": {
            "user": "test-bot",
            "cost_usd": 0.027228,
            "share": 0.3699
          }
        },
        "release": {
          "active": "1.1.0",
          "canary_release": "1.2.0",
          "canary_percent": 20,
          "active_repeat_rate": 0.0773,
          "canary_repeat_rate": 0.625
        },
        "health": {
          "runs": "red",
          "alerts": "yellow",
          "dlq": "yellow",
          "quality": "yellow",
          "cost": "green",
          "release": "red"
        },
        "red": [
          "runs",
          "release"
        ],
        "yellow": [
          "alerts",
          "dlq",
          "quality"
        ],
        "overall": "red"
      }
Passed 5/5
report: runs/lab01-challenge-20261101-185142.json
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
    → request  POST http://localhost:8800/ops-reset
      {}
    ← response HTTP 200
      {
        "seeded_at": "2026-11-04T08:30:00",
        "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)",
        "counts": {
          "alerts": 6,
          "dlq": 3,
          "quality_log": 4,
          "quarantine": 1,
          "usage": 192
        }
      }

$ uv run tools/flow.py test cases/lab01.json
✔ before: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
POST http://localhost:5678/webhook/my-ops-facts · 5 case(s) × 1
✔ 지난주 실행 (기본 기간) · 0.1s
✔ 지금 상태 · 알림 · DLQ · 품질 · 0.1s
✔ 비용 · 릴리스 · 0.1s
✔ 첫 주 (기간을 바꾸면 실행만 바뀜) · 0.1s
✔ before: POST http://localhost:8800/dlq/update · HTTP 200 · 0.0s · {"removed": true, "size": 2}
✔ 상태가 바뀌면 숫자도 (DLQ #3 처리) · 0.1s
Passed 5/5
report: runs/lab01-20261101-185143.json
✔ after: POST http://localhost:8800/ops-reset · HTTP 200 · 0.0s · {"seeded_at": "2026-11-04T08:30:00", "note": "79일차 운영 아침 상태 (연습 데이터, seed 79)", "counts": {"alerts": 6, "dlq": 3, "quali
