// @vitest-environment node import { createHash } from "node:crypto"; import { readFileSync, readdirSync } from "node:fs"; import { expect, it } from "vitest"; import { ENVIRONMENTS } from "../src/content"; // 과거 ASR/보존 검사는 해당 버전의 정확한 바이트를 읽는다. 새 음원의 인수로 재사용하지 않는다. const readHistoricalBytes = (path: string) => readFileSync( path.startsWith("public/ambience/") ? path.replace( "public/ambience/", ".agent/evidence-v0.2.5/baseline/ambience/", ) : path, ); // 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다. it("여덟 현장 녹음은 고유 PCM16 WAV·짧은 fade·완전한 provenance/notice를 가진다", () => { const manifest = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); expect(manifest.version).toBe("0.2.5"); 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("v0.2.3 최종 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( ".agent/evidence-v0.2.5/baseline/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(".agent/evidence-v0.2.5/baseline/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("v0.2.3 역사 snapshot은 당시 101개 음성·다섯 현장음 보존을 증명한다", () => { 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(readHistoricalBytes(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(readHistoricalBytes(path)).digest("hex"), path, ).toBe(before); } }); it("v0.2.3 재시도는 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(readHistoricalBytes(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]); }); // 이번 릴리스는 8 배경을 모두 바꾸되 원본·선택 구간·권리와 101 voice/660 역사 파일을 보존한다. it("v0.2.5는 이전 증거·음성 바이트와 모든 출처 필드를 그대로 보존한다", () => { const baseline = JSON.parse( readFileSync(".agent/evidence-v0.2.5/baseline.json", "utf8"), ); for (const [path, expected] of Object.entries({ ...baseline.immutable, ...baseline.voiceWavs, })) expect( createHash("sha256").update(readFileSync(path)).digest("hex"), path, ).toBe(expected); const prior = JSON.parse( readFileSync( ".agent/evidence-v0.2.5/baseline/ambience/manifest.json", "utf8", ), ); const current = JSON.parse( readFileSync("public/ambience/manifest.json", "utf8"), ); for (const [index, before] of prior.files.entries()) { const after = current.files[index]; for (const key of [ "environmentId", "pageUrl", "directUrl", "creator", "license", "licenseUrl", "attribution", "expectedSourceSha256", "sourceBytes", "sourceSha256", "directMediaDurationSeconds", "sourceVerification", "selectedSegmentStartSeconds", "outputLicense", ]) expect(after[key], key).toEqual(before[key]); expect(after.sha256).not.toBe(before.sha256); } }); // 새 바이트의 ASR은 과거 승인으로 대체하지 않는다. 성공을 강제하려고 결과를 지우면 실패한다. it("거부된 loudnorm WAV의 ASR 실패와 privacy 인수 경계를 보존한다", () => { const screening = JSON.parse( readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"), ); const manifestBytes = readFileSync( ".agent/evidence-v0.2.5/review-fixes/rejected-loudnorm-bundle/manifest.json", ); const manifest = JSON.parse(manifestBytes.toString()); expect(screening.method.manifestSha256).toBe( createHash("sha256").update(manifestBytes).digest("hex"), ); expect(screening.requiredEmptyPassed).toBe(false); expect(screening.limitations).toMatchObject({ humanListeningAcceptance: false, legalPrivacyClearance: false, textIsVerifiedTranscript: false, }); for (const entry of manifest.files) { const record = screening.files.find( (r: { environmentId: string }) => r.environmentId === entry.environmentId, ); expect(record).toMatchObject({ sha256: entry.sha256, sourceSha256: entry.sourceSha256, selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds, }); expect(record.segments).toHaveLength( entry.environmentId === "market" ? 1 : 0, ); } }); // 원래 true 요구 테스트와 FAIL 로그는 release-approval/before-ambience.test.ts.txt 및 // 02-original-ASR-FAIL.log에 보존했다. 현재 검사는 실패 진단의 불변성을 강제한다. // 실제 릴리스 OR 계약은 release-approval.test.ts에서 별도로 검사한다. it("v0.2.5 exact WAV의 ASR 실패 진단은 원문 바이트·market segment를 보존한다", () => { expect( createHash("sha256") .update( readFileSync(".agent/evidence-v0.2.5/review-fixes/final-asr.json"), ) .digest("hex"), ).toBe("98acdb16bc5dce57bb3609b705b63125e5511c2d5f7b26bdde54f4b7bd9a5e49"); const screening = JSON.parse( readFileSync(".agent/evidence-v0.2.5/review-fixes/final-asr.json", "utf8"), ); const prior = JSON.parse( readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"), ); const bytes = readFileSync("public/ambience/manifest.json"); const manifest = JSON.parse(bytes.toString()); expect(screening.tool).toEqual(prior.tool); expect(screening.model).toEqual(prior.model); for (const key of [ "device", "computeType", "cpuThreads", "numWorkers", "randomSeed", "parameters", "vadModelSha256", ]) expect(screening.method[key]).toEqual(prior.method[key]); expect(screening.method.manifestSha256).toBe( createHash("sha256").update(bytes).digest("hex"), ); expect(screening.requiredEmptyEnvironmentIds).toEqual([ "mall", "market", "cafe", ]); expect(screening.requiredEmptyPassed).toBe(false); expect(screening.limitations).toMatchObject({ humanListeningAcceptance: false, legalPrivacyClearance: false, textIsVerifiedTranscript: false, }); for (const entry of manifest.files) { const row = screening.files.find( (r: { environmentId: string }) => r.environmentId === entry.environmentId, ); expect(row).toMatchObject({ sha256: entry.sha256, bytes: entry.bytes, sourceSha256: entry.sourceSha256, selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds, }); expect(row.segments).toHaveLength(entry.environmentId === "market" ? 1 : 0); if (["mall", "cafe"].includes(entry.environmentId)) { expect(row.segments).toEqual([]); expect(row.text).toBe(""); expect(row.info.duration_after_vad).toBe(0); } } });