Files
AriWeb-Suite/docs/verification/local-e2e-2026-10-26.json
T

2870 lines
81 KiB
JSON

{
"generatedAt": "2026-10-26T13:02:10.786Z",
"startedAt": "2026-10-26T12:57:29.622Z",
"env": {
"agents": 4,
"ports": {
"hal": 35361,
"portal": 35287,
"wm": 43619,
"am": 33137,
"mail": 40085
},
"mailServerRegistered": true,
"node": "v24.22.1"
},
"summary": {
"verified_local": 13,
"partial": 15,
"not_verifiable_locally": 2,
"failed": 4,
"unrecorded": 0
},
"flows": [
{
"no": 1,
"title": "신규 고객 가입과 이메일 확인",
"verdict": "verified_local",
"note": "가입→확인 메일(포획)→1회용 토큰 확인→개인 그룹·별칭→중복 가입 안내→기기 목록·세션 종료",
"blockedBy": null,
"steps": [
{
"name": "가입(이메일·표시 이름·비밀번호·필수 동의) → 세션 발급",
"ok": true,
"detail": {
"status": 200,
"hasSession": "[redacted]"
}
},
{
"name": "확인 전 유료·운영 기능 차단(그룹 생성 403 email_unverified)",
"ok": true,
"detail": "email_unverified"
},
{
"name": "확인 메일 수신(발신 포획)에서 토큰 추출",
"ok": true,
"detail": {
"subject": "[Ariweb] 이메일 주소를 확인해 주세요"
}
},
{
"name": "이메일 확인(1회용) → 재사용 거부",
"ok": true,
"detail": {
"first": 200,
"second": "token_used"
}
},
{
"name": "내 정보: 이메일 확인됨 · 개인 그룹 · owner · 별칭 user-01",
"ok": true,
"detail": {
"verified": true,
"groupKind": "personal",
"role": "owner",
"alias": "user-01"
}
},
{
"name": "중복 가입은 존재를 드러내지 않는 안내(409 signup_unavailable)",
"ok": true,
"detail": "signup_unavailable"
},
{
"name": "로그인 기기 목록 · 다른 세션 종료",
"ok": true,
"detail": {
"sessionsBefore": "[redacted]",
"revoked": 2
}
}
],
"evidence": {
"me": {
"verified": true,
"groupKind": "personal",
"role": "owner",
"alias": "user-01"
}
}
},
{
"no": 2,
"title": "개인 사용자 그룹 생성과 다른 사용자 초대",
"verdict": "verified_local",
"note": "팀 그룹 생성·초대 메일·1회용 수락·별칭 user-02·역할 거부·그룹 전환 격리",
"blockedBy": null,
"steps": [
{
"name": "팀 그룹 만들기 → 현재 그룹 전환",
"ok": true,
"detail": "g_01a17849a2c2dec27ee9f14777"
},
{
"name": "이메일 초대(7일) → 초대 메일 링크",
"ok": true,
"detail": "RAXi6X0pnmr-nCEKiIX63pk26vTbIB5d"
},
{
"name": "초대 수락(1회용) → 별칭 user-02 · 역할 member",
"ok": true,
"detail": {
"alias": "user-02",
"role": "member",
"reuse": "invite_invalid"
}
},
{
"name": "권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할",
"ok": true,
"detail": {
"requiredRole": [
"owner",
"admin"
],
"yourRole": "member"
}
},
{
"name": "그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음",
"ok": true,
"detail": {
"team": {
"live": 0,
"purchased": 0,
"inUse": 0,
"reserved": 0,
"unallocated": 0
},
"personal": {
"live": 0,
"purchased": 0,
"inUse": 0,
"reserved": 0,
"unallocated": 0
}
}
}
],
"evidence": {}
},
{
"no": 3,
"title": "상품 주문, 결제 확인, 권리 활성화",
"verdict": "verified_local",
"note": "상품 목록→주문(멱등·수동 결제)→입금 확인 전 승인 거부→입금 확인→승인→권리 활성화→재승인 중복 없음→청구 화면",
"blockedBy": null,
"steps": [
{
"name": "상품 목록(공개 버전만·가격·한도·미확정 정책 표시)",
"ok": true,
"detail": {
"count": 2,
"team": {
"id": "pv_01a17849a127747d9c2bbbb24c",
"price": 49000,
"sites": 3
},
"deferred": 3,
"paymentMethods": [
"manual_transfer"
]
}
},
{
"name": "주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음",
"ok": true,
"detail": {
"orderNo": "AW-20261026-0001",
"status": "awaiting_payment",
"total": 63800,
"dueAt": "2026-11-02T12:57:34.263Z",
"hasInstructions": true
}
},
{
"name": "입금 확인 전 승인 거부(409 payment_not_confirmed)",
"ok": true,
"detail": "payment_not_confirmed"
},
{
"name": "운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시",
"ok": true,
"detail": {
"status": "awaiting_approval",
"delta": [
"sites",
"web_disk",
"web_out",
"mail_storage",
"mail_msgs",
"mailboxes",
"mail_services",
"mail_storage",
"mail_msgs",
"mailboxes"
]
}
},
{
"name": "운영자 승인 → 권리 활성화(재승인은 중복 없음)",
"ok": true,
"detail": {
"granted": 10,
"already": true
}
},
{
"name": "고객 결제 화면: 활성 상품·권리·다음 청구일",
"ok": true,
"detail": {
"sites": 3,
"subs": 1,
"nextBilling": "2026-11-26T12:57:34.327Z"
}
}
],
"evidence": {
"ap": {
"granted": 10,
"already": true
},
"bill": {
"sites": 3,
"subs": 1,
"nextBilling": "2026-11-26T12:57:34.327Z"
}
}
},
{
"no": 4,
"title": "초안 사이트 생성",
"verdict": "verified_local",
"note": "초안 생성·Webmaker 프로젝트 연결·사이트 수 미소비·목록 필터",
"blockedBy": null,
"steps": [
{
"name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내",
"ok": true,
"detail": {
"id": "s_01a17849a3be6f07d129dbdb60",
"lifecycle": "draft",
"projectId": "00000000000000000000000000000001",
"nextSteps": [
"build",
"import",
"domain",
"mode",
"perms"
]
}
},
{
"name": "사이트 목록 필터(초안)·사용량 요약에 예약 0",
"ok": true,
"detail": {
"drafts": 1,
"sitesReserved": 0,
"sitesAllocated": 0
}
}
],
"evidence": {}
},
{
"no": 5,
"title": "Webmaker에서 페이지 제작",
"verdict": "verified_local",
"note": "SSO 티켓(1회용) 진입·저장본 목록·배포 전 확인(가짜 Webmaker — 실제 편집 UI 는 포함하지 않음)",
"blockedBy": null,
"steps": [
{
"name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)",
"ok": true,
"detail": {
"portalRedirect": "http://127.0.0.1:43619/api/auth/sso?ticket=…",
"webmaker": 302,
"reuse": 401
}
},
{
"name": "Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과",
"ok": true,
"detail": {
"versions": 1,
"precheckOk": true
}
}
],
"evidence": {
"sso": {
"portalRedirect": "http://127.0.0.1:43619/api/auth/sso?ticket=…",
"webmaker": 302,
"reuse": 401
},
"ver": {
"versions": 1,
"precheckOk": true
}
}
},
{
"no": 6,
"title": "관리형 배포",
"verdict": "failed",
"note": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])",
"blockedBy": null,
"steps": [
{
"name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나",
"ok": true,
"detail": {
"jobId": "j_01a17849a4096e33f476b17d1b",
"steps": 10,
"again": false,
"siteCount": "reserved",
"server": null
}
},
{
"name": "HAL·Agent 를 거쳐 작업 완료 대기(최대 40초)",
"ok": false,
"detail": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])"
},
{
"name": "[HAL 직접] 같은 Agent 에 로컬 저장본으로 site.deploy → 성공하면 결함은 포털→Agent 번들 전달 구간",
"ok": true,
"detail": {
"status": "done",
"error": null,
"layout": true,
"externalCheck": {
"ok": false,
"status": 404,
"ms": 19,
"url": "http://direct_e2e.preview.localhost/",
"markerFound": false
}
}
},
{
"name": "사용자 취소(진행 중 작업) → 중간 결과 정리·예약 반환",
"ok": true,
"detail": {
"cancel": 200,
"cleanup": 200,
"siteCount": "reserved",
"lifecycle": "building"
}
}
],
"evidence": {
"halLog": [
"2026-10-26T12:58:01.231Z [jobs] site.deploy j_01a1784a0781bd4cb2d160762e 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni",
"2026-10-26T12:58:06.242Z [jobs] site.deploy j_01a1784a1b1b6362b7ba3a604c 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni",
"2026-10-26T12:58:11.248Z [jobs] site.deploy j_01a1784a2eb913f75494650303 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni"
]
}
},
{
"no": 7,
"title": "도메인 연결과 HTTPS 발급",
"verdict": "partial",
"note": "도메인 추가·복사 가능한 TXT/DNS 값·외부 대기 상태까지 확인. 실 DNS 반영·인증서 발급·대표 도메인 확정은 로컬 확인 불가",
"blockedBy": null,
"steps": [
{
"name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)",
"ok": true,
"detail": {
"domainId": "d_01a1784a491476e7f378e8217c",
"txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…",
"copyable": true
}
},
{
"name": "도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시",
"ok": true,
"detail": {}
},
{
"name": "(정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다",
"ok": true,
"detail": {
"queuedProbes": 1
}
}
],
"evidence": {
"dm": {
"domainId": "d_01a1784a491476e7f378e8217c",
"txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…",
"copyable": true
}
}
},
{
"no": 8,
"title": "외부 주소에서 사이트 확인",
"verdict": "not_verifiable_locally",
"note": "외부 주소 확인은 공인 서버·DNS 필요",
"blockedBy": 6,
"steps": [],
"evidence": {}
},
{
"no": 9,
"title": "사이트 정보와 접속·사용량 통계 확인",
"verdict": "partial",
"note": "미배포 사이트는 통계 대신 준비 단계 표시(§8.1) 확인. 실제 접속·네트워크 통계는 배포 뒤 수집",
"blockedBy": 6,
"steps": [
{
"name": "개요: 배포 전/후에 따라 준비 단계 또는 실제 통계",
"ok": true,
"detail": {
"kind": "not_deployed",
"lifecycle": "building",
"hasStats": false,
"readiness": [
{
"id": "build",
"label": "Webmaker 에서 사이트 제작",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build",
"done": true
},
{
"id": "import",
"label": "콘텐츠 가져오기",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build",
"done": false,
"unsupported": "이번 버전에서 지원하지 않음"
},
{
"id": "domain",
"label": "도메인 연결",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=domains",
"done": false
},
{
"id": "mode",
"label": "배포 방식 선택",
"href": "/sites/s_01a17849a3be6f07d129dbdb60/deploy/mode",
"done": true
},
{
"id": "perms",
"label": "구성원 권한 설정",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=settings",
"done": false
}
],
"disk": null
}
},
{
"name": "사용량 탭(배정·실제 사용·마지막 계측)",
"ok": true,
"detail": {
"allocations": [
"web_disk",
"web_out"
]
}
}
],
"evidence": {
"ov": {
"kind": "not_deployed",
"lifecycle": "building",
"hasStats": false,
"readiness": [
{
"id": "build",
"label": "Webmaker 에서 사이트 제작",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build",
"done": true
},
{
"id": "import",
"label": "콘텐츠 가져오기",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=build",
"done": false,
"unsupported": "이번 버전에서 지원하지 않음"
},
{
"id": "domain",
"label": "도메인 연결",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=domains",
"done": false
},
{
"id": "mode",
"label": "배포 방식 선택",
"href": "/sites/s_01a17849a3be6f07d129dbdb60/deploy/mode",
"done": true
},
{
"id": "perms",
"label": "구성원 권한 설정",
"href": "/sites/s_01a17849a3be6f07d129dbdb60?tab=settings",
"done": false
}
],
"disk": null
},
"us": {
"allocations": [
"web_disk",
"web_out"
]
}
}
},
{
"no": 10,
"title": "사이트 대표 메일과 메일함 생성",
"verdict": "partial",
"note": "대표 메일 생성(AriMail 테넌트·보장량·ariweb 표시)·메일함(비밀번호 1회·초대)·DNS 미반영 구분·활성화 작업 외부 대기까지 확인. DNS 반영 뒤 활성화 완료는 실 DNS 필요",
"blockedBy": null,
"steps": [
{
"name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시",
"ok": true,
"detail": {
"id": "msv_01a1784ad1a3c31d4ea6cb7447",
"status": "needs_setup",
"tenant": true,
"ariwebKind": "site_bound",
"limitAccounts": 6,
"siteCountAllocated": 0
}
},
{
"name": "같은 사이트에 두 번째 대표 메일 거부",
"ok": true,
"detail": "mail_domain_in_use"
},
{
"name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음",
"ok": true,
"detail": {
"id": "mb_01a1784ad1df7ea80529a8cf8a",
"address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]",
"mustChange": true,
"imapHost": "127.0.0.1",
"leakedOnReload": false,
"inArimail": true
}
},
{
"name": "초대 방식 메일함(링크 만료·재발송·사용 완료 상태)",
"ok": true,
"detail": {
"inviteState": "pending",
"accepted": 200,
"reuse": "invite_used"
}
},
{
"name": "DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing)",
"ok": true,
"detail": {
"ok": false,
"missing": 3,
"items": [
"mx:missing",
"spf:missing",
"dkim:missing",
"dmarc:recommended"
]
}
},
{
"name": "활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등",
"ok": true,
"detail": {
"status": "awaiting_external",
"waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요",
"sameJob": true,
"steps": [
"entitlement:done",
"dns:waiting",
"prepare:pending",
"verify:pending",
"activate:pending"
]
}
},
{
"name": "보안 보고서(SPF·DKIM·DMARC·MX 상태 + 복사 값)",
"ok": true,
"detail": {
"items": [
"sender:error",
"mx:error",
"spf:error",
"dkim:error",
"dmarc:recommended",
"tls:error",
"roundtrip:recommended"
],
"records": 4
}
}
],
"evidence": {
"svc": {
"id": "msv_01a1784ad1a3c31d4ea6cb7447",
"status": "needs_setup",
"tenant": true,
"ariwebKind": "site_bound",
"limitAccounts": 6,
"siteCountAllocated": 0
},
"mb": {
"id": "mb_01a1784ad1df7ea80529a8cf8a",
"address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]",
"mustChange": true,
"imapHost": "127.0.0.1",
"leakedOnReload": false,
"inArimail": true
},
"dns": {
"ok": false,
"missing": 3,
"items": [
"mx:missing",
"spf:missing",
"dkim:missing",
"dmarc:recommended"
]
},
"act": {
"status": "awaiting_external",
"waitReason": "DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요",
"sameJob": true,
"steps": [
"entitlement:done",
"dns:waiting",
"prepare:pending",
"verify:pending",
"activate:pending"
]
}
}
},
{
"no": 11,
"title": "외부 송신, 수신, 회신 확인",
"verdict": "not_verifiable_locally",
"note": "외부 송신·수신·회신은 실제 SMTP 제출(587)·수신함·외부 자동 회신 주소가 필요하다. 포털은 검증 주소 미설정 시 needs_user, 왕복 실패 시 활성화하지 않는 규칙(단위 테스트 test/mail.test.mjs)으로만 확인됨",
"blockedBy": null,
"steps": [],
"evidence": {}
},
{
"no": 12,
"title": "독립 메일 서비스 생성",
"verdict": "verified_local",
"note": "독립 메일 서비스 생성: 독립 옵션 1 소비, 운영 사이트 수 미소비, 목록에 백업 미제공 문구",
"blockedBy": null,
"steps": [
{
"name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)",
"ok": true,
"detail": {
"id": "d_01a1784af801ae5cf98ef55e1f",
"jobCancelled": null
}
},
{
"name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)",
"ok": true,
"detail": {
"id": "msv_01a1784af809f14b89bc606f73",
"kind": "standalone",
"mailServicesAllocated": 1,
"sitesAllocated": 0
}
}
],
"evidence": {
"r": {
"id": "msv_01a1784af809f14b89bc606f73",
"kind": "standalone",
"mailServicesAllocated": 1,
"sitesAllocated": 0
},
"listCount": 2,
"noBackupNote": "메일 원문은 고객용 백업·복원에 포함되지 않으며, 메일 백업 기능을 제공하지 않습니다."
}
},
{
"no": 13,
"title": "백업 옵션 구매와 첫 백업 확인",
"verdict": "partial",
"note": "옵션 구매·활성화까지 확인. 실제 백업 생성은 배포된 사이트 파일이 있어야 함(흐름 6 결함에 막힘)",
"blockedBy": 6,
"steps": [
{
"name": "백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리",
"ok": true,
"detail": {
"granted": 7,
"orderStatus": "approved"
}
},
{
"name": "백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서)",
"ok": true,
"detail": {
"eligible": true,
"reasons": [],
"deferred": [],
"policy": null,
"status": 200
}
},
{
"name": "옵션 활성화(확인 필수) → 첫 백업 예정 시각",
"ok": true,
"detail": {
"status": "active",
"firstDue": "2026-10-27T12:59:01.665Z",
"nextRun": "2026-10-26T12:59:01.665Z"
}
},
{
"name": "지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증",
"ok": true,
"detail": {
"timedOut": true,
"optionStatus": "active",
"lastJob": null,
"siteServer": null,
"backup": null,
"job": null
}
}
],
"evidence": {
"el": {
"eligible": true,
"reasons": [],
"deferred": [],
"policy": null,
"status": 200
},
"opt": {
"status": "active",
"firstDue": "2026-10-27T12:59:01.665Z",
"nextRun": "2026-10-26T12:59:01.665Z"
},
"run1": {
"timedOut": true,
"optionStatus": "active",
"lastJob": null,
"siteServer": null,
"backup": null,
"job": null
}
}
},
{
"no": 14,
"title": "백업 복원과 외부 사이트 확인",
"verdict": "partial",
"note": "완료된 백업이 없어 복원을 요청할 수 없음",
"blockedBy": 13,
"steps": [],
"evidence": {}
},
{
"no": 15,
"title": "관리형 사이트 서버 이전과 외부 확인",
"verdict": "partial",
"note": "이전 경계값·요청 규칙 확인. 실제 두 Agent 간 사전 복사·전환·외부 확인은 배포된 사이트(흐름 6)와 공인 확인 주소가 필요",
"blockedBy": 6,
"steps": [
{
"name": "이전 화면 경계값: 취소 가능 마지막 단계·쓰기 제한·목표 중단 시간·되돌림 기간·이전 위치 보존·비용(플랫폼 사유 0원)",
"ok": true,
"detail": {
"auto": {
"ok": false,
"reasons": [
"사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다"
]
},
"boundaries": {
"lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다",
"revertHours": 72,
"cost": 10000,
"platformCost": 0
}
}
},
{
"name": "고객 요청(유료·승인 대기) 또는 조건 미충족 사유",
"ok": true,
"detail": {
"status": 200,
"relocation": {
"status": "awaiting_approval",
"kind": "customer_paid",
"cost": 10000
}
}
},
{
"name": "전환 전 고객 취소 가능(요청 취소 → 소유권 이전 등 다른 작업 차단 해제)",
"ok": true,
"detail": {
"status": 200,
"rstatus": "cancelled"
}
}
],
"evidence": {
"b": {
"auto": {
"ok": false,
"reasons": [
"사이트 대표 메일이 있는 사이트는 승인형으로 진행합니다"
]
},
"boundaries": {
"lastCancel": "새 위치 확인(연결 전환 직전)까지 취소할 수 있습니다",
"revertHours": 72,
"cost": 10000,
"platformCost": 0
}
},
"req": {
"status": 200,
"relocation": {
"status": "awaiting_approval",
"kind": "customer_paid",
"cost": 10000
}
}
}
},
{
"no": 16,
"title": "외부 배포 사이트 생성과 배포",
"verdict": "failed",
"note": "동의(버전)·토큰 발급 거부/허용 규칙까지 확인했으나 토큰 발급 API 가 DB 제약으로 500 — 결함 항목 참조",
"blockedBy": null,
"steps": [
{
"name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true,
"detail": {
"id": "s_01a1784b5d03e229abe92a46a1",
"group_id": "g_01a17849a2c2dec27ee9f14777",
"name": "E2E 외부 사이트",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "external",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a17849a21df78a8debb70c18",
"created_at": "2026-10-26T12:59:27.363Z",
"updated_at": "2026-10-26T12:59:27.368Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
},
{
"name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전",
"ok": true,
"detail": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
}
},
{
"name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)",
"ok": true,
"detail": {
"before": "consent_required",
"consent": 200
}
},
{
"name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)",
"ok": false,
"detail": "500 {\"ok\":false,\"error\":\"ERR_SQLITE_ERROR\",\"message\":\"일시적인 오류가 발생했습니다. 잠시 후 다시 시도하세요\"} | portal: 2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ar"
},
{
"name": "[HAL 직접] 외부 Agent 토큰 발급(포털 서비스 키)",
"ok": true,
"detail": {
"token": "[redacted]",
"expiresAt": "2026-10-26T13:09:27.408Z"
}
},
{
"name": "[HAL 직접] 외부 Agent 등록·실행(사이트 귀속) → by-site 상태 connected",
"ok": true,
"detail": {
"agentId": "[redacted]",
"status": "connected",
"siteId": "s_01a1784b5d03e229abe92a46a1"
}
},
{
"name": "[HAL 직접] 외부 Agent 배포(site.deploy, 로컬 저장본) → releases/current 생성",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125937-v1",
"leftovers": []
},
"layout": true
}
},
{
"name": "[HAL 직접] 재배포(v2) → 이전 버전 기록",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125950-v2",
"leftovers": []
}
}
},
{
"name": "[HAL 직접] 실패 배포(표식 불일치) → 이전 버전 유지·남은 파일(leftovers) 보고",
"ok": true,
"detail": {
"status": "failed",
"error": "내부 확인 실패(404) — 이전 버전을 유지합니다",
"errorCode": "internal_check_failed",
"result": null
}
},
{
"name": "[HAL 직접] 다른 사이트 작업 거부(site_mismatch) · 허용되지 않는 종류 거부",
"ok": true,
"detail": {}
},
{
"name": "[HAL 직접] 배포 제거(site.remove) → 파일·vhost 제거",
"ok": true,
"detail": {
"layoutAfter": false
}
}
],
"evidence": {
"mode": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
}
}
},
{
"no": 17,
"title": "사용량 경고와 한도 동작 확인",
"verdict": "verified_local",
"note": "메일 저장 공간 95%·월간 통수 100% 를 수집해 구간·제한 사유·KST 초기화·알림을 확인(웹 디스크/아웃바운드는 배포가 있어야 수집)",
"blockedBy": null,
"steps": [
{
"name": "가짜 AriMail 사용량 95%/월간 100% → 수집(refresh) → 구간·제한 사유·다음 KST 초기화·마지막 계측 표시",
"ok": true,
"detail": {
"storageLevel": 95,
"monthlyLevel": 100,
"nextReset": "2026-10-31T15:00:00.000Z",
"measuredAt": true,
"limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.",
"countingRule": "per_final_recipient",
"evaluate": {
"status": 200,
"notified": 2,
"limited": 1
}
}
},
{
"name": "알림 센터: 95%·100% 경고가 사건으로 묶여 표시(필수 알림)",
"ok": true,
"detail": {
"count": 3,
"titles": [
"aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)",
"aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용",
"메일함 kim@aw-e2e.localhost 저장 공간 95% 경고"
],
"mandatory": true
}
},
{
"name": "사용량 화면 제한 플래그(/api/usage/limits)·그룹 요약",
"ok": true,
"detail": {
"limits": [
"mail_msgs:100",
"mail_storage:95"
],
"warnLevels": [
70,
80,
90,
95
]
}
}
],
"evidence": {
"warn": {
"storageLevel": 95,
"monthlyLevel": 100,
"nextReset": "2026-10-31T15:00:00.000Z",
"measuredAt": true,
"limit100": "월간 통수 100%: 새 발신을 막고 새 수신은 임시 실패로 응답합니다. 다음 KST 월 시작 또는 추가 용량 적용 후 복구됩니다.",
"countingRule": "per_final_recipient",
"evaluate": {
"status": 200,
"notified": 2,
"limited": 1
}
},
"notif": {
"count": 3,
"titles": [
"aw-e2e.localhost 메일 저장 공간 96% — 곧 제한됩니다(최종 경고)",
"aw-e2e.localhost 월간 메일 통수(송신+수신) 100% — 제한 적용",
"메일함 kim@aw-e2e.localhost 저장 공간 95% 경고"
],
"mandatory": true
},
"lim": {
"limits": [
"mail_msgs:100",
"mail_storage:95"
],
"warnLevels": [
70,
80,
90,
95
]
}
}
},
{
"no": 18,
"title": "상품 축소 보류와 정리 후 적용",
"verdict": "verified_local",
"note": "축소 미리보기→보류→정리(배정 축소·독립 메일 해지)→재검증 적합→축소 주문·승인→권리 교체",
"blockedBy": null,
"steps": [
{
"name": "배정 화면 수치(구매·내 배정·다른 서비스·예약·미배정·변경 후 최대) + 마지막 단위 동시 요청",
"ok": true,
"detail": {
"purchased": 45,
"mine": 6,
"others": 2,
"unallocated": 37,
"race": [
409,
409
],
"mineAfter": 6
}
},
{
"name": "축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시",
"ok": true,
"detail": {
"shortages": [
"mailboxes:1"
]
}
},
{
"name": "축소 요청 → 보류(사용 중 권리 즉시 회수 없음)",
"ok": true,
"detail": {
"fits": false,
"hold": "holding",
"sitesStillPurchased": 3
}
},
{
"name": "정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성",
"ok": true,
"detail": {
"allocate": 200,
"fits": true,
"shortages": [],
"order": "awaiting_payment"
}
},
{
"name": "축소 주문 입금 확인·승인 → 권리 교체(베이직)",
"ok": true,
"detail": {
"approve": 200,
"sites": 1,
"mailboxes": 5,
"product": "pv_01a17849a126e838aa8bb17065"
}
}
],
"evidence": {
"alloc": {
"purchased": 45,
"mine": 6,
"others": 2,
"unallocated": 37,
"race": [
409,
409
],
"mineAfter": 6
},
"pv": {
"shortages": [
"mailboxes:1"
]
},
"hold": {
"fits": false,
"hold": "holding",
"sitesStillPurchased": 3
},
"clean": {
"allocate": 200,
"fits": true,
"shortages": [],
"order": "awaiting_payment"
},
"applied": {
"approve": 200,
"sites": 1,
"mailboxes": 5,
"product": "pv_01a17849a126e838aa8bb17065"
}
}
},
{
"no": 19,
"title": "사이트 수 반환과 대표 메일의 독립 전환",
"verdict": "partial",
"note": "반환 점검표와 메일 유지/종료 분기 규칙 확인. 실제 반환은 운영 사이트(사이트 수 사용 중) 필요(단위 테스트 test/lifecycle.test.mjs·test/mail.test.mjs 로 확인)",
"blockedBy": 6,
"steps": [
{
"name": "§14 반환 점검표(8단계)·차단 사유(대표 메일 유지 선택·독립 옵션)",
"ok": true,
"detail": {
"ok": false,
"blocking": [
"domains_unrouted",
"mail_choice",
"mail_converted"
],
"steps": 8
}
},
{
"name": "메일 유지 선택: 독립 옵션 부족이면 반환하지 않음 / 종료 선택 규칙",
"ok": true,
"detail": {
"status": 409,
"error": "quota_insufficient",
"detail": [
"resource",
"needed",
"available",
"link"
]
}
}
],
"evidence": {
"ck": {
"ok": false,
"blocking": [
"domains_unrouted",
"mail_choice",
"mail_converted"
],
"steps": 8
},
"keep": {
"status": 409,
"error": "quota_insufficient",
"detail": [
"resource",
"needed",
"available",
"link"
]
}
}
},
{
"no": 20,
"title": "사이트 소유권 이전",
"verdict": "verified_local",
"note": "미리보기→요청·알림→수락(보류·재확인)→심사·최종 확인→원자적 완료→접근 전환·감사",
"blockedBy": null,
"steps": [
{
"name": "이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목)",
"ok": true,
"detail": {
"sites": 1,
"domains": 1,
"siteMail": 1,
"needed": {
"backup": 1,
"web_disk": 1073741824,
"mail_msgs": 500,
"mail_storage": 2147483648,
"mailboxes": 6
},
"excluded": 3
}
},
{
"name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림",
"ok": true,
"detail": {
"id": "tr_01a1784c7a9e2007160a39f536",
"status": "requested",
"expiresAt": "2026-11-02T13:00:40.478Z",
"bobNotified": true
}
},
{
"name": "(정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개)",
"ok": true,
"detail": {
"endBackup": 200,
"alloc": 200
}
},
{
"name": "받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내)",
"ok": true,
"detail": {
"status": 200,
"tstatus": "reviewing",
"hold": null
}
},
{
"name": "운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각",
"ok": true,
"detail": {
"bad": "final_confirmation_mismatch",
"ok": 200,
"status": "completed",
"cutover": "2026-10-26T13:00:40.527Z"
}
},
{
"name": "완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사",
"ok": true,
"detail": {
"aliceSees": 404,
"bobSees": 200,
"bobUsageSites": 0,
"audit": true
}
}
],
"evidence": {
"pv": {
"sites": 1,
"domains": 1,
"siteMail": 1,
"needed": {
"backup": 1,
"web_disk": 1073741824,
"mail_msgs": 500,
"mail_storage": 2147483648,
"mailboxes": 6
},
"excluded": 3
},
"rq": {
"id": "tr_01a1784c7a9e2007160a39f536",
"status": "requested",
"expiresAt": "2026-11-02T13:00:40.478Z",
"bobNotified": true
},
"acc": {
"status": 200,
"tstatus": "reviewing",
"hold": null
},
"ap": {
"bad": "final_confirmation_mismatch",
"ok": 200,
"status": "completed",
"cutover": "2026-10-26T13:00:40.527Z"
},
"after": {
"aliceSees": 404,
"bobSees": 200,
"bobUsageSites": 0,
"audit": true
}
}
},
{
"no": 21,
"title": "중지, 해지, 보존 예정일 표시",
"verdict": "partial",
"note": "생명주기 화면·중지/해지 영향 안내·보존 정책 표시 확인. 운영 중 사이트의 실제 중지·해지는 배포 성공(흐름 6) 필요",
"blockedBy": 6,
"steps": [
{
"name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림",
"ok": true,
"detail": {
"state": "제작 중",
"siteCount": false,
"publicAccess": "중지됨",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
}
},
{
"name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유",
"ok": true,
"detail": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "lifecycle_transition"
}
},
{
"name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)",
"ok": true,
"detail": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
],
"evidence": {
"v": {
"state": "제작 중",
"siteCount": false,
"publicAccess": "중지됨",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
},
"pause": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "lifecycle_transition"
},
"mailLc": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
},
{
"no": 22,
"title": "HAL 우선 배포 후 HAL을 통한 전체 모듈 배포",
"verdict": "failed",
"note": "HAL 우선 순서(hal_first 거부)·릴리스 승인·부트스트랩 잠김은 확인. 모듈 배포 작업은 완료되지 않음(진단 증거 참조)",
"blockedBy": null,
"steps": [
{
"name": "릴리스 화면: 버전·main 커밋·승인 상태·HAL 실행 버전·부트스트랩(최초 Agent)·배포 순서",
"ok": true,
"detail": {
"releases": [
"0.1.2:approved",
"0.1.1:approved"
],
"running": {
"complete": true,
"firstInstall": false,
"releaseVersion": null,
"mainCommit": null,
"artifactSha256": null,
"halRunningVersion": "0.1.1",
"firstAgent": "[redacted]",
"manualInstallLocked": true
},
"order": [
"hal",
"agent",
"portal",
"webmaker",
"arimail",
"backup",
"observability"
]
}
},
{
"name": "HAL 이 아직 0.1.2 가 아닐 때 다른 모듈 0.1.2 배포 → hal_first 거부",
"ok": true,
"detail": {
"status": "failed",
"error": "hal_first"
}
},
{
"name": "승인되지 않은 릴리스는 거부 / 수동 설치 잠김 표시",
"ok": true,
"detail": {
"manualInstallLocked": true,
"complete": true
}
},
{
"name": "HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인)",
"ok": true,
"detail": {
"status": "queued",
"steps": [
"precheck:pending",
"fanout:pending",
"collect:pending"
],
"error": null,
"children": [],
"agentJobs": [],
"timedOut": true
}
}
],
"evidence": {
"rel": {
"releases": [
"0.1.2:approved",
"0.1.1:approved"
],
"running": {
"complete": true,
"firstInstall": false,
"releaseVersion": null,
"mainCommit": null,
"artifactSha256": null,
"halRunningVersion": "0.1.1",
"firstAgent": "[redacted]",
"manualInstallLocked": true
},
"order": [
"hal",
"agent",
"portal",
"webmaker",
"arimail",
"backup",
"observability"
]
},
"hf": {
"status": "failed",
"error": "hal_first"
},
"na": {
"manualInstallLocked": true,
"complete": true
},
"dep": {
"status": "queued",
"steps": [
"precheck:pending",
"fanout:pending",
"collect:pending"
],
"error": null,
"children": [],
"agentJobs": [],
"timedOut": true
},
"diag": {
"stuckNow": [
{
"id": "j_01a1784d7f02143ed47d1fcaf9",
"kind": "domain.probe",
"status": "queued",
"attempt": 0,
"step": "dispatch",
"detail": "",
"updatedAt": "2026-10-26T13:01:47.138Z"
},
{
"id": "j_01a1784d6b6a20949d03d2dee8",
"kind": "domain.probe",
"status": "queued",
"attempt": 0,
"step": "dispatch",
"detail": "",
"updatedAt": "2026-10-26T13:01:42.122Z"
},
{
"id": "j_01a1784d671c8f951049094a53",
"kind": "module.deploy",
"status": "queued",
"attempt": 0,
"step": "precheck",
"detail": "",
"updatedAt": "2026-10-26T13:01:41.020Z"
},
{
"id": "j_01a1784d57cf28f7528a1a8ccb",
"kind": "domain.probe",
"status": "queued",
"attempt": 0,
"step": "dispatch",
"detail": "",
"updatedAt": "2026-10-26T13:01:37.103Z"
},
{
"id": "j_01a1784d44338bf61b6691d5a8",
"kind": "domain.probe",
"status": "running",
"attempt": 1,
"step": "await",
"detail": "",
"updatedAt": "2026-10-26T13:01:46.885Z"
}
],
"halLog": [
"2026-10-26T12:58:56.345Z [jobs] site.deploy j_01a1784ac7d0815275cc3e88aa 실패: Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/lIw5d3l2igVGOIlJa1BCaKwXYo7r2bJJ",
"2026-10-26T12:59:11.375Z [jobs] backup.create j_01a1784afb5dd2de92b7a63b4f 실패: 대상 서버 또는 Agent 가 필요합니다",
"2026-10-26T13:00:04.476Z [jobs] site.deploy j_01a1784bbc086436dc87f4b997 실패: 내부 확인 실패(404) — 이전 버전을 유지합니다",
"2026-10-26T13:00:18.498Z [jobs] site.deploy j_01a1784befea6c15eb6d7782eb 실패: 이 외부 Agent 는 다른 사이트에 귀속되어 있습니다",
"2026-10-26T13:00:31.517Z [jobs] backup.create j_01a1784c1721827ff3a7fd81e1 실패: 외부 Agent 는 backup.create 작업을 받을 수 없습니다",
"2026-10-26T13:01:40.630Z [jobs] module.deploy j_01a1784d2b6df1c41a7815b555 실패: HAL 이 아직 0.1.2 으로 실행되고 있지 않습니다(현재 0.1.1). HAL 을 먼저 배포하세요"
],
"ctlAgentLog": [
"2026-10-26T12:57:32.329Z [agent] Ariweb Node Agent v0.1.1 ag_01a178499afa793eb4b22d9b01 (managed) → http://127.0.0.1:35361"
]
}
}
},
{
"no": 23,
"title": "실패 작업의 재시도와 되돌리기",
"verdict": "partial",
"note": "실패 기록·재시도 API·되돌리기 거부/허용 조건 확인. 성공한 재배포 뒤 실제 되돌리기는 배포 성공이 필요",
"blockedBy": 6,
"steps": [
{
"name": "배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호",
"ok": true,
"detail": {
"status": "cancelled",
"failedStep": null,
"canRetry": false,
"jobId": "j_01a17849a4096e33f476b17d1b"
}
},
{
"name": "재시도 요청(POST /api/sites/:id/jobs/:jobId/retry)",
"ok": true,
"detail": {
"status": 500,
"error": "job_not_retryable"
}
},
{
"name": "되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부",
"ok": true,
"detail": {
"status": 409,
"error": "no_previous",
"message": "되돌릴 이전 버전이 없습니다"
}
}
],
"evidence": {
"dep": {
"status": "cancelled",
"failedStep": null,
"canRetry": false,
"jobId": "j_01a17849a4096e33f476b17d1b"
},
"rt": {
"status": 500,
"error": "job_not_retryable"
},
"rb": {
"status": 409,
"error": "no_previous",
"message": "되돌릴 이전 버전이 없습니다"
}
}
},
{
"no": 24,
"title": "운영자 승인, 감사 기록, 고객 알림의 일치",
"verdict": "verified_local",
"note": "승인 → 고객 알림·불변 감사 기록·운영자/고객 화면 상태 일치(주문 기준)",
"blockedBy": null,
"steps": [
{
"name": "고객 알림 센터에 승인 알림",
"ok": true,
"detail": {
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
"category": "billing",
"link": "/orders/o_01a17849a33601ca26016fa94a"
}
},
{
"name": "감사 기록: order.approve 는 삭제 불가(immutable)",
"ok": true,
"detail": {
"action": "order.approve",
"immutable": true
}
},
{
"name": "운영자 화면과 고객 주문 화면의 상태 일치",
"ok": true,
"detail": {
"status": "approved",
"approvedAt": "2026-10-26T12:57:34.327Z"
}
}
],
"evidence": {
"n": {
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
"category": "billing",
"link": "/orders/o_01a17849a33601ca26016fa94a"
},
"a": {
"action": "order.approve",
"immutable": true
},
"same": {
"status": "approved",
"approvedAt": "2026-10-26T12:57:34.327Z"
}
}
},
{
"no": 25,
"title": "HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련",
"verdict": "partial",
"note": "DR 설정(RTO/RPO/주기/보존)·사본 생성·검증·외부 보관·복원 승인/기존 제어 중지 확인/완료 항목·훈련 실패/성공→준비 완료 기록까지 확인. 실제 다른 호스트 복원은 실 서버 필요",
"blockedBy": null,
"steps": [
{
"name": "설정 전 준비 상태(미설정 항목)",
"ok": true,
"detail": {
"ready": false,
"missingConfig": [
"rto_minutes",
"rpo_minutes",
"snapshot_interval_minutes",
"retention_count"
],
"lastDrill": null,
"lastSnapshot": null,
"config": {
"rto_minutes": null,
"rpo_minutes": null,
"snapshot_interval_minutes": null,
"retention_count": null,
"external_store": null
},
"label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)"
}
},
{
"name": "RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정",
"ok": true,
"detail": {
"id": 1,
"rto_minutes": 60,
"rpo_minutes": 30,
"snapshot_interval_minutes": 60,
"retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026125729/dr-external",
"updated_at": "2026-10-26T13:02:07.823Z",
"updated_by": "op_01a178499a27f1ba88344e1145"
}
},
{
"name": "복구 사본 생성·검증·외부 보관(vault.key 제외)",
"ok": true,
"detail": {
"verify": "ok",
"external": true,
"sha": true
}
},
{
"name": "복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료",
"ok": true,
"detail": {
"early": "restore_not_approved",
"noStop": "old_control_not_confirmed",
"command": true,
"incomplete": "checks_incomplete",
"final": "completed_with_findings"
}
},
{
"name": "복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료",
"ok": true,
"detail": {
"first": "failed",
"second": "success",
"ready": true,
"measured": {}
}
}
],
"evidence": {
"r0": {
"ready": false,
"missingConfig": [
"rto_minutes",
"rpo_minutes",
"snapshot_interval_minutes",
"retention_count"
],
"lastDrill": null,
"lastSnapshot": null,
"config": {
"rto_minutes": null,
"rpo_minutes": null,
"snapshot_interval_minutes": null,
"retention_count": null,
"external_store": null
},
"label": "정식 운영 준비 전(복구 훈련 또는 설정 필요)"
},
"cfg": {
"id": 1,
"rto_minutes": 60,
"rpo_minutes": 30,
"snapshot_interval_minutes": 60,
"retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026125729/dr-external",
"updated_at": "2026-10-26T13:02:07.823Z",
"updated_by": "op_01a178499a27f1ba88344e1145"
},
"snap": {
"verify": "ok",
"external": true,
"sha": true
},
"rs": {
"early": "restore_not_approved",
"noStop": "old_control_not_confirmed",
"command": true,
"incomplete": "checks_incomplete",
"final": "completed_with_findings"
},
"drill": {
"first": "failed",
"second": "success",
"ready": true,
"measured": {}
}
}
},
{
"no": 26,
"title": "새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지",
"verdict": "verified_local",
"note": "포털·HAL 재시작 뒤 세션·진행 작업(외부 대기)·작업 목록 유지 확인. 진행 중(running) 배포의 단계 재개는 배포 성공(흐름 6) 뒤 확인 필요",
"blockedBy": null,
"steps": [
{
"name": "재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록",
"ok": true,
"detail": {
"mailJob": {
"id": "j_01a1784ae61b14370f923139a1",
"status": "awaiting_external"
},
"halJobs": 50
}
},
{
"name": "포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지",
"ok": true,
"detail": {
"sessionKept": "[redacted]",
"mailJob": {
"id": "j_01a1784ae61b14370f923139a1",
"status": "awaiting_external"
},
"same": true
}
},
{
"name": "HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지",
"ok": true,
"detail": {
"opsSessionKept": "[redacted]",
"halJobs": 50,
"sameCount": true
}
}
],
"evidence": {
"before": {
"mailJob": {
"id": "j_01a1784ae61b14370f923139a1",
"status": "awaiting_external"
},
"halJobs": 50
},
"pr": {
"sessionKept": "[redacted]",
"mailJob": {
"id": "j_01a1784ae61b14370f923139a1",
"status": "awaiting_external"
},
"same": true
},
"hr": {
"opsSessionKept": "[redacted]",
"halJobs": 50,
"sameCount": true
}
}
},
{
"no": 27,
"title": "마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공",
"verdict": "verified_local",
"note": "마지막 사이트 수 1개를 두 배포가 동시에 요청 → 하나만 예약 성공, 다른 쪽은 부족 사유(§13.3)",
"blockedBy": null,
"steps": [
{
"name": "밥 개인 그룹: 베이직(사이트 1) 주문·승인",
"ok": true,
"detail": {
"granted": 6
}
},
{
"name": "사이트 2개 생성·저장본",
"ok": true,
"detail": [
{
"id": "s_01a1784ac5a389af03b57b32a3",
"group_id": "g_01a17849a2ee454ab48b65c13d",
"name": "동시 A",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000002",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "managed",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a17849a2eef6c9267cb0d204",
"created_at": "2026-10-26T12:58:48.611Z",
"updated_at": "2026-10-26T12:58:48.619Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
},
{
"id": "s_01a1784ac5a7fa23018c39de03",
"group_id": "g_01a17849a2ee454ab48b65c13d",
"name": "동시 B",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000003",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "managed",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a17849a2eef6c9267cb0d204",
"created_at": "2026-10-26T12:58:48.615Z",
"updated_at": "2026-10-26T12:58:48.622Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
]
},
{
"name": "두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족)",
"ok": true,
"detail": {
"ok": 1,
"blocked": "precheck_failed",
"shortage": "block"
}
},
{
"name": "(정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환",
"ok": true,
"detail": {
"cancel": 200,
"cleanup": 200,
"unallocated": 0
}
}
],
"evidence": {
"race": {
"ok": 1,
"blocked": "precheck_failed",
"shortage": "block"
}
}
},
{
"no": 28,
"title": "외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리",
"verdict": "partial",
"note": "HAL·외부 Agent 경로(등록·연결·배포·재배포·실패 시 이전 버전 유지와 남은 파일·다른 사이트/허용되지 않는 종류 거부·제거)는 확인. 포털 화면 경로는 토큰 발급 결함으로 막힘",
"blockedBy": null,
"steps": [
{
"name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true,
"detail": {
"id": "s_01a1784b5d03e229abe92a46a1",
"group_id": "g_01a17849a2c2dec27ee9f14777",
"name": "E2E 외부 사이트",
"purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004",
"template": null,
"lang": "ko",
"timezone": "Asia/Seoul",
"lifecycle": "draft",
"deploy_mode": "external",
"primary_domain": null,
"site_count_state": "none",
"site_count_reservation_id": null,
"server_id": null,
"external_agent_id": null,
"current_version": null,
"current_deployment_id": null,
"deploy_verify": null,
"health": "unknown",
"health_checked_at": null,
"maintenance": 0,
"search_visible": 1,
"description": null,
"settings_json": null,
"paused_at": null,
"cancel_at": null,
"retention_until": null,
"delete_after": null,
"deleted_at": null,
"created_by": "u_01a17849a21df78a8debb70c18",
"created_at": "2026-10-26T12:59:27.363Z",
"updated_at": "2026-10-26T12:59:27.368Z",
"runtime": "static",
"consent_version": null,
"pending_cleanup_json": null,
"return_due_at": null,
"last_deploy_at": null,
"last_external_check_json": null,
"next_job_json": null,
"mail_choice": null,
"next_notice_at": null,
"delete_hold_reason": null,
"public_stopped_at": null,
"prev_lifecycle": null,
"settings": {},
"pendingCleanup": null,
"lastExternalCheck": null,
"nextJob": null
}
},
{
"name": "배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전",
"ok": true,
"detail": {
"notProvided": "[redacted]",
"consentVersion": "external-deploy-2026-10-0.1.1",
"agreed": false
}
},
{
"name": "동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전)",
"ok": true,
"detail": {
"before": "consent_required",
"consent": 200
}
},
{
"name": "외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음)",
"ok": false,
"detail": "500 {\"ok\":false,\"error\":\"ERR_SQLITE_ERROR\",\"message\":\"일시적인 오류가 발생했습니다. 잠시 후 다시 시도하세요\"} | portal: 2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ar"
},
{
"name": "[HAL 직접] 외부 Agent 토큰 발급(포털 서비스 키)",
"ok": true,
"detail": {
"token": "[redacted]",
"expiresAt": "2026-10-26T13:09:27.408Z"
}
},
{
"name": "[HAL 직접] 외부 Agent 등록·실행(사이트 귀속) → by-site 상태 connected",
"ok": true,
"detail": {
"agentId": "[redacted]",
"status": "connected",
"siteId": "s_01a1784b5d03e229abe92a46a1"
}
},
{
"name": "[HAL 직접] 외부 Agent 배포(site.deploy, 로컬 저장본) → releases/current 생성",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125937-v1",
"leftovers": []
},
"layout": true
}
},
{
"name": "[HAL 직접] 재배포(v2) → 이전 버전 기록",
"ok": true,
"detail": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125950-v2",
"leftovers": []
}
}
},
{
"name": "[HAL 직접] 실패 배포(표식 불일치) → 이전 버전 유지·남은 파일(leftovers) 보고",
"ok": true,
"detail": {
"status": "failed",
"error": "내부 확인 실패(404) — 이전 버전을 유지합니다",
"errorCode": "internal_check_failed",
"result": null
}
},
{
"name": "[HAL 직접] 다른 사이트 작업 거부(site_mismatch) · 허용되지 않는 종류 거부",
"ok": true,
"detail": {}
},
{
"name": "[HAL 직접] 배포 제거(site.remove) → 파일·vhost 제거",
"ok": true,
"detail": {
"layoutAfter": false
}
}
],
"evidence": {
"halTok": {
"expiresAt": "2026-10-26T13:09:27.408Z"
},
"d1": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125937-v1",
"leftovers": []
},
"layout": true
},
"d2": {
"status": "done",
"error": null,
"errorCode": null,
"result": {
"version": "20261026125950-v2",
"leftovers": []
}
},
"d3": {
"status": "failed",
"error": "내부 확인 실패(404) — 이전 버전을 유지합니다",
"errorCode": "internal_check_failed",
"result": null
},
"d4": {},
"d5": {
"layoutAfter": false
}
}
},
{
"no": 29,
"title": "HAL 장애 중 기존 서비스 유지와 신규 변경 제한 표시",
"verdict": "verified_local",
"note": "HAL 중단 중 포털 조회 유지·신규 배포 차단(정보 확인 지연), HAL 재기동 뒤 서버·Agent 복귀",
"blockedBy": null,
"steps": [
{
"name": "HAL 중단 중: 기존 조회(사이트 목록·개요)는 제공, 배포 전 확인/배포는 정보 확인 지연·차단",
"ok": true,
"detail": {
"list": 200,
"overview": 200,
"precheck": {
"status": 200,
"blocks": [
"target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정"
]
},
"deploy": {
"status": 409,
"error": "precheck_failed",
"message": "배포 전 확인에서 차단된 항목이 있습니다"
}
}
},
{
"name": "대시보드/알림에 중앙 상태 확인 지연 표시(가능하면)",
"ok": true,
"detail": {
"status": 200,
"halState": null
}
},
{
"name": "HAL 재기동(같은 호스트의 죽은 인스턴스 임대 이어받기) → 서버·Agent 상태 복귀",
"ok": true,
"detail": {
"takeoverNeeded": false,
"bootstrap": true,
"readyServers": 4
}
}
],
"evidence": {
"during": {
"list": 200,
"overview": 200,
"precheck": {
"status": 200,
"blocks": [
"target:HAL(중앙 관리)에 연결할 수 없어 배포 위치를 확인하지 못했습니다(정"
]
},
"deploy": {
"status": 409,
"error": "precheck_failed",
"message": "배포 전 확인에서 차단된 항목이 있습니다"
}
},
"dash": {
"status": 200,
"halState": null
},
"back": {
"takeoverNeeded": false,
"bootstrap": true,
"readyServers": 4
}
}
},
{
"no": 30,
"title": "모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정",
"verdict": "failed",
"note": "대상별 자식 작업 생성·되돌리기 3단계 확인 거부는 확인. 일부 실패 요약·건강 확인·drift 조정은 작업 엔진 교착(흐름 22 결함)으로 완료되지 않음",
"blockedBy": 22,
"steps": [
{
"name": "agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)",
"ok": true,
"detail": {
"status": "queued",
"errorCode": null,
"overall": "none",
"children": [],
"timedOut": true
}
},
{
"name": "모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업",
"ok": true,
"detail": {
"health": "timeout",
"drift": 0,
"reconcile": 200,
"overviewComponents": [
"HAL:ok",
"portal:unknown",
"webmaker:unknown",
"arimail:unknown",
"backup:ok",
"observability:ok"
]
}
},
{
"name": "되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부",
"ok": true,
"detail": {
"step1": "confirm_impact_required",
"step2": "identifiers_mismatch"
}
}
],
"evidence": {
"d": {
"status": "queued",
"errorCode": null,
"overall": "none",
"children": [],
"timedOut": true
},
"h": {
"health": "timeout",
"drift": 0,
"reconcile": 200,
"overviewComponents": [
"HAL:ok",
"portal:unknown",
"webmaker:unknown",
"arimail:unknown",
"backup:ok",
"observability:ok"
]
},
"rb": {
"step1": "confirm_impact_required",
"step2": "identifiers_mismatch"
}
}
},
{
"no": 31,
"title": "긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구",
"verdict": "partial",
"note": "정상 상태 선언 거부·3단계 승인(순서/식별정보/문구 거부)·자격 1회 발급·일반 배포/범위 밖/잘못된 자격 거부·수행·폐기 뒤 거부·조정·종료 확인. 자격 만료 거부는 유효 시간(분 단위 설정) 때문에 로컬 실행에서 생략(단위 테스트 test/hal.test.mjs 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "HAL 정상 상태에서 선언 거부(hal_healthy)",
"ok": true,
"detail": "hal_healthy"
},
{
"name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)",
"ok": true,
"detail": {
"id": "em_01a1784dd0ec3f08f6ddf4281a",
"noCred": "approvals_insufficient",
"wrongStep": "wrong_step",
"badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch",
"approved": "approved",
"approvals": 3,
"credential": "[redacted]",
"expiresAt": "2026-10-26T13:32:08.135Z"
}
},
{
"name": "일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료",
"ok": true,
"detail": {
"forbidden": "action_forbidden",
"outOfScope": "out_of_scope",
"badCred": "bad_credential",
"performed": 400,
"revoked": true,
"afterRevoke": "credential_revoked",
"reconcile": 200,
"closed": "closed"
}
}
],
"evidence": {
"healthy": "hal_healthy",
"sess": {
"id": "em_01a1784dd0ec3f08f6ddf4281a",
"noCred": "approvals_insufficient",
"wrongStep": "wrong_step",
"badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch",
"approved": "approved",
"approvals": 3,
"expiresAt": "2026-10-26T13:32:08.135Z"
},
"perf": {
"forbidden": "action_forbidden",
"outOfScope": "out_of_scope",
"badCred": "bad_credential",
"performed": 400,
"revoked": true,
"afterRevoke": "credential_revoked",
"reconcile": 200,
"closed": "closed"
}
}
},
{
"no": 32,
"title": "Source 옵션 만료 후 30일 read-only, clone·export와 삭제 완료",
"verdict": "partial",
"note": "활성화·export·해제→읽기 전용(삭제 예정일)까지 확인. 30일 뒤 삭제 완료는 시간 경과가 필요(단위 테스트 test/sites.test.mjs 에서 sweep 로 확인)",
"blockedBy": null,
"steps": [
{
"name": "Source 옵션 주문(대상 사이트)·승인 → source 권리",
"ok": true,
"detail": {
"granted": 7
}
},
{
"name": "Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보",
"ok": true,
"detail": {
"activate": 200,
"status": "active",
"provider": "gitea",
"changes": 1,
"clone": true
}
},
{
"name": "export(저장본 내려받기)",
"ok": true,
"detail": {
"status": 200,
"type": "application/gzip"
}
},
{
"name": "옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단",
"ok": true,
"detail": {
"status": "readonly",
"deleteAfter": "2026-11-25T13:00:40.412Z",
"readonlyUntil": "2026-11-25T13:00:40.412Z"
}
},
{
"name": "삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음",
"ok": true,
"detail": {
"sweep": 200,
"statusAfter": "readonly"
}
}
],
"evidence": {
"act": {
"activate": 200,
"status": "active",
"provider": "gitea",
"changes": 1,
"clone": true
},
"ex": {
"status": 200,
"type": "application/gzip"
},
"un": {
"status": "readonly",
"deleteAfter": "2026-11-25T13:00:40.412Z",
"readonlyUntil": "2026-11-25T13:00:40.412Z"
},
"sweep": {
"sweep": 200,
"statusAfter": "readonly"
}
}
},
{
"no": 33,
"title": "메일 서비스의 사이트 대표→독립 전환 후 항목 유지",
"verdict": "partial",
"note": "전환 점검표 8항목과 미충족 시 차단 확인. 전후 실제 송수신 확인·DNS 유지 검증은 실 DNS/AriMail 필요(단위 테스트 test/mail.test.mjs 에서 가짜로 전환 완료·되돌리기 확인)",
"blockedBy": null,
"steps": [
{
"name": "사이트 대표→독립 전환 점검표(주소·메일함·별칭·전달·비밀번호·DNS·옵션·작업)",
"ok": true,
"detail": {
"canConvert": false,
"items": [
"addresses:ok",
"data:ok",
"aliases:ok",
"forwards:ok",
"passwords:ok",
"dns:x",
"entitlement:x",
"jobs:x"
]
}
},
{
"name": "DNS 미확인 상태에서 전환 요청은 차단",
"ok": true,
"detail": {
"status": 409,
"error": "conversion_blocked"
}
}
],
"evidence": {
"ck": {
"canConvert": false,
"items": [
"addresses:ok",
"data:ok",
"aliases:ok",
"forwards:ok",
"passwords:ok",
"dns:x",
"entitlement:x",
"jobs:x"
]
},
"tr": {
"status": 409,
"error": "conversion_blocked"
}
}
},
{
"no": 34,
"title": "일시 중지·해지 예약·보존·삭제 단계의 표시 일치",
"verdict": "partial",
"note": "해지된 메일 서비스의 읽기 전용 보존·삭제 예정 표시와 사이트 생명주기 화면의 단계별 항목 확인. 사이트의 해지 예약→보존→삭제 전이는 운영 중 사이트 필요(단위 테스트 test/lifecycle.test.mjs 로 확인)",
"blockedBy": 6,
"steps": [
{
"name": "§23.3 한 화면 보기: 공개/관리자 접근·사이트 수·청구·도메인/메일/백업 동작·내보내기·되돌릴 수 없는 시점·보존·다음 알림",
"ok": true,
"detail": {
"state": "제작 중",
"siteCount": false,
"publicAccess": "중지됨",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
}
},
{
"name": "일시 중지(확인 필수) 또는 현재 상태에서 거부 사유",
"ok": true,
"detail": {
"noConfirm": "confirmation_required",
"status": 409,
"error": "lifecycle_transition"
}
},
{
"name": "메일 서비스 생명주기 화면(해지된 독립 메일: 송수신 중지·보존 종료·삭제 예정·백업 미제공)",
"ok": true,
"detail": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
],
"evidence": {
"v": {
"state": "제작 중",
"siteCount": false,
"publicAccess": "중지됨",
"retention": {
"site_days": 30,
"mail_days": 30,
"source_days": 30,
"backup_days": 7,
"delete_grace_days": 1,
"notice_before_days": 3,
"readonly_retention_days": 30,
"delete_after_days": 30,
"pause_keeps_site_count": true,
"cancel_effective": "next_billing_date",
"source": "상품정책 베이직 (retention 정책) v1"
},
"pauseImpactKeys": [
"publicAccess",
"adminAccess",
"mail",
"backups",
"usageAndCost",
"resume",
"siteCount"
],
"cancelImpactKeys": [
"target",
"linked",
"immediateStops",
"remainingData",
"retentionUntil",
"deleteAfter",
"retentionSource",
"export",
"unpaid",
"refund",
"pointOfNoReturn",
"mailOptions",
"siteCount"
],
"steps14": 8
},
"mailLc": {
"status": 200,
"sendReceive": "송수신 가능",
"retentionUntil": false,
"deleteAfter": false,
"backupNote": "고객용 메일 백업·복원은 제공하지 않습니다(§17.4)"
}
}
}
],
"bugs": [
{
"title": "Agent 가 포털 저장본(bundleUrl, 절대 URL)을 내려받지 못함(HAL 주소를 앞에 덧붙임)",
"file": "agent/client.mjs (download: fetchImpl(base + path)) / agent/jobs/site.mjs (fetchBundle)",
"route": "HAL site.deploy → Agent fetch 단계",
"expected": "bundleUrl 이 절대 URL 이면 그대로 내려받는다(서명은 HAL 전용이므로 포털 번들 URL 에는 붙이지 않음)",
"actual": "Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni",
"repro": "포털 배포 → HAL site.deploy → Agent: \"Failed to parse URL from http://<hal>http://<portal>/internal/bundles/…\"",
"flows": [
6,
8,
9,
13,
14,
15,
23
]
},
{
"title": "포털 배포 작업이 실패한 HAL 작업을 실패로 보지 않고 5초마다 새 HAL 작업을 무한 재제출(배포가 영원히 \"진행 중\")",
"file": "portal/sites/service.mjs (halJobFor: prev HAL 작업이 failed/cancelled 면 fresh 가 아니어도 새 작업을 만든다 → awaitHal 이 실패를 보지 못함)",
"route": "POST /api/sites/:id/deploy",
"expected": "HAL 작업이 failed 면 포털 작업도 failed(이전 버전 유지·재시도 버튼), 새 HAL 작업은 사용자 재시도(fresh) 때만",
"actual": "portal job queued attempt 9 (배포 서버 작업 진행 중); HAL 에 같은 사이트의 site.deploy 실패 작업이 attempt 수만큼 쌓임",
"repro": "Agent 가 실패하는 배포를 요청하고 /api/sites/:id/jobs/:id 를 지켜보면 attempt 가 5초마다 증가",
"flows": [
6,
23,
13,
20
]
},
{
"title": "관리형 배포 종단 흐름 실패(포털→HAL→Agent 저장본 전달)",
"file": "agent/client.mjs (download: fetchImpl(base + path)) / agent/jobs/site.mjs (fetchBundle: client.download(p.bundleUrl))",
"route": "site.deploy → Agent fetch 단계",
"expected": "절대 URL(bundleUrl=http://<portal>/internal/bundles/<token>)은 HAL base 를 붙이지 않고 그대로 내려받아야 한다",
"actual": "시간 초과 — 포털 작업 queued(attempt 8, 배포 서버 작업 진행 중) · HAL site.deploy 작업 8개(최근: [\"failed\",\"Failed to parse URL from http://127.0.0.1:35361http://127.0.0.1:35287/internal/bundles/v74iR3dd-nVRU4N1LEVDmEe0ZuK300Ni\"])",
"repro": "사이트 생성→저장본→POST /api/sites/:id/deploy→HAL site.deploy→Agent site.deploy fetch 단계에서 \"HAL 내려받기 실패\" 또는 잘못된 URL 오류",
"flows": [
6,
8,
9,
13,
14,
15,
23
]
},
{
"title": "취소된 배포 작업의 재시도 요청이 500(서버 오류)으로 응답(작업 상태 오류는 4xx 여야 함)",
"file": "portal/sites/api.mjs (jobs/:jobId/:action → lib/jobs JobError 를 HttpError 로 바꾸지 않음)",
"route": "POST /api/sites/:id/jobs/:jobId/retry",
"expected": "409 job_not_retryable + 안내",
"actual": "500 job_not_retryable",
"repro": "취소된 작업에 retry",
"flows": [
23
]
},
{
"title": "외부 Agent 토큰 발급이 DB 제약(external_agents.hal_agent_id NOT NULL)으로 500",
"file": "portal/sites/service.mjs (createExternalAgent) / portal/sites/model.mjs (external_agents.hal_agent_id)",
"route": "POST /api/sites/:id/external",
"expected": "HAL 토큰 발급 응답의 agentId 가 null(등록 전)이어도 토큰·설치 명령을 돌려주고, Agent 등록 뒤 by-site 조회로 hal_agent_id 를 채운다",
"actual": "2026-10-26T12:59:27.404Z [http] POST /api/sites/s_01a1784b5d03e229abe92a46a1/external 500: Error: NOT NULL constraint failed: external_agents.hal_agent_id | at Object.run (file:///home/master/work/Ariweb-suite/lib/db.mjs:52:41) | at Object.createExternalAgent (file:///home/master/work/Ariweb",
"repro": "외부 배포 사이트 → 동의 → POST /api/sites/:id/external → 500 \"NOT NULL constraint failed: external_agents.hal_agent_id\"",
"flows": [
16,
28
]
},
{
"title": "HAL 작업 엔진이 작업을 한 번에 하나씩만 실행(tick 이 execute 를 await)해 느린 작업이 큐 전체를 막고, module.deploy 부모가 자식 완료를 기다리면 자식이 실행될 수 없어 교착",
"file": "lib/jobs.mjs (createJobEngine.start/tick) / hal/jobs.mjs (fanout 이 자식 완료를 폴링)",
"route": "POST /ops/api/modules/:name/deploy",
"expected": "엔진은 running 수가 concurrency 미만이면 실행을 분리(detach)해 여러 작업을 동시에 진행해야 한다(부모가 자식을 기다려도 자식이 실행됨)",
"actual": {
"status": "queued",
"steps": [
"precheck:pending",
"fanout:pending",
"collect:pending"
],
"error": null,
"children": [],
"agentJobs": [],
"timedOut": true
},
"repro": "(1) Agent 가 수십 초 걸리는 domain.probe 가 queued 로 쌓이면 다른 모든 HAL 작업이 기다린다 (2) 큐가 비어도 모듈 배포는 부모 collect:running + 자식 queued 로 영원히 멈춘다(test/hal.test.mjs 는 pump() 로 tick 을 동시에 돌려 통과)",
"flows": [
22,
30
]
}
],
"needsInfrastructure": [
"도메인 소유 확인(TXT)·DNS 반영·HTTPS 발급·외부 접속 확인은 실제 DNS 와 공인 서버가 필요하다(흐름 7·8·11·14·15)",
"메일 DNS(MX·SPF·DKIM·DMARC) 반영과 외부 송신·수신·회신 왕복은 실 도메인·실 AriMail(SMTP 587/수신함)·외부 검증 주소가 필요하다(흐름 10 활성화 완료·11·33 완료)",
"재해복구의 실제 복원(hal-restore.sh 로 다른 호스트에 HAL 재기동)·실제 운영자 로그인·HTTPS·고객 서비스 외부 확인은 실 서버가 필요하다(흐름 25 기록·훈련 절차만 로컬 확인)"
]
}