Files
Find-Word/tests/ambience.test.ts
T

513 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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<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);
});
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);
}
}
});