Files
Find-Word/tests/ambience.test.ts
T
2026-10-14 10:01:46 +00:00

355 lines
13 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";
// 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다.
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<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("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]);
});