/* * 로그에 **시각이 있다** — 그리고 어떤 인자도 버리지 않는다 (2026-08-22). * * 루프가 55초 막힌 것을 계기로 알아냈는데 그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다. * 트윈의 줄이 `console.log` 로 나가 시각이 없었기 때문이다. 이 시험이 지키는 것: * ① 형식 — 프레임워크 줄과 같은 모양, **밀리초까지**(계기의 값이 epoch ms 다) * ② 지역 시각 — 프레임워크의 다른 줄과 같은 기준(UTC 로 찍으면 나란히 읽을 수 없다) * ③ 인자를 버리지 않는다 — 오류는 스택까지(`console.error(msg, err)` 로 쓰던 자리가 많다) * ④ 트윈 소스에 `console.` 직접 호출이 남지 않는다 — 하나라도 남으면 그 줄만 시각이 없다 */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync, readdirSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { line, stamp } from '../server/engine/log.ts' test('시각은 밀리초와 오프셋까지 적는다 — 같은 초 안의 순서를 가릴 수 있게', () => { const s = stamp(new Date(2026, 7, 22, 5, 54, 12, 345)) assert.match(s, /^2026-08-22T05:54:12\.345[+-]\d\d:\d\d$/, `형식이 다르다: ${s}`) }) test('지역 시각으로 적는다 — 프레임워크의 다른 줄과 같은 기준', () => { const at = new Date(2026, 0, 2, 3, 4, 5, 6) const s = stamp(at) assert.ok(s.startsWith('2026-01-02T03:04:05.006'), `지역 시각이 아니다: ${s}`) }) test('오류는 스택까지 남는다 — 「어디서 났나」를 잃지 않게', () => { const err = new Error('불이 났다') const out = line(['[twin-engine] 저장 실패', err]) assert.ok(out.includes('[twin-engine] 저장 실패')) assert.ok(out.includes('불이 났다')) assert.ok(out.includes('log-stamp.test'), '스택이 있어야 어느 자리에서 났는지 알 수 있다') }) test('값이 없어도 있었다는 사실은 남긴다 — 알리지 않고 지우지 않는다', () => { assert.equal(line(['a', undefined, null, 0]), 'a undefined null 0') const circular: any = { name: 'ㄱ' } circular.self = circular assert.ok(line(['보고', circular]).startsWith('보고 '), '순환 참조에도 줄이 남아야 한다') }) test('트윈 소스에 console 직접 호출이 남아 있지 않다 — 한 줄이라도 남으면 그 줄만 시각이 없다', () => { const root = fileURLToPath(new URL('../server/', import.meta.url)) const offenders: string[] = [] const walk = (dir: string) => { for (const e of readdirSync(dir, { withFileTypes: true })) { const p = dir + e.name if (e.isDirectory()) walk(p + '/') else if (e.name.endsWith('.ts') && e.name !== 'log.ts') { const src = readFileSync(p, 'utf-8') for (const m of src.matchAll(/console\.(log|warn|error)\(/g)) offenders.push(`${p.slice(root.length)} console.${m[1]}`) } } } walk(root) assert.deepEqual(offenders, [], `시각 없는 로그가 남아 있다 — twinLog·twinWarn·twinError 를 쓴다:\n ${offenders.join('\n ')}`) })