$ uv run tools/flow.py push reference/lab01-example2-check.json ✔ pushed '78 FAQ check (reference)' (id w318880a1bd61aa2) POST http://localhost:5678/webhook/my-faq-check $ uv run tools/flow.py test cases/lab01.json -v POST http://localhost:5678/webhook/my-faq-check · 5 case(s) × 1 ✔ 빈 값 (제목 · 답 · 키워드) · 0.9s → request POST /webhook/my-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": 5, "invalid": 8, "with_warnings": 0, "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": false, "errors": [ "id_format" ], "warnings": [] }, { "id": "FAQ-105", "ok": false, "errors": [ "answer_missing" ], "warnings": [] }, { "id": "FAQ-106", "ok": false, "errors": [ "answer_too_long" ], "warnings": [] }, { "id": "FAQ-102", "ok": false, "errors": [ "duplicate_in_batch" ], "warnings": [] }, { "id": "FAQ-108", "ok": false, "errors": [ "keywords_missing" ], "warnings": [] }, { "id": "FAQ-109", "ok": false, "errors": [ "pii" ], "warnings": [] }, { "id": "FAQ-110", "ok": false, "errors": [ "external_link" ], "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, "id_format": 1, "answer_missing": 1, "answer_too_long": 1, "duplicate_in_batch": 1, "keywords_missing": 1, "pii": 1, "external_link": 1 } } ✔ 형식 (id · 답 길이) · 0.0s → request POST /webhook/my-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": 5, "invalid": 8, "with_warnings": 0, "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": false, "errors": [ "id_format" ], "warnings": [] }, { "id": "FAQ-105", "ok": false, "errors": [ "answer_missing" ], "warnings": [] }, { "id": "FAQ-106", "ok": false, "errors": [ "answer_too_long" ], "warnings": [] }, { "id": "FAQ-102", "ok": false, "errors": [ "duplicate_in_batch" ], "warnings": [] }, { "id": "FAQ-108", "ok": false, "errors": [ "keywords_missing" ], "warnings": [] }, { "id": "FAQ-109", "ok": false, "errors": [ "pii" ], "warnings": [] }, { "id": "FAQ-110", "ok": false, "errors": [ "external_link" ], "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, "id_format": 1, "answer_missing": 1, "answer_too_long": 1, "duplicate_in_batch": 1, "keywords_missing": 1, "pii": 1, "external_link": 1 } } ✔ 배치 안 중복 · 개인정보 · 바깥 링크 · 0.1s → request POST /webhook/my-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": 5, "invalid": 8, "with_warnings": 0, "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": false, "errors": [ "id_format" ], "warnings": [] }, { "id": "FAQ-105", "ok": false, "errors": [ "answer_missing" ], "warnings": [] }, { "id": "FAQ-106", "ok": false, "errors": [ "answer_too_long" ], "warnings": [] }, { "id": "FAQ-102", "ok": false, "errors": [ "duplicate_in_batch" ], "warnings": [] }, { "id": "FAQ-108", "ok": false, "errors": [ "keywords_missing" ], "warnings": [] }, { "id": "FAQ-109", "ok": false, "errors": [ "pii" ], "warnings": [] }, { "id": "FAQ-110", "ok": false, "errors": [ "external_link" ], "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, "id_format": 1, "answer_missing": 1, "answer_too_long": 1, "duplicate_in_batch": 1, "keywords_missing": 1, "pii": 1, "external_link": 1 } } ✔ 배치 2 · 메일 주소만 걸림, 사내 링크는 통과 · 0.0s → request POST /webhook/my-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": 9, "invalid": 1, "with_warnings": 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": false, "errors": [ "pii" ], "warnings": [] }, { "id": "FAQ-210", "ok": true, "errors": [], "warnings": [] } ], "codes": { "pii": 1 } } ✔ 목록이 아닌 본문 → 400 · 0.1s → request POST /webhook/my-faq-check { "batch": "B-x", "items": "FAQ-101" } ← response HTTP 400 { "error": "items must be a list" } Passed 5/5 report: runs/lab01-20261101-180518.json $ uv run tools/flow.py call my-faq-check requests/faq-batch-1.json HTTP 200 · 0.1s { "batch": "B-1", "total": 13, "valid": 5, "invalid": 8, "with_warnings": 0, "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": false, "errors": [ "id_format" ], "warnings": [] }, { "id": "FAQ-105", "ok": false, "errors": [ "answer_missing" ], "warnings": [] }, { "id": "FAQ-106", "ok": false, "errors": [ "answer_too_long" ], "warnings": [] }, { "id": "FAQ-102", "ok": false, "errors": [ "duplicate_in_batch" ], "warnings": [] }, { "id": "FAQ-108", "ok": false, "errors": [ "keywords_missing" ], "warnings": [] }, { "id": "FAQ-109", "ok": false, "errors": [ "pii" ], "warnings": [] }, { "id": "FAQ-110", "ok": false, "errors": [ "external_link" ], "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, "id_format": 1, "answer_missing": 1, "answer_too_long": 1, "duplicate_in_batch": 1, "keywords_missing": 1, "pii": 1, "external_link": 1 } }