/* * 커맨드 감사 기록 — 이 검사가 지키는 것은 **기록이 증명력을 갖는가** 다. * * 감사 기록의 가치는 "남아 있다" 가 아니라 "믿을 수 있다" 에 있다. 클라이언트가 보낸 신원을 그대로 * 적으면 위조가 자유로워 아무것도 증명하지 못하고, 거절된 시도를 안 남기면 "무엇을 하려 했는가" 가 * 사라진다. 그 둘이 이 파일의 핵심 검사다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { buildCommandAudit, normalizeOrigin } from '../server/service/twin-audit/command-audit.ts' const AT = new Date('2026-08-06T12:00:00.000Z') test('명령 종류를 그대로 싣는다 — 무엇을 하려 했는지가 감사의 시작이다', () => { const row = buildCommandAudit({ command: { commandId: 'c1', type: 'order.hold' }, ack: { accepted: true }, at: AT }) assert.equal(row.commandType, 'order.hold') assert.equal(row.commandId, 'c1') }) test('신원은 서버가 준 것만 적는다 — 커맨드에 실려 온 신원 주장은 무시한다', () => { const row = buildCommandAudit({ /* 위조 시도: 클라이언트가 자기를 관리자라고 주장한다. */ command: { type: 'order.hold', actorId: 'admin', actorEmail: 'admin@corp', actorName: '관리자' }, ack: { accepted: true }, actor: { id: 'u-7', email: 'kim@corp', name: '김현장' }, at: AT }) assert.equal(row.actorId, 'u-7') assert.equal(row.actorEmail, 'kim@corp') assert.equal(row.actorName, '김현장') }) test('세션에 사용자가 없으면 비운다 — 모르는 것을 지어내지 않는다', () => { const row = buildCommandAudit({ command: { type: 'order.hold' }, ack: { accepted: true }, at: AT }) assert.equal(row.actorId, undefined) assert.equal(row.actorEmail, undefined) }) test('커널이 거절한 시도도 남는다 — 무엇을 하려 했는지가 감사의 절반이다', () => { const row = buildCommandAudit({ command: { type: 'equipment.repair', args: { id: 'cutter-1' } }, ack: { accepted: false, errorCode: 'not-broken' }, at: AT }) assert.equal(row.accepted, false) assert.equal(row.errorCode, 'not-broken') assert.deepEqual(row.args, { id: 'cutter-1' }) }) test('출처는 아는 값만 — 모르면 unknown 이지 사람이 한 것으로 치지 않는다', () => { assert.equal(normalizeOrigin('ai'), 'ai') assert.equal(normalizeOrigin('user'), 'user') assert.equal(normalizeOrigin('automation'), 'automation') assert.equal(normalizeOrigin(undefined), 'unknown') assert.equal(normalizeOrigin('관리자승인'), 'unknown') }) test('근거는 문자열일 때만 싣는다 — 객체를 그대로 넣어 화면이 [object Object] 를 띄우지 않게', () => { const ok = buildCommandAudit({ command: { type: 't', reason: '대기 15분 절감' }, ack: {}, at: AT }) assert.equal(ok.reason, '대기 15분 절감') const bad = buildCommandAudit({ command: { type: 't', reason: { why: 'x' } }, ack: {}, at: AT }) assert.equal(bad.reason, undefined) }) test('args 가 지나치게 크면 자르되 잘랐다고 알린다 — 알리지 않고 버리지 않는다', () => { const huge = { blob: 'x'.repeat(9000) } const row = buildCommandAudit({ command: { type: 't', args: huge }, ack: {}, at: AT }) assert.equal(row.argsTruncated, true) assert.equal(typeof row.args, 'string') assert.ok((row.args as string).length <= 4000) }) test('작은 args 는 자르지 않고 원형 그대로 — 나중에 따질 수 있어야 한다', () => { const row = buildCommandAudit({ command: { type: 't', args: { id: 'a', qty: 3 } }, ack: {}, at: AT }) assert.equal(row.argsTruncated, false) assert.deepEqual(row.args, { id: 'a', qty: 3 }) }) test('시각은 호출부가 준 서버 시각을 그대로 쓴다 — 클라이언트 시계를 믿지 않는다', () => { const row = buildCommandAudit({ command: { type: 't', eventTime: '1999-01-01T00:00:00.000Z' }, ack: {}, at: AT }) assert.equal(row.eventTime.toISOString(), AT.toISOString()) })