Files

54 lines
5.2 KiB
JavaScript

/**
* portal/lifecycle/testing/fakehal.mjs — 테스트용 가짜 HAL 어댑터(docs/HAL-JOBS.md 계약과 같은 모양).
* 작업은 기본적으로 즉시 완료되며 종류별 결과를 돌려준다. behave[kind] 로 결과·오류를 바꾸고, gate 가 켜진 site.relocate 는 전환 직전에 awaiting_external 로 멈춘다(resumeJob 으로 진행).
*/
import { newId } from '../../../lib/crypto.mjs';
export function createFakeHal({ servers = null } = {}) {
const jobs = new Map(); const calls = [];
const fake = {
servers: servers || [
{ id: 'srv-a', name: 'web-1', role: 'web', location: 'seoul', status: 'ready', acceptsNew: true, health: 'ok', lastSeenAt: new Date().toISOString(), capacity: { diskFreeBytes: 50e9, diskTotalBytes: 100e9, sitesHosted: 3 } },
{ id: 'srv-b', name: 'backup-1', role: 'backup', location: 'seoul', status: 'ready', acceptsNew: true, health: 'ok', lastSeenAt: new Date().toISOString(), capacity: { diskFreeBytes: 500e9, diskTotalBytes: 1e12 } },
{ id: 'srv-c', name: 'web-2', role: 'web', location: 'busan', status: 'ready', acceptsNew: true, health: 'ok', lastSeenAt: new Date().toISOString(), capacity: { diskFreeBytes: 80e9, diskTotalBytes: 100e9, sitesHosted: 1 } },
],
behave: {}, calls, jobs, unreachable: false, gateRelocate: false,
async createJob(req) {
if (fake.unreachable) { const e = new Error('unreachable'); e.code = 'service_unreachable'; throw e; }
calls.push(req);
const ex = req.idempotencyKey && [...jobs.values()].find((j) => j.idempotencyKey === req.idempotencyKey); if (ex) return { halJobId: ex.id, status: ex.status };
const id = newId('hj_'); const job = { id, kind: req.kind, target: req.target, payload: req.payload, idempotencyKey: req.idempotencyKey, status: 'done', steps: [], progress: 100, result: null, error: null, updatedAt: new Date().toISOString() };
try {
const b = fake.behave[req.kind]; const r = b ? await b(req, job) : defaultResult(req);
if (r && r.__pending) { job.status = r.status || 'running'; job.result = null; } else job.result = r;
if (req.kind === 'site.relocate' && fake.gateRelocate && !(r && r.__pending)) { job.status = 'awaiting_external'; job.steps = ['precheck', 'prepare', 'precopy', 'verify_copy', 'final_sync', 'verify_new'].map((s) => ({ id: s, state: 'done' })).concat([{ id: 'switch', state: 'waiting', detail: '전환 대기' }]); job.pendingResult = r; job.result = null; }
} catch (e) { job.status = 'failed'; job.error = e.message; job.errorCode = e.code || 'failed'; }
jobs.set(id, job); return { halJobId: id, status: job.status };
},
async getJob(id) { if (fake.unreachable) { const e = new Error('unreachable'); e.code = 'service_unreachable'; throw e; } const j = jobs.get(id); if (!j) throw Object.assign(new Error('없음'), { code: 'not_found' }); return j; },
async resumeJob(id) { const j = jobs.get(id); if (j && j.status === 'awaiting_external') { j.status = 'done'; j.result = j.pendingResult; j.steps.forEach((s) => (s.state = 'done')); } return j; },
async cancelJob(id) { const j = jobs.get(id); if (j) { j.status = 'cancelled'; } return j; },
async retryJob(id) { return jobs.get(id); }, async revertJob(id) { const j = jobs.get(id); if (j) j.status = 'reverted'; return j; },
async listServers({ role } = {}) { if (fake.unreachable) { const e = new Error('unreachable'); e.code = 'service_unreachable'; throw e; } return fake.servers.filter((s) => !role || s.role === role); },
async pickServer({ role = 'web' } = {}) { const s = fake.servers.find((x) => x.role === role && x.acceptsNew); return s ? { serverId: s.id } : null; },
async health() { return { ok: !fake.unreachable, version: 'fake' }; },
/** 테스트: 진행 중이던 이전 작업을 전환 직전 멈춤 상태로 옮긴다 */
reachGate(id, result) { const j = jobs.get(id); j.status = 'awaiting_external'; j.pendingResult = result || defaultResult({ kind: 'site.relocate', payload: j.payload }); j.steps = ['precheck', 'prepare', 'precopy', 'verify_copy', 'final_sync', 'verify_new'].map((s) => ({ id: s, state: 'done' })).concat([{ id: 'switch', state: 'waiting', detail: '전환 대기' }]); },
complete(id, result) { const j = jobs.get(id); j.status = 'done'; j.result = result; },
};
return fake;
}
function defaultResult(req) {
const p = req.payload || {};
switch (req.kind) {
case 'backup.create': return { backupId: newId('hb_'), location: `${p.storeServerId || req.target}:/srv/ariweb/backups/${p.siteId}/${Date.now()}.tar.gz`, sizeBytes: 1000, checksum: 'sha256:fake', verified: true, createdAt: new Date().toISOString() };
case 'backup.verify': return { ok: true };
case 'backup.restore': return { protectPointLocation: 'pp', restoredVersion: 'v-restored', functionCheck: { ok: true }, externalCheck: { ok: true, url: 'https://example.test' } };
case 'backup.delete': return { deleted: true };
case 'site.check': return { checks: (p.urls || []).map((u) => ({ url: u, status: 200, ok: true, ms: 10 })) };
case 'site.relocate': return { newServerId: p.toServerId, switchedAt: new Date().toISOString(), oldRetainedUntil: null };
case 'site.remove': return { removed: true, leftovers: [] };
default: return { ok: true };
}
}