Files

513 lines
18 KiB
TypeScript
Raw Permalink Normal View History

2026-10-14 10:01:46 +00:00
// @vitest-environment node
import { createHash } from "node:crypto";
import { readFileSync, readdirSync } from "node:fs";
import { expect, it } from "vitest";
import { ENVIRONMENTS } from "../src/content";
2026-10-14 12:12:13 +00:00
// 과거 ASR/보존 검사는 해당 버전의 정확한 바이트를 읽는다. 새 음원의 인수로 재사용하지 않는다.
const readHistoricalBytes = (path: string) =>
readFileSync(
path.startsWith("public/ambience/")
? path.replace(
"public/ambience/",
".agent/evidence-v0.2.5/baseline/ambience/",
)
: path,
);
2026-10-14 10:01:46 +00:00
// 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다.
it("여덟 현장 녹음은 고유 PCM16 WAV·짧은 fade·완전한 provenance/notice를 가진다", () => {
const manifest = JSON.parse(
readFileSync("public/ambience/manifest.json", "utf8"),
);
2026-10-14 12:12:13 +00:00
expect(manifest.version).toBe("0.2.5");
2026-10-14 10:01:46 +00:00
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}.`);
}
});
2026-10-14 12:12:13 +00:00
it("v0.2.3 최종 ASR 증거는 정확한 WAV와 연결되며 청취·법적 인수로 주장하지 않는다", () => {
2026-10-14 10:01:46 +00:00
const directory = ".agent/evidence-v0.2.3/privacy-screening";
const screening = JSON.parse(
readFileSync(`${directory}/final-asr.json`, "utf8"),
);
const manifest = JSON.parse(
2026-10-14 12:12:13 +00:00
readFileSync(
".agent/evidence-v0.2.5/baseline/ambience/manifest.json",
"utf8",
),
2026-10-14 10:01:46 +00:00
);
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")
2026-10-14 12:12:13 +00:00
.update(
readFileSync(".agent/evidence-v0.2.5/baseline/ambience/manifest.json"),
)
2026-10-14 10:01:46 +00:00
.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<string, number> = { 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);
});
2026-10-14 12:12:13 +00:00
it("v0.2.3 역사 snapshot은 당시 101개 음성·다섯 현장음 보존을 증명한다", () => {
2026-10-14 10:01:46 +00:00
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")
2026-10-14 12:12:13 +00:00
.update(readHistoricalBytes(path))
2026-10-14 10:01:46 +00:00
.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(
2026-10-14 12:12:13 +00:00
createHash("sha256").update(readHistoricalBytes(path)).digest("hex"),
2026-10-14 10:01:46 +00:00
path,
).toBe(before);
}
});
2026-10-14 12:12:13 +00:00
it("v0.2.3 재시도는 market/cafe만 바꾸고 mall 및 실패·no-VAD 증거를 바이트 그대로 보존한다", () => {
2026-10-14 10:01:46 +00:00
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) =>
2026-10-14 12:12:13 +00:00
createHash("sha256").update(readHistoricalBytes(path)).digest("hex");
2026-10-14 10:01:46 +00:00
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]);
});
2026-10-14 12:12:13 +00:00
// 이번 릴리스는 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);
}
}
});