$ uv run tools/flow.py test cases/lab01.json --webhook base-faq-check -v
POST http://localhost:5678/webhook/base-faq-check · 5 case(s) × 1
✔ 빈 값 (제목 · 답 · 키워드) · 0.0s
    → request  POST /webhook/base-faq-check
      {
        "batch": "B-1",
        "items": [
          {
            "id": "FAQ-101",
            "title": "사내 와이파이 연결",
            "keywords": [
              "와이파이",
              "wifi",
              "무선"
            ],
            "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약",
            "keywords": [
              "회의실",
              "예약"
            ],
            "answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
          },
          {
            "id": "FAQ-103",
            "title": "",
            "keywords": [
              "출입증"
            ],
            "answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
          },
          {
            "id": "FAQ-10",
            "title": "노트북 반납",
            "keywords": [
              "노트북",
              "반납"
            ],
            "answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
          },
          {
            "id": "FAQ-105",
            "title": "모니터 추가 신청",
            "keywords": [
              "모니터"
            ],
            "answer": ""
          },
          {
            "id": "FAQ-106",
            "title": "회의실 예약 규칙",
            "keywords": [
              "회의실 규칙"
            ],
            "answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약 (중복)",
            "keywords": [
              "회의실"
            ],
            "answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
          },
          {
            "id": "FAQ-108",
            "title": "화상회의 장비",
            "keywords": [],
            "answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
          },
          {
            "id": "FAQ-109",
            "title": "보안 사고 신고",
            "keywords": [
              "보안",
              "신고"
            ],
            "answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
          },
          {
            "id": "FAQ-110",
            "title": "사내 메신저 설치",
            "keywords": [
              "메신저"
            ],
            "answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
          },
          {
            "id": "FAQ-003",
            "title": "VPN 접속 오류 (새 판)",
            "keywords": [
              "재택근무"
            ],
            "answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
          },
          {
            "id": "FAQ-112",
            "title": "VPN 접속 오류",
            "keywords": [
              "vpn",
              "재택근무"
            ],
            "answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
          },
          {
            "id": "FAQ-113",
            "title": "프린터 토너 교체",
            "keywords": [
              "토너"
            ],
            "answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
          }
        ]
      }
    ← response HTTP 200
      {
        "batch": "B-1",
        "total": 13,
        "valid": 10,
        "invalid": 3,
        "rows": [
          {
            "id": "FAQ-101",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-103",
            "ok": false,
            "errors": [
              "title_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-10",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-105",
            "ok": false,
            "errors": [
              "answer_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-106",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-108",
            "ok": false,
            "errors": [
              "keywords_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-109",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-110",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-003",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-112",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-113",
            "ok": true,
            "errors": [],
            "warnings": []
          }
        ],
        "codes": {
          "title_missing": 1,
          "answer_missing": 1,
          "keywords_missing": 1
        }
      }
✘ 형식 (id · 답 길이) · 0.1s · rows.3.errors has none of ['id_format']; rows.5.errors has none of ['answer_too_long']
    → request  POST /webhook/base-faq-check
      {
        "batch": "B-1",
        "items": [
          {
            "id": "FAQ-101",
            "title": "사내 와이파이 연결",
            "keywords": [
              "와이파이",
              "wifi",
              "무선"
            ],
            "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약",
            "keywords": [
              "회의실",
              "예약"
            ],
            "answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
          },
          {
            "id": "FAQ-103",
            "title": "",
            "keywords": [
              "출입증"
            ],
            "answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
          },
          {
            "id": "FAQ-10",
            "title": "노트북 반납",
            "keywords": [
              "노트북",
              "반납"
            ],
            "answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
          },
          {
            "id": "FAQ-105",
            "title": "모니터 추가 신청",
            "keywords": [
              "모니터"
            ],
            "answer": ""
          },
          {
            "id": "FAQ-106",
            "title": "회의실 예약 규칙",
            "keywords": [
              "회의실 규칙"
            ],
            "answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약 (중복)",
            "keywords": [
              "회의실"
            ],
            "answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
          },
          {
            "id": "FAQ-108",
            "title": "화상회의 장비",
            "keywords": [],
            "answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
          },
          {
            "id": "FAQ-109",
            "title": "보안 사고 신고",
            "keywords": [
              "보안",
              "신고"
            ],
            "answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
          },
          {
            "id": "FAQ-110",
            "title": "사내 메신저 설치",
            "keywords": [
              "메신저"
            ],
            "answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
          },
          {
            "id": "FAQ-003",
            "title": "VPN 접속 오류 (새 판)",
            "keywords": [
              "재택근무"
            ],
            "answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
          },
          {
            "id": "FAQ-112",
            "title": "VPN 접속 오류",
            "keywords": [
              "vpn",
              "재택근무"
            ],
            "answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
          },
          {
            "id": "FAQ-113",
            "title": "프린터 토너 교체",
            "keywords": [
              "토너"
            ],
            "answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
          }
        ]
      }
    ← response HTTP 200
      {
        "batch": "B-1",
        "total": 13,
        "valid": 10,
        "invalid": 3,
        "rows": [
          {
            "id": "FAQ-101",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-103",
            "ok": false,
            "errors": [
              "title_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-10",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-105",
            "ok": false,
            "errors": [
              "answer_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-106",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-108",
            "ok": false,
            "errors": [
              "keywords_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-109",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-110",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-003",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-112",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-113",
            "ok": true,
            "errors": [],
            "warnings": []
          }
        ],
        "codes": {
          "title_missing": 1,
          "answer_missing": 1,
          "keywords_missing": 1
        }
      }
✘ 배치 안 중복 · 개인정보 · 바깥 링크 · 0.0s · rows.6.errors has none of ['duplicate_in_batch']; rows.8.errors has none of ['pii']; rows.9.errors has none of ['external_link']
    → request  POST /webhook/base-faq-check
      {
        "batch": "B-1",
        "items": [
          {
            "id": "FAQ-101",
            "title": "사내 와이파이 연결",
            "keywords": [
              "와이파이",
              "wifi",
              "무선"
            ],
            "answer": "사내 와이파이는 'CORP-WIFI'에 사번으로 로그인합니다. 잡히지 않으면 공유기 번호를 IT 포털에 신고합니다."
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약",
            "keywords": [
              "회의실",
              "예약"
            ],
            "answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
          },
          {
            "id": "FAQ-103",
            "title": "",
            "keywords": [
              "출입증"
            ],
            "answer": "출입증을 잃어버렸으면 총무팀에 재발급을 신청합니다."
          },
          {
            "id": "FAQ-10",
            "title": "노트북 반납",
            "keywords": [
              "노트북",
              "반납"
            ],
            "answer": "퇴사 5영업일 전까지 IT팀에 노트북을 반납합니다."
          },
          {
            "id": "FAQ-105",
            "title": "모니터 추가 신청",
            "keywords": [
              "모니터"
            ],
            "answer": ""
          },
          {
            "id": "FAQ-106",
            "title": "회의실 예약 규칙",
            "keywords": [
              "회의실 규칙"
            ],
            "answer": "회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. 회의실은 사내 포털의 '회의실 예약' 메뉴에서 날짜와 시간을 고른 뒤 인원과 목적을 적어 예약합니다. "
          },
          {
            "id": "FAQ-102",
            "title": "회의실 예약 (중복)",
            "keywords": [
              "회의실"
            ],
            "answer": "사내 포털에서 회의실을 예약합니다. 취소도 같은 메뉴에서 합니다."
          },
          {
            "id": "FAQ-108",
            "title": "화상회의 장비",
            "keywords": [],
            "answer": "화상회의 장비는 각 층 비품실에서 빌릴 수 있습니다."
          },
          {
            "id": "FAQ-109",
            "title": "보안 사고 신고",
            "keywords": [
              "보안",
              "신고"
            ],
            "answer": "보안 사고는 IT팀 김민지(010-1234-5678)에게 바로 전화해 주세요."
          },
          {
            "id": "FAQ-110",
            "title": "사내 메신저 설치",
            "keywords": [
              "메신저"
            ],
            "answer": "사내 메신저는 http://bit.ly/corp-msg 에서 받아 설치합니다."
          },
          {
            "id": "FAQ-003",
            "title": "VPN 접속 오류 (새 판)",
            "keywords": [
              "재택근무"
            ],
            "answer": "VPN 클라이언트를 최신으로 업데이트하고 인증서를 확인합니다."
          },
          {
            "id": "FAQ-112",
            "title": "VPN 접속 오류",
            "keywords": [
              "vpn",
              "재택근무"
            ],
            "answer": "재택근무 중 VPN이 끊기면 클라이언트를 다시 시작하고 인증서 만료일을 확인합니다."
          },
          {
            "id": "FAQ-113",
            "title": "프린터 토너 교체",
            "keywords": [
              "토너"
            ],
            "answer": "메일함 용량이 꽉 차면 오래된 메일을 보관함으로 옮겨 용량을 확보합니다."
          }
        ]
      }
    ← response HTTP 200
      {
        "batch": "B-1",
        "total": 13,
        "valid": 10,
        "invalid": 3,
        "rows": [
          {
            "id": "FAQ-101",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-103",
            "ok": false,
            "errors": [
              "title_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-10",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-105",
            "ok": false,
            "errors": [
              "answer_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-106",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-102",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-108",
            "ok": false,
            "errors": [
              "keywords_missing"
            ],
            "warnings": []
          },
          {
            "id": "FAQ-109",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-110",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-003",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-112",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-113",
            "ok": true,
            "errors": [],
            "warnings": []
          }
        ],
        "codes": {
          "title_missing": 1,
          "answer_missing": 1,
          "keywords_missing": 1
        }
      }
✘ 배치 2 · 메일 주소만 걸림, 사내 링크는 통과 · 0.1s · valid = 10 (expected 9); codes.pii = null (expected 1); rows.8.ok = true (expected false)
    → request  POST /webhook/base-faq-check
      {
        "batch": "B-2",
        "items": [
          {
            "id": "FAQ-201",
            "title": "회의실 예약",
            "keywords": [
              "회의실",
              "예약"
            ],
            "answer": "사내 포털의 '회의실 예약' 메뉴에서 날짜 · 시간 · 인원을 적어 예약합니다."
          },
          {
            "id": "FAQ-202",
            "title": "출입증 재발급",
            "keywords": [
              "출입증",
              "재발급"
            ],
            "answer": "출입증을 잃어버렸으면 총무팀에 신분증을 가지고 가서 재발급을 신청합니다."
          },
          {
            "id": "FAQ-203",
            "title": "노트북 반납",
            "keywords": [
              "노트북",
              "반납"
            ],
            "answer": "퇴사 5영업일 전까지 IT팀에 노트북과 충전기를 반납합니다."
          },
          {
            "id": "FAQ-204",
            "title": "모니터 추가 신청",
            "keywords": [
              "모니터",
              "추가"
            ],
            "answer": "IT 포털의 '장비 신청'에서 모니터 추가를 신청하면 재고 확인 뒤 지급합니다."
          },
          {
            "id": "FAQ-205",
            "title": "윈도우 업데이트",
            "keywords": [
              "업데이트",
              "윈도우"
            ],
            "answer": "윈도우 업데이트는 매달 둘째 주 수요일 밤에 자동으로 설치됩니다. 끝나면 다시 시작합니다."
          },
          {
            "id": "FAQ-206",
            "title": "화상회의 장비 대여",
            "keywords": [
              "화상회의",
              "카메라"
            ],
            "answer": "화상회의 카메라와 마이크는 각 층 비품실에서 빌리고 같은 날 돌려줍니다."
          },
          {
            "id": "FAQ-207",
            "title": "사내 메신저 설치",
            "keywords": [
              "메신저"
            ],
            "answer": "사내 메신저는 IT 포털(https://intra.example.com/msg)에서 받아 설치합니다."
          },
          {
            "id": "FAQ-208",
            "title": "공유 드라이브 권한",
            "keywords": [
              "공유 드라이브",
              "권한"
            ],
            "answer": "공유 드라이브 권한은 팀장이 IT 포털에서 신청하면 하루 안에 열립니다."
          },
          {
            "id": "FAQ-209",
            "title": "보안 사고 신고",
            "keywords": [
              "보안",
              "신고"
            ],
            "answer": "보안 사고는 security.desk@example.com 으로 바로 메일을 보내 주세요."
          },
          {
            "id": "FAQ-210",
            "title": "모니터 밝기 조절",
            "keywords": [
              "밝기"
            ],
            "answer": "연차 휴가는 입사 1년 뒤부터 15일이며 인사 포털에서 신청합니다."
          }
        ]
      }
    ← response HTTP 200
      {
        "batch": "B-2",
        "total": 10,
        "valid": 10,
        "invalid": 0,
        "rows": [
          {
            "id": "FAQ-201",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-202",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-203",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-204",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-205",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-206",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-207",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-208",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-209",
            "ok": true,
            "errors": [],
            "warnings": []
          },
          {
            "id": "FAQ-210",
            "ok": true,
            "errors": [],
            "warnings": []
          }
        ],
        "codes": {}
      }
✘ 목록이 아닌 본문 → 400 · 0.1s · status 500 (expected 400)
    → request  POST /webhook/base-faq-check
      {
        "batch": "B-x",
        "items": "FAQ-101"
      }
    ← response HTTP 500
      {
        "message": "Error in workflow"
      }
Passed 1/5
report: runs/lab01-20261101-180505.json
