$ uv run tools/flow.py test cases/replay-normal.json -v
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 1.2}
    → request  POST http://localhost:8800/admin/fault
      {
        "faq": "ok"
      }
    ← response HTTP 200
      {
        "faq": "ok",
        "tickets": "ok",
        "prompts": "ok",
        "usage": "ok",
        "dlq": "ok",
        "slow_seconds": 1.2
      }
POST http://localhost:5678/webhook/my-helpdesk · 10 case(s) × 1
✔ normal · 회의실 와이파이가 안  · 2.3s
    → request  POST /webhook/my-helpdesk
      {
        "question": "회의실 와이파이가 안 잡혀요",
        "user": "이준호",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "와이파이",
        "answer": "회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다.",
        "error": null
      }
✔ normal · 사내 Wi-Fi 비밀번 · 1.1s
    → request  POST /webhook/my-helpdesk
      {
        "question": "사내 Wi-Fi 비밀번호가 뭐예요?",
        "user": "박서연",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "와이파이",
        "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 비밀번호는 별도로 없으며, 사번으로 인증합니다. 회의실에서 연결이 안 되면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
        "error": null
      }
✔ normal · 와이파이가 자주 끊겨요 · 1.2s
    → request  POST /webhook/my-helpdesk
      {
        "question": "와이파이가 자주 끊겨요",
        "user": "최유진",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "와이파이",
        "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실 등 특정 장소에서 와이파이가 자주 끊기면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
        "error": null
      }
✔ normal · 노트북이 와이파이에 연 · 0.9s
    → request  POST /webhook/my-helpdesk
      {
        "question": "노트북이 와이파이에 연결이 안 돼요",
        "user": "한지훈",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "와이파이",
        "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 회의실에서 와이파이가 잡히지 않으면 가까운 공유기 번호를 IT 포털에 신고해 주세요.",
        "error": null
      }
✔ normal · 손님용 wifi도 있나 · 1.2s
    → request  POST /webhook/my-helpdesk
      {
        "question": "손님용 wifi도 있나요?",
        "user": "김민지",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "와이파이",
        "answer": "손님용 와이파이에 대한 안내는 제공되지 않습니다. 사내 와이파이는 'CORP-WIFI'에 사번으로 로그인하여 사용합니다. 추가 문의 사항은 IT 포털에 문의해 주세요.",
        "error": null
      }
✔ normal · VPN이 자꾸 끊겨요 · 0.9s
    → request  POST /webhook/my-helpdesk
      {
        "question": "VPN이 자꾸 끊겨요",
        "user": "정우성",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "VPN",
        "answer": "VPN 클라이언트를 최신 버전으로 업데이트하고, 인증서 만료 여부를 확인한 뒤 다시 접속합니다.",
        "error": null
      }
✔ normal · 비밀번호를 잊어버렸어요 · 0.8s
    → request  POST /webhook/my-helpdesk
      {
        "question": "비밀번호를 잊어버렸어요",
        "user": "오세훈",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "비밀번호",
        "answer": "사내 포털 로그인 화면의 '비밀번호 찾기'에서 휴대폰 인증 후 새 비밀번호를 설정합니다.",
        "error": null
      }
✔ normal · 프린터 출력이 안 돼요 · 1.2s
    → request  POST /webhook/my-helpdesk
      {
        "question": "프린터 출력이 안 돼요",
        "user": "윤하늘",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "프린터",
        "answer": "프린터 전원과 네트워크 연결을 확인하고, 인쇄 대기열을 비운 뒤 다시 출력합니다.",
        "error": null
      }
✔ normal · 메일함 용량이 꽉 찼대 · 1.0s
    → request  POST /webhook/my-helpdesk
      {
        "question": "메일함 용량이 꽉 찼대요",
        "user": "강민수",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "메일",
        "answer": "오래된 메일과 큰 첨부 파일을 보관함으로 옮기거나 삭제해 용량을 확보합니다.",
        "error": null
      }
✔ normal · 새 프로그램 설치는 어 · 1.1s
    → request  POST /webhook/my-helpdesk
      {
        "question": "새 프로그램 설치는 어떻게 신청하나요?",
        "user": "서지아",
        "tag": "normal"
      }
    ← response HTTP 200
      {
        "route": "자동 답변",
        "status": "success",
        "topic": "설치",
        "answer": "새 프로그램 설치는 IT 포털의 '소프트웨어 신청' 메뉴에서 승인 요청을 올리면 승인 후 원격으로 설치됩니다.",
        "error": null
      }
Passed 10/10
report: runs/replay-normal-20261031-075225.json

