// @vitest-environment node import { createHash } from "node:crypto"; import { readFileSync, readdirSync } from "node:fs"; import { expect, it } from "vitest"; import { ENVIRONMENTS } from "../src/content"; // 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다. it("여덟 현장 녹음은 고유 PCM16 WAV·짧은 fade·완전한 provenance/notice를 가진다", () => { const manifest = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); expect(manifest.version).toBe("0.2.3"); expect( manifest.files.map((f: { environmentId: string }) => f.environmentId), ).toEqual(ENVIRONMENTS.map((e) => e.id)); expect(readdirSync("public/ambience").sort()).toEqual( [ ...ENVIRONMENTS.map((e) => `${e.id}.wav`), "manifest.json", "AMBIENCE_NOTICES.md", ].sort(), ); const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8"); const hashes = new Set(); for (const entry of manifest.files) { for (const key of [ "displayName", "pageUrl", "directUrl", "creator", "license", "licenseUrl", "attribution", "conversion", "sourceSha256", "sha256", "bytes", "duration", "rms", "peak", ]) expect(entry[key], key).toBeTruthy(); expect(entry.sourceSha256).toMatch(/^[a-f0-9]{64}$/); expect(entry.license).toMatch(/CC0|Public domain|CC BY(?:-SA)? 4\.0/); const section = notice .split("## ") .find((s: string) => s.startsWith(`${entry.displayName} — ${entry.environmentId}\n`), )!; expect(section).toContain(entry.attribution); for (const line of [ `Creator: ${entry.creator}`, `Source: ${entry.pageUrl}`, `Download: ${entry.directUrl}`, `License: ${entry.license} — ${entry.licenseUrl}`, `Output license: ${entry.outputLicense}`, `Changes: ${entry.conversion.operations}`, ]) expect(section).toContain(line); expect(notice).toContain(entry.licenseUrl); expect(notice).toContain(entry.pageUrl); const bytes = readFileSync(`public/ambience/${entry.file}`); const hash = createHash("sha256").update(bytes).digest("hex"); hashes.add(hash); expect(hash).toBe(entry.sha256); expect(bytes.length).toBe(entry.bytes); expect(bytes.toString("ascii", 0, 4)).toBe("RIFF"); expect(bytes.toString("ascii", 8, 12)).toBe("WAVE"); expect(bytes.readUInt16LE(20)).toBe(1); expect(bytes.readUInt16LE(22)).toBe(1); expect(bytes.readUInt32LE(24)).toBe(24000); expect(bytes.readUInt16LE(34)).toBe(16); expect(bytes.readUInt32LE(40)).toBe(bytes.length - 44); const samples = Array.from( { length: (bytes.length - 44) / 2 }, (_, i) => bytes.readInt16LE(44 + i * 2) / 32768, ); const rms = Math.sqrt( samples.reduce((sum, n) => sum + n * n, 0) / samples.length, ); const peak = samples.reduce((max, n) => Math.max(max, Math.abs(n)), 0); expect(rms).toBeCloseTo(entry.rms, 10); expect(peak).toBeCloseTo(entry.peak, 10); expect(rms).toBeGreaterThan(0.003); expect(peak).toBeLessThanOrEqual(0.85); expect(samples[0]).toBe(0); expect(samples.at(-1)).toBe(0); expect(entry.duration).toBe(samples.length / 24000); expect(entry.duration).toBeGreaterThanOrEqual(8); expect(entry.duration).toBeLessThanOrEqual(20); expect(entry.conversion.fadeSeconds).toBeGreaterThanOrEqual(0.025); expect(entry.conversion.startSeconds).toBeGreaterThanOrEqual(0); } expect(hashes.size).toBe(8); }); it("원본 계획은 임시 경로 대신 검증된 해시·바이트·직접 미디어 길이를 고정한다", () => { const plan = JSON.parse( readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"), ); const manifest = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); const evidence = JSON.parse( readFileSync(".agent/evidence-v0.2.3/source-integrity.json", "utf8"), ); for (const [index, asset] of plan.assets.entries()) { const original = evidence.find( (e: { file: string }) => e.file.split(".")[0] === asset.environmentId, ); expect.soft(asset).not.toHaveProperty("verifiedLocalSource"); expect.soft(asset).not.toHaveProperty("sourceDurationSeconds"); expect.soft(asset.expectedSourceSha256).toBe(original.sha256); expect.soft(asset.sourceBytes).toBe(original.bytes); expect .soft(asset.directMediaDurationSeconds) .toBe(Number(original.probe.format.duration)); expect.soft(asset.sourceVerification).toEqual({ status: "verified", dateUtc: "2026-10-14", method: "SHA-256, byte count, ffprobe format.duration, full FFmpeg decode", evidence: ".agent/evidence-v0.2.3/source-integrity.json", }); expect.soft(manifest.files[index]).toMatchObject(asset); expect .soft(manifest.files[index].sourceSha256) .toBe(asset.expectedSourceSha256); } }); it("선택 구간은 승인 계획의 불변 provenance이며 manifest와 고지에 동일하게 남는다", () => { const plan = JSON.parse( readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"), ); const manifest = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8"); const starts = { street: 35, mall: 36, theater: 45, department: 90, market: 24, subway: 65, cafe: 0, rain: 180, }; expect( Object.fromEntries( plan.assets.map( (asset: { environmentId: string; selectedSegmentStartSeconds: number; }) => [asset.environmentId, asset.selectedSegmentStartSeconds], ), ), ).toEqual(starts); for (const asset of plan.assets) { const start = asset.selectedSegmentStartSeconds; expect(Number.isFinite(start)).toBe(true); expect(start).toBeGreaterThanOrEqual(0); expect(start + 12).toBeLessThanOrEqual(asset.directMediaDurationSeconds); const entry = manifest.files.find( (e: { environmentId: string }) => e.environmentId === asset.environmentId, ); expect(entry.selectedSegmentStartSeconds).toBe(start); expect(entry.conversion.startSeconds).toBe(start); expect(entry.conversion.durationSeconds).toBe(12); const section = notice .split("## ") .find((s: string) => s.startsWith(`${asset.displayName} — ${asset.environmentId}\n`), ); expect(section).toContain(`source seconds ${start}–${start + 12}.`); } }); it("최종 ASR 증거는 정확한 WAV와 연결되며 청취·법적 인수로 주장하지 않는다", () => { const directory = ".agent/evidence-v0.2.3/privacy-screening"; const screening = JSON.parse( readFileSync(`${directory}/final-asr.json`, "utf8"), ); const manifest = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); expect(screening.tool).toMatchObject({ name: "faster-whisper", version: "1.2.1", }); expect(screening.model).toMatchObject({ name: "tiny", multilingual: true }); expect(screening.model.revision).toMatch(/^[a-f0-9]{40}$/); expect(screening.requiredEmptyPassed).toBe(true); expect(screening.requiredEmptyEnvironmentIds).toEqual([ "mall", "market", "cafe", ]); expect(screening.method).toMatchObject({ device: "cpu", computeType: "int8", }); expect(screening.method.parameters).toMatchObject({ task: "transcribe", language: null, beam_size: 5, best_of: 5, vad_filter: true, vad_parameters: null, condition_on_previous_text: true, }); expect(screening.method.manifestSha256).toBe( createHash("sha256") .update(readFileSync("public/ambience/manifest.json")) .digest("hex"), ); expect(screening.limitations).toMatchObject({ screeningOnly: true, humanListeningAcceptance: false, legalPrivacyClearance: false, textIsVerifiedTranscript: false, }); expect( screening.files.map((e: { environmentId: string }) => e.environmentId), ).toEqual(ENVIRONMENTS.map((e) => e.id)); for (const entry of manifest.files) { const result = screening.files.find( (e: { environmentId: string }) => e.environmentId === entry.environmentId, ); expect(result).toMatchObject({ file: `public/ambience/${entry.file}`, sha256: entry.sha256, bytes: entry.bytes, durationSeconds: entry.duration, selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds, sourceSha256: entry.sourceSha256, }); expect(result.language).toEqual(expect.any(String)); expect(result.languageProbability).toEqual(expect.any(Number)); expect(Array.isArray(result.segments)).toBe(true); if (["mall", "market", "cafe"].includes(entry.environmentId)) { const starts: Record = { mall: 36, market: 24, cafe: 0 }; expect(result.selectedSegmentStartSeconds).toBe( starts[entry.environmentId], ); expect(result.segments).toEqual([]); expect(result.text).toBe(""); expect(result.info.duration_after_vad).toBe(0); } } const provenance = JSON.parse( readFileSync(`${directory}/earlier-asr-provenance.json`, "utf8"), ); expect(provenance.textIsVerifiedTranscript).toBe(false); for (const record of provenance.records) expect( createHash("sha256") .update(readFileSync(`${directory}/${record.savedFile}`)) .digest("hex"), ).toBe(record.sha256); }); it("101개 음성·다섯 현장음 및 이전 evidence 바이트를 보존한다", () => { const baseline = JSON.parse( readFileSync( ".agent/evidence-v0.2.3/privacy-screening/baseline.json", "utf8", ), ); let voices = 0, unchangedAmbience = 0, changedAmbience = 0; for (const [path, before] of Object.entries(baseline.wavSha256)) { const actual = createHash("sha256") .update(readFileSync(path)) .digest("hex"); if (/public\/ambience\/(mall|market|cafe)\.wav$/.test(path)) { expect(actual).not.toBe(before); changedAmbience++; } else { expect(actual).toBe(before); if (path.startsWith("public/audio/")) voices++; else unchangedAmbience++; } } expect([voices, unchangedAmbience, changedAmbience]).toEqual([101, 5, 3]); for (const [path, before] of Object.entries(baseline.priorAgentFiles)) { if (baseline.mutableAgentFiles.includes(path)) continue; expect( createHash("sha256").update(readFileSync(path)).digest("hex"), path, ).toBe(before); } }); it("재시도는 market/cafe만 바꾸고 mall 및 실패·no-VAD 증거를 바이트 그대로 보존한다", () => { const directory = ".agent/evidence-v0.2.3/privacy-screening"; const baseline = JSON.parse( readFileSync(`${directory}/retry-market24-cafe0/baseline.json`, "utf8"), ); const hash = (path: string) => createHash("sha256").update(readFileSync(path)).digest("hex"); const prior = `${directory}/prior-attempt-market60-cafe180`; expect(hash(`${prior}/final-asr.json`)).toBe( "01c8a92fab9f8df7a5f778c5f7e478ce96bcedc33e11a1055bd29154a08b706b", ); const failed = JSON.parse(readFileSync(`${prior}/final-asr.json`, "utf8")); expect(failed.requiredEmptyPassed).toBe(false); expect(failed.limitations).toMatchObject({ humanListeningAcceptance: false, legalPrivacyClearance: false, }); for (const [environmentId, start, count] of [ ["mall", 36, 0], ["market", 60, 3], ["cafe", 180, 1], ]) { const result = failed.files.find( (f: { environmentId: string }) => f.environmentId === environmentId, ); expect(result.selectedSegmentStartSeconds).toBe(start); expect(result.segments).toHaveLength(count as number); } expect(hash(`${directory}/no-vad-asr.json`)).toBe( "14b42ee0b80c194caa3c0de2644f647d6d7720c60c2208374778619f47c2e969", ); for (const [path, before] of Object.entries( baseline.immutablePriorAttemptFiles, )) expect(hash(path), path).toBe(before); for (const [path, before] of Object.entries(baseline.priorAgentFiles)) { if (!baseline.mutableAgentFiles.includes(path)) expect(hash(path), path).toBe(before); } const changed: string[] = []; let voices = 0, preservedAmbience = 0; for (const [path, before] of Object.entries(baseline.wavSha256)) { if (/public\/ambience\/(market|cafe)\.wav$/.test(path)) { expect(hash(path), path).not.toBe(before); changed.push(path); } else { expect(hash(path), path).toBe(before); if (path.startsWith("public/audio/")) voices++; else preservedAmbience++; } } expect(changed.sort()).toEqual([ "public/ambience/cafe.wav", "public/ambience/market.wav", ]); expect([voices, preservedAmbience]).toEqual([101, 6]); });