Agent 모듈 설치 경로의 루트 재정의 반영, E2E 흐름 30 판정 로직 수정, 검증 기록 갱신

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-26 13:18:20 +00:00
co-authored by Claude Fable 5.1
parent 98f4182bbb
commit acb69c7650
4 changed files with 235 additions and 274 deletions
+12 -2
View File
@@ -32,6 +32,16 @@ async function hook(ctx, name, { appDir, cur, unit }) {
if (name === 'webmaker.unitCheck') { const r = await ctx.sys.run(process.execPath, [join(cur, 'scripts', 'unit-check.js')], { allowStub: true, timeoutMs: 60000 }).catch(() => ({ code: 1 })); return { unitCheck: r.code === 0 ? 'ok' : 'warn' }; } if (name === 'webmaker.unitCheck') { const r = await ctx.sys.run(process.execPath, [join(cur, 'scripts', 'unit-check.js')], { allowStub: true, timeoutMs: 60000 }).catch(() => ({ code: 1 })); return { unitCheck: r.code === 0 ? 'ok' : 'warn' }; }
return { skipped: `unknown hook ${name}` }; return { skipped: `unknown hook ${name}` };
} }
/**
* [E2E 결함 수정] 설치 디렉터리 결정. HAL 이 준 appDir 가 기본 루트(/srv/ariweb/…) 아래이고 이 Agent 의 루트(ARIWEB_SRV_ROOT)가 다른 곳으로
* 재정의되어 있으면(검증 환경·일반 계정) 그 루트 아래로 옮긴다. 서버에서 발견한 기존 경로(/srv/webmaker 등)는 그대로 둔다.
*/
function resolveAppDir(ctx, p) {
const srv = ctx.roots.srv; const given = p.appDir;
if (!given) return join(srv, p.module);
if (srv !== '/srv/ariweb' && String(given).startsWith('/srv/ariweb/')) return join(srv, String(given).slice('/srv/ariweb/'.length));
return given;
}
async function installRelease(ctx, { module, version, appDir, desc, unit, action, toVersion }) { async function installRelease(ctx, { module, version, appDir, desc, unit, action, toVersion }) {
const relDir = join(appDir, 'releases', action === 'rollback' ? (toVersion || '') : version); const prev = readLink(join(appDir, 'current')); const relDir = join(appDir, 'releases', action === 'rollback' ? (toVersion || '') : version); const prev = readLink(join(appDir, 'current'));
if (action === 'rollback') { const target = toVersion ? relDir : readJson(join(appDir, '.previous.json'), {}).previous; if (!target || !existsSync(target)) throw Object.assign(new Error('되돌릴 이전 릴리스가 없습니다'), { code: 'no_previous_release' }); return { relDir: target, prev }; } if (action === 'rollback') { const target = toVersion ? relDir : readJson(join(appDir, '.previous.json'), {}).previous; if (!target || !existsSync(target)) throw Object.assign(new Error('되돌릴 이전 릴리스가 없습니다'), { code: 'no_previous_release' }); return { relDir: target, prev }; }
@@ -42,10 +52,10 @@ async function installRelease(ctx, { module, version, appDir, desc, unit, action
export const kinds = { export const kinds = {
'module.install': { title: '모듈 설치·갱신', async run(ctx) { return installOrRollback(ctx, ctx.payload.action || 'install'); } }, 'module.install': { title: '모듈 설치·갱신', async run(ctx) { return installOrRollback(ctx, ctx.payload.action || 'install'); } },
'module.rollback': { title: '모듈 되돌리기', async run(ctx) { return installOrRollback(ctx, 'rollback'); } }, 'module.rollback': { title: '모듈 되돌리기', async run(ctx) { return installOrRollback(ctx, 'rollback'); } },
'module.health': { title: '모듈 건강 확인', async run(ctx) { const p = ctx.payload; const desc = p.descriptor || {}; const appDir = p.appDir || join(ctx.roots.srv, p.module); const unit = p.unit || desc.unit; const active = unit ? await ctx.sys.isActive(unit) : true; const h = await health(ctx, desc); return { module: p.module, actualVersion: await detectVersion(ctx, desc, appDir), active, health: { ok: active && h.ok, ...h }, checkedAt: new Date().toISOString() }; } }, 'module.health': { title: '모듈 건강 확인', async run(ctx) { const p = ctx.payload; const desc = p.descriptor || {}; const appDir = resolveAppDir(ctx, p); const unit = p.unit || desc.unit; const active = unit ? await ctx.sys.isActive(unit) : true; const h = await health(ctx, desc); return { module: p.module, actualVersion: await detectVersion(ctx, desc, appDir), active, health: { ok: active && h.ok, ...h }, checkedAt: new Date().toISOString() }; } },
}; };
async function installOrRollback(ctx, action) { async function installOrRollback(ctx, action) {
const p = ctx.payload; const { module, version, descriptor: desc = {}, toVersion = null } = p; const appDir = p.appDir || join(ctx.roots.srv, module); const unit = (desc && desc.unit) || `ariweb-${module}`; const p = ctx.payload; const { module, version, descriptor: desc = {}, toVersion = null } = p; const appDir = resolveAppDir(ctx, p); const unit = (desc && desc.unit) || `ariweb-${module}`;
mkdirSync(join(appDir, 'releases'), { recursive: true }); mkdirSync(join(appDir, 'releases'), { recursive: true });
const { relDir, prev } = await ctx.step('place', () => installRelease(ctx, { module, version, appDir, desc, unit, action, toVersion }), action === 'rollback' ? '되돌릴 릴리스 확인' : '릴리스 배치'); const { relDir, prev } = await ctx.step('place', () => installRelease(ctx, { module, version, appDir, desc, unit, action, toVersion }), action === 'rollback' ? '되돌릴 릴리스 확인' : '릴리스 배치');
await ctx.progress(40); await ctx.progress(40);
+132 -169
View File
@@ -1,23 +1,23 @@
{ {
"generatedAt": "2026-10-26T13:12:56.197Z", "generatedAt": "2026-10-26T13:18:11.518Z",
"startedAt": "2026-10-26T13:10:36.090Z", "startedAt": "2026-10-26T13:15:47.881Z",
"env": { "env": {
"agents": 4, "agents": 4,
"ports": { "ports": {
"hal": 40927, "hal": 42511,
"portal": 43283, "portal": 32983,
"wm": 40985, "wm": 35307,
"am": 42921, "am": 33813,
"mail": 34497 "mail": 41473
}, },
"mailServerRegistered": true, "mailServerRegistered": true,
"node": "v24.22.1" "node": "v24.22.1"
}, },
"summary": { "summary": {
"verified_local": 17, "verified_local": 19,
"partial": 14, "partial": 14,
"not_verifiable_locally": 1, "not_verifiable_locally": 1,
"failed": 2, "failed": 0,
"unrecorded": 0 "unrecorded": 0
}, },
"flows": [ "flows": [
@@ -99,12 +99,12 @@
{ {
"name": "팀 그룹 만들기 → 현재 그룹 전환", "name": "팀 그룹 만들기 → 현재 그룹 전환",
"ok": true, "ok": true,
"detail": "g_01a17855a4277ac98f458d3057" "detail": "g_01a1785a66113e63a08c972cc5"
}, },
{ {
"name": "이메일 초대(7일) → 초대 메일 링크", "name": "이메일 초대(7일) → 초대 메일 링크",
"ok": true, "ok": true,
"detail": "Y4dXy0GV5b7pvqunRITvwVbgXFNedpwd" "detail": "uVdQrHwu9UjZmbh1PlCVMIPsJFxbHnvo"
}, },
{ {
"name": "초대 수락(1회용) → 별칭 user-02 · 역할 member", "name": "초대 수락(1회용) → 별칭 user-02 · 역할 member",
@@ -162,7 +162,7 @@
"detail": { "detail": {
"count": 2, "count": 2,
"team": { "team": {
"id": "pv_01a17855a17f402b30366a2d5a", "id": "pv_01a1785a6358a08c3194ac03c2",
"price": 49000, "price": 49000,
"sites": 3 "sites": 3
}, },
@@ -179,7 +179,7 @@
"orderNo": "AW-20261026-0001", "orderNo": "AW-20261026-0001",
"status": "awaiting_payment", "status": "awaiting_payment",
"total": 63800, "total": 63800,
"dueAt": "2026-11-02T13:10:41.050Z", "dueAt": "2026-11-02T13:15:52.839Z",
"hasInstructions": true "hasInstructions": true
} }
}, },
@@ -221,7 +221,7 @@
"detail": { "detail": {
"sites": 3, "sites": 3,
"subs": 1, "subs": 1,
"nextBilling": "2026-11-26T13:10:41.118Z" "nextBilling": "2026-11-26T13:15:52.905Z"
} }
} }
], ],
@@ -233,7 +233,7 @@
"bill": { "bill": {
"sites": 3, "sites": 3,
"subs": 1, "subs": 1,
"nextBilling": "2026-11-26T13:10:41.118Z" "nextBilling": "2026-11-26T13:15:52.905Z"
} }
} }
}, },
@@ -248,7 +248,7 @@
"name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내", "name": "사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "s_01a17855a51f4c70a0da049568", "id": "s_01a1785a670945da067e955da4",
"lifecycle": "draft", "lifecycle": "draft",
"projectId": "00000000000000000000000000000001", "projectId": "00000000000000000000000000000001",
"nextSteps": [ "nextSteps": [
@@ -283,7 +283,7 @@
"name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)", "name": "제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용)",
"ok": true, "ok": true,
"detail": { "detail": {
"portalRedirect": "http://127.0.0.1:40985/api/auth/sso?ticket=…", "portalRedirect": "http://127.0.0.1:35307/api/auth/sso?ticket=…",
"webmaker": 302, "webmaker": 302,
"reuse": 401 "reuse": 401
} }
@@ -299,7 +299,7 @@
], ],
"evidence": { "evidence": {
"sso": { "sso": {
"portalRedirect": "http://127.0.0.1:40985/api/auth/sso?ticket=…", "portalRedirect": "http://127.0.0.1:35307/api/auth/sso?ticket=…",
"webmaker": 302, "webmaker": 302,
"reuse": 401 "reuse": 401
}, },
@@ -320,7 +320,7 @@
"name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나", "name": "배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나",
"ok": true, "ok": true,
"detail": { "detail": {
"jobId": "j_01a17855a56c302b142ccac6a7", "jobId": "j_01a1785a6757555a33d6269bd8",
"steps": 10, "steps": 10,
"again": false, "again": false,
"siteCount": "reserved", "siteCount": "reserved",
@@ -351,7 +351,7 @@
"ok": true, "ok": true,
"detail": { "detail": {
"lifecycle": "needs_domain", "lifecycle": "needs_domain",
"version": "20261026131041-rev-c1-000000", "version": "20261026131553-rev-c1-000000",
"verify": "needs_domain", "verify": "needs_domain",
"health": "ok", "health": "ok",
"agentHasSite": true "agentHasSite": true
@@ -360,7 +360,7 @@
], ],
"evidence": { "evidence": {
"d": { "d": {
"jobId": "j_01a17855a56c302b142ccac6a7", "jobId": "j_01a1785a6757555a33d6269bd8",
"steps": 10, "steps": 10,
"again": false, "again": false,
"siteCount": "reserved", "siteCount": "reserved",
@@ -383,7 +383,7 @@
}, },
"after": { "after": {
"lifecycle": "needs_domain", "lifecycle": "needs_domain",
"version": "20261026131041-rev-c1-000000", "version": "20261026131553-rev-c1-000000",
"verify": "needs_domain", "verify": "needs_domain",
"health": "ok", "health": "ok",
"agentHasSite": true "agentHasSite": true
@@ -401,7 +401,7 @@
"name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)", "name": "도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT)",
"ok": true, "ok": true,
"detail": { "detail": {
"domainId": "d_01a17855b5405b38f36942a6e1", "domainId": "d_01a1785a772932b8936927ad0a",
"txtName": "_ariweb-verify.aw-e2e.localhost", "txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…", "txtValueShape": "ariweb-verify=…",
"copyable": true "copyable": true
@@ -422,7 +422,7 @@
], ],
"evidence": { "evidence": {
"dm": { "dm": {
"domainId": "d_01a17855b5405b38f36942a6e1", "domainId": "d_01a1785a772932b8936927ad0a",
"txtName": "_ariweb-verify.aw-e2e.localhost", "txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…", "txtValueShape": "ariweb-verify=…",
"copyable": true "copyable": true
@@ -500,7 +500,7 @@
"name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시", "name": "사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "msv_01a178563dd6f7ac39957c38a1", "id": "msv_01a1785affa395a9602c99bbb5",
"status": "needs_setup", "status": "needs_setup",
"tenant": true, "tenant": true,
"ariwebKind": "site_bound", "ariwebKind": "site_bound",
@@ -517,7 +517,7 @@
"name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음", "name": "메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "mb_01a178563e14cf15ea191a4de5", "id": "mb_01a1785affda99dffac745d1e9",
"address": "kim@aw-e2e.localhost", "address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]", "passwordShownOnce": "[redacted]",
"mustChange": true, "mustChange": true,
@@ -584,7 +584,7 @@
], ],
"evidence": { "evidence": {
"svc": { "svc": {
"id": "msv_01a178563dd6f7ac39957c38a1", "id": "msv_01a1785affa395a9602c99bbb5",
"status": "needs_setup", "status": "needs_setup",
"tenant": true, "tenant": true,
"ariwebKind": "site_bound", "ariwebKind": "site_bound",
@@ -592,7 +592,7 @@
"siteCountAllocated": 0 "siteCountAllocated": 0
}, },
"mb": { "mb": {
"id": "mb_01a178563e14cf15ea191a4de5", "id": "mb_01a1785affda99dffac745d1e9",
"address": "kim@aw-e2e.localhost", "address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]", "passwordShownOnce": "[redacted]",
"mustChange": true, "mustChange": true,
@@ -644,7 +644,7 @@
"name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)", "name": "메일 전용 도메인 등록(solo-e2e.localhost, purpose mail)",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "d_01a17856662c41b1d8aaf0c851", "id": "d_01a1785b2be9c7ee6d0d94a95c",
"jobCancelled": null "jobCancelled": null
} }
}, },
@@ -652,7 +652,7 @@
"name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)", "name": "독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비)",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "msv_01a178566634f50d5e0c8ce0c7", "id": "msv_01a1785b2bf14cefa9e1f81fd7",
"kind": "standalone", "kind": "standalone",
"mailServicesAllocated": 1, "mailServicesAllocated": 1,
"sitesAllocated": 0 "sitesAllocated": 0
@@ -661,7 +661,7 @@
], ],
"evidence": { "evidence": {
"r": { "r": {
"id": "msv_01a178566634f50d5e0c8ce0c7", "id": "msv_01a1785b2bf14cefa9e1f81fd7",
"kind": "standalone", "kind": "standalone",
"mailServicesAllocated": 1, "mailServicesAllocated": 1,
"sitesAllocated": 0 "sitesAllocated": 0
@@ -701,8 +701,8 @@
"ok": true, "ok": true,
"detail": { "detail": {
"status": "active", "status": "active",
"firstDue": "2026-10-27T13:11:30.755Z", "firstDue": "2026-10-27T13:16:43.530Z",
"nextRun": "2026-10-26T13:11:30.755Z" "nextRun": "2026-10-26T13:16:43.530Z"
} }
}, },
{ {
@@ -712,7 +712,7 @@
"timedOut": false, "timedOut": false,
"optionStatus": "active", "optionStatus": "active",
"lastJob": null, "lastJob": null,
"siteServer": "srv_01a178559b3ef8b19bacf9e7bf", "siteServer": "srv_01a1785a5d26219326f7ad26ec",
"backup": { "backup": {
"status": "done", "status": "done",
"tier": "monthly", "tier": "monthly",
@@ -733,14 +733,14 @@
}, },
"opt": { "opt": {
"status": "active", "status": "active",
"firstDue": "2026-10-27T13:11:30.755Z", "firstDue": "2026-10-27T13:16:43.530Z",
"nextRun": "2026-10-26T13:11:30.755Z" "nextRun": "2026-10-26T13:16:43.530Z"
}, },
"run1": { "run1": {
"timedOut": false, "timedOut": false,
"optionStatus": "active", "optionStatus": "active",
"lastJob": null, "lastJob": null,
"siteServer": "srv_01a178559b3ef8b19bacf9e7bf", "siteServer": "srv_01a1785a5d26219326f7ad26ec",
"backup": { "backup": {
"status": "done", "status": "done",
"tier": "monthly", "tier": "monthly",
@@ -763,19 +763,19 @@
"ok": true, "ok": true,
"detail": { "detail": {
"backup": { "backup": {
"id": "bk_01a1785678cb538e124484669b", "id": "bk_01a1785b465fad18eaeb8a679e",
"createdAt": "2026-10-26T13:11:30.762Z", "createdAt": "2026-10-26T13:16:43.538Z",
"scope": { "scope": {
"files": true, "files": true,
"db": true, "db": true,
"uploads": true, "uploads": true,
"config": true, "config": true,
"storeServerId": "srv_01a178559bee1c3a73f5640021", "storeServerId": "srv_01a1785a5dd874fe86b67f0f38",
"verifyMethod": "sha256 + 격리 환경 읽기 확인" "verifyMethod": "sha256 + 격리 환경 읽기 확인"
}, },
"sizeBytes": 1909, "sizeBytes": 1896,
"verifyState": "verified", "verifyState": "verified",
"expiresAt": "2027-01-27T13:11:35.372Z", "expiresAt": "2027-01-27T13:16:50.144Z",
"tier": "monthly" "tier": "monthly"
}, },
"included": [ "included": [
@@ -809,7 +809,7 @@
"name": "복원 요청 → 운영자 승인 대기 · 중복 요청 없음", "name": "복원 요청 → 운영자 승인 대기 · 중복 요청 없음",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "rs_01a178567e7ec8dd46d83c957c", "id": "rs_01a1785b4a2808cc16818090d9",
"status": "awaiting_approval", "status": "awaiting_approval",
"dup": false, "dup": false,
"finalConfirmation": { "finalConfirmation": {
@@ -838,19 +838,19 @@
"evidence": { "evidence": {
"info": { "info": {
"backup": { "backup": {
"id": "bk_01a1785678cb538e124484669b", "id": "bk_01a1785b465fad18eaeb8a679e",
"createdAt": "2026-10-26T13:11:30.762Z", "createdAt": "2026-10-26T13:16:43.538Z",
"scope": { "scope": {
"files": true, "files": true,
"db": true, "db": true,
"uploads": true, "uploads": true,
"config": true, "config": true,
"storeServerId": "srv_01a178559bee1c3a73f5640021", "storeServerId": "srv_01a1785a5dd874fe86b67f0f38",
"verifyMethod": "sha256 + 격리 환경 읽기 확인" "verifyMethod": "sha256 + 격리 환경 읽기 확인"
}, },
"sizeBytes": 1909, "sizeBytes": 1896,
"verifyState": "verified", "verifyState": "verified",
"expiresAt": "2027-01-27T13:11:35.372Z", "expiresAt": "2027-01-27T13:16:50.144Z",
"tier": "monthly" "tier": "monthly"
}, },
"included": [ "included": [
@@ -880,7 +880,7 @@
"deferred": [] "deferred": []
}, },
"rq": { "rq": {
"id": "rs_01a178567e7ec8dd46d83c957c", "id": "rs_01a1785b4a2808cc16818090d9",
"status": "awaiting_approval", "status": "awaiting_approval",
"dup": false, "dup": false,
"finalConfirmation": { "finalConfirmation": {
@@ -970,8 +970,8 @@
"name": "외부 배포 사이트 생성(초안) + 저장본", "name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "s_01a1785769fa0becc28ee2efbc", "id": "s_01a1785c35f8d649e375f0c391",
"group_id": "g_01a17855a4277ac98f458d3057", "group_id": "g_01a1785a66113e63a08c972cc5",
"name": "E2E 외부 사이트", "name": "E2E 외부 사이트",
"purpose": "e2e", "purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004", "webmaker_project_id": "00000000000000000000000000000004",
@@ -999,9 +999,9 @@
"retention_until": null, "retention_until": null,
"delete_after": null, "delete_after": null,
"deleted_at": null, "deleted_at": null,
"created_by": "u_01a17855a37efae6dc46b789fa", "created_by": "u_01a1785a65614c4deeec61ea3d",
"created_at": "2026-10-26T13:12:37.114Z", "created_at": "2026-10-26T13:17:51.480Z",
"updated_at": "2026-10-26T13:12:37.124Z", "updated_at": "2026-10-26T13:17:51.485Z",
"runtime": "static", "runtime": "static",
"consent_version": null, "consent_version": null,
"pending_cleanup_json": null, "pending_cleanup_json": null,
@@ -1042,8 +1042,8 @@
"ok": true, "ok": true,
"detail": { "detail": {
"token": "[redacted]", "token": "[redacted]",
"install": "curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc", "install": "curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391",
"expiresAt": "2026-10-26T13:22:37.404Z", "expiresAt": "2026-10-26T13:27:51.761Z",
"status": "pending" "status": "pending"
} }
}, },
@@ -1126,8 +1126,8 @@
"agreed": false "agreed": false
}, },
"tok": { "tok": {
"install": "curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc", "install": "curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391",
"expiresAt": "2026-10-26T13:22:37.404Z" "expiresAt": "2026-10-26T13:27:51.761Z"
}, },
"conn": { "conn": {
"status": "connected", "status": "connected",
@@ -1311,7 +1311,7 @@
"approve": 200, "approve": 200,
"sites": 1, "sites": 1,
"mailboxes": 5, "mailboxes": 5,
"product": "pv_01a17855a17f31a67b1ac8d3f0" "product": "pv_01a1785a6358a0946ad508ee8d"
} }
} }
], ],
@@ -1347,7 +1347,7 @@
"approve": 200, "approve": 200,
"sites": 1, "sites": 1,
"mailboxes": 5, "mailboxes": 5,
"product": "pv_01a17855a17f31a67b1ac8d3f0" "product": "pv_01a1785a6358a0946ad508ee8d"
} }
} }
}, },
@@ -1430,9 +1430,9 @@
"name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림", "name": "요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "tr_01a1785788f3921de196eb902e", "id": "tr_01a1785c58c47561320ed6f007",
"status": "requested", "status": "requested",
"expiresAt": "2026-11-02T13:12:45.043Z", "expiresAt": "2026-11-02T13:18:00.388Z",
"bobNotified": true "bobNotified": true
} }
}, },
@@ -1460,7 +1460,7 @@
"bad": "final_confirmation_mismatch", "bad": "final_confirmation_mismatch",
"ok": 200, "ok": 200,
"status": "completed", "status": "completed",
"cutover": "2026-10-26T13:12:45.093Z" "cutover": "2026-10-26T13:18:00.432Z"
} }
}, },
{ {
@@ -1489,9 +1489,9 @@
"excluded": 3 "excluded": 3
}, },
"rq": { "rq": {
"id": "tr_01a1785788f3921de196eb902e", "id": "tr_01a1785c58c47561320ed6f007",
"status": "requested", "status": "requested",
"expiresAt": "2026-11-02T13:12:45.043Z", "expiresAt": "2026-11-02T13:18:00.388Z",
"bobNotified": true "bobNotified": true
}, },
"acc": { "acc": {
@@ -1503,7 +1503,7 @@
"bad": "final_confirmation_mismatch", "bad": "final_confirmation_mismatch",
"ok": 200, "ok": 200,
"status": "completed", "status": "completed",
"cutover": "2026-10-26T13:12:45.093Z" "cutover": "2026-10-26T13:18:00.432Z"
}, },
"after": { "after": {
"aliceSees": 404, "aliceSees": 404,
@@ -1649,8 +1649,8 @@
{ {
"no": 22, "no": 22,
"title": "HAL 우선 배포 후 HAL을 통한 전체 모듈 배포", "title": "HAL 우선 배포 후 HAL을 통한 전체 모듈 배포",
"verdict": "failed", "verdict": "verified_local",
"note": "HAL 우선 순서(hal_first 거부)·릴리스 승인·부트스트랩 잠김은 확인. 모듈 배포 작업은 완료되지 않음(진단 증거 참조)", "note": "HAL 우선 순서·릴리스 승인·모듈 설치",
"blockedBy": null, "blockedBy": null,
"steps": [ "steps": [
{ {
@@ -1702,15 +1702,15 @@
"name": "HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인)", "name": "HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인)",
"ok": true, "ok": true,
"detail": { "detail": {
"status": "failed", "status": "done",
"steps": [ "steps": [
"precheck:done", "precheck:done",
"fanout:done", "fanout:done",
"collect:failed" "collect:done"
], ],
"error": "1대 중 1대 실패·대기 — 대상별 결과를 확인하세요", "error": null,
"children": [ "children": [
"failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'" "done:"
], ],
"agentJobs": [], "agentJobs": [],
"timedOut": false "timedOut": false
@@ -1752,31 +1752,20 @@
"complete": true "complete": true
}, },
"dep": { "dep": {
"status": "failed", "status": "done",
"steps": [ "steps": [
"precheck:done", "precheck:done",
"fanout:done", "fanout:done",
"collect:failed" "collect:done"
], ],
"error": "1대 중 1대 실패·대기 — 대상별 결과를 확인하세요", "error": null,
"children": [ "children": [
"failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'" "done:"
], ],
"agentJobs": [], "agentJobs": [],
"timedOut": false "timedOut": false
}, },
"diag": { "diag": null
"stuckNow": [],
"halLog": [
"2026-10-26T13:10:38.150Z [hal] Ariweb HAL v0.1.1 — http://127.0.0.1:40927 (공개 http://127.0.0.1:40927, 데이터 /home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131036/hal, 부트스트랩 진행 중)",
"2026-10-26T13:12:45.488Z [jobs] module.deploy j_01a17857894f044d5d9552218a 실패: HAL 이 아직 0.1.2 으로 실행되고 있지 않습니다(현재 0.1.1). HAL 을 먼저 배포하세요",
"2026-10-26T13:12:47.496Z [jobs] module.deploy.target j_01a178578ca73b9fe8276f58e3 실패: EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'",
"2026-10-26T13:12:47.996Z [jobs] module.deploy j_01a178578c7e94c12a61d85f10 실패: 1대 중 1대 실패·대기 — 대상별 결과를 확인하세요"
],
"ctlAgentLog": [
"2026-10-26T13:10:38.789Z [agent] Ariweb Node Agent v0.1.1 ag_01a178559b15aed8b0e25f47a5 (managed) → http://127.0.0.1:40927"
]
}
} }
}, },
{ {
@@ -1792,7 +1781,7 @@
"detail": { "detail": {
"status": "done", "status": "done",
"canRetry": false, "canRetry": false,
"jobId": "j_01a17855a56c302b142ccac6a7" "jobId": "j_01a1785a6757555a33d6269bd8"
} }
}, },
{ {
@@ -1817,7 +1806,7 @@
"dep": { "dep": {
"status": "done", "status": "done",
"canRetry": false, "canRetry": false,
"jobId": "j_01a17855a56c302b142ccac6a7" "jobId": "j_01a1785a6757555a33d6269bd8"
}, },
"rt": { "rt": {
"status": 400, "status": 400,
@@ -1843,7 +1832,7 @@
"detail": { "detail": {
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다", "title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
"category": "billing", "category": "billing",
"link": "/orders/o_01a17855a499a9b2c94a482c83" "link": "/orders/o_01a1785a6686ffeba5d2781add"
} }
}, },
{ {
@@ -1859,7 +1848,7 @@
"ok": true, "ok": true,
"detail": { "detail": {
"status": "approved", "status": "approved",
"approvedAt": "2026-10-26T13:10:41.118Z" "approvedAt": "2026-10-26T13:15:52.905Z"
} }
} }
], ],
@@ -1867,7 +1856,7 @@
"n": { "n": {
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다", "title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
"category": "billing", "category": "billing",
"link": "/orders/o_01a17855a499a9b2c94a482c83" "link": "/orders/o_01a1785a6686ffeba5d2781add"
}, },
"a": { "a": {
"action": "order.approve", "action": "order.approve",
@@ -1875,7 +1864,7 @@
}, },
"same": { "same": {
"status": "approved", "status": "approved",
"approvedAt": "2026-10-26T13:10:41.118Z" "approvedAt": "2026-10-26T13:15:52.905Z"
} }
} }
}, },
@@ -1918,9 +1907,9 @@
"rpo_minutes": 30, "rpo_minutes": 30,
"snapshot_interval_minutes": 60, "snapshot_interval_minutes": 60,
"retention_count": 3, "retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131036/dr-external", "external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131547/dr-external",
"updated_at": "2026-10-26T13:12:53.244Z", "updated_at": "2026-10-26T13:18:08.570Z",
"updated_by": "op_01a178559a40f4802a91a5fb1a" "updated_by": "op_01a1785a5c27caed1a96f25d99"
} }
}, },
{ {
@@ -1980,9 +1969,9 @@
"rpo_minutes": 30, "rpo_minutes": 30,
"snapshot_interval_minutes": 60, "snapshot_interval_minutes": 60,
"retention_count": 3, "retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131036/dr-external", "external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131547/dr-external",
"updated_at": "2026-10-26T13:12:53.244Z", "updated_at": "2026-10-26T13:18:08.570Z",
"updated_by": "op_01a178559a40f4802a91a5fb1a" "updated_by": "op_01a1785a5c27caed1a96f25d99"
}, },
"snap": { "snap": {
"verify": "ok", "verify": "ok",
@@ -2016,7 +2005,7 @@
"ok": true, "ok": true,
"detail": { "detail": {
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"halJobs": 18 "halJobs": 18
@@ -2028,7 +2017,7 @@
"detail": { "detail": {
"sessionKept": "[redacted]", "sessionKept": "[redacted]",
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"same": true "same": true
@@ -2047,7 +2036,7 @@
"evidence": { "evidence": {
"before": { "before": {
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"halJobs": 18 "halJobs": 18
@@ -2055,7 +2044,7 @@
"pr": { "pr": {
"sessionKept": "[redacted]", "sessionKept": "[redacted]",
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"same": true "same": true
@@ -2086,8 +2075,8 @@
"ok": true, "ok": true,
"detail": [ "detail": [
{ {
"id": "s_01a1785631d62a2fbdf27a60d6", "id": "s_01a1785af3ab8d0d00e0063124",
"group_id": "g_01a17855a453ba2b2c5bf88ecd", "group_id": "g_01a1785a663da04c2c5802e990",
"name": "동시 A", "name": "동시 A",
"purpose": "e2e", "purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000002", "webmaker_project_id": "00000000000000000000000000000002",
@@ -2115,9 +2104,9 @@
"retention_until": null, "retention_until": null,
"delete_after": null, "delete_after": null,
"deleted_at": null, "deleted_at": null,
"created_by": "u_01a17855a4526739e73bc5adbf", "created_by": "u_01a1785a663df67cc036d6bc02",
"created_at": "2026-10-26T13:11:17.206Z", "created_at": "2026-10-26T13:16:28.971Z",
"updated_at": "2026-10-26T13:11:17.215Z", "updated_at": "2026-10-26T13:16:28.978Z",
"runtime": "static", "runtime": "static",
"consent_version": null, "consent_version": null,
"pending_cleanup_json": null, "pending_cleanup_json": null,
@@ -2136,8 +2125,8 @@
"nextJob": null "nextJob": null
}, },
{ {
"id": "s_01a1785631da943306e342eba7", "id": "s_01a1785af3aeacb2a4c3e486f3",
"group_id": "g_01a17855a453ba2b2c5bf88ecd", "group_id": "g_01a1785a663da04c2c5802e990",
"name": "동시 B", "name": "동시 B",
"purpose": "e2e", "purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000003", "webmaker_project_id": "00000000000000000000000000000003",
@@ -2165,9 +2154,9 @@
"retention_until": null, "retention_until": null,
"delete_after": null, "delete_after": null,
"deleted_at": null, "deleted_at": null,
"created_by": "u_01a17855a4526739e73bc5adbf", "created_by": "u_01a1785a663df67cc036d6bc02",
"created_at": "2026-10-26T13:11:17.210Z", "created_at": "2026-10-26T13:16:28.974Z",
"updated_at": "2026-10-26T13:11:17.218Z", "updated_at": "2026-10-26T13:16:28.980Z",
"runtime": "static", "runtime": "static",
"consent_version": null, "consent_version": null,
"pending_cleanup_json": null, "pending_cleanup_json": null,
@@ -2225,8 +2214,8 @@
"name": "외부 배포 사이트 생성(초안) + 저장본", "name": "외부 배포 사이트 생성(초안) + 저장본",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "s_01a1785769fa0becc28ee2efbc", "id": "s_01a1785c35f8d649e375f0c391",
"group_id": "g_01a17855a4277ac98f458d3057", "group_id": "g_01a1785a66113e63a08c972cc5",
"name": "E2E 외부 사이트", "name": "E2E 외부 사이트",
"purpose": "e2e", "purpose": "e2e",
"webmaker_project_id": "00000000000000000000000000000004", "webmaker_project_id": "00000000000000000000000000000004",
@@ -2254,9 +2243,9 @@
"retention_until": null, "retention_until": null,
"delete_after": null, "delete_after": null,
"deleted_at": null, "deleted_at": null,
"created_by": "u_01a17855a37efae6dc46b789fa", "created_by": "u_01a1785a65614c4deeec61ea3d",
"created_at": "2026-10-26T13:12:37.114Z", "created_at": "2026-10-26T13:17:51.480Z",
"updated_at": "2026-10-26T13:12:37.124Z", "updated_at": "2026-10-26T13:17:51.485Z",
"runtime": "static", "runtime": "static",
"consent_version": null, "consent_version": null,
"pending_cleanup_json": null, "pending_cleanup_json": null,
@@ -2297,8 +2286,8 @@
"ok": true, "ok": true,
"detail": { "detail": {
"token": "[redacted]", "token": "[redacted]",
"install": "curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc", "install": "curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391",
"expiresAt": "2026-10-26T13:22:37.404Z", "expiresAt": "2026-10-26T13:27:51.761Z",
"status": "pending" "status": "pending"
} }
}, },
@@ -2481,21 +2470,21 @@
{ {
"no": 30, "no": 30,
"title": "모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정", "title": "모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정",
"verdict": "failed", "verdict": "verified_local",
"note": "대상별 자식 작업 생성·되돌리기 3단계 확인 거부는 확인. 일부 실패 요약·건강 확인·drift 조정은 작업 엔진 교착(흐름 22 결함)으로 완료되지 않음", "note": "대상별 자식 작업·일부 실패 partial 요약(전체 성공으로 묶지 않음)·건강 확인·drift 조정·되돌리기 3단계 확인 거부",
"blockedBy": 22, "blockedBy": null,
"steps": [ "steps": [
{ {
"name": "agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)", "name": "agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)",
"ok": true, "ok": true,
"detail": { "detail": {
"status": "failed", "status": "failed",
"errorCode": "all_failed", "errorCode": "partial_failure",
"overall": "failed", "overall": "partial",
"children": [ "children": [
"srv_missing_e2e:failed:서버가 없습니다", "srv_missing_e2e:failed:서버가 없습니다",
"srv_01a178559bee1c3a73f5640021:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'", "srv_01a1785a5d26219326f7ad26ec:done:",
"srv_01a178559b3ef8b19bacf9e7bf:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'" "srv_01a1785a5dd874fe86b67f0f38:done:"
], ],
"timedOut": false "timedOut": false
} }
@@ -2505,7 +2494,7 @@
"ok": true, "ok": true,
"detail": { "detail": {
"health": "done", "health": "done",
"drift": 1, "drift": 0,
"reconcile": 200, "reconcile": 200,
"overviewComponents": [ "overviewComponents": [
"HAL:ok", "HAL:ok",
@@ -2529,18 +2518,18 @@
"evidence": { "evidence": {
"d": { "d": {
"status": "failed", "status": "failed",
"errorCode": "all_failed", "errorCode": "partial_failure",
"overall": "failed", "overall": "partial",
"children": [ "children": [
"srv_missing_e2e:failed:서버가 없습니다", "srv_missing_e2e:failed:서버가 없습니다",
"srv_01a178559bee1c3a73f5640021:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'", "srv_01a1785a5d26219326f7ad26ec:done:",
"srv_01a178559b3ef8b19bacf9e7bf:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'" "srv_01a1785a5dd874fe86b67f0f38:done:"
], ],
"timedOut": false "timedOut": false
}, },
"h": { "h": {
"health": "done", "health": "done",
"drift": 1, "drift": 0,
"reconcile": 200, "reconcile": 200,
"overviewComponents": [ "overviewComponents": [
"HAL:ok", "HAL:ok",
@@ -2573,7 +2562,7 @@
"name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)", "name": "critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회)",
"ok": true, "ok": true,
"detail": { "detail": {
"id": "em_01a17857aa1262003c4dfd08d9", "id": "em_01a1785c79d6aa634fc6516b7b",
"noCred": "approvals_insufficient", "noCred": "approvals_insufficient",
"wrongStep": "wrong_step", "wrongStep": "wrong_step",
"badIds": "identifiers_mismatch", "badIds": "identifiers_mismatch",
@@ -2581,7 +2570,7 @@
"approved": "approved", "approved": "approved",
"approvals": 3, "approvals": 3,
"credential": "[redacted]", "credential": "[redacted]",
"expiresAt": "2026-10-26T13:42:53.551Z" "expiresAt": "2026-10-26T13:48:08.882Z"
} }
}, },
{ {
@@ -2602,14 +2591,14 @@
"evidence": { "evidence": {
"healthy": "hal_healthy", "healthy": "hal_healthy",
"sess": { "sess": {
"id": "em_01a17857aa1262003c4dfd08d9", "id": "em_01a1785c79d6aa634fc6516b7b",
"noCred": "approvals_insufficient", "noCred": "approvals_insufficient",
"wrongStep": "wrong_step", "wrongStep": "wrong_step",
"badIds": "identifiers_mismatch", "badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch", "badText": "final_confirm_mismatch",
"approved": "approved", "approved": "approved",
"approvals": 3, "approvals": 3,
"expiresAt": "2026-10-26T13:42:53.551Z" "expiresAt": "2026-10-26T13:48:08.882Z"
}, },
"perf": { "perf": {
"forbidden": "action_forbidden", "forbidden": "action_forbidden",
@@ -2661,8 +2650,8 @@
"ok": true, "ok": true,
"detail": { "detail": {
"status": "readonly", "status": "readonly",
"deleteAfter": "2026-11-25T13:12:44.974Z", "deleteAfter": "2026-11-25T13:18:00.329Z",
"readonlyUntil": "2026-11-25T13:12:44.974Z" "readonlyUntil": "2026-11-25T13:18:00.329Z"
} }
}, },
{ {
@@ -2688,8 +2677,8 @@
}, },
"un": { "un": {
"status": "readonly", "status": "readonly",
"deleteAfter": "2026-11-25T13:12:44.974Z", "deleteAfter": "2026-11-25T13:18:00.329Z",
"readonlyUntil": "2026-11-25T13:12:44.974Z" "readonlyUntil": "2026-11-25T13:18:00.329Z"
}, },
"sweep": { "sweep": {
"sweep": 200, "sweep": 200,
@@ -2879,33 +2868,7 @@
} }
} }
], ],
"bugs": [ "bugs": [],
{
"title": "모듈 설치가 ARIWEB_SRV_ROOT 를 무시하고 절대 경로(/srv/ariweb/<module>)에 쓰려 해 일반 계정·검증 환경에서 실패",
"file": "hal/jobs.mjs (module.deploy.target: appDir 기본값) / agent/jobs/module.mjs",
"route": "POST /ops/api/modules/:name/deploy",
"expected": "Agent 가 payload.appDir 가 없거나 루트 override 가 있으면 roots.srv 아래에 설치",
"actual": {
"status": "failed",
"steps": [
"precheck:done",
"fanout:done",
"collect:failed"
],
"error": "1대 중 1대 실패·대기 — 대상별 결과를 확인하세요",
"children": [
"failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'"
],
"agentJobs": [],
"timedOut": false
},
"repro": "ARIWEB_SRV_ROOT 를 둔 Agent 에 module.deploy → mkdir /srv/ariweb/hal 권한 오류",
"flows": [
22,
30
]
}
],
"needsInfrastructure": [ "needsInfrastructure": [
"도메인 소유 확인(TXT)·DNS 반영·HTTPS 발급·외부 접속 확인은 실제 DNS 와 공인 서버가 필요하다(흐름 7·8·11·14·15)", "도메인 소유 확인(TXT)·DNS 반영·HTTPS 발급·외부 접속 확인은 실제 DNS 와 공인 서버가 필요하다(흐름 7·8·11·14·15)",
"메일 DNS(MX·SPF·DKIM·DMARC) 반영과 외부 송신·수신·회신 왕복은 실 도메인·실 AriMail(SMTP 587/수신함)·외부 검증 주소가 필요하다(흐름 10 활성화 완료·11·33 완료)", "메일 DNS(MX·SPF·DKIM·DMARC) 반영과 외부 송신·수신·회신 왕복은 실 도메인·실 AriMail(SMTP 587/수신함)·외부 검증 주소가 필요하다(흐름 10 활성화 완료·11·33 완료)",
+88 -102
View File
@@ -1,6 +1,6 @@
# 로컬 종단 간 검증 기록 — 2026-10-26 # 로컬 종단 간 검증 기록 — 2026-10-26
생성: 2026-10-26T13:12:56.197Z · `node --no-warnings test/e2e.test.mjs` (test/e2e/) 생성: 2026-10-26T13:18:11.518Z · `node --no-warnings test/e2e.test.mjs` (test/e2e/)
실행 환경: 실제 HAL·고객 포털·Node Agent(명령 stub 모드) 프로세스 4대 + 가짜 Webmaker/AriMail/발신 메일 서버(127.0.0.1). 실 DNS·실 메일 왕복·실 서버는 포함하지 않는다. 실행 환경: 실제 HAL·고객 포털·Node Agent(명령 stub 모드) 프로세스 4대 + 가짜 Webmaker/AriMail/발신 메일 서버(127.0.0.1). 실 DNS·실 메일 왕복·실 서버는 포함하지 않는다.
@@ -8,10 +8,10 @@
| 판정 | 수 | | 판정 | 수 |
|---|---| |---|---|
| 로컬 확인 | 17 | | 로컬 확인 | 19 |
| 부분 확인 | 14 | | 부분 확인 | 14 |
| 로컬 확인 불가 | 1 | | 로컬 확인 불가 | 1 |
| 실패(결함) | 2 | | 실패(결함) | 0 |
| unrecorded | 0 | | unrecorded | 0 |
## 흐름별 판정(Release 0.1.1 완료 조건 34개) ## 흐름별 판정(Release 0.1.1 완료 조건 34개)
@@ -39,7 +39,7 @@
| 19 | 사이트 수 반환과 대표 메일의 독립 전환 | 부분 확인 | 반환 점검표와 메일 유지/종료 분기 규칙 확인. 실제 반환은 운영 사이트(사이트 수 사용 중) 필요(단위 테스트 test/lifecycle.test.mjs·test/mail.test.mjs 로 확인) | | 19 | 사이트 수 반환과 대표 메일의 독립 전환 | 부분 확인 | 반환 점검표와 메일 유지/종료 분기 규칙 확인. 실제 반환은 운영 사이트(사이트 수 사용 중) 필요(단위 테스트 test/lifecycle.test.mjs·test/mail.test.mjs 로 확인) |
| 20 | 사이트 소유권 이전 | 로컬 확인 | 미리보기→요청·알림→수락(보류·재확인)→심사·최종 확인→원자적 완료→접근 전환·감사 | | 20 | 사이트 소유권 이전 | 로컬 확인 | 미리보기→요청·알림→수락(보류·재확인)→심사·최종 확인→원자적 완료→접근 전환·감사 |
| 21 | 중지, 해지, 보존 예정일 표시 | 부분 확인 | 중지·재개·생명주기 화면 확인 | | 21 | 중지, 해지, 보존 예정일 표시 | 부분 확인 | 중지·재개·생명주기 화면 확인 |
| 22 | HAL 우선 배포 후 HAL을 통한 전체 모듈 배포 | 실패(결함) | HAL 우선 순서(hal_first 거부)·릴리스 승인·부트스트랩 잠김은 확인. 모듈 배포 작업은 완료되지 않음(진단 증거 참조) | | 22 | HAL 우선 배포 후 HAL을 통한 전체 모듈 배포 | 로컬 확인 | HAL 우선 순서·릴리스 승인·모듈 설치 |
| 23 | 실패 작업의 재시도와 되돌리기 | 부분 확인 | 실패 기록·재시도 API·되돌리기 거부/허용 조건 확인. 성공한 재배포 뒤 실제 되돌리기는 배포 성공이 필요 | | 23 | 실패 작업의 재시도와 되돌리기 | 부분 확인 | 실패 기록·재시도 API·되돌리기 거부/허용 조건 확인. 성공한 재배포 뒤 실제 되돌리기는 배포 성공이 필요 |
| 24 | 운영자 승인, 감사 기록, 고객 알림의 일치 | 로컬 확인 | 승인 → 고객 알림·불변 감사 기록·운영자/고객 화면 상태 일치(주문 기준) | | 24 | 운영자 승인, 감사 기록, 고객 알림의 일치 | 로컬 확인 | 승인 → 고객 알림·불변 감사 기록·운영자/고객 화면 상태 일치(주문 기준) |
| 25 | HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련 | 부분 확인 | DR 설정(RTO/RPO/주기/보존)·사본 생성·검증·외부 보관·복원 승인/기존 제어 중지 확인/완료 항목·훈련 실패/성공→준비 완료 기록까지 확인. 실제 다른 호스트 복원은 실 서버 필요 | | 25 | HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련 | 부분 확인 | DR 설정(RTO/RPO/주기/보존)·사본 생성·검증·외부 보관·복원 승인/기존 제어 중지 확인/완료 항목·훈련 실패/성공→준비 완료 기록까지 확인. 실제 다른 호스트 복원은 실 서버 필요 |
@@ -47,7 +47,7 @@
| 27 | 마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공 | 로컬 확인 | 마지막 사이트 수 1개를 두 배포가 동시에 요청 → 하나만 예약 성공, 다른 쪽은 부족 사유(§13.3) | | 27 | 마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공 | 로컬 확인 | 마지막 사이트 수 1개를 두 배포가 동시에 요청 → 하나만 예약 성공, 다른 쪽은 부족 사유(§13.3) |
| 28 | 외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리 | 로컬 확인 | 외부 Agent 연결·제거·해제 API 확인. 재배포·실패 정리는 배포 성공이 필요(증거 참조) | | 28 | 외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리 | 로컬 확인 | 외부 Agent 연결·제거·해제 API 확인. 재배포·실패 정리는 배포 성공이 필요(증거 참조) |
| 29 | HAL 장애 중 기존 서비스 유지와 신규 변경 제한 표시 | 로컬 확인 | HAL 중단 중 포털 조회 유지·신규 배포 차단(정보 확인 지연), HAL 재기동 뒤 서버·Agent 복귀 | | 29 | HAL 장애 중 기존 서비스 유지와 신규 변경 제한 표시 | 로컬 확인 | HAL 중단 중 포털 조회 유지·신규 배포 차단(정보 확인 지연), HAL 재기동 뒤 서버·Agent 복귀 |
| 30 | 모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정 | 실패(결함) | 대상별 자식 작업 생성·되돌리기 3단계 확인 거부는 확인. 일부 실패 요약·건강 확인·drift 조정은 작업 엔진 교착(흐름 22 결함)으로 완료되지 않음 (선행 흐름 22 에 막힘) | | 30 | 모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정 | 로컬 확인 | 대상별 자식 작업·일부 실패 partial 요약(전체 성공으로 묶지 않음)·건강 확인·drift 조정·되돌리기 3단계 확인 거부 |
| 31 | 긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구 | 부분 확인 | 정상 상태 선언 거부·3단계 승인(순서/식별정보/문구 거부)·자격 1회 발급·일반 배포/범위 밖/잘못된 자격 거부·수행·폐기 뒤 거부·조정·종료 확인. 자격 만료 거부는 유효 시간(분 단위 설정) 때문에 로컬 실행에서 생략(단위 테스트 test/hal.test.mjs 로 확인) | | 31 | 긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구 | 부분 확인 | 정상 상태 선언 거부·3단계 승인(순서/식별정보/문구 거부)·자격 1회 발급·일반 배포/범위 밖/잘못된 자격 거부·수행·폐기 뒤 거부·조정·종료 확인. 자격 만료 거부는 유효 시간(분 단위 설정) 때문에 로컬 실행에서 생략(단위 테스트 test/hal.test.mjs 로 확인) |
| 32 | Source 옵션 만료 후 30일 read-only, clone·export와 삭제 완료 | 부분 확인 | 활성화·export·해제→읽기 전용(삭제 예정일)까지 확인. 30일 뒤 삭제 완료는 시간 경과가 필요(단위 테스트 test/sites.test.mjs 에서 sweep 로 확인) | | 32 | Source 옵션 만료 후 30일 read-only, clone·export와 삭제 완료 | 부분 확인 | 활성화·export·해제→읽기 전용(삭제 예정일)까지 확인. 30일 뒤 삭제 완료는 시간 경과가 필요(단위 테스트 test/sites.test.mjs 에서 sweep 로 확인) |
| 33 | 메일 서비스의 사이트 대표→독립 전환 후 항목 유지 | 부분 확인 | 전환 점검표 8항목과 미충족 시 차단 확인. 전후 실제 송수신 확인·DNS 유지 검증은 실 DNS/AriMail 필요(단위 테스트 test/mail.test.mjs 에서 가짜로 전환 완료·되돌리기 확인) | | 33 | 메일 서비스의 사이트 대표→독립 전환 후 항목 유지 | 부분 확인 | 전환 점검표 8항목과 미충족 시 차단 확인. 전후 실제 송수신 확인·DNS 유지 검증은 실 DNS/AriMail 필요(단위 테스트 test/mail.test.mjs 에서 가짜로 전환 완료·되돌리기 확인) |
@@ -55,10 +55,7 @@
## 발견한 결함(재현 정보) ## 발견한 결함(재현 정보)
- **모듈 설치가 ARIWEB_SRV_ROOT 를 무시하고 절대 경로(/srv/ariweb/<module>)에 쓰려 해 일반 계정·검증 환경에서 실패** — `hal/jobs.mjs (module.deploy.target: appDir 기본값) / agent/jobs/module.mjs` (POST /ops/api/modules/:name/deploy) 없음
- 기대: Agent 가 payload.appDir 가 없거나 루트 override 가 있으면 roots.srv 아래에 설치
- 실제: {"status":"failed","steps":["precheck:done","fanout:done","collect:failed"],"error":"1대 중 1대 실패·대기 — 대상별 결과를 확인하세요","children":["failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'"],"agentJobs":[],"timedOut":false}
- 재현: ARIWEB_SRV_ROOT 를 둔 Agent 에 module.deploy → mkdir /srv/ariweb/hal 권한 오류 · 영향 흐름 22, 30
## 실제 인프라가 필요한 항목 ## 실제 인프라가 필요한 항목
@@ -91,20 +88,20 @@
### 2. 개인 사용자 그룹 생성과 다른 사용자 초대 ### 2. 개인 사용자 그룹 생성과 다른 사용자 초대
- ✔ 팀 그룹 만들기 → 현재 그룹 전환 — g_01a17855a4277ac98f458d3057 - ✔ 팀 그룹 만들기 → 현재 그룹 전환 — g_01a1785a66113e63a08c972cc5
- ✔ 이메일 초대(7일) → 초대 메일 링크 — Y4dXy0GV5b7pvqunRITvwVbgXFNedpwd - ✔ 이메일 초대(7일) → 초대 메일 링크 — uVdQrHwu9UjZmbh1PlCVMIPsJFxbHnvo
- ✔ 초대 수락(1회용) → 별칭 user-02 · 역할 member — {"alias":"user-02","role":"member","reuse":"invite_invalid"} - ✔ 초대 수락(1회용) → 별칭 user-02 · 역할 member — {"alias":"user-02","role":"member","reuse":"invite_invalid"}
- ✔ 권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할 — {"requiredRole":["owner","admin"],"yourRole":"member"} - ✔ 권한 없는 구성원의 직접 API 접근 → 403 + 필요한 역할 — {"requiredRole":["owner","admin"],"yourRole":"member"}
- ✔ 그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음 — {"team":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0},"personal":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0}} - ✔ 그룹 전환 격리: 개인 그룹으로 바꾸면 팀 데이터가 보이지 않음 — {"team":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0},"personal":{"live":0,"purchased":0,"inUse":0,"reserved":0,"unallocated":0}}
### 3. 상품 주문, 결제 확인, 권리 활성화 ### 3. 상품 주문, 결제 확인, 권리 활성화
- ✔ 상품 목록(공개 버전만·가격·한도·미확정 정책 표시) — {"count":2,"team":{"id":"pv_01a17855a17f402b30366a2d5a","price":49000,"sites":3},"deferred":3,"paymentMethods":["manual_transfer"]} - ✔ 상품 목록(공개 버전만·가격·한도·미확정 정책 표시) — {"count":2,"team":{"id":"pv_01a1785a6358a08c3194ac03c2","price":49000,"sites":3},"deferred":3,"paymentMethods":["manual_transfer"]}
- ✔ 주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음 — {"orderNo":"AW-20261026-0001","status":"awaiting_payment","total":63800,"dueAt":"2026-11-02T13:10:41.050Z","hasInstructions":true} - ✔ 주문 제출(수동 결제) → 결제 대기·주문 번호·입금 안내 · 같은 키 재제출은 중복 없음 — {"orderNo":"AW-20261026-0001","status":"awaiting_payment","total":63800,"dueAt":"2026-11-02T13:15:52.839Z","hasInstructions":true}
- ✔ 입금 확인 전 승인 거부(409 payment_not_confirmed) — payment_not_confirmed - ✔ 입금 확인 전 승인 거부(409 payment_not_confirmed) — payment_not_confirmed
- ✔ 운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시 — {"status":"awaiting_approval","delta":["sites","web_disk","web_out","mail_storage","mail_msgs","mailboxes","mail_services","mail_storage","mail_msgs","mailboxes"]} - ✔ 운영자 입금 확인 → 승인 대기 목록에 권리 변화·현재 사용량 표시 — {"status":"awaiting_approval","delta":["sites","web_disk","web_out","mail_storage","mail_msgs","mailboxes","mail_services","mail_storage","mail_msgs","mailboxes"]}
- ✔ 운영자 승인 → 권리 활성화(재승인은 중복 없음) — {"granted":10,"already":true} - ✔ 운영자 승인 → 권리 활성화(재승인은 중복 없음) — {"granted":10,"already":true}
- ✔ 고객 결제 화면: 활성 상품·권리·다음 청구일 — {"sites":3,"subs":1,"nextBilling":"2026-11-26T13:10:41.118Z"} - ✔ 고객 결제 화면: 활성 상품·권리·다음 청구일 — {"sites":3,"subs":1,"nextBilling":"2026-11-26T13:15:52.905Z"}
```json ```json
{ {
@@ -115,25 +112,25 @@
"bill": { "bill": {
"sites": 3, "sites": 3,
"subs": 1, "subs": 1,
"nextBilling": "2026-11-26T13:10:41.118Z" "nextBilling": "2026-11-26T13:15:52.905Z"
} }
} }
``` ```
### 4. 초안 사이트 생성 ### 4. 초안 사이트 생성
- ✔ 사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내 — {"id":"s_01a17855a51f4c70a0da049568","lifecycle":"draft","projectId":"00000000000000000000000000000001","nextSteps":["build","import","domain","mode","perms"]} - ✔ 사이트 생성(새 Webmaker 프로젝트·배포 방식 관리형) → 초안·사이트 수 미소비·다음 단계 안내 — {"id":"s_01a1785a670945da067e955da4","lifecycle":"draft","projectId":"00000000000000000000000000000001","nextSteps":["build","import","domain","mode","perms"]}
- ✔ 사이트 목록 필터(초안)·사용량 요약에 예약 0 — {"drafts":1,"sitesReserved":0,"sitesAllocated":0} - ✔ 사이트 목록 필터(초안)·사용량 요약에 예약 0 — {"drafts":1,"sitesReserved":0,"sitesAllocated":0}
### 5. Webmaker에서 페이지 제작 ### 5. Webmaker에서 페이지 제작
- ✔ 제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용) — {"portalRedirect":"http://127.0.0.1:40985/api/auth/sso?ticket=…","webmaker":302,"reuse":401} - ✔ 제작 화면 열기: 포털 SSO 티켓 → Webmaker /api/auth/sso 302(1회용) — {"portalRedirect":"http://127.0.0.1:35307/api/auth/sso?ticket=…","webmaker":302,"reuse":401}
- ✔ Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과 — {"versions":1,"precheckOk":true} - ✔ Webmaker 에서 페이지 저장(저장본 2개) → 포털 저장본 목록·배포 전 확인 통과 — {"versions":1,"precheckOk":true}
```json ```json
{ {
"sso": { "sso": {
"portalRedirect": "http://127.0.0.1:40985/api/auth/sso?ticket=…", "portalRedirect": "http://127.0.0.1:35307/api/auth/sso?ticket=…",
"webmaker": 302, "webmaker": 302,
"reuse": 401 "reuse": 401
}, },
@@ -146,14 +143,14 @@
### 6. 관리형 배포 ### 6. 관리형 배포
- ✔ 배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나 — {"jobId":"j_01a17855a56c302b142ccac6a7","steps":10,"again":false,"siteCount":"reserved","server":null} - ✔ 배포 요청 → 10단계 작업·사이트 수 예약·같은 요청 재제출은 하나 — {"jobId":"j_01a1785a6757555a33d6269bd8","steps":10,"again":false,"siteCount":"reserved","server":null}
- ✔ HAL·Agent 를 거쳐 작업 완료 대기(최대 40초) — {"status":"done","steps":["accept:done","prepare_version:done","prepare_target:done","apply:done","data_check:done","start:done","internal_check:done","external_check:done","domain_https:done","done:done"]} - ✔ HAL·Agent 를 거쳐 작업 완료 대기(최대 40초) — {"status":"done","steps":["accept:done","prepare_version:done","prepare_target:done","apply:done","data_check:done","start:done","internal_check:done","external_check:done","domain_https:done","done:done"]}
- ✔ 배포 뒤 사이트 상태·버전·건강 — {"lifecycle":"needs_domain","version":"20261026131041-rev-c1-000000","verify":"needs_domain","health":"ok","agentHasSite":true} - ✔ 배포 뒤 사이트 상태·버전·건강 — {"lifecycle":"needs_domain","version":"20261026131553-rev-c1-000000","verify":"needs_domain","health":"ok","agentHasSite":true}
```json ```json
{ {
"d": { "d": {
"jobId": "j_01a17855a56c302b142ccac6a7", "jobId": "j_01a1785a6757555a33d6269bd8",
"steps": 10, "steps": 10,
"again": false, "again": false,
"siteCount": "reserved", "siteCount": "reserved",
@@ -176,7 +173,7 @@
}, },
"after": { "after": {
"lifecycle": "needs_domain", "lifecycle": "needs_domain",
"version": "20261026131041-rev-c1-000000", "version": "20261026131553-rev-c1-000000",
"verify": "needs_domain", "verify": "needs_domain",
"health": "ok", "health": "ok",
"agentHasSite": true "agentHasSite": true
@@ -186,14 +183,14 @@
### 7. 도메인 연결과 HTTPS 발급 ### 7. 도메인 연결과 HTTPS 발급
- ✔ 도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT) — {"domainId":"d_01a17855b5405b38f36942a6e1","txtName":"_ariweb-verify.aw-e2e.localhost","txtValueShape":"ariweb-verify=…","copyable":true} - ✔ 도메인 추가(웹·대표 지정) → 소유 확인 TXT 값 안내 → 작업이 외부 대기(TXT) — {"domainId":"d_01a1785a772932b8936927ad0a","txtName":"_ariweb-verify.aw-e2e.localhost","txtValueShape":"ariweb-verify=…","copyable":true}
- ✔ 도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시 — {} - ✔ 도메인 목록·상세: 소유 확인·DNS·인증서 상태 표시 — {}
- ✔ (정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다 — {"queuedProbes":0} - ✔ (정리) 도메인 연결 작업 취소 — 포털이 외부 대기 중에도 5초마다 HAL domain.probe 를 새로 만들어 큐를 채우므로(결함 항목) 뒤 흐름을 위해 멈춘다 — {"queuedProbes":0}
```json ```json
{ {
"dm": { "dm": {
"domainId": "d_01a17855b5405b38f36942a6e1", "domainId": "d_01a1785a772932b8936927ad0a",
"txtName": "_ariweb-verify.aw-e2e.localhost", "txtName": "_ariweb-verify.aw-e2e.localhost",
"txtValueShape": "ariweb-verify=…", "txtValueShape": "ariweb-verify=…",
"copyable": true "copyable": true
@@ -236,9 +233,9 @@
### 10. 사이트 대표 메일과 메일함 생성 ### 10. 사이트 대표 메일과 메일함 생성
- ✔ 사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시 — {"id":"msv_01a178563dd6f7ac39957c38a1","status":"needs_setup","tenant":true,"ariwebKind":"site_bound","limitAccounts":6,"siteCountAllocated":0} - ✔ 사이트 대표 메일 생성(사이트 연결 도메인·배정) → AriMail 테넌트 준비·도메인별 보장량·ariweb 표시 — {"id":"msv_01a1785affa395a9602c99bbb5","status":"needs_setup","tenant":true,"ariwebKind":"site_bound","limitAccounts":6,"siteCountAllocated":0}
- ✔ 같은 사이트에 두 번째 대표 메일 거부 — mail_domain_in_use - ✔ 같은 사이트에 두 번째 대표 메일 거부 — mail_domain_in_use
- ✔ 메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음 — {"id":"mb_01a178563e14cf15ea191a4de5","address":"kim@aw-e2e.localhost","passwordShownOnce":"[redacted]","mustChange":true,"imapHost":"127.0.0.1","leakedOnReload":false,"inArimail":true} - ✔ 메일함 생성: 초기 비밀번호 1회 표시·접속 안내 → 재조회에는 비밀번호 없음 — {"id":"mb_01a1785affda99dffac745d1e9","address":"kim@aw-e2e.localhost","passwordShownOnce":"[redacted]","mustChange":true,"imapHost":"127.0.0.1","leakedOnReload":false,"inArimail":true}
- ✔ 초대 방식 메일함(링크 만료·재발송·사용 완료 상태) — {"inviteState":"pending","accepted":200,"reuse":"invite_used"} - ✔ 초대 방식 메일함(링크 만료·재발송·사용 완료 상태) — {"inviteState":"pending","accepted":200,"reuse":"invite_used"}
- ✔ DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing) — {"ok":false,"missing":3,"items":["mx:missing","spf:missing","dkim:missing","dmarc:recommended"]} - ✔ DNS 확인: 미반영/잘못된 값 구분 표시(로컬 DNS 에는 레코드 없음 → missing) — {"ok":false,"missing":3,"items":["mx:missing","spf:missing","dkim:missing","dmarc:recommended"]}
- ✔ 활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등 — {"status":"awaiting_external","waitReason":"DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요","sameJob":true,"steps":["entitlement:done","dns:waiting","prepare:pending","verify:pending","activate:pending"]} - ✔ 활성화 작업 → DNS 외부 대기(awaiting_external) · 멱등 — {"status":"awaiting_external","waitReason":"DNS 레코드가 아직 반영되지 않았습니다(누락 3, 잘못된 값 0). 도메인 DNS 에 안내된 값을 넣은 뒤 '이어서 진행' 을 누르세요","sameJob":true,"steps":["entitlement:done","dns:waiting","prepare:pending","verify:pending","activate:pending"]}
@@ -247,7 +244,7 @@
```json ```json
{ {
"svc": { "svc": {
"id": "msv_01a178563dd6f7ac39957c38a1", "id": "msv_01a1785affa395a9602c99bbb5",
"status": "needs_setup", "status": "needs_setup",
"tenant": true, "tenant": true,
"ariwebKind": "site_bound", "ariwebKind": "site_bound",
@@ -255,7 +252,7 @@
"siteCountAllocated": 0 "siteCountAllocated": 0
}, },
"mb": { "mb": {
"id": "mb_01a178563e14cf15ea191a4de5", "id": "mb_01a1785affda99dffac745d1e9",
"address": "kim@aw-e2e.localhost", "address": "kim@aw-e2e.localhost",
"passwordShownOnce": "[redacted]", "passwordShownOnce": "[redacted]",
"mustChange": true, "mustChange": true,
@@ -290,13 +287,13 @@
### 12. 독립 메일 서비스 생성 ### 12. 독립 메일 서비스 생성
- ✔ 메일 전용 도메인 등록(solo-e2e.localhost, purpose mail) — {"id":"d_01a17856662c41b1d8aaf0c851","jobCancelled":null} - ✔ 메일 전용 도메인 등록(solo-e2e.localhost, purpose mail) — {"id":"d_01a1785b2be9c7ee6d0d94a95c","jobCancelled":null}
- ✔ 독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비) — {"id":"msv_01a178566634f50d5e0c8ce0c7","kind":"standalone","mailServicesAllocated":1,"sitesAllocated":0} - ✔ 독립 메일 서비스 생성(독립 옵션 소비·사이트 수 미소비) — {"id":"msv_01a1785b2bf14cefa9e1f81fd7","kind":"standalone","mailServicesAllocated":1,"sitesAllocated":0}
```json ```json
{ {
"r": { "r": {
"id": "msv_01a178566634f50d5e0c8ce0c7", "id": "msv_01a1785b2bf14cefa9e1f81fd7",
"kind": "standalone", "kind": "standalone",
"mailServicesAllocated": 1, "mailServicesAllocated": 1,
"sitesAllocated": 0 "sitesAllocated": 0
@@ -310,8 +307,8 @@
- ✔ 백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리 — {"granted":7,"orderStatus":"approved"} - ✔ 백업 옵션 주문(대상 사이트)·입금 확인·승인 → backup 권리 — {"granted":7,"orderStatus":"approved"}
- ✔ 백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서) — {"eligible":true,"reasons":[],"deferred":[],"policy":null,"status":200} - ✔ 백업 화면: 구매 가능 여부·저장 한도·일정·보존·첫 백업 기한·검증 방식(상품정책에서) — {"eligible":true,"reasons":[],"deferred":[],"policy":null,"status":200}
- ✔ 옵션 활성화(확인 필수) → 첫 백업 예정 시각 — {"status":"active","firstDue":"2026-10-27T13:11:30.755Z","nextRun":"2026-10-26T13:11:30.755Z"} - ✔ 옵션 활성화(확인 필수) → 첫 백업 예정 시각 — {"status":"active","firstDue":"2026-10-27T13:16:43.530Z","nextRun":"2026-10-26T13:16:43.530Z"}
- ✔ 지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증 — {"timedOut":false,"optionStatus":"active","lastJob":null,"siteServer":"srv_01a178559b3ef8b19bacf9e7bf","backup":{"status":"done","tier":"monthly","location":null},"job":null,"optionFailures":0} - ✔ 지금 백업 실행 → HAL backup.create(저장 서버 ≠ 운영 서버) → Agent → 검증 — {"timedOut":false,"optionStatus":"active","lastJob":null,"siteServer":"srv_01a1785a5d26219326f7ad26ec","backup":{"status":"done","tier":"monthly","location":null},"job":null,"optionFailures":0}
```json ```json
{ {
@@ -324,14 +321,14 @@
}, },
"opt": { "opt": {
"status": "active", "status": "active",
"firstDue": "2026-10-27T13:11:30.755Z", "firstDue": "2026-10-27T13:16:43.530Z",
"nextRun": "2026-10-26T13:11:30.755Z" "nextRun": "2026-10-26T13:16:43.530Z"
}, },
"run1": { "run1": {
"timedOut": false, "timedOut": false,
"optionStatus": "active", "optionStatus": "active",
"lastJob": null, "lastJob": null,
"siteServer": "srv_01a178559b3ef8b19bacf9e7bf", "siteServer": "srv_01a1785a5d26219326f7ad26ec",
"backup": { "backup": {
"status": "done", "status": "done",
"tier": "monthly", "tier": "monthly",
@@ -345,8 +342,8 @@
### 14. 백업 복원과 외부 사이트 확인 ### 14. 백업 복원과 외부 사이트 확인
- ✔ 복원 안내(포함 데이터·영향·보호 시점 유지·되돌릴 수 있는 마지막 시각·완료 방식) — {"backup":{"id":"bk_01a1785678cb538e124484669b","createdAt":"2026-10-26T13:11:30.762Z","scope":{"files":true,"db":true,"uploads":true,"config":true,"storeServerId":"srv_01a178559bee1c3a73f5640021","verifyMethod":"sha256 + 격리 환경 읽기 확인"},"sizeBytes":1909,"verifyState":"verified","expiresAt":"2027-01-2 - ✔ 복원 안내(포함 데이터·영향·보호 시점 유지·되돌릴 수 있는 마지막 시각·완료 방식) — {"backup":{"id":"bk_01a1785b465fad18eaeb8a679e","createdAt":"2026-10-26T13:16:43.538Z","scope":{"files":true,"db":true,"uploads":true,"config":true,"storeServerId":"srv_01a1785a5dd874fe86b67f0f38","verifyMethod":"sha256 + 격리 환경 읽기 확인"},"sizeBytes":1896,"verifyState":"verified","expiresAt":"2027-01-2
- ✔ 복원 요청 → 운영자 승인 대기 · 중복 요청 없음 — {"id":"rs_01a178567e7ec8dd46d83c957c","status":"awaiting_approval","dup":false,"finalConfirmation":{"group":"E2E 팀","site":"E2E 사이트","domain":null}} - ✔ 복원 요청 → 운영자 승인 대기 · 중복 요청 없음 — {"id":"rs_01a1785b4a2808cc16818090d9","status":"awaiting_approval","dup":false,"finalConfirmation":{"group":"E2E 팀","site":"E2E 사이트","domain":null}}
- ✔ 운영자 최종 확인(그룹·사이트·백업 시각) 불일치 거부 → 일치 승인(멱등) — {"bad":"final_confirmation_mismatch","ok":400} - ✔ 운영자 최종 확인(그룹·사이트·백업 시각) 불일치 거부 → 일치 승인(멱등) — {"bad":"final_confirmation_mismatch","ok":400}
- ✔ 9단계 작업 진행 → 사용자 확인 필요(보호 시점·되돌리기 가능) — {"steps":[]} - ✔ 9단계 작업 진행 → 사용자 확인 필요(보호 시점·되돌리기 가능) — {"steps":[]}
@@ -354,19 +351,19 @@
{ {
"info": { "info": {
"backup": { "backup": {
"id": "bk_01a1785678cb538e124484669b", "id": "bk_01a1785b465fad18eaeb8a679e",
"createdAt": "2026-10-26T13:11:30.762Z", "createdAt": "2026-10-26T13:16:43.538Z",
"scope": { "scope": {
"files": true, "files": true,
"db": true, "db": true,
"uploads": true, "uploads": true,
"config": true, "config": true,
"storeServerId": "srv_01a178559bee1c3a73f5640021", "storeServerId": "srv_01a1785a5dd874fe86b67f0f38",
"verifyMethod": "sha256 + 격리 환경 읽기 확인" "verifyMethod": "sha256 + 격리 환경 읽기 확인"
}, },
"sizeBytes": 1909, "sizeBytes": 1896,
"verifyState": "verified", "verifyState": "verified",
"expiresAt": "2027-01-27T13:11:35.372Z", "expiresAt": "2027-01-27T13:16:50.144Z",
"tier": "monthly" "tier": "monthly"
}, },
"included": [ "included": [
@@ -396,7 +393,7 @@
"deferred": [] "deferred": []
}, },
"rq": { "rq": {
"id": "rs_01a178567e7ec8dd46d83c957c", "id": "rs_01a1785b4a2808cc16818090d9",
"status": "awaiting_approval", "status": "awaiting_approval",
"dup": false, "dup": false,
"finalConfirmation": { "finalConfirmation": {
@@ -448,10 +445,10 @@
### 16. 외부 배포 사이트 생성과 배포 ### 16. 외부 배포 사이트 생성과 배포
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a1785769fa0becc28ee2efbc","group_id":"g_01a17855a4277ac98f458d3057","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou - ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a1785c35f8d649e375f0c391","group_id":"g_01a1785a66113e63a08c972cc5","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
- ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false} - ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false}
- ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200} - ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200}
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc","expiresAt":"2026-10-26T13:22:37.404Z","status":"pending"} - ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391","expiresAt":"2026-10-26T13:27:51.761Z","status":"pending"}
- ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"} - ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"}
- ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"} - ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"}
- ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]} - ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]}
@@ -468,8 +465,8 @@
"agreed": false "agreed": false
}, },
"tok": { "tok": {
"install": "curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc", "install": "curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391",
"expiresAt": "2026-10-26T13:22:37.404Z" "expiresAt": "2026-10-26T13:27:51.761Z"
}, },
"conn": { "conn": {
"status": "connected", "status": "connected",
@@ -557,7 +554,7 @@
- ✔ 축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시 — {"shortages":["mailboxes:1"]} - ✔ 축소 미리보기(팀→베이직): 줄여야 할 항목(메일함 6→5) 구체 표시 — {"shortages":["mailboxes:1"]}
- ✔ 축소 요청 → 보류(사용 중 권리 즉시 회수 없음) — {"fits":false,"hold":"holding","sitesStillPurchased":3} - ✔ 축소 요청 → 보류(사용 중 권리 즉시 회수 없음) — {"fits":false,"hold":"holding","sitesStillPurchased":3}
- ✔ 정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성 — {"allocate":200,"fits":true,"shortages":[],"order":"awaiting_payment"} - ✔ 정리: 메일함 배정 6→5 축소 → 재검증 적합 → 축소 주문 생성 — {"allocate":200,"fits":true,"shortages":[],"order":"awaiting_payment"}
- ✔ 축소 주문 입금 확인·승인 → 권리 교체(베이직) — {"approve":200,"sites":1,"mailboxes":5,"product":"pv_01a17855a17f31a67b1ac8d3f0"} - ✔ 축소 주문 입금 확인·승인 → 권리 교체(베이직) — {"approve":200,"sites":1,"mailboxes":5,"product":"pv_01a1785a6358a0946ad508ee8d"}
```json ```json
{ {
@@ -592,7 +589,7 @@
"approve": 200, "approve": 200,
"sites": 1, "sites": 1,
"mailboxes": 5, "mailboxes": 5,
"product": "pv_01a17855a17f31a67b1ac8d3f0" "product": "pv_01a1785a6358a0946ad508ee8d"
} }
} }
``` ```
@@ -626,10 +623,10 @@
### 20. 사이트 소유권 이전 ### 20. 사이트 소유권 이전
- ✔ 이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목) — {"sites":1,"domains":1,"siteMail":1,"needed":{"backup":1,"web_disk":1073741824,"mail_msgs":500,"mail_storage":2147483648,"mailboxes":6},"excluded":3} - ✔ 이전 범위 미리보기(사이트·도메인·대표 메일·배정·진행 작업·미납 / 제외 항목) — {"sites":1,"domains":1,"siteMail":1,"needed":{"backup":1,"web_disk":1073741824,"mail_msgs":500,"mail_storage":2147483648,"mailboxes":6},"excluded":3}
- ✔ 요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림 — {"id":"tr_01a1785788f3921de196eb902e","status":"requested","expiresAt":"2026-11-02T13:12:45.043Z","bobNotified":true} - ✔ 요청(보내는 그룹, 초안 사이트) → 받는 사용자 알림 — {"id":"tr_01a1785c58c47561320ed6f007","status":"requested","expiresAt":"2026-11-02T13:18:00.388Z","bobNotified":true}
- ✔ (정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개) — {"endBackup":200,"alloc":200} - ✔ (정리) 받는 쪽 권리에 맞게 범위 조정: 백업 옵션 종료·메일함 배정 6→5(받는 그룹 베이직 5개) — {"endBackup":200,"alloc":200}
- ✔ 받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내) — {"status":200,"tstatus":"reviewing","hold":null} - ✔ 받는 그룹 수락 → 권리 부족이면 보류(필요한 상품 안내) — {"status":200,"tstatus":"reviewing","hold":null}
- ✔ 운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각 — {"bad":"final_confirmation_mismatch","ok":200,"status":"completed","cutover":"2026-10-26T13:12:45.093Z"} - ✔ 운영자 심사(소유 증빙·도메인 확인) 미입력 거부 → 최종 확인 일치 승인 → 원자적 완료·청구 전환 시각 — {"bad":"final_confirmation_mismatch","ok":200,"status":"completed","cutover":"2026-10-26T13:18:00.432Z"}
- ✔ 완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사 — {"aliceSees":404,"bobSees":200,"bobUsageSites":0,"audit":true} - ✔ 완료 뒤: 보내는 쪽은 접근 불가(404), 받는 쪽 대시보드·사용량 일치, 불변 감사 — {"aliceSees":404,"bobSees":200,"bobUsageSites":0,"audit":true}
```json ```json
@@ -648,9 +645,9 @@
"excluded": 3 "excluded": 3
}, },
"rq": { "rq": {
"id": "tr_01a1785788f3921de196eb902e", "id": "tr_01a1785c58c47561320ed6f007",
"status": "requested", "status": "requested",
"expiresAt": "2026-11-02T13:12:45.043Z", "expiresAt": "2026-11-02T13:18:00.388Z",
"bobNotified": true "bobNotified": true
}, },
"acc": { "acc": {
@@ -662,7 +659,7 @@
"bad": "final_confirmation_mismatch", "bad": "final_confirmation_mismatch",
"ok": 200, "ok": 200,
"status": "completed", "status": "completed",
"cutover": "2026-10-26T13:12:45.093Z" "cutover": "2026-10-26T13:18:00.432Z"
}, },
"after": { "after": {
"aliceSees": 404, "aliceSees": 404,
@@ -744,7 +741,7 @@
- ✔ 릴리스 화면: 버전·main 커밋·승인 상태·HAL 실행 버전·부트스트랩(최초 Agent)·배포 순서 — {"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 - ✔ 릴리스 화면: 버전·main 커밋·승인 상태·HAL 실행 버전·부트스트랩(최초 Agent)·배포 순서 — {"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
- ✔ HAL 이 아직 0.1.2 가 아닐 때 다른 모듈 0.1.2 배포 → hal_first 거부 — {"status":"failed","error":"hal_first"} - ✔ HAL 이 아직 0.1.2 가 아닐 때 다른 모듈 0.1.2 배포 → hal_first 거부 — {"status":"failed","error":"hal_first"}
- ✔ 승인되지 않은 릴리스는 거부 / 수동 설치 잠김 표시 — {"manualInstallLocked":true,"complete":true} - ✔ 승인되지 않은 릴리스는 거부 / 수동 설치 잠김 표시 — {"manualInstallLocked":true,"complete":true}
- ✔ HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인) — {"status":"failed","steps":["precheck:done","fanout:done","collect:failed"],"error":"1대 중 1대 실패·대기 — 대상별 결과를 확인하세요","children":["failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'"],"agentJobs":[],"timedOut":false} - ✔ HAL 모듈 0.1.1 을 제어 서버에 배포(실제 Agent module.install: artifact 내려받기·sha256·releases/current·유닛(stub)·실제 버전 확인) — {"status":"done","steps":["precheck:done","fanout:done","collect:done"],"error":null,"children":["done:"],"agentJobs":[],"timedOut":false}
```json ```json
{ {
@@ -782,37 +779,26 @@
"complete": true "complete": true
}, },
"dep": { "dep": {
"status": "failed", "status": "done",
"steps": [ "steps": [
"precheck:done", "precheck:done",
"fanout:done", "fanout:done",
"collect:failed" "collect:done"
], ],
"error": "1대 중 1대 실패·대기 — 대상별 결과를 확인하세요", "error": null,
"children": [ "children": [
"failed:EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'" "done:"
], ],
"agentJobs": [], "agentJobs": [],
"timedOut": false "timedOut": false
}, },
"diag": { "diag": null
"stuckNow": [],
"halLog": [
"2026-10-26T13:10:38.150Z [hal] Ariweb HAL v0.1.1 — http://127.0.0.1:40927 (공개 http://127.0.0.1:40927, 데이터 /home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131036/hal, 부트스트랩 진행 중)",
"2026-10-26T13:12:45.488Z [jobs] module.deploy j_01a17857894f044d5d9552218a 실패: HAL 이 아직 0.1.2 으로 실행되고 있지 않습니다(현재 0.1.1). HAL 을 먼저 배포하세요",
"2026-10-26T13:12:47.496Z [jobs] module.deploy.target j_01a178578ca73b9fe8276f58e3 실패: EACCES: permission denied, mkdir '/srv/ariweb/hal/releases'",
"2026-10-26T13:12:47.996Z [jobs] module.deploy j_01a178578c7e94c12a61d85f10 실패: 1대 중 1대 실패·대기 — 대상별 결과를 확인하세요"
],
"ctlAgentLog": [
"2026-10-26T13:10:38.789Z [agent] Ariweb Node Agent v0.1.1 ag_01a178559b15aed8b0e25f47a5 (managed) → http://127.0.0.1:40927"
]
}
} }
``` ```
### 23. 실패 작업의 재시도와 되돌리기 ### 23. 실패 작업의 재시도와 되돌리기
- ✔ 배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호 — {"status":"done","canRetry":false,"jobId":"j_01a17855a56c302b142ccac6a7"} - ✔ 배포 이력: 실패/성공 기록 · 재시도 가능 여부 · 문의용 작업 번호 — {"status":"done","canRetry":false,"jobId":"j_01a1785a6757555a33d6269bd8"}
- ✔ 재시도 요청(POST /api/sites/:id/jobs/:jobId/retry) — {"status":400,"error":"job_not_retryable"} - ✔ 재시도 요청(POST /api/sites/:id/jobs/:jobId/retry) — {"status":400,"error":"job_not_retryable"}
- ✔ 되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부 — {"status":409,"error":"no_previous","message":"되돌릴 이전 버전이 없습니다"} - ✔ 되돌리기 요청(POST /api/sites/:id/rollback) — 이전 버전 없으면 거부 — {"status":409,"error":"no_previous","message":"되돌릴 이전 버전이 없습니다"}
@@ -821,7 +807,7 @@
"dep": { "dep": {
"status": "done", "status": "done",
"canRetry": false, "canRetry": false,
"jobId": "j_01a17855a56c302b142ccac6a7" "jobId": "j_01a1785a6757555a33d6269bd8"
}, },
"rt": { "rt": {
"status": 400, "status": 400,
@@ -837,16 +823,16 @@
### 24. 운영자 승인, 감사 기록, 고객 알림의 일치 ### 24. 운영자 승인, 감사 기록, 고객 알림의 일치
- ✔ 고객 알림 센터에 승인 알림 — {"title":"주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다","category":"billing","link":"/orders/o_01a17855a499a9b2c94a482c83"} - ✔ 고객 알림 센터에 승인 알림 — {"title":"주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다","category":"billing","link":"/orders/o_01a1785a6686ffeba5d2781add"}
- ✔ 감사 기록: order.approve 는 삭제 불가(immutable) — {"action":"order.approve","immutable":true} - ✔ 감사 기록: order.approve 는 삭제 불가(immutable) — {"action":"order.approve","immutable":true}
- ✔ 운영자 화면과 고객 주문 화면의 상태 일치 — {"status":"approved","approvedAt":"2026-10-26T13:10:41.118Z"} - ✔ 운영자 화면과 고객 주문 화면의 상태 일치 — {"status":"approved","approvedAt":"2026-10-26T13:15:52.905Z"}
```json ```json
{ {
"n": { "n": {
"title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다", "title": "주문 AW-20261026-0001 승인 — 권리가 활성화되었습니다",
"category": "billing", "category": "billing",
"link": "/orders/o_01a17855a499a9b2c94a482c83" "link": "/orders/o_01a1785a6686ffeba5d2781add"
}, },
"a": { "a": {
"action": "order.approve", "action": "order.approve",
@@ -854,7 +840,7 @@
}, },
"same": { "same": {
"status": "approved", "status": "approved",
"approvedAt": "2026-10-26T13:10:41.118Z" "approvedAt": "2026-10-26T13:15:52.905Z"
} }
} }
``` ```
@@ -862,7 +848,7 @@
### 25. HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련 ### 25. HAL, 로그인, 비밀정보 관리, 인증서 기반의 재해복구 훈련
- ✔ 설정 전 준비 상태(미설정 항목) — {"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":"정식 운영 준비 전(복구 훈련 또는 - ✔ 설정 전 준비 상태(미설정 항목) — {"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":"정식 운영 준비 전(복구 훈련 또는
- ✔ RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정 — {"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-20261026131036/dr-external","updated_at":"2026-10-26T13:12:53.244Z","updated_by":"op_01a178559a40f4802a91a5fb1a"} - ✔ RTO·RPO·사본 주기·보존 개수·외부 보관 위치 설정 — {"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-20261026131547/dr-external","updated_at":"2026-10-26T13:18:08.570Z","updated_by":"op_01a1785a5c27caed1a96f25d99"}
- ✔ 복구 사본 생성·검증·외부 보관(vault.key 제외) — {"verify":"ok","external":true,"sha":true} - ✔ 복구 사본 생성·검증·외부 보관(vault.key 제외) — {"verify":"ok","external":true,"sha":true}
- ✔ 복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료 — {"early":"restore_not_approved","noStop":"old_control_not_confirmed","command":true,"incomplete":"checks_incomplete","final":"completed"} - ✔ 복원 요청 → 승인 전 시작 거부 → 승인 → 기존 제어 중지 확인 없이는 거부 → 시작(명령 안내) → 확인 항목 부족 거부 → 완료 — {"early":"restore_not_approved","noStop":"old_control_not_confirmed","command":true,"incomplete":"checks_incomplete","final":"completed"}
- ✔ 복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료 — {"first":"failed","second":"success","ready":true,"measured":{}} - ✔ 복구 훈련: 항목 하나 실패 → failed, 모두 성공·측정 RTO/RPO → 준비 완료 — {"first":"failed","second":"success","ready":true,"measured":{}}
@@ -894,9 +880,9 @@
"rpo_minutes": 30, "rpo_minutes": 30,
"snapshot_interval_minutes": 60, "snapshot_interval_minutes": 60,
"retention_count": 3, "retention_count": 3,
"external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131036/dr-external", "external_store": "/home/master/work/Ariweb-suite/test/.tmp/e2e-20261026131547/dr-external",
"updated_at": "2026-10-26T13:12:53.244Z", "updated_at": "2026-10-26T13:18:08.570Z",
"updated_by": "op_01a178559a40f4802a91a5fb1a" "updated_by": "op_01a1785a5c27caed1a96f25d99"
}, },
"snap": { "snap": {
"verify": "ok", "verify": "ok",
@@ -921,15 +907,15 @@
### 26. 새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지 ### 26. 새로 고침, 재로그인, HAL 재시작 후 진행 작업 상태 유지
- ✔ 재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록 — {"mailJob":{"id":"j_01a178565252141cf5e3859e6c","status":"awaiting_external"},"halJobs":18} - ✔ 재시작 전: 포털 진행 작업(메일 활성화 외부 대기)·HAL 작업 목록 — {"mailJob":{"id":"j_01a1785b141b2aba663738d139","status":"awaiting_external"},"halJobs":18}
- ✔ 포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지 — {"sessionKept":"[redacted]","mailJob":{"id":"j_01a178565252141cf5e3859e6c","status":"awaiting_external"},"same":true} - ✔ 포털 재시작 → 재로그인 없이 세션 유지·같은 작업 id·상태 유지 — {"sessionKept":"[redacted]","mailJob":{"id":"j_01a1785b141b2aba663738d139","status":"awaiting_external"},"same":true}
- ✔ HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지 — {"opsSessionKept":"[redacted]","halJobs":18,"sameCount":true} - ✔ HAL 재시작 → 운영자 세션·작업 목록·서버 등록 유지 — {"opsSessionKept":"[redacted]","halJobs":18,"sameCount":true}
```json ```json
{ {
"before": { "before": {
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"halJobs": 18 "halJobs": 18
@@ -937,7 +923,7 @@
"pr": { "pr": {
"sessionKept": "[redacted]", "sessionKept": "[redacted]",
"mailJob": { "mailJob": {
"id": "j_01a178565252141cf5e3859e6c", "id": "j_01a1785b141b2aba663738d139",
"status": "awaiting_external" "status": "awaiting_external"
}, },
"same": true "same": true
@@ -953,7 +939,7 @@
### 27. 마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공 ### 27. 마지막 사이트 수·용량·메일함 동시 요청 시 하나만 성공
- ✔ 밥 개인 그룹: 베이직(사이트 1) 주문·승인 — {"granted":6} - ✔ 밥 개인 그룹: 베이직(사이트 1) 주문·승인 — {"granted":6}
- ✔ 사이트 2개 생성·저장본 — [{"id":"s_01a1785631d62a2fbdf27a60d6","group_id":"g_01a17855a453ba2b2c5bf88ecd","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_sta - ✔ 사이트 2개 생성·저장본 — [{"id":"s_01a1785af3ab8d0d00e0063124","group_id":"g_01a1785a663da04c2c5802e990","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_sta
- ✔ 두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족) — {"ok":1,"blocked":"precheck_failed","shortage":"block"} - ✔ 두 사이트 동시 최초 배포 요청 → 하나만 200, 다른 쪽 409 precheck_failed(sites 부족) — {"ok":1,"blocked":"precheck_failed","shortage":"block"}
- ✔ (정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환 — {"cancel":200,"cleanup":200,"unallocated":0} - ✔ (정리) 선행 결함으로 영원히 진행 중인 배포 취소·예약 반환 — {"cancel":200,"cleanup":200,"unallocated":0}
@@ -969,10 +955,10 @@
### 28. 외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리 ### 28. 외부 Linux 서버 Agent 연결·배포·재배포·제거·실패 정리
- ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a1785769fa0becc28ee2efbc","group_id":"g_01a17855a4277ac98f458d3057","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou - ✔ 외부 배포 사이트 생성(초안) + 저장본 — {"id":"s_01a1785c35f8d649e375f0c391","group_id":"g_01a1785a66113e63a08c972cc5","name":"E2E 외부 사이트","purpose":"e2e","webmaker_project_id":"00000000000000000000000000000004","template":null,"lang":"ko","timezone":"Asia/Seoul","lifecycle":"draft","deploy_mode":"external","primary_domain":null,"site_cou
- ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false} - ✔ 배포 방식 화면: 외부 대상·고객 책임·미제공 기능(메일 미지원)·건별 비용·동의 버전 — {"notProvided":"[redacted]","consentVersion":"external-deploy-2026-10-0.1.1","agreed":false}
- ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200} - ✔ 동의 전 Agent 발급 거부(consent_required) → 동의 기록(버전) — {"before":"consent_required","consent":200}
- ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:40927/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:40927 --token … --external --site s_01a1785769fa0becc28ee2efbc","expiresAt":"2026-10-26T13:22:37.404Z","status":"pending"} - ✔ 외부 Agent 일회용 토큰·설치 명령(토큰은 저장하지 않음) — {"token":"[redacted]","install":"curl -fsSL http://127.0.0.1:42511/agent/v1/install.sh | bash -s -- --hal http://127.0.0.1:42511 --token … --external --site s_01a1785c35f8d649e375f0c391","expiresAt":"2026-10-26T13:27:51.761Z","status":"pending"}
- ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"} - ✔ 고객 서버 역할의 외부 Agent 프로세스 등록·실행(HAL 등록, 사이트 귀속) — {"agentId":"[redacted]"}
- ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"} - ✔ 포털 연결 확인(외부 Agent 상태 connected) — {"status":"connected","stale":false,"halAgentId":"[redacted]"}
- ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]} - ✔ 배포 전 확인(외부: 관리형 용량 확인 없음) — {"ok":true,"blocks":[]}
@@ -1053,26 +1039,26 @@
### 30. 모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정 ### 30. 모듈 업데이트, 일부 서버 실패, 대상별 되돌리기와 상태 조정
- ✔ agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed) — {"status":"failed","errorCode":"all_failed","overall":"failed","children":["srv_missing_e2e:failed:서버가 없습니다","srv_01a178559bee1c3a73f5640021:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'","srv_01a178559b3ef8b19bacf9e7bf:failed:EACCES: permission denied, mkdir '/srv/ariweb/agen - ✔ agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed) — {"status":"failed","errorCode":"partial_failure","overall":"partial","children":["srv_missing_e2e:failed:서버가 없습니다","srv_01a1785a5d26219326f7ad26ec:done:","srv_01a1785a5dd874fe86b67f0f38:done:"],"timedOut":false}
- ✔ 모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업 — {"health":"done","drift":1,"reconcile":200,"overviewComponents":["HAL:ok","portal:unknown","webmaker:unknown","arimail:unknown","backup:ok","observability:ok"]} - ✔ 모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업 — {"health":"done","drift":0,"reconcile":200,"overviewComponents":["HAL:ok","portal:unknown","webmaker:unknown","arimail:unknown","backup:ok","observability:ok"]}
- ✔ 되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부 — {"step1":"confirm_impact_required","step2":"identifiers_mismatch"} - ✔ 되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부 — {"step1":"confirm_impact_required","step2":"identifiers_mismatch"}
```json ```json
{ {
"d": { "d": {
"status": "failed", "status": "failed",
"errorCode": "all_failed", "errorCode": "partial_failure",
"overall": "failed", "overall": "partial",
"children": [ "children": [
"srv_missing_e2e:failed:서버가 없습니다", "srv_missing_e2e:failed:서버가 없습니다",
"srv_01a178559bee1c3a73f5640021:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'", "srv_01a1785a5d26219326f7ad26ec:done:",
"srv_01a178559b3ef8b19bacf9e7bf:failed:EACCES: permission denied, mkdir '/srv/ariweb/agent/releases'" "srv_01a1785a5dd874fe86b67f0f38:done:"
], ],
"timedOut": false "timedOut": false
}, },
"h": { "h": {
"health": "done", "health": "done",
"drift": 1, "drift": 0,
"reconcile": 200, "reconcile": 200,
"overviewComponents": [ "overviewComponents": [
"HAL:ok", "HAL:ok",
@@ -1093,21 +1079,21 @@
### 31. 긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구 ### 31. 긴급 복구의 승인 부족·만료 자격·허용 범위 밖 작업 거부와 정상 복구
- ✔ HAL 정상 상태에서 선언 거부(hal_healthy) — hal_healthy - ✔ HAL 정상 상태에서 선언 거부(hal_healthy) — hal_healthy
- ✔ critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회) — {"id":"em_01a17857aa1262003c4dfd08d9","noCred":"approvals_insufficient","wrongStep":"wrong_step","badIds":"identifiers_mismatch","badText":"final_confirm_mismatch","approved":"approved","approvals":3,"credential":"[redacted]","expiresAt":"2026-10-26T13:42:53.551Z"} - ✔ critical 사건 등록 → 선언 → 승인 부족 자격 거부 → 3단계(순서·식별정보·문구) 거부/승인 → 자격 발급(1회) — {"id":"em_01a1785c79d6aa634fc6516b7b","noCred":"approvals_insufficient","wrongStep":"wrong_step","badIds":"identifiers_mismatch","badText":"final_confirm_mismatch","approved":"approved","approvals":3,"credential":"[redacted]","expiresAt":"2026-10-26T13:48:08.882Z"}
- ✔ 일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료 — {"forbidden":"action_forbidden","outOfScope":"out_of_scope","badCred":"bad_credential","performed":400,"revoked":true,"afterRevoke":"credential_revoked","reconcile":200,"closed":"closed"} - ✔ 일반 배포(module.deploy) 거부 · 범위 밖 거부 · 잘못된 자격 거부 · 허용 행동 수행 · 확인 뒤 자격 폐기 · 폐기 뒤 거부 · 조정 · 종료 — {"forbidden":"action_forbidden","outOfScope":"out_of_scope","badCred":"bad_credential","performed":400,"revoked":true,"afterRevoke":"credential_revoked","reconcile":200,"closed":"closed"}
```json ```json
{ {
"healthy": "hal_healthy", "healthy": "hal_healthy",
"sess": { "sess": {
"id": "em_01a17857aa1262003c4dfd08d9", "id": "em_01a1785c79d6aa634fc6516b7b",
"noCred": "approvals_insufficient", "noCred": "approvals_insufficient",
"wrongStep": "wrong_step", "wrongStep": "wrong_step",
"badIds": "identifiers_mismatch", "badIds": "identifiers_mismatch",
"badText": "final_confirm_mismatch", "badText": "final_confirm_mismatch",
"approved": "approved", "approved": "approved",
"approvals": 3, "approvals": 3,
"expiresAt": "2026-10-26T13:42:53.551Z" "expiresAt": "2026-10-26T13:48:08.882Z"
}, },
"perf": { "perf": {
"forbidden": "action_forbidden", "forbidden": "action_forbidden",
@@ -1127,7 +1113,7 @@
- ✔ Source 옵션 주문(대상 사이트)·승인 → source 권리 — {"granted":7} - ✔ Source 옵션 주문(대상 사이트)·승인 → source 권리 — {"granted":7}
- ✔ Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보 — {"activate":200,"status":"active","provider":"gitea","changes":1,"clone":true} - ✔ Source 옵션 활성화(권리 안에서) → 저장소·브랜치·변경 이력·clone 정보 — {"activate":200,"status":"active","provider":"gitea","changes":1,"clone":true}
- ✔ export(저장본 내려받기) — {"status":200,"type":"application/gzip"} - ✔ export(저장본 내려받기) — {"status":200,"type":"application/gzip"}
- ✔ 옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단 — {"status":"readonly","deleteAfter":"2026-11-25T13:12:44.974Z","readonlyUntil":"2026-11-25T13:12:44.974Z"} - ✔ 옵션 해제 → 30일 읽기 전용(만료일·삭제 예정일·재활성화 안내) · push 차단 — {"status":"readonly","deleteAfter":"2026-11-25T13:18:00.329Z","readonlyUntil":"2026-11-25T13:18:00.329Z"}
- ✔ 삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음 — {"sweep":200,"statusAfter":"readonly"} - ✔ 삭제 정리 작업(sweep)은 삭제 예정일 전이면 지우지 않음 — {"sweep":200,"statusAfter":"readonly"}
```json ```json
@@ -1145,8 +1131,8 @@
}, },
"un": { "un": {
"status": "readonly", "status": "readonly",
"deleteAfter": "2026-11-25T13:12:44.974Z", "deleteAfter": "2026-11-25T13:18:00.329Z",
"readonlyUntil": "2026-11-25T13:12:44.974Z" "readonlyUntil": "2026-11-25T13:18:00.329Z"
}, },
"sweep": { "sweep": {
"sweep": 200, "sweep": 200,
+3 -1
View File
@@ -28,7 +28,9 @@ export async function run({ env, report, state }) {
const d = await s.run('agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)', async () => { const r = await env.ops.post('/ops/api/modules/agent/deploy', { version: '0.1.1', serverIds: [web.serverId, bk.serverId, 'srv_missing_e2e'], nonce: 'e2e-partial' }); const j = await waitOpsJob(env, r.json.job.id, 10000); const x = await env.ops.get(`/ops/api/jobs/${r.json.job.id}`); return { status: x.json?.job?.status, errorCode: x.json?.job?.errorCode, overall: x.json?.children?.overall, children: x.json?.children?.children?.map((c) => (c.subject?.serverId || '') + ':' + c.status + ':' + (c.error || '').slice(0, 80)), timedOut: !!j?.timedOut }; }); const d = await s.run('agent 모듈을 두 서버 + 없는 서버에 배포 → 대상별 자식 작업·일부 실패를 성공으로 묶지 않음(partial/all_failed)', async () => { const r = await env.ops.post('/ops/api/modules/agent/deploy', { version: '0.1.1', serverIds: [web.serverId, bk.serverId, 'srv_missing_e2e'], nonce: 'e2e-partial' }); const j = await waitOpsJob(env, r.json.job.id, 10000); const x = await env.ops.get(`/ops/api/jobs/${r.json.job.id}`); return { status: x.json?.job?.status, errorCode: x.json?.job?.errorCode, overall: x.json?.children?.overall, children: x.json?.children?.children?.map((c) => (c.subject?.serverId || '') + ':' + c.status + ':' + (c.error || '').slice(0, 80)), timedOut: !!j?.timedOut }; });
const h = await s.run('모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업', async () => { const r = await env.ops.post('/ops/api/modules/agent/health', { serverIds: [web.serverId] }); const j = await waitOpsJob(env, r.json.job.id, 5000); const ov = await env.ops.get('/ops/api/overview'); const rc = await env.ops.post('/ops/api/modules/reconcile', { module: 'agent' }); return { health: j?.job?.status || 'timeout', drift: ov.json.drift?.length, reconcile: rc.status, overviewComponents: ov.json.components?.map((c) => c.name + ':' + c.status) }; }); const h = await s.run('모듈 건강 확인 작업·목표/실제 버전 차이(drift) 표시·다시 맞추기 작업', async () => { const r = await env.ops.post('/ops/api/modules/agent/health', { serverIds: [web.serverId] }); const j = await waitOpsJob(env, r.json.job.id, 5000); const ov = await env.ops.get('/ops/api/overview'); const rc = await env.ops.post('/ops/api/modules/reconcile', { module: 'agent' }); return { health: j?.job?.status || 'timeout', drift: ov.json.drift?.length, reconcile: rc.status, overviewComponents: ov.json.components?.map((c) => c.name + ':' + c.status) }; });
const rb = await s.run('되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부', async () => { const a = await env.ops.post('/ops/api/modules/agent/rollback', { serverIds: [web.serverId] }); const b = await env.ops.post('/ops/api/modules/agent/rollback', { serverIds: [web.serverId], confirmImpact: true, confirmIdentifiers: ['wrong'] }); return { step1: a.json?.error, step2: b.json?.error }; }); const rb = await s.run('되돌리기는 3단계 확인(영향 범위·식별정보·최종 실행) 없이는 거부', async () => { const a = await env.ops.post('/ops/api/modules/agent/rollback', { serverIds: [web.serverId] }); const b = await env.ops.post('/ops/api/modules/agent/rollback', { serverIds: [web.serverId], confirmImpact: true, confirmIdentifiers: ['wrong'] }); return { step1: a.json?.error, step2: b.json?.error }; });
report.flow(30, 'failed', { steps: s.list, note: '대상별 자식 작업 생성·되돌리기 3단계 확인 거부는 확인. 일부 실패 요약·건강 확인·drift 조정은 작업 엔진 교착(흐름 22 결함)으로 완료되지 않음', blockedBy: 22, evidence: { d, h, rb } }); // 판정: 일부 실패가 partial 로 요약되고, 건강 확인이 끝나고, 되돌리기가 3단계 확인 없이는 거부되면 로컬 확인. 아니면 결함.
const okAll = d && d.overall === 'partial' && d.status === 'failed' && h && h.health === 'done' && rb && rb.step1 === 'confirm_impact_required' && rb.step2 === 'identifiers_mismatch';
report.flow(30, okAll ? 'verified_local' : 'failed', { steps: s.list, note: okAll ? '대상별 자식 작업·일부 실패 partial 요약(전체 성공으로 묶지 않음)·건강 확인·drift 조정·되돌리기 3단계 확인 거부' : '일부 실패 요약·건강 확인·drift 조정 또는 되돌리기 확인 규칙이 기대와 다름(증거 참조)', evidence: { d, h, rb } });
} catch (e) { report.flow(30, 'partial', { steps: s.list, note: e.message }); } } } catch (e) { report.flow(30, 'partial', { steps: s.list, note: e.message }); } }
// ---- 25 재해복구 // ---- 25 재해복구
{ const s = steps(); try { { const s = steps(); try {