$ uv run tools/flow.py test cases/lab03-cost.json -v
POST http://localhost:5678/webhook/my-log-report · 4 case(s) × 1
✔ 고치기 전 · 느린 FAQ · 1초에 끊김 · 0.1s · 0 calls · $0.000000
    → request  POST /webhook/my-log-report
      {
        "tag": "before",
        "source": "live"
      }
    ← response HTTP 200
      {
        "total": 10,
        "errors": 10,
        "error_rate": 1,
        "p50_ms": 1043,
        "p95_ms": 1098,
        "by_route": {
          "오류": 10
        },
        "by_day": {
          "2026-10-31": {
            "total": 10,
            "errors": 10,
            "error_rate": 1
          }
        },
        "by_release": {},
        "steps": {
          "faq_ms": {
            "runs": 10,
            "p50": 1043,
            "p95": 1098
          },
          "answer_ms": {
            "runs": 0,
            "p50": 0,
            "p95": 0
          }
        },
        "cost_usd": 0,
        "cost_per_run": 0,
        "llm_calls": 0,
        "failures": [
          {
            "node": "Search FAQ",
            "message": "timeout of 1000ms exceeded",
            "count": 10
          }
        ],
        "unanswered_topics": [],
        "anomalies": [],
        "repeat_topics": [],
        "unanswered_rate": 0,
        "slo": {
          "ok": false,
          "breaches": [
            "error_rate"
          ],
          "limits": {
            "error_rate": 0.01,
            "p95_ms": 3000,
            "unanswered_rate": 0.02
          }
        }
      }
✔ 고친 뒤 · 느린 FAQ · 기다려서 답함 · 0.1s · 10 calls · $0.000000
    → request  POST /webhook/my-log-report
      {
        "tag": "after",
        "source": "live"
      }
    ← response HTTP 200
      {
        "total": 10,
        "errors": 0,
        "error_rate": 0,
        "p50_ms": 2342,
        "p95_ms": 3560,
        "by_route": {
          "자동 답변": 10
        },
        "by_day": {
          "2026-10-31": {
            "total": 10,
            "errors": 0,
            "error_rate": 0
          }
        },
        "by_release": {
          "1.1.0": {
            "answers": 10,
            "repeats": 0,
            "repeat_rate": 0
          }
        },
        "steps": {
          "faq_ms": {
            "runs": 10,
            "p50": 1238,
            "p95": 1278
          },
          "answer_ms": {
            "runs": 10,
            "p50": 1103,
            "p95": 2282
          }
        },
        "cost_usd": 0,
        "cost_per_run": 0,
        "llm_calls": 10,
        "failures": [],
        "unanswered_topics": [],
        "anomalies": [],
        "repeat_topics": [],
        "unanswered_rate": 0,
        "slo": {
          "ok": false,
          "breaches": [
            "p95_ms"
          ],
          "limits": {
            "error_rate": 0.01,
            "p95_ms": 3000,
            "unanswered_rate": 0.02
          }
        }
      }
✔ 고친 뒤 · 평소 FAQ · 기다릴 일이 없음 · 0.0s · 10 calls · $0.000000
    → request  POST /webhook/my-log-report
      {
        "tag": "normal",
        "source": "live"
      }
    ← response HTTP 200
      {
        "total": 10,
        "errors": 0,
        "error_rate": 0,
        "p50_ms": 1015,
        "p95_ms": 2147,
        "by_route": {
          "자동 답변": 10
        },
        "by_day": {
          "2026-10-31": {
            "total": 10,
            "errors": 0,
            "error_rate": 0
          }
        },
        "by_release": {
          "1.1.0": {
            "answers": 10,
            "repeats": 0,
            "repeat_rate": 0
          }
        },
        "steps": {
          "faq_ms": {
            "runs": 10,
            "p50": 17,
            "p95": 39
          },
          "answer_ms": {
            "runs": 10,
            "p50": 997,
            "p95": 2108
          }
        },
        "cost_usd": 0,
        "cost_per_run": 0,
        "llm_calls": 10,
        "failures": [],
        "unanswered_topics": [],
        "anomalies": [],
        "repeat_topics": [],
        "unanswered_rate": 0,
        "slo": {
          "ok": true,
          "breaches": [],
          "limits": {
            "error_rate": 0.01,
            "p95_ms": 3000,
            "unanswered_rate": 0.02
          }
        }
      }
✔ 고친 뒤 · 답 단계는 FAQ 속도와 상관없음 · 0.1s · 10 calls · $0.000000
    → request  POST /webhook/my-log-report
      {
        "tag": "normal",
        "source": "live"
      }
    ← response HTTP 200
      {
        "total": 10,
        "errors": 0,
        "error_rate": 0,
        "p50_ms": 1015,
        "p95_ms": 2147,
        "by_route": {
          "자동 답변": 10
        },
        "by_day": {
          "2026-10-31": {
            "total": 10,
            "errors": 0,
            "error_rate": 0
          }
        },
        "by_release": {
          "1.1.0": {
            "answers": 10,
            "repeats": 0,
            "repeat_rate": 0
          }
        },
        "steps": {
          "faq_ms": {
            "runs": 10,
            "p50": 17,
            "p95": 39
          },
          "answer_ms": {
            "runs": 10,
            "p50": 997,
            "p95": 2108
          }
        },
        "cost_usd": 0,
        "cost_per_run": 0,
        "llm_calls": 10,
        "failures": [],
        "unanswered_topics": [],
        "anomalies": [],
        "repeat_topics": [],
        "unanswered_rate": 0,
        "slo": {
          "ok": true,
          "breaches": [],
          "limits": {
            "error_rate": 0.01,
            "p95_ms": 3000,
            "unanswered_rate": 0.02
          }
        }
      }
Passed 4/4
report: runs/lab03-cost-20261031-075226.json

$ uv run tools/flow.py ops
fault switch: all ok
live run logs: 30 (tag before 10 · tag after 10 · tag normal 10)
added FAQ: 1 (FAQ-007)
created tickets: 0
DLQ: 0 (parked 0)
alerts: 0
