76일차 자료 추가 (답 포함): 실습 15개 · 의도 · 생활 속 예 · 실행 결과, LAB-A · 정답 워크플로우와 25단계 실행 기록, 교안 개요 30장 · 4단계 정리, 바이브 코딩 tretry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f36605043
commit
bbf17e4696
141 files changed
+34072
No files matched your search
@@ -0,0 +1,34 @@
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ created tickets, DLQ and alerts emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py budget --reset
|
||||
✔ budget back to data/budget.json
|
||||
budget $1.0 / used $0.000000 · remaining $1.000000 · over False · per user 20/min · question 500 chars
|
||||
|
||||
$ uv run tools/flow.py usage --clear
|
||||
✔ usage cleared: {"cleared": 0}
|
||||
|
||||
$ uv run tools/flow.py off my-intake my-replay my-ops-status reference/lab02-example2-error-handler.json
|
||||
✔ unpublished my-intake (id w75a95815facf2d6)
|
||||
✔ unpublished my-replay (id w7d8de63c811909b)
|
||||
✔ unpublished my-ops-status (id w07b5812eaa88ef9)
|
||||
✔ n8n restarted. Turn it back on with: uv run tools/flow.py push <the workflow file>
|
||||
|
||||
$ uv run tools/flow.py push reference/baseline-intake.json reference/nightly-report.json
|
||||
✔ pushed '76 Intake baseline (provided)' (id w4ef8dc8a25b82e3)
|
||||
POST http://localhost:5678/webhook/base-intake
|
||||
✔ pushed '76 Nightly report (provided, has a bug)' (id wb9ce32c18d7df56)
|
||||
POST http://localhost:5678/webhook/nightly-report
|
||||
|
||||
$ uv run tools/flow.py doctor
|
||||
! n8n install folder /Users/mcchae/day65-n8n
|
||||
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
|
||||
✔ n8n container day65-n8n-n8n-1
|
||||
✔ n8n http://localhost:5678
|
||||
✔ data API http://localhost:8800
|
||||
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||
✔ n8n → http://data-api:8800
|
||||
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||
✔ node catalog (for check)
|
||||
✔ CLI coding agent: codex, claude, agy
|
||||
ready
|
||||
@@ -0,0 +1,64 @@
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ created tickets, DLQ and alerts emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py call base-intake requests/batch-ok.json
|
||||
HTTP 200 · 5.2s
|
||||
{
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2001",
|
||||
"T-2002",
|
||||
"T-2003"
|
||||
]
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py call base-intake requests/batch-bad-item.json
|
||||
HTTP 500 · 3.2s
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 5
|
||||
T-2001 key 10057-0 네트워크/높음 VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있
|
||||
T-2002 key 10057-1 장비/낮음 새 모니터를 하나 더 받을 수 있을까요?
|
||||
T-2003 key 10057-2 소프트웨어/높음 엑셀 라이선스가 만료됐다고 나와요
|
||||
T-2004 key 10058-0 장비/보통 프린터 용지가 걸려요
|
||||
T-2005 key 10058-2 소프트웨어/보통 메일 첨부가 안 열려요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
|
||||
$ uv run tools/flow.py fault tickets flaky
|
||||
✔ fault switch: {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call base-intake requests/batch-two.json
|
||||
HTTP 500 · 2.5s
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py fault tickets ok
|
||||
✔ fault switch: {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call base-intake requests/batch-ok.json
|
||||
HTTP 200 · 3.0s
|
||||
{
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2006",
|
||||
"T-2008",
|
||||
"T-2007"
|
||||
]
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 8
|
||||
T-2004 key 10058-0 장비/보통 프린터 용지가 걸려요
|
||||
T-2005 key 10058-2 소프트웨어/보통 메일 첨부가 안 열려요
|
||||
T-2006 key 10060-0 네트워크/높음 VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있
|
||||
T-2007 key 10060-2 소프트웨어/높음 엑셀 라이선스가 만료됐다고 나와요
|
||||
T-2008 key 10060-1 장비/낮음 새 모니터를 하나 더 받을 수 있을까요?
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
@@ -0,0 +1,236 @@
|
||||
{
|
||||
"cases": "lab01.json",
|
||||
"webhook": "base-intake",
|
||||
"passed": 0,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "정상 3건",
|
||||
"webhook": "base-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"created = null (expected 3)",
|
||||
"failed = null (expected 0)",
|
||||
"rejected = null (expected 0)"
|
||||
],
|
||||
"seconds": 2.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2001",
|
||||
"T-2002",
|
||||
"T-2003"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "같은 요청을 다시 보내도 중복 없음",
|
||||
"webhook": "base-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"created = null (expected 3)",
|
||||
"duplicates = null (expected 3)"
|
||||
],
|
||||
"seconds": 3.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2004",
|
||||
"T-2005",
|
||||
"T-2006"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "만들어진 접수 건은 3개",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 6 (expected 3)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 6,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "10061-0",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "10061-1",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "10061-2",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "10062-0",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "10062-1",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2006",
|
||||
"key": "10062-2",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "잘못된 항목은 그 항목만 거절",
|
||||
"webhook": "base-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 200)",
|
||||
"created = null (expected 2)",
|
||||
"rejected = null (expected 1)",
|
||||
"failed = null (expected 0)",
|
||||
"results has none of ['text is required']"
|
||||
],
|
||||
"seconds": 3.4,
|
||||
"status": 500,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "목록이 아니면 400",
|
||||
"webhook": "base-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 400)"
|
||||
],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {
|
||||
"requests": "VPN이 끊겨요"
|
||||
},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "가끔 실패하는 API · 다시 해 보기로 성공",
|
||||
"webhook": "base-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 200)",
|
||||
"created = null (expected 2)",
|
||||
"failed = null (expected 0)"
|
||||
],
|
||||
"seconds": 2.0,
|
||||
"status": 500,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
$ uv run tools/flow.py test cases/lab01.json --webhook base-intake -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 8
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/base-intake · 6 case(s) × 1
|
||||
✘ 정상 3건 · 2.7s · created = null (expected 3); failed = null (expected 0); rejected = null (expected 0)
|
||||
→ request POST /webhook/base-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2001",
|
||||
"T-2002",
|
||||
"T-2003"
|
||||
]
|
||||
}
|
||||
✘ 같은 요청을 다시 보내도 중복 없음 · 3.1s · created = null (expected 3); duplicates = null (expected 3)
|
||||
→ request POST /webhook/base-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"processed": 3,
|
||||
"tickets": [
|
||||
"T-2004",
|
||||
"T-2005",
|
||||
"T-2006"
|
||||
]
|
||||
}
|
||||
✘ 만들어진 접수 건은 3개 · 0.0s · count = 6 (expected 3)
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 6,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "10061-0",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "10061-1",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "10061-2",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "10062-0",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "10062-1",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2006",
|
||||
"key": "10062-2",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 잘못된 항목은 그 항목만 거절 · 3.4s · status 500 (expected 200); created = null (expected 2); rejected = null (expected 1); failed = null (expected 0); results has none of ['text is required']
|
||||
→ request POST /webhook/base-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
✘ 목록이 아니면 400 · 0.1s · status 500 (expected 400)
|
||||
→ request POST /webhook/base-intake
|
||||
{
|
||||
"requests": "VPN이 끊겨요"
|
||||
}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "flaky"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "flaky",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.0s · status 500 (expected 200); created = null (expected 2); failed = null (expected 0)
|
||||
→ request POST /webhook/base-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
Passed 0/6
|
||||
report: runs/lab01-20261029-141145.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"cases": "lab01.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "정상 3건",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "같은 요청을 다시 보내도 중복 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 3,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": true,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": true,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "만들어진 접수 건은 3개",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 3,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "잘못된 항목은 그 항목만 거절",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "목록이 아니면 400",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 400,
|
||||
"request": {
|
||||
"requests": "VPN이 끊겨요"
|
||||
},
|
||||
"response": {
|
||||
"error": "requests must be a list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "가끔 실패하는 API · 다시 해 보기로 성공",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2006",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2007",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
$ uv run tools/flow.py push reference/lab01-example2-intake.json
|
||||
✔ pushed '76 Intake · retry and idempotency key (reference, Lab 01 example 2)' (id w8399f905b79a073)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 8}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 8
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ 정상 3건 · 3.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 같은 요청을 다시 보내도 중복 없음 · 3.1s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 3,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": true,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": true,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 만들어진 접수 건은 3개 · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 3,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 잘못된 항목은 그 항목만 거절 · 1.7s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 목록이 아니면 400 · 0.0s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": "VPN이 끊겨요"
|
||||
}
|
||||
← response HTTP 400
|
||||
{
|
||||
"error": "requests must be a list"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "flaky"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "flaky",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2006",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2007",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab01-20261029-141207.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 7
|
||||
T-2003 key R-103 소프트웨어/높음 엑셀 라이선스가 만료됐다고 나와요
|
||||
T-2004 key R-201 장비/보통 프린터 용지가 걸려요
|
||||
T-2005 key R-203 소프트웨어/보통 메일 첨부가 안 열려요
|
||||
T-2006 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
T-2007 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
@@ -0,0 +1,280 @@
|
||||
{
|
||||
"cases": "lab01-faults.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "flaky · 다시 해 보면 됨",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "error · 세 번 해도 실패",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "slow · 3초씩 세 번 기다리다 실패",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 12.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "실패라던 3건이 늦게 만들어짐 (2 + 3)",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "같은 요청을 다시 · 멱등 키로 중복 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 3,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": true,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": true,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
$ uv run tools/flow.py test cases/lab01-faults.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 7}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 7
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 5 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "flaky"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "flaky",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ flaky · 다시 해 보면 됨 · 2.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ error · 세 번 해도 실패 · 3.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "slow", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "slow"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "slow",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ slow · 3초씩 세 번 기다리다 실패 · 12.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
… before: wait 8s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 실패라던 3건이 늦게 만들어짐 (2 + 3) · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 같은 요청을 다시 · 멱등 키로 중복 없음 · 2.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 3,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": true,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": true,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab01-faults-20261029-141237.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 5
|
||||
T-2001 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
T-2002 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
T-2003 key R-103 소프트웨어/높음 엑셀 라이선스가 만료됐다고 나와요
|
||||
T-2004 key R-102 장비/낮음 새 모니터를 하나 더 받을 수 있을까요?
|
||||
T-2005 key R-101 네트워크/높음 VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"cases": "lab01-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 1,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "평소에는 분류됨",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "LLM이 멈추면 미분류로 접수",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 500 (expected 200)",
|
||||
"created = null (expected 3)",
|
||||
"unclassified = null (expected 3)",
|
||||
"failed = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.8,
|
||||
"status": 500,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "미분류도 접수 건으로",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"tickets has none of ['미분류']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 5
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 3 case(s) × 1
|
||||
✔ 평소에는 분류됨 · 2.1s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
|
||||
✘ LLM이 멈추면 미분류로 접수 · 0.8s · status 500 (expected 200); created = null (expected 3); unclassified = null (expected 3); failed = null (expected 0)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✘ 미분류도 접수 건으로 · 0.0s · tickets has none of ['미분류']
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 1/3
|
||||
report: runs/lab01-challenge-20261029-141245.json
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"cases": "lab01-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 3,
|
||||
"total": 3,
|
||||
"results": [
|
||||
{
|
||||
"case": "평소에는 분류됨",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.5,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "LLM이 멈추면 미분류로 접수",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 3,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "미분류도 접수 건으로",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
$ uv run tools/flow.py push answers/76-lab01-challenge-fallback.json
|
||||
! unpublished '76 Intake · retry and idempotency key (reference, Lab 01 example 2)' (w8399f905b79a073): it used the same webhook path
|
||||
✔ pushed '76 Intake · LLM fallback (answer, Lab 01 task 1)' (id w0f5e1ef11aa4c14)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 3 case(s) × 1
|
||||
✔ 평소에는 분류됨 · 3.5s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
|
||||
✔ LLM이 멈추면 미분류로 접수 · 0.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 3,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✔ 미분류도 접수 건으로 · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "미분류",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 3/3
|
||||
report: runs/lab01-challenge-20261029-141310.json
|
||||
$ bash 65/n8n/register-credentials.sh → exit 0
|
||||
Successfully imported 2 credentials.
|
||||
Registered 'OpenAI (.env)' and 'Ollama local' from /Users/mcchae/work/AIO-01D4/Last3Mon/.env
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ 정상 3건 · 3.1s
|
||||
✔ 같은 요청을 다시 보내도 중복 없음 · 3.1s
|
||||
✔ 만들어진 접수 건은 3개 · 0.0s
|
||||
✔ 잘못된 항목은 그 항목만 거절 · 1.8s
|
||||
✔ 목록이 아니면 400 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.4s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
Passed 6/6
|
||||
report: runs/lab01-20261029-141323.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
@@ -0,0 +1,427 @@
|
||||
{
|
||||
"cases": "lab01-challenge2.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 1,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "같은 id가 두 번이면 두 번째는 거절",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"created = 3 (expected 2)",
|
||||
"rejected = 0 (expected 1)",
|
||||
"duplicates = 1 (expected 0)",
|
||||
"results has none of ['duplicate id']"
|
||||
],
|
||||
"seconds": 2.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 1,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "11건은 400 (한 번에 10건까지)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 200 (expected 400)",
|
||||
"error has none of ['too many']"
|
||||
],
|
||||
"seconds": 11.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"user": "박서연",
|
||||
"text": "모니터 1번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"user": "최유진",
|
||||
"text": "모니터 2번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"user": "정우성",
|
||||
"text": "모니터 3번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 4번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"user": "김민지",
|
||||
"text": "모니터 5번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"user": "이준호",
|
||||
"text": "모니터 6번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"user": "박서연",
|
||||
"text": "모니터 7번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"user": "최유진",
|
||||
"text": "모니터 8번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"user": "정우성",
|
||||
"text": "모니터 9번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 10번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"user": "김민지",
|
||||
"text": "모니터 11번이 켜지지 않아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 11,
|
||||
"created": 11,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2006",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2007",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2008",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2013",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2012",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2010",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2011",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2009",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "400이면 하나도 만들지 않음",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 13 (expected 2)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 13,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-401",
|
||||
"title": "회의실 화면 공유가 안 돼요",
|
||||
"requester": "김민지",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-402",
|
||||
"title": "비밀번호를 바꾸라는 창이 계속 떠요",
|
||||
"requester": "최유진",
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-502",
|
||||
"title": "모니터 2번이 켜지지 않아요",
|
||||
"requester": "최유진",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-501",
|
||||
"title": "모니터 1번이 켜지지 않아요",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-503",
|
||||
"title": "모니터 3번이 켜지지 않아요",
|
||||
"requester": "정우성",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2006",
|
||||
"key": "R-504",
|
||||
"title": "모니터 4번이 켜지지 않아요",
|
||||
"requester": "한지훈",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2007",
|
||||
"key": "R-505",
|
||||
"title": "모니터 5번이 켜지지 않아요",
|
||||
"requester": "김민지",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2008",
|
||||
"key": "R-506",
|
||||
"title": "모니터 6번이 켜지지 않아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2009",
|
||||
"key": "R-511",
|
||||
"title": "모니터 11번이 켜지지 않아요",
|
||||
"requester": "김민지",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2010",
|
||||
"key": "R-509",
|
||||
"title": "모니터 9번이 켜지지 않아요",
|
||||
"requester": "정우성",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2011",
|
||||
"key": "R-510",
|
||||
"title": "모니터 10번이 켜지지 않아요",
|
||||
"requester": "한지훈",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2012",
|
||||
"key": "R-508",
|
||||
"title": "모니터 8번이 켜지지 않아요",
|
||||
"requester": "최유진",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2013",
|
||||
"key": "R-507",
|
||||
"title": "모니터 7번이 켜지지 않아요",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "평소 요청은 그대로",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.2,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2014",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2015",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2016",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 7}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 7
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✘ 같은 id가 두 번이면 두 번째는 거절 · 2.8s · created = 3 (expected 2); rejected = 0 (expected 1); duplicates = 1 (expected 0); results has none of ['duplicate id']
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 1,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": true,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 11건은 400 (한 번에 10건까지) · 11.0s · status 200 (expected 400); error has none of ['too many']
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"user": "박서연",
|
||||
"text": "모니터 1번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"user": "최유진",
|
||||
"text": "모니터 2번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"user": "정우성",
|
||||
"text": "모니터 3번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 4번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"user": "김민지",
|
||||
"text": "모니터 5번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"user": "이준호",
|
||||
"text": "모니터 6번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"user": "박서연",
|
||||
"text": "모니터 7번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"user": "최유진",
|
||||
"text": "모니터 8번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"user": "정우성",
|
||||
"text": "모니터 9번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 10번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"user": "김민지",
|
||||
"text": "모니터 11번이 켜지지 않아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 11,
|
||||
"created": 11,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2006",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2007",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2008",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2013",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2012",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2010",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2011",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2009",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 400이면 하나도 만들지 않음 · 0.0s · count = 13 (expected 2)
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 13,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-401",
|
||||
"title": "회의실 화면 공유가 안 돼요",
|
||||
"requester": "김민지",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-402",
|
||||
"title": "비밀번호를 바꾸라는 창이 계속 떠요",
|
||||
"requester": "최유진",
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-502",
|
||||
"title": "모니터 2번이 켜지지 않아요",
|
||||
"requester": "최유진",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-501",
|
||||
"title": "모니터 1번이 켜지지 않아요",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-503",
|
||||
"title": "모니터 3번이 켜지지 않아요",
|
||||
"requester": "정우성",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2006",
|
||||
"key": "R-504",
|
||||
"title": "모니터 4번이 켜지지 않아요",
|
||||
"requester": "한지훈",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2007",
|
||||
"key": "R-505",
|
||||
"title": "모니터 5번이 켜지지 않아요",
|
||||
"requester": "김민지",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2008",
|
||||
"key": "R-506",
|
||||
"title": "모니터 6번이 켜지지 않아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2009",
|
||||
"key": "R-511",
|
||||
"title": "모니터 11번이 켜지지 않아요",
|
||||
"requester": "김민지",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2010",
|
||||
"key": "R-509",
|
||||
"title": "모니터 9번이 켜지지 않아요",
|
||||
"requester": "정우성",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2011",
|
||||
"key": "R-510",
|
||||
"title": "모니터 10번이 켜지지 않아요",
|
||||
"requester": "한지훈",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2012",
|
||||
"key": "R-508",
|
||||
"title": "모니터 8번이 켜지지 않아요",
|
||||
"requester": "최유진",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2013",
|
||||
"key": "R-507",
|
||||
"title": "모니터 7번이 켜지지 않아요",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 평소 요청은 그대로 · 3.2s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2014",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2015",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2016",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 1/4
|
||||
report: runs/lab01-challenge2-20261029-141341.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"cases": "lab01-challenge2.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 4,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "같은 id가 두 번이면 두 번째는 거절",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "11건은 400 (한 번에 10건까지)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.2,
|
||||
"status": 400,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"user": "박서연",
|
||||
"text": "모니터 1번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"user": "최유진",
|
||||
"text": "모니터 2번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"user": "정우성",
|
||||
"text": "모니터 3번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 4번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"user": "김민지",
|
||||
"text": "모니터 5번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"user": "이준호",
|
||||
"text": "모니터 6번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"user": "박서연",
|
||||
"text": "모니터 7번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"user": "최유진",
|
||||
"text": "모니터 8번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"user": "정우성",
|
||||
"text": "모니터 9번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 10번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"user": "김민지",
|
||||
"text": "모니터 11번이 켜지지 않아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"error": "too many requests (max 10)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "400이면 하나도 만들지 않음",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-401",
|
||||
"title": "회의실 화면 공유가 안 돼요",
|
||||
"requester": "김민지",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-402",
|
||||
"title": "비밀번호를 바꾸라는 창이 계속 떠요",
|
||||
"requester": "최유진",
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "평소 요청은 그대로",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
$ uv run tools/flow.py push answers/76-lab01-challenge2-rules.json
|
||||
! unpublished '76 Intake · LLM fallback (answer, Lab 01 task 1)' (w0f5e1ef11aa4c14): it used the same webhook path
|
||||
✔ pushed '76 Intake · request rules (answer, Lab 01 task 2)' (id w4bb85fc171aea9d)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 16}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 16
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ 같은 id가 두 번이면 두 번째는 거절 · 3.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 11건은 400 (한 번에 10건까지) · 0.2s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-501",
|
||||
"user": "박서연",
|
||||
"text": "모니터 1번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-502",
|
||||
"user": "최유진",
|
||||
"text": "모니터 2번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-503",
|
||||
"user": "정우성",
|
||||
"text": "모니터 3번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-504",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 4번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-505",
|
||||
"user": "김민지",
|
||||
"text": "모니터 5번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-506",
|
||||
"user": "이준호",
|
||||
"text": "모니터 6번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-507",
|
||||
"user": "박서연",
|
||||
"text": "모니터 7번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-508",
|
||||
"user": "최유진",
|
||||
"text": "모니터 8번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-509",
|
||||
"user": "정우성",
|
||||
"text": "모니터 9번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-510",
|
||||
"user": "한지훈",
|
||||
"text": "모니터 10번이 켜지지 않아요"
|
||||
},
|
||||
{
|
||||
"id": "R-511",
|
||||
"user": "김민지",
|
||||
"text": "모니터 11번이 켜지지 않아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 400
|
||||
{
|
||||
"error": "too many requests (max 10)"
|
||||
}
|
||||
✔ 400이면 하나도 만들지 않음 · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-401",
|
||||
"title": "회의실 화면 공유가 안 돼요",
|
||||
"requester": "김민지",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-402",
|
||||
"title": "비밀번호를 바꾸라는 창이 계속 떠요",
|
||||
"requester": "최유진",
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 평소 요청은 그대로 · 3.7s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2004",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2005",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 4/4
|
||||
report: runs/lab01-challenge2-20261029-141401.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ 정상 3건 · 2.8s
|
||||
✔ 같은 요청을 다시 보내도 중복 없음 · 2.8s
|
||||
✔ 만들어진 접수 건은 3개 · 0.0s
|
||||
✔ 잘못된 항목은 그 항목만 거절 · 1.8s
|
||||
✔ 목록이 아니면 400 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.4s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
Passed 6/6
|
||||
report: runs/lab01-20261029-141411.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 7}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 3 case(s) × 1
|
||||
✔ 평소에는 분류됨 · 2.0s
|
||||
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
|
||||
✔ LLM이 멈추면 미분류로 접수 · 1.4s
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✔ 미분류도 접수 건으로 · 0.0s
|
||||
Passed 3/3
|
||||
report: runs/lab01-challenge-20261029-141421.json
|
||||
✔ OpenAI credential: real key from .env restored
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 2,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "예제 1 · API가 멈추면 실패로 표시 · 200",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 1 · 실패한 2건은 DLQ에",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"size = 0 (expected 2)",
|
||||
"items has none of ['R-301', 'R-302']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · 예상 못 한 오류 (nightly-report)",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · Error Workflow가 알림을 남김",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"severity = null (expected critical)",
|
||||
"reason = null (expected unexpected)",
|
||||
"workflow = null (expected 76 Nightly report (provided, has a bug))",
|
||||
"node has none of ['Count by category']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ created tickets, DLQ and alerts emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py fault tickets error
|
||||
✔ fault switch: {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call my-intake requests/batch-two.json
|
||||
HTTP 200 · 2.8s
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py fault tickets ok
|
||||
✔ fault switch: {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 0
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
|
||||
$ uv run tools/flow.py call nightly-report
|
||||
HTTP 500 · 0.1s
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 2.8s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 예제 1 · 실패한 2건은 DLQ에 · 0.0s · size = 0 (expected 2); items has none of ['R-301', 'R-302']
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.0s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✘ 예제 2 · Error Workflow가 알림을 남김 · 0.0s · severity = null (expected critical); reason = null (expected unexpected); workflow = null (expected 76 Nightly report (provided, has a bug)); node has none of ['Count by category']
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{}
|
||||
Passed 2/4
|
||||
report: runs/lab02-20261029-141433.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 3,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "예제 1 · API가 멈추면 실패로 표시 · 200",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 1 · 실패한 2건은 DLQ에",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250889.918716
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250889.918718
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · 예상 못 한 오류 (nightly-report)",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · Error Workflow가 알림을 남김",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"severity = null (expected critical)",
|
||||
"reason = null (expected unexpected)",
|
||||
"workflow = null (expected 76 Nightly report (provided, has a bug))",
|
||||
"node has none of ['Count by category']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
$ uv run tools/flow.py push reference/lab02-example1-dlq.json
|
||||
! unpublished '76 Intake · request rules (answer, Lab 01 task 2)' (w4bb85fc171aea9d): it used the same webhook path
|
||||
✔ pushed '76 Intake · DLQ (reference, Lab 02 example 1, includes Lab 01 tasks)' (id w92de92194fc1657)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 4.1s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 예제 1 · 실패한 2건은 DLQ에 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250889.918716
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250889.918718
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.1s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✘ 예제 2 · Error Workflow가 알림을 남김 · 0.0s · severity = null (expected critical); reason = null (expected unexpected); workflow = null (expected 76 Nightly report (provided, has a bug)); node has none of ['Count by category']
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{}
|
||||
Passed 3/4
|
||||
report: runs/lab02-20261029-141452.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 0
|
||||
DLQ: 2 (parked 0)
|
||||
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
alerts: 0
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"cases": "lab02.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 4,
|
||||
"total": 4,
|
||||
"results": [
|
||||
{
|
||||
"case": "예제 1 · API가 멈추면 실패로 표시 · 200",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 1 · 실패한 2건은 DLQ에",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250914.161773
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250914.161775
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · 예상 못 한 오류 (nightly-report)",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "예제 2 · Error Workflow가 알림을 남김",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10104",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250914.256605
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
$ uv run tools/flow.py push reference/lab02-example2-error-handler.json reference/lab02-example2-intake.json
|
||||
! unpublished '76 Intake · DLQ (reference, Lab 02 example 1, includes Lab 01 tasks)' (w92de92194fc1657): it used the same webhook path
|
||||
✔ pushed '76 Error handler (reference)' (id errHandler76001)
|
||||
✔ pushed '76 Intake · DLQ + error workflow (reference, Lab 02 example 2)' (id w01376519d41c1ce)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 4.8s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 예제 1 · 실패한 2건은 DLQ에 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250914.161773
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250914.161775
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.1s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✔ 예제 2 · Error Workflow가 알림을 남김 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10104",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250914.256605
|
||||
}
|
||||
Passed 4/4
|
||||
report: runs/lab02-20261029-141517.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 0
|
||||
DLQ: 2 (parked 0)
|
||||
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
alerts: 1
|
||||
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ 정상 3건 · 2.9s
|
||||
✔ 같은 요청을 다시 보내도 중복 없음 · 2.6s
|
||||
✔ 만들어진 접수 건은 3개 · 0.0s
|
||||
✔ 잘못된 항목은 그 항목만 거절 · 1.9s
|
||||
✔ 목록이 아니면 400 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "flaky", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.5s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
Passed 6/6
|
||||
report: runs/lab01-20261029-141527.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"cases": "lab02-nets.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 5,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "1 · 잘못된 항목 → 결과에만 (rejected)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "2 · API 오류 → failed",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "3 · failed는 DLQ에",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250932.1935513
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250932.1935537
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "4 · 버그 → 500",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "5 · Error Workflow → critical 알림",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10114",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250932.293378
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "6 · DLQ마저 멈추면 결과가 알려야",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"dlq_saved = null (expected false)"
|
||||
],
|
||||
"seconds": 4.1,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "7 · DLQ가 멈춘 것을 누가 아나 (critical · dlq_down)",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"reason = unexpected (expected dlq_down)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10114",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250932.293378
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
$ uv run tools/flow.py test cases/lab02-nets.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 7}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 7
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ 1 · 잘못된 항목 → 결과에만 (rejected) · 1.7s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 2 · API 오류 → failed · 2.8s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 3 · failed는 DLQ에 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250932.1935513
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250932.1935537
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 4 · 버그 → 500 · 0.0s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✔ 5 · Error Workflow → critical 알림 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10114",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250932.293378
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error",
|
||||
"dlq": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "error",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 6 · DLQ마저 멈추면 결과가 알려야 · 4.1s · dlq_saved = null (expected false)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 7 · DLQ가 멈춘 것을 누가 아나 (critical · dlq_down) · 0.0s · reason = unexpected (expected dlq_down)
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10114",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793250932.293378
|
||||
}
|
||||
Passed 5/7
|
||||
report: runs/lab02-nets-20261029-141539.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 2
|
||||
T-2001 key R-201 장비/보통 프린터 용지가 걸려요
|
||||
T-2002 key R-203 소프트웨어/보통 메일 첨부가 안 열려요
|
||||
DLQ: 2 (parked 0)
|
||||
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
alerts: 1
|
||||
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"cases": "lab02-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 0,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 멈춤 → DLQ",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"dlq_saved = null (expected true)",
|
||||
"alert = null (expected sent)"
|
||||
],
|
||||
"seconds": 2.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "처리한 실패도 알림 (high · 개수)",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"severity = null (expected high)",
|
||||
"reason = null (expected dlq)",
|
||||
"count = null (expected 2)",
|
||||
"workflow = null (expected intake)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {}
|
||||
},
|
||||
{
|
||||
"case": "실패가 없으면 알림 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"alert = null (expected none)"
|
||||
],
|
||||
"seconds": 2.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "알림은 그대로 1개",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 0 (expected 1)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 0,
|
||||
"alerts": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ도 멈추면 critical",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"dlq_saved = null (expected false)"
|
||||
],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "잃어버린 id를 알림에",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"severity = null (expected critical)",
|
||||
"reason = null (expected dlq_down)",
|
||||
"count = null (expected 2)",
|
||||
"message has none of ['R-201']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 1
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ API가 멈춤 → DLQ · 2.7s · dlq_saved = null (expected true); alert = null (expected sent)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 처리한 실패도 알림 (high · 개수) · 0.0s · severity = null (expected high); reason = null (expected dlq); count = null (expected 2); workflow = null (expected intake)
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{}
|
||||
✘ 실패가 없으면 알림 없음 · 2.8s · alert = null (expected none)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 알림은 그대로 1개 · 0.0s · count = 0 (expected 1)
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 0,
|
||||
"alerts": []
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error",
|
||||
"dlq": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "error",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ DLQ도 멈추면 critical · 2.4s · dlq_saved = null (expected false)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 잃어버린 id를 알림에 · 0.0s · severity = null (expected critical); reason = null (expected dlq_down); count = null (expected 2); message has none of ['R-201']
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{}
|
||||
Passed 0/6
|
||||
report: runs/lab02-challenge-20261029-141547.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"cases": "lab02-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 멈춤 → DLQ",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.3,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "처리한 실패도 알림 (high · 개수)",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10120",
|
||||
"at": 1793250965.2341518
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "실패가 없으면 알림 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "알림은 그대로 1개",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10120",
|
||||
"at": 1793250965.2341518
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ도 멈추면 critical",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "잃어버린 id를 알림에",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "critical",
|
||||
"reason": "dlq_down",
|
||||
"count": 2,
|
||||
"message": "DLQ save failed: dlq service is failing (fault switch) · lost ids R-201, R-203",
|
||||
"execution_id": "10122",
|
||||
"ids": [
|
||||
"R-201",
|
||||
"R-203"
|
||||
],
|
||||
"at": 1793250970.5394928
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
$ uv run tools/flow.py push answers/76-lab02-challenge-alert.json
|
||||
! unpublished '76 Intake · DLQ + error workflow (reference, Lab 02 example 2)' (w01376519d41c1ce): it used the same webhook path
|
||||
✔ pushed '76 Intake · alert on handled failures (answer, Lab 02 task 1)' (id w82959c505ef75f7)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 3
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ API가 멈춤 → DLQ · 4.3s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 처리한 실패도 알림 (high · 개수) · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10120",
|
||||
"at": 1793250965.2341518
|
||||
}
|
||||
✔ 실패가 없으면 알림 없음 · 2.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "낮음"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
✔ 알림은 그대로 1개 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10120",
|
||||
"at": 1793250965.2341518
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error",
|
||||
"dlq": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "error",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ DLQ도 멈추면 critical · 2.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 잃어버린 id를 알림에 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "critical",
|
||||
"reason": "dlq_down",
|
||||
"count": 2,
|
||||
"message": "DLQ save failed: dlq service is failing (fault switch) · lost ids R-201, R-203",
|
||||
"execution_id": "10122",
|
||||
"ids": [
|
||||
"R-201",
|
||||
"R-203"
|
||||
],
|
||||
"at": 1793250970.5394928
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab02-challenge-20261029-141610.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-nets.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ 1 · 잘못된 항목 → 결과에만 (rejected) · 2.1s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 2 · API 오류 → failed · 2.9s
|
||||
✔ 3 · failed는 DLQ에 · 0.0s
|
||||
✔ 4 · 버그 → 500 · 0.1s
|
||||
… before: wait 3s
|
||||
✔ 5 · Error Workflow → critical 알림 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
✔ 6 · DLQ마저 멈추면 결과가 알려야 · 3.7s
|
||||
✔ 7 · DLQ가 멈춘 것을 누가 아나 (critical · dlq_down) · 0.0s
|
||||
Passed 7/7
|
||||
report: runs/lab02-nets-20261029-141622.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 예제 1 · API가 멈추면 실패로 표시 · 200 · 2.7s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 예제 1 · 실패한 2건은 DLQ에 · 0.0s
|
||||
✔ 예제 2 · 예상 못 한 오류 (nightly-report) · 0.0s
|
||||
… before: wait 3s
|
||||
✔ 예제 2 · Error Workflow가 알림을 남김 · 0.0s
|
||||
Passed 4/4
|
||||
report: runs/lab02-20261029-141628.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 0
|
||||
DLQ: 2 (parked 0)
|
||||
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
alerts: 2
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
|
||||
@@ -0,0 +1,345 @@
|
||||
{
|
||||
"cases": "lab02-challenge2.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 4,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "첫 실패 → 알림 보냄",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "10분 안에 또 실패 → 보내지 않음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"alert = sent (expected suppressed)"
|
||||
],
|
||||
"seconds": 3.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "dlq 알림은 1개",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 2 (expected 1)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"reason": "dlq"
|
||||
},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10131",
|
||||
"at": 1793250991.527381
|
||||
},
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 3,
|
||||
"message": "3 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10132",
|
||||
"at": 1793250995.206434
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "그래도 DLQ에는 5건 모두",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 5,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250991.5196192
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250991.5196211
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-3",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972318
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-4",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972342
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-5",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972353
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ가 멈춘 것은 늘 보냄 (critical)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.5,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤 · 실패 없음 · 알림 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 첫 실패 → 알림 보냄 · 2.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✘ 10분 안에 또 실패 → 보내지 않음 · 3.7s · alert = sent (expected suppressed)
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✘ dlq 알림은 1개 · 0.0s · count = 2 (expected 1)
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{
|
||||
"reason": "dlq"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10131",
|
||||
"at": 1793250991.527381
|
||||
},
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 3,
|
||||
"message": "3 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10132",
|
||||
"at": 1793250995.206434
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 그래도 DLQ에는 5건 모두 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 5,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250991.5196192
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250991.5196211
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-3",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972318
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-4",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972342
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-5",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793250995.1972353
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error",
|
||||
"dlq": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "error",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ DLQ가 멈춘 것은 늘 보냄 (critical) · 2.5s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 고친 뒤 · 실패 없음 · 알림 없음 · 2.0s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
Passed 4/6
|
||||
report: runs/lab02-challenge2-20261029-141639.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"cases": "lab02-challenge2.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "첫 실패 → 알림 보냄",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.5,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "10분 안에 또 실패 → 보내지 않음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "suppressed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "dlq 알림은 1개",
|
||||
"webhook": "http://localhost:8800/alerts/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"reason": "dlq"
|
||||
},
|
||||
"response": {
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10135",
|
||||
"at": 1793251017.2530532
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "그래도 DLQ에는 5건 모두",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 5,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251017.2315063
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251017.2315085
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-3",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.9757996
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-4",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.9758017
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-5",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.975803
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ가 멈춘 것은 늘 보냄 (critical)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤 · 실패 없음 · 알림 없음",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.8,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
$ uv run tools/flow.py push answers/76-lab02-challenge2-throttle.json
|
||||
! unpublished '76 Intake · alert on handled failures (answer, Lab 02 task 1)' (w82959c505ef75f7): it used the same webhook path
|
||||
✔ pushed '76 Intake · alert once per 10 minutes (answer, Lab 02 task 2)' (id w75a95815facf2d6)
|
||||
POST http://localhost:5678/webhook/my-intake
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 5
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 3
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 첫 실패 → 알림 보냄 · 4.5s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ 10분 안에 또 실패 → 보내지 않음 · 3.7s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 3,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "suppressed"
|
||||
}
|
||||
✔ dlq 알림은 1개 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/list
|
||||
{
|
||||
"reason": "dlq"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 1,
|
||||
"alerts": [
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10135",
|
||||
"at": 1793251017.2530532
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 그래도 DLQ에는 5건 모두 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 5,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251017.2315063
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251017.2315085
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-3",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.9757996
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-4",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.9758017
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-5",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251020.975803
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error",
|
||||
"dlq": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "error",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ DLQ가 멈춘 것은 늘 보냄 (critical) · 2.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"user": "한지훈",
|
||||
"text": "프린터 용지가 걸려요"
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"user": "한지훈",
|
||||
"text": ""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"user": "김민지",
|
||||
"text": "메일 첨부가 안 열려요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-201",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-203",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-202",
|
||||
"status": "rejected",
|
||||
"reason": "text is required"
|
||||
}
|
||||
],
|
||||
"dlq_saved": false,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 고친 뒤 · 실패 없음 · 알림 없음 · 2.8s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"user": "김민지",
|
||||
"text": "회의실 화면 공유가 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"user": "최유진",
|
||||
"text": "비밀번호를 바꾸라는 창이 계속 떠요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 2,
|
||||
"failed": 0,
|
||||
"rejected": 1,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-402",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "계정",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-401",
|
||||
"status": "rejected",
|
||||
"reason": "duplicate id in this request"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab02-challenge2-20261029-141706.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ API가 멈춤 → DLQ · 2.7s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 처리한 실패도 알림 (high · 개수) · 0.0s
|
||||
✔ 실패가 없으면 알림 없음 · 2.9s
|
||||
✔ 알림은 그대로 1개 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
✔ DLQ도 멈추면 critical · 2.3s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 잃어버린 id를 알림에 · 0.0s
|
||||
Passed 6/6
|
||||
report: runs/lab02-challenge-20261029-141714.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab01-challenge2.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 4 case(s) × 1
|
||||
✔ 같은 id가 두 번이면 두 번째는 거절 · 2.1s
|
||||
✔ 11건은 400 (한 번에 10건까지) · 0.0s
|
||||
✔ 400이면 하나도 만들지 않음 · 0.0s
|
||||
✔ 평소 요청은 그대로 · 2.7s
|
||||
Passed 4/4
|
||||
report: runs/lab01-challenge2-20261029-141719.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 5
|
||||
T-2001 key R-401 네트워크/높음 회의실 화면 공유가 안 돼요
|
||||
T-2002 key R-402 계정/보통 비밀번호를 바꾸라는 창이 계속 떠요
|
||||
T-2003 key R-101 네트워크/높음 VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있
|
||||
T-2004 key R-102 장비/보통 새 모니터를 하나 더 받을 수 있을까요?
|
||||
T-2005 key R-103 소프트웨어/높음 엑셀 라이선스가 만료됐다고 나와요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 0
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"cases": "lab03.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 1,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 멈춘 동안 2건 → DLQ",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤 다시 돌리기",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"replayed = null (expected 2)",
|
||||
"ok = null (expected 2)",
|
||||
"still_failed = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-replay\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ가 비었음",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"size = 2 (expected 0)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251042.6309726
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251042.630976
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "접수 건은 2개 (중복 없음)",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"count = 0 (expected 2)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 0,
|
||||
"tickets": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "한 번 더 돌려도 할 일 없음",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"replayed = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-replay\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
$ uv run tools/flow.py test cases/lab03.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 5
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 5 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ API가 멈춘 동안 2건 → DLQ · 2.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 고친 뒤 다시 돌리기 · 0.0s · status 404 (expected 200); replayed = null (expected 2); ok = null (expected 2); still_failed = null (expected 0)
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-replay\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
✘ DLQ가 비었음 · 0.0s · size = 2 (expected 0)
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251042.6309726
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251042.630976
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 접수 건은 2개 (중복 없음) · 0.0s · count = 0 (expected 2)
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 0,
|
||||
"tickets": []
|
||||
}
|
||||
✘ 한 번 더 돌려도 할 일 없음 · 0.0s · status 404 (expected 200); replayed = null (expected 0)
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-replay\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
Passed 1/5
|
||||
report: runs/lab03-20261029-141722.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"cases": "lab03.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 5,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 멈춘 동안 2건 → DLQ",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤 다시 돌리기",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "DLQ가 비었음",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "접수 건은 2개 (중복 없음)",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "한 번 더 돌려도 할 일 없음",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
$ uv run tools/flow.py push reference/lab03-example1-replay.json
|
||||
✔ pushed '76 Replay (reference, Lab 03 example 1)' (id we4c5fdffca93da1)
|
||||
POST http://localhost:5678/webhook/my-replay
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 1
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 5 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ API가 멈춘 동안 2건 → DLQ · 4.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 고친 뒤 다시 돌리기 · 1.1s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ DLQ가 비었음 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
✔ 접수 건은 2개 (중복 없음) · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 한 번 더 돌려도 할 일 없음 · 0.0s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 0
|
||||
}
|
||||
Passed 5/5
|
||||
report: runs/lab03-20261029-141738.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 2
|
||||
T-2001 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
T-2002 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 1
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"cases": "lab03-drill.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 7,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "1 · 평소",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.6,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "2 · 장애 시작 · API 오류",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.0,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "3 · DLQ 확인",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251069.7296107
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251069.7296135
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "4 · 보고서 작업도 실패",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "5 · 알림 확인",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10159",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793251069.8482687
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "6 · 고친 뒤 재처리",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.4,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "7 · 결과 확인 · 중복 없이 5건",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ created tickets, DLQ and alerts emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py fault tickets error
|
||||
✔ fault switch: {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call my-intake requests/batch-two.json
|
||||
HTTP 200 · 2.8s
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py call nightly-report
|
||||
HTTP 500 · 0.1s
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: {"tickets": "error"}
|
||||
created tickets: 0
|
||||
DLQ: 2 (parked 0)
|
||||
D-1 R-301 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
D-2 R-302 attempts 1 parked False 500 - "{\"error\": \"tickets service is failing (f
|
||||
alerts: 2
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
|
||||
|
||||
$ uv run tools/flow.py fault tickets ok
|
||||
✔ fault switch: {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call my-replay
|
||||
HTTP 200 · 1.4s
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 2
|
||||
T-2001 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
T-2002 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 2
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
[critical] 76 Nightly report (provided, has a bug) · unexpected · Count by category · rows is not iterable [line 4]
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-drill.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ 1 · 평소 · 2.6s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"user": "이준호",
|
||||
"text": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"user": "박서연",
|
||||
"text": "새 모니터를 하나 더 받을 수 있을까요?"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"user": "최유진",
|
||||
"text": "엑셀 라이선스가 만료됐다고 나와요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 3,
|
||||
"created": 3,
|
||||
"failed": 0,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-101",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2001",
|
||||
"duplicate": false,
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"id": "R-102",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2002",
|
||||
"duplicate": false,
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"id": "R-103",
|
||||
"status": "created",
|
||||
"ticket_id": "T-2003",
|
||||
"duplicate": false,
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
}
|
||||
],
|
||||
"alert": "none"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 2 · 장애 시작 · API 오류 · 3.0s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ 3 · DLQ 확인 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251069.7296107
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251069.7296135
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 4 · 보고서 작업도 실패 · 0.0s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✔ 5 · 알림 확인 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "76 Nightly report (provided, has a bug)",
|
||||
"workflow_id": "wb9ce32c18d7df56",
|
||||
"node": "Count by category",
|
||||
"message": "rows is not iterable [line 4]",
|
||||
"execution_id": "10159",
|
||||
"severity": "critical",
|
||||
"reason": "unexpected",
|
||||
"at": 1793251069.8482687
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 6 · 고친 뒤 재처리 · 1.4s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ 7 · 결과 확인 · 중복 없이 5건 · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 5,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-101",
|
||||
"title": "VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있어요",
|
||||
"requester": "이준호",
|
||||
"category": "네트워크",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-102",
|
||||
"title": "새 모니터를 하나 더 받을 수 있을까요?",
|
||||
"requester": "박서연",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2003",
|
||||
"key": "R-103",
|
||||
"title": "엑셀 라이선스가 만료됐다고 나와요",
|
||||
"requester": "최유진",
|
||||
"category": "소프트웨어",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2004",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2005",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
}
|
||||
]
|
||||
}
|
||||
Passed 7/7
|
||||
report: runs/lab03-drill-20261029-141754.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"cases": "lab03-half.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 6,
|
||||
"total": 6,
|
||||
"results": [
|
||||
{
|
||||
"case": "1 · API가 느림 → 기다리다 실패",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 11.6,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "2 · DLQ에 2건",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "timeout of 3000ms exceeded",
|
||||
"workflow": "intake",
|
||||
"at": 1793251086.0805922
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "timeout of 3000ms exceeded",
|
||||
"workflow": "intake",
|
||||
"at": 1793251086.080595
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "3 · 그런데 접수 건은 이미 2개",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "4 · 재처리 · 이미 있는 건이라 duplicate",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "5 · 그래도 2개 (중복 없음)",
|
||||
"webhook": "http://localhost:8800/tickets-created/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "6 · DLQ가 비었음",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
$ uv run tools/flow.py test cases/lab03-half.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 5}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 5
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "slow", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "slow"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "slow",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 1 · API가 느림 → 기다리다 실패 · 11.6s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "timeout of 3000ms exceeded"
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ 2 · DLQ에 2건 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "timeout of 3000ms exceeded",
|
||||
"workflow": "intake",
|
||||
"at": 1793251086.0805922
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 1,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "timeout of 3000ms exceeded",
|
||||
"workflow": "intake",
|
||||
"at": 1793251086.080595
|
||||
}
|
||||
]
|
||||
}
|
||||
… before: wait 8s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 3 · 그런데 접수 건은 이미 2개 · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 4 · 재처리 · 이미 있는 건이라 duplicate · 1.1s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 2
|
||||
}
|
||||
✔ 5 · 그래도 2개 (중복 없음) · 0.0s
|
||||
→ request POST http://localhost:8800/tickets-created/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"count": 2,
|
||||
"tickets": [
|
||||
{
|
||||
"ticket_id": "T-2001",
|
||||
"key": "R-301",
|
||||
"title": "사내 위키 로그인이 안 돼요",
|
||||
"requester": "정우성",
|
||||
"category": "계정",
|
||||
"priority": "높음"
|
||||
},
|
||||
{
|
||||
"ticket_id": "T-2002",
|
||||
"key": "R-302",
|
||||
"title": "노트북 배터리가 금방 닳아요",
|
||||
"requester": "이준호",
|
||||
"category": "장비",
|
||||
"priority": "보통"
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 6 · DLQ가 비었음 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 0,
|
||||
"parked": 0,
|
||||
"items": []
|
||||
}
|
||||
Passed 6/6
|
||||
report: runs/lab03-half-20261029-141815.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 2
|
||||
T-2001 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
T-2002 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 1
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"cases": "lab03-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 3,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 오래 멈춤 · 처음 실패",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.4,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 1 · 또 실패",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.2,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 2 · 또 실패",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.4,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 3 · 세 번 실패한 것은 멈춤 (parked)",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"replayed = 2 (expected 0)",
|
||||
"parked = 0 (expected 2)"
|
||||
],
|
||||
"seconds": 2.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "parked로 남음",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"parked = 0 (expected 2)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 4,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251105.2922482
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 4,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251105.5231082
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "멈춘 것은 알림",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"reason = dlq (expected parked)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10168",
|
||||
"at": 1793251099.0033967
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤에도 parked는 자동으로 돌리지 않음",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"replayed = 2 (expected 0)"
|
||||
],
|
||||
"seconds": 1.6,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 1
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ API가 오래 멈춤 · 처음 실패 · 3.4s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ 재처리 1 · 또 실패 · 2.2s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ 재처리 2 · 또 실패 · 2.4s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✘ 재처리 3 · 세 번 실패한 것은 멈춤 (parked) · 2.0s · replayed = 2 (expected 0); parked = 0 (expected 2)
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✘ parked로 남음 · 0.0s · parked = 0 (expected 2)
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 0,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 4,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251105.2922482
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 4,
|
||||
"parked": false,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251105.5231082
|
||||
}
|
||||
]
|
||||
}
|
||||
✘ 멈춘 것은 알림 · 0.0s · reason = dlq (expected parked)
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "intake",
|
||||
"node": "Send to DLQ",
|
||||
"severity": "high",
|
||||
"reason": "dlq",
|
||||
"count": 2,
|
||||
"message": "2 request(s) could not be created and wait in the DLQ",
|
||||
"execution_id": "10168",
|
||||
"at": 1793251099.0033967
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 고친 뒤에도 parked는 자동으로 돌리지 않음 · 1.6s · replayed = 2 (expected 0)
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
Passed 3/7
|
||||
report: runs/lab03-challenge-20261029-141827.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"cases": "lab03-challenge.json",
|
||||
"webhook": "my-intake",
|
||||
"passed": 7,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "API가 오래 멈춤 · 처음 실패",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 4.3,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 1 · 또 실패",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.9,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 2 · 또 실패",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "재처리 3 · 세 번 실패한 것은 멈춤 (parked)",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 2,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "parked로 남음",
|
||||
"webhook": "http://localhost:8800/dlq/list",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"size": 2,
|
||||
"parked": 2,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 3,
|
||||
"parked": true,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "gave up after 3 attempts: 500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251128.6803358
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 3,
|
||||
"parked": true,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "gave up after 3 attempts: 500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251128.9017823
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "멈춘 것은 알림",
|
||||
"webhook": "http://localhost:8800/alerts/latest",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"workflow": "replay",
|
||||
"node": "Park it",
|
||||
"severity": "high",
|
||||
"reason": "parked",
|
||||
"dlq_id": "D-2",
|
||||
"message": "D-2 (R-302) parked after 3 attempts",
|
||||
"at": 1793251129.0694785
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "고친 뒤에도 parked는 자동으로 돌리지 않음",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
$ uv run tools/flow.py push answers/76-lab03-challenge-park.json
|
||||
! unpublished '76 Replay (reference, Lab 03 example 1)' (we4c5fdffca93da1): it used the same webhook path
|
||||
✔ pushed '76 Replay · park after 3 attempts (answer, Lab 03 task 1)' (id w7d8de63c811909b)
|
||||
POST http://localhost:5678/webhook/my-replay
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 1
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ API가 오래 멈춤 · 처음 실패 · 4.3s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ 재처리 1 · 또 실패 · 1.9s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ 재처리 2 · 또 실패 · 2.1s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 0,
|
||||
"still_failed": 2,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ 재처리 3 · 세 번 실패한 것은 멈춤 (parked) · 0.1s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 2,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ parked로 남음 · 0.0s
|
||||
→ request POST http://localhost:8800/dlq/list
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"size": 2,
|
||||
"parked": 2,
|
||||
"items": [
|
||||
{
|
||||
"dlq_id": "D-1",
|
||||
"attempts": 3,
|
||||
"parked": true,
|
||||
"item": {
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
"reason": "gave up after 3 attempts: 500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251128.6803358
|
||||
},
|
||||
{
|
||||
"dlq_id": "D-2",
|
||||
"attempts": 3,
|
||||
"parked": true,
|
||||
"item": {
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
},
|
||||
"reason": "gave up after 3 attempts: 500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\"",
|
||||
"workflow": "intake",
|
||||
"at": 1793251128.9017823
|
||||
}
|
||||
]
|
||||
}
|
||||
✔ 멈춘 것은 알림 · 0.0s
|
||||
→ request POST http://localhost:8800/alerts/latest
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"workflow": "replay",
|
||||
"node": "Park it",
|
||||
"severity": "high",
|
||||
"reason": "parked",
|
||||
"dlq_id": "D-2",
|
||||
"message": "D-2 (R-302) parked after 3 attempts",
|
||||
"at": 1793251129.0694785
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 고친 뒤에도 parked는 자동으로 돌리지 않음 · 0.0s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 0,
|
||||
"ok": 0,
|
||||
"still_failed": 0,
|
||||
"parked": 0
|
||||
}
|
||||
Passed 7/7
|
||||
report: runs/lab03-challenge-20261029-141849.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 5 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ API가 멈춘 동안 2건 → DLQ · 3.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 고친 뒤 다시 돌리기 · 1.4s
|
||||
✔ DLQ가 비었음 · 0.0s
|
||||
✔ 접수 건은 2개 (중복 없음) · 0.0s
|
||||
✔ 한 번 더 돌려도 할 일 없음 · 0.0s
|
||||
Passed 5/5
|
||||
report: runs/lab03-20261029-141853.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py ops
|
||||
fault switch: all ok
|
||||
created tickets: 2
|
||||
T-2001 key R-301 계정/높음 사내 위키 로그인이 안 돼요
|
||||
T-2002 key R-302 장비/보통 노트북 배터리가 금방 닳아요
|
||||
DLQ: 0 (parked 0)
|
||||
alerts: 1
|
||||
[high] intake · dlq · Send to DLQ · count 2 · 2 request(s) could not be created and wait in the DLQ
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"cases": "lab03-challenge2.json",
|
||||
"webhook": "my-ops-status",
|
||||
"passed": 3,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "1 · 아무 일 없음 → ok",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"status = null (expected ok)",
|
||||
"dlq = null (expected 0)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "2 · (접수 실패 2건)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 2.9,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "3 · DLQ가 있으면 warn",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"status = null (expected warn)",
|
||||
"dlq = null (expected 2)",
|
||||
"reasons has none of ['재처리']"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "4 · (보고서 버그)",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "5 · critical 알림이 있으면 critical",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"status = null (expected critical)",
|
||||
"alerts.critical = null (expected 1)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "6 · (재처리 · 알림 확인 뒤 비움)",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "7 · 고친 뒤 → ok",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": false,
|
||||
"problems": [
|
||||
"status 404 (expected 200)",
|
||||
"status = null (expected ok)",
|
||||
"dlq = null (expected 0)",
|
||||
"created = null (expected 2)"
|
||||
],
|
||||
"seconds": 0.0,
|
||||
"status": 404,
|
||||
"request": {},
|
||||
"response": {
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 1}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 1
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-status · 7 case(s) × 1
|
||||
✘ 1 · 아무 일 없음 → ok · 0.0s · status 404 (expected 200); status = null (expected ok); dlq = null (expected 0)
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 2 · (접수 실패 2건) · 2.9s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✘ 3 · DLQ가 있으면 warn · 0.0s · status 404 (expected 200); status = null (expected warn); dlq = null (expected 2); reasons has none of ['재처리']
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
✔ 4 · (보고서 버그) · 0.0s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✘ 5 · critical 알림이 있으면 critical · 0.0s · status 404 (expected 200); status = null (expected critical); alerts.critical = null (expected 1)
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
✔ 6 · (재처리 · 알림 확인 뒤 비움) · 1.1s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ after: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✘ 7 · 고친 뒤 → ok · 0.0s · status 404 (expected 200); status = null (expected ok); dlq = null (expected 0); created = null (expected 2)
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 404
|
||||
{
|
||||
"code": 404,
|
||||
"message": "The requested webhook \"POST my-ops-status\" is not registered.",
|
||||
"hint": "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"
|
||||
}
|
||||
Passed 3/7
|
||||
report: runs/lab03-challenge2-20261029-141901.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"cases": "lab03-challenge2.json",
|
||||
"webhook": "my-ops-status",
|
||||
"passed": 7,
|
||||
"total": 7,
|
||||
"results": [
|
||||
{
|
||||
"case": "1 · 아무 일 없음 → ok",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.9,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"status": "ok",
|
||||
"dlq": 0,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 0
|
||||
},
|
||||
"reasons": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "2 · (접수 실패 2건)",
|
||||
"webhook": "my-intake",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 3.7,
|
||||
"status": 200,
|
||||
"request": {
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "3 · DLQ가 있으면 warn",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"status": "warn",
|
||||
"dlq": 2,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 1
|
||||
},
|
||||
"reasons": [
|
||||
"DLQ 2건: 원인을 고친 뒤 재처리",
|
||||
"high 알림 1건 (10분)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "4 · (보고서 버그)",
|
||||
"webhook": "nightly-report",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 500,
|
||||
"request": {},
|
||||
"response": {
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "5 · critical 알림이 있으면 critical",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.1,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"status": "critical",
|
||||
"dlq": 2,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 1,
|
||||
"high": 1
|
||||
},
|
||||
"reasons": [
|
||||
"critical 알림 1건 (10분)",
|
||||
"DLQ 2건: 원인을 고친 뒤 재처리",
|
||||
"high 알림 1건 (10분)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "6 · (재처리 · 알림 확인 뒤 비움)",
|
||||
"webhook": "my-replay",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 1.2,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case": "7 · 고친 뒤 → ok",
|
||||
"webhook": "my-ops-status",
|
||||
"run": 1,
|
||||
"passed": true,
|
||||
"problems": [],
|
||||
"seconds": 0.0,
|
||||
"status": 200,
|
||||
"request": {},
|
||||
"response": {
|
||||
"status": "ok",
|
||||
"dlq": 0,
|
||||
"parked": 0,
|
||||
"created": 2,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 0
|
||||
},
|
||||
"reasons": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
$ uv run tools/flow.py push answers/76-lab03-challenge2-ops-status.json
|
||||
✔ pushed '76 Ops status (answer)' (id w07b5812eaa88ef9)
|
||||
POST http://localhost:5678/webhook/my-ops-status
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge2.json -v
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/tickets-reset
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/dlq-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 0
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
POST http://localhost:5678/webhook/my-ops-status · 7 case(s) × 1
|
||||
✔ 1 · 아무 일 없음 → ok · 0.9s
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"status": "ok",
|
||||
"dlq": 0,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 0
|
||||
},
|
||||
"reasons": []
|
||||
}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"tickets": "error"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "error",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 2 · (접수 실패 2건) · 3.7s
|
||||
→ request POST /webhook/my-intake
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"user": "정우성",
|
||||
"text": "사내 위키 로그인이 안 돼요"
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"user": "이준호",
|
||||
"text": "노트북 배터리가 금방 닳아요"
|
||||
}
|
||||
]
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"total": 2,
|
||||
"created": 0,
|
||||
"failed": 2,
|
||||
"rejected": 0,
|
||||
"duplicates": 0,
|
||||
"unclassified": 0,
|
||||
"results": [
|
||||
{
|
||||
"id": "R-301",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
},
|
||||
{
|
||||
"id": "R-302",
|
||||
"status": "failed",
|
||||
"reason": "500 - \"{\\\"error\\\": \\\"tickets service is failing (fault switch)\\\"}\""
|
||||
}
|
||||
],
|
||||
"dlq_saved": true,
|
||||
"alert": "sent"
|
||||
}
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
✔ 3 · DLQ가 있으면 warn · 0.1s
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"status": "warn",
|
||||
"dlq": 2,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 1
|
||||
},
|
||||
"reasons": [
|
||||
"DLQ 2건: 원인을 고친 뒤 재처리",
|
||||
"high 알림 1건 (10분)"
|
||||
]
|
||||
}
|
||||
✔ 4 · (보고서 버그) · 0.1s
|
||||
→ request POST /webhook/nightly-report
|
||||
{}
|
||||
← response HTTP 500
|
||||
{
|
||||
"message": "Error in workflow"
|
||||
}
|
||||
… before: wait 3s
|
||||
✔ 5 · critical 알림이 있으면 critical · 0.1s
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"status": "critical",
|
||||
"dlq": 2,
|
||||
"parked": 0,
|
||||
"created": 0,
|
||||
"alerts": {
|
||||
"critical": 1,
|
||||
"high": 1
|
||||
},
|
||||
"reasons": [
|
||||
"critical 알림 1건 (10분)",
|
||||
"DLQ 2건: 원인을 고친 뒤 재처리",
|
||||
"high 알림 1건 (10분)"
|
||||
]
|
||||
}
|
||||
✔ 6 · (재처리 · 알림 확인 뒤 비움) · 1.2s
|
||||
→ request POST /webhook/my-replay
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"replayed": 2,
|
||||
"ok": 2,
|
||||
"still_failed": 0,
|
||||
"parked": 0,
|
||||
"duplicates": 0
|
||||
}
|
||||
✔ after: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
→ request POST http://localhost:8800/alerts-clear
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"cleared": 2
|
||||
}
|
||||
✔ 7 · 고친 뒤 → ok · 0.0s
|
||||
→ request POST /webhook/my-ops-status
|
||||
{}
|
||||
← response HTTP 200
|
||||
{
|
||||
"status": "ok",
|
||||
"dlq": 0,
|
||||
"parked": 0,
|
||||
"created": 2,
|
||||
"alerts": {
|
||||
"critical": 0,
|
||||
"high": 0
|
||||
},
|
||||
"reasons": []
|
||||
}
|
||||
Passed 7/7
|
||||
report: runs/lab03-challenge2-20261029-141921.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
→ request POST http://localhost:8800/admin/fault
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok"
|
||||
}
|
||||
← response HTTP 200
|
||||
{
|
||||
"faq": "ok",
|
||||
"tickets": "ok",
|
||||
"prompts": "ok",
|
||||
"usage": "ok",
|
||||
"dlq": "ok",
|
||||
"slow_seconds": 8
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab03-challenge.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 7 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ API가 오래 멈춤 · 처음 실패 · 2.8s
|
||||
✔ 재처리 1 · 또 실패 · 1.9s
|
||||
✔ 재처리 2 · 또 실패 · 2.5s
|
||||
✔ 재처리 3 · 세 번 실패한 것은 멈춤 (parked) · 0.1s
|
||||
✔ parked로 남음 · 0.0s
|
||||
✔ 멈춘 것은 알림 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 고친 뒤에도 parked는 자동으로 돌리지 않음 · 0.0s
|
||||
Passed 7/7
|
||||
report: runs/lab03-challenge-20261029-141929.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py test cases/lab02-challenge2.json
|
||||
✔ before: POST http://localhost:8800/tickets-reset · HTTP 200 · 0.0s · {"cleared": 0}
|
||||
✔ before: POST http://localhost:8800/dlq-clear · HTTP 200 · 0.0s · {"cleared": 2}
|
||||
✔ before: POST http://localhost:8800/alerts-clear · HTTP 200 · 0.0s · {"cleared": 3}
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
POST http://localhost:5678/webhook/my-intake · 6 case(s) × 1
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 첫 실패 → 알림 보냄 · 2.9s
|
||||
✔ 10분 안에 또 실패 → 보내지 않음 · 3.4s
|
||||
✔ dlq 알림은 1개 · 0.0s
|
||||
✔ 그래도 DLQ에는 5건 모두 · 0.0s
|
||||
✔ before: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "error", "prompts": "ok", "usage": "ok", "dlq": "error", "slow_seconds": 8}
|
||||
✔ DLQ가 멈춘 것은 늘 보냄 (critical) · 2.8s
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
✔ 고친 뒤 · 실패 없음 · 알림 없음 · 3.4s
|
||||
Passed 6/6
|
||||
report: runs/lab02-challenge2-20261029-141941.json
|
||||
✔ after: POST http://localhost:8800/admin/fault · HTTP 200 · 0.0s · {"faq": "ok", "tickets": "ok", "prompts": "ok", "usage": "ok", "dlq": "ok", "slow_seconds": 8}
|
||||
|
||||
$ uv run tools/flow.py call my-ops-status
|
||||
HTTP 200 · 0.1s
|
||||
{
|
||||
"status": "critical",
|
||||
"dlq": 5,
|
||||
"parked": 0,
|
||||
"created": 2,
|
||||
"alerts": {
|
||||
"critical": 1,
|
||||
"high": 1
|
||||
},
|
||||
"reasons": [
|
||||
"critical 알림 1건 (10분)",
|
||||
"DLQ 5건: 원인을 고친 뒤 재처리",
|
||||
"high 알림 1건 (10분)"
|
||||
]
|
||||
}
|
||||
|
||||
$ uv run tools/flow.py ops --reset
|
||||
✔ created tickets, DLQ and alerts emptied, fault switch all ok
|
||||
|
||||
$ uv run tools/flow.py doctor
|
||||
! n8n install folder /Users/mcchae/day65-n8n
|
||||
→ day 65 SETUP-N8N 5단계 (get.n8n.io 설치). 다른 곳에 설치했다면 Last3Mon/65/n8n/.env 에 N8N_HOME=<폴더>
|
||||
✔ n8n container day65-n8n-n8n-1
|
||||
✔ n8n http://localhost:5678
|
||||
✔ data API http://localhost:8800
|
||||
✔ dead letter queue /dlq · alerts · fault switch dlq (day 76 data API)
|
||||
✔ n8n → http://data-api:8800
|
||||
✔ credential 'OpenAI (.env)' (openAiCourse0001)
|
||||
✔ node catalog (for check)
|
||||
✔ CLI coding agent: codex, claude, agy
|
||||
ready
|
||||
@@ -0,0 +1,428 @@
|
||||
{
|
||||
"name": "76 Intake · LLM fallback (answer, Lab 01 task 1)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"requests must be a list\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const { failed_requests, ...rest } = $('Summarize').first().json;\nreturn [{ json: rest }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
{
|
||||
"name": "76 Intake · request rules (answer, Lab 01 task 2)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) && $json.body.requests.length <= 10 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: Array.isArray($json.body.requests) ? 'too many requests (max 10)' : 'requests must be a list' }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nconst seen = new Set();\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n const id = String(r.id ?? '').trim();\n if (id && seen.has(id)) problems.push('duplicate id in this request');\n if (id) seen.add(id);\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const { failed_requests, ...rest } = $('Summarize').first().json;\nreturn [{ json: rest }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
{
|
||||
"name": "76 Intake · alert on handled failures (answer, Lab 02 task 1)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) && $json.body.requests.length <= 10 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: Array.isArray($json.body.requests) ? 'too many requests (max 10)' : 'requests must be a list' }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nconst seen = new Set();\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n const id = String(r.id ?? '').trim();\n if (id && seen.has(id)) problems.push('duplicate id in this request');\n if (id) seen.add(id);\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst { failed_requests, ...rest } = $('Summarize').first().json;\nconst out = { ...rest };\nif (true) {\n if (ran('Send to DLQ')) out.dlq_saved = Number($('Send to DLQ').first().json.stored ?? 0) > 0;\n out.alert = ran('Send alert') ? 'sent' : (ran('Decide alert') && $('Decide alert').first().json.suppressed ? 'suppressed' : 'none');\n}\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Any failed?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.failed > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send to DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: $json.failed_requests.map((r, i) => ({ item: r, reason: $json.results.filter((x) => x.status === 'failed')[i]?.reason ?? 'failed', workflow: 'intake' })) }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Make alert",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const s = $('Summarize').first().json;\nconst saved = Number($json.stored ?? 0) > 0;\nreturn [{ json: saved\n ? { workflow: 'intake', node: 'Send to DLQ', severity: 'high', reason: 'dlq', count: s.failed,\n message: s.failed + ' request(s) could not be created and wait in the DLQ', execution_id: $execution.id }\n : { workflow: 'intake', node: 'Send to DLQ', severity: 'critical', reason: 'dlq_down', count: s.failed,\n message: 'DLQ save failed: ' + String($json.error ?? JSON.stringify($json)).slice(0, 150) + ' · lost ids ' + s.failed_requests.map((r) => r.id).join(', '),\n execution_id: $execution.id, ids: s.failed_requests.map((r) => r.id) } }];"
|
||||
},
|
||||
"position": [
|
||||
2640,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send alert",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify((({ suppressed, recent, ...a }) => a)($json)) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
3080,
|
||||
60
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Any failed?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Any failed?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send to DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send to DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Make alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Make alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"errorWorkflow": "errHandler76001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
{
|
||||
"name": "76 Intake · alert once per 10 minutes (answer, Lab 02 task 2)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Intake webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-intake",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Is a list?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ Array.isArray($json.body.requests) && $json.body.requests.length <= 10 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Reject body",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ error: Array.isArray($json.body.requests) ? 'too many requests (max 10)' : 'requests must be a list' }) }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Check items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const requests = $('Intake webhook').first().json.body.requests;\nconst seen = new Set();\nreturn requests.map((r, index) => {\n const problems = [];\n if (!r || typeof r !== 'object') return { json: { index, valid: false, reason: 'not an object', id: null } };\n if (!String(r.id ?? '').trim()) problems.push('id is required');\n if (typeof r.text !== 'string' || !r.text.trim()) problems.push('text is required');\n if (!String(r.user ?? '').trim()) problems.push('user is required');\n const id = String(r.id ?? '').trim();\n if (id && seen.has(id)) problems.push('duplicate id in this request');\n if (id) seen.add(id);\n return { json: { index, valid: problems.length === 0, reason: problems.join(', '), id: r.id ?? null, user: r.user ?? '', text: typeof r.text === 'string' ? r.text.trim() : '' } };\n});"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.valid }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Classify",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "={{ $json.text }}",
|
||||
"options": {
|
||||
"systemMessage": "You sort one employee IT request. Return only JSON, no code fence: {\"category\": \"계정\" | \"네트워크\" | \"장비\" | \"소프트웨어\" | \"기타\", \"priority\": \"높음\" | \"보통\" | \"낮음\"}. 높음 only when work is blocked now.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
200
|
||||
],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Classify model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
420
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Parse",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Valid?').all().filter((item) => item.json.valid);\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n let c = {}, fallback = false;\n if (item.json.error || item.json.output === undefined) { fallback = true; }\n else { try { c = JSON.parse(String(item.json.output).replace(/^```(json)?\\s*|```$/g, '')); } catch (e) { fallback = true; } }\n return { json: { ...req, category: fallback ? '미분류' : (c.category ?? '기타'), priority: fallback ? '보통' : (c.priority ?? '보통'), classified: !fallback } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Create ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/tickets",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ key: $json.id, title: $json.text, requester: $json.user, category: $json.category, priority: $json.priority }) }}",
|
||||
"options": {
|
||||
"timeout": 3000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
],
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 300,
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"name": "Mark results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const inputs = $('Parse').all();\nreturn $input.all().map((item, i) => {\n const req = inputs[i].json;\n if (item.json.ticket_id) return { json: { id: req.id, status: 'created', ticket_id: item.json.ticket_id, duplicate: item.json.duplicate === true, category: req.category, priority: req.priority } };\n const message = item.json.error?.message ?? item.json.error ?? 'create failed';\n return { json: { id: req.id, status: 'failed', reason: String(message).slice(0, 200), request: { id: req.id, user: req.user, text: req.text } } };\n});"
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mark rejected",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "return $input.all().map((item) => ({ json: { id: item.json.id, status: 'rejected', reason: item.json.reason } }));"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
640
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const rows = $input.all().map((item) => item.json);\nconst created = rows.filter((r) => r.status === 'created');\nconst failed = rows.filter((r) => r.status === 'failed');\nconst rejected = rows.filter((r) => r.status === 'rejected');\nreturn [{ json: { total: rows.length, created: created.length, failed: failed.length, rejected: rejected.length,\n duplicates: created.filter((r) => r.duplicate).length, unclassified: created.filter((r) => r.category === '미분류').length,\n results: rows.map(({ request, ...rest }) => rest), failed_requests: failed.map((r) => r.request) } }];"
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Public result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst { failed_requests, ...rest } = $('Summarize').first().json;\nconst out = { ...rest };\nif (true) {\n if (ran('Send to DLQ')) out.dlq_saved = Number($('Send to DLQ').first().json.stored ?? 0) > 0;\n out.alert = ran('Send alert') ? 'sent' : (ran('Decide alert') && $('Decide alert').first().json.suppressed ? 'suppressed' : 'none');\n}\nreturn [{ json: out }];"
|
||||
},
|
||||
"position": [
|
||||
3740,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3960,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Any failed?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.failed > 0 }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send to DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ items: $json.failed_requests.map((r, i) => ({ item: r, reason: $json.results.filter((x) => x.status === 'failed')[i]?.reason ?? 'failed', workflow: 'intake' })) }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Make alert",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const s = $('Summarize').first().json;\nconst saved = Number($json.stored ?? 0) > 0;\nreturn [{ json: saved\n ? { workflow: 'intake', node: 'Send to DLQ', severity: 'high', reason: 'dlq', count: s.failed,\n message: s.failed + ' request(s) could not be created and wait in the DLQ', execution_id: $execution.id }\n : { workflow: 'intake', node: 'Send to DLQ', severity: 'critical', reason: 'dlq_down', count: s.failed,\n message: 'DLQ save failed: ' + String($json.error ?? JSON.stringify($json)).slice(0, 150) + ' · lost ids ' + s.failed_requests.map((r) => r.id).join(', '),\n execution_id: $execution.id, ids: s.failed_requests.map((r) => r.id) } }];"
|
||||
},
|
||||
"position": [
|
||||
2640,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send alert",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify((({ suppressed, recent, ...a }) => a)($json)) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
3520,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Recent alerts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts/list",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ workflow: 'intake', reason: $json.reason, within: 600 }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
2860,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Decide alert",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const alert = $('Make alert').first().json;\nconst recent = Number($json.count ?? 0);\n// critical is never held back; a high alert with the same reason in the last 10 minutes is enough\nconst suppressed = alert.severity === 'high' && recent > 0;\nreturn [{ json: { ...alert, suppressed, recent } }];"
|
||||
},
|
||||
"position": [
|
||||
3080,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send it?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ !$json.suppressed }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
3300,
|
||||
140
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Intake webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is a list?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is a list?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Valid?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Valid?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Mark rejected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Classify",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Classify": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mark results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mark rejected": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Public result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Any failed?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Any failed?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send to DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send to DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Make alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Make alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Recent alerts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Recent alerts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Decide alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Decide alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send it?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send it?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send alert",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Send alert": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Public result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"errorWorkflow": "errHandler76001"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
{
|
||||
"name": "76 Replay · park after 3 attempts (answer, Lab 03 task 1)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Replay webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-replay",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pick items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const max = Number($('Replay webhook').first().json.body?.max_attempts ?? 3);\nconst items = ($json.items ?? []).filter((e) => !e.parked);\nconst ready = items.filter((e) => e.attempts < max);\nconst tooMany = items.filter((e) => e.attempts >= max);\nreturn [...ready.map((e) => ({ json: { ...e, action: 'replay' } })), ...tooMany.map((e) => ({ json: { ...e, action: 'park' } }))];"
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"alwaysOutputData": true
|
||||
},
|
||||
{
|
||||
"name": "Has items?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.dlq_id }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "exists",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nothing to do",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({ replayed: 0, ok: 0, still_failed: 0, parked: 0 }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
520
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Run intake again",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:5678/webhook/my-intake",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ requests: [$json.item] }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
},
|
||||
"timeout": 60000
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Outcome",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const picked = $('Replay it?').all().filter((item) => item.json.action === 'replay');\nreturn $input.all().map((item, i) => ({ json: { dlq_id: picked[i].json.dlq_id, id: picked[i].json.item?.id, ok: item.json.created === 1, duplicate: item.json.duplicates === 1 } }));"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Remove done",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq/update",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ dlq_id: $json.dlq_id, remove: $json.ok }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summarize",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const ran = (n) => { try { return $(n).isExecuted; } catch (e) { return false; } };\nconst outcomes = ran('Outcome') ? $('Outcome').all().map((i) => i.json) : [];\nconst parked = ran('Park it') ? $('Park it').all().length : 0;\nconst ok = outcomes.filter((o) => o.ok).length;\nreturn [{ json: { replayed: outcomes.length, ok, still_failed: outcomes.length - ok, parked, duplicates: outcomes.filter((o) => o.duplicate).length } }];"
|
||||
},
|
||||
"position": [
|
||||
2200,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send summary",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
2420,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Replay it?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "replay",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Park it",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/dlq/update",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ dlq_id: $json.dlq_id, parked: true, reason: 'gave up after ' + $json.attempts + ' attempts: ' + $json.reason }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
420
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Alert parked",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ workflow: 'replay', node: 'Park it', severity: 'high', reason: 'parked', dlq_id: $json.entry?.dlq_id, message: $json.entry?.dlq_id + ' (' + $json.entry?.item?.id + ') parked after ' + $json.entry?.attempts + ' attempts' }) }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
420
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Collect",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"parameters": {
|
||||
"numberInputs": 2
|
||||
},
|
||||
"position": [
|
||||
1980,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Replay webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pick items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pick items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has items?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has items?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Replay it?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Nothing to do",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Run intake again": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Outcome",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Outcome": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Remove done",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Summarize": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send summary",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Replay it?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run intake again",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Park it",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Park it": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Alert parked",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Remove done": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Alert parked": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Collect",
|
||||
"type": "main",
|
||||
"index": 1
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Collect": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"name": "76 Ops status (answer)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Status webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "my-ops-status",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get DLQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/dlq",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Recent alerts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://data-api:8800/alerts/list",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ within: 600 }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get created",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/tickets-created",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Decide status",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const dlq = $('Get DLQ').first().json;\nconst alerts = $('Recent alerts').first().json.alerts ?? [];\nconst created = $('Get created').first().json.count ?? 0;\nconst parked = (dlq.items ?? []).filter((e) => e.parked).length;\nconst waiting = (dlq.items ?? []).length - parked;\nconst critical = alerts.filter((a) => a.severity === 'critical').length;\nconst high = alerts.filter((a) => a.severity === 'high').length;\nconst reasons = [];\nif (parked) reasons.push(`parked ${parked}건: 사람이 보고 결정`);\nif (critical) reasons.push(`critical 알림 ${critical}건 (10분)`);\nif (waiting) reasons.push(`DLQ ${waiting}건: 원인을 고친 뒤 재처리`);\nif (high) reasons.push(`high 알림 ${high}건 (10분)`);\nconst status = parked || critical ? 'critical' : (waiting || high ? 'warn' : 'ok');\nreturn [{ json: { status, dlq: waiting, parked, created, alerts: { critical, high }, reasons } }];"
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Send status",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Status webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get DLQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get DLQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Recent alerts",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Recent alerts": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get created",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get created": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Decide status",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Decide status": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send status",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user