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

350 lines
13 KiB
TypeScript
Raw Permalink 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 { execFileSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
// 실제 배포는 하지 않는다. worktree 내부 임시 폴더에 public 자산만 복사한 뒤
// 정상·누락·변조를 검사하여 빌드 검증기가 실패를 정말 감지하는지 먼저 확인한다.
describe("production 자산 검증기", () => {
it("101개 음성 포함을 확인하고 누락·변조는 실패시킨다", () => {
mkdirSync(".cache", { recursive: true });
const temporary = mkdtempSync(".cache/build-test-");
const verify = () =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from './scripts/check-build.mjs'; console.log(verifyBuild(${JSON.stringify(temporary)}));`,
],
{ encoding: "utf8", stdio: "pipe" },
);
try {
cpSync("public", temporary, { recursive: true });
expect(verify().trim()).toBe("101");
writeFileSync(join(temporary, "audio/intro.wav"), "broken");
expect(verify).toThrow();
rmSync(join(temporary, "audio/intro.wav"));
expect(verify).toThrow();
} finally {
rmSync(temporary, { recursive: true, force: true });
}
});
it.each(["누락", "축약"])(
"dist의 런타임 notice %s을 실패시킨다",
(damage) => {
mkdirSync(".cache", { recursive: true });
const temporary = mkdtempSync(".cache/notice-test-");
const verify = () =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from './scripts/check-build.mjs'; console.log(verifyBuild(${JSON.stringify(temporary)}));`,
],
{ encoding: "utf8", stdio: "pipe" },
);
try {
cpSync("public", temporary, { recursive: true });
expect(verify().trim()).toBe("101");
const notice = join(temporary, "THIRD_PARTY_NOTICES.md");
if (damage === "누락") rmSync(notice, { force: true });
else
writeFileSync(notice, "React / ReactDOM / scheduler — MIT License");
expect(verify).toThrow(/notice/i);
} finally {
rmSync(temporary, { recursive: true, force: true });
}
},
);
});
// 새 배경 경로는 음성과 독립적이므로 실제 dist 사본의 누락/변조/과잉 번들을 검증한다.
it.each(["wav", "manifest", "notice", "extra"])(
"현장음 %s 손상은 빌드 실패로 처리한다",
(damage) => {
const temporary = mkdtempSync(".cache/ambience-build-");
const verify = () =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from './scripts/check-build.mjs'; verifyBuild(${JSON.stringify(temporary)});`,
],
{ encoding: "utf8", stdio: "pipe" },
);
try {
cpSync("public", temporary, { recursive: true });
expect(verify).not.toThrow();
if (damage === "wav")
writeFileSync(join(temporary, "ambience/street.wav"), "bad");
if (damage === "manifest")
writeFileSync(join(temporary, "ambience/manifest.json"), "{}");
if (damage === "notice")
rmSync(join(temporary, "ambience/AMBIENCE_NOTICES.md"));
if (damage === "extra")
writeFileSync(
join(temporary, "ambience/original.ogg"),
"should not ship",
);
expect(verify).toThrow(/현장음/);
} finally {
rmSync(temporary, { recursive: true, force: true });
}
},
);
// public/manifest와 dist를 함께 훼손해도 독립적인 콘텐츠 목록과 개수가 막아야 한다.
it.each(["empty", "truncated", "duplicate", "renamed", "extra", "missing"])(
"음성 목록과 폴더를 함께 바꾼 %s 배포를 거부한다",
(damage) => {
mkdirSync(".cache", { recursive: true });
const temporary = mkdtempSync(".cache/voice-inventory-");
try {
cpSync("public", join(temporary, "public"), { recursive: true });
cpSync("public", join(temporary, "dist"), { recursive: true });
for (const file of ["THIRD_PARTY_NOTICES.md", "package-lock.json"])
cpSync(file, join(temporary, file));
mkdirSync(join(temporary, ".agent"));
cpSync(
".agent/AMBIENCE-SOURCES-v0.2.3.json",
join(temporary, ".agent/AMBIENCE-SOURCES-v0.2.3.json"),
);
symlinkSync(resolve("node_modules"), join(temporary, "node_modules"));
const manifest = JSON.parse(
readFileSync("public/audio/manifest.json", "utf8"),
);
const first = manifest.files[0];
for (const directory of ["public", "dist"]) {
const path = join(temporary, directory, "audio");
if (damage === "empty" || damage === "truncated") {
const removed = damage === "empty" ? manifest.files : [first];
for (const entry of removed) rmSync(join(path, entry.file));
}
if (damage === "renamed") {
cpSync(join(path, first.file), join(path, "unexpected.wav"));
rmSync(join(path, first.file));
}
}
if (damage === "empty") manifest.files = [];
if (damage === "truncated") manifest.files.shift();
if (damage === "duplicate") manifest.files[0] = manifest.files[1];
if (damage === "renamed")
manifest.files[0] = { ...first, file: "unexpected.wav" };
if (damage === "extra")
cpSync(
join(temporary, "dist/audio", first.file),
join(temporary, "dist/audio/extra.wav"),
);
if (damage === "missing")
rmSync(join(temporary, "dist/audio", first.file));
for (const directory of ["public", "dist"])
writeFileSync(
join(temporary, directory, "audio/manifest.json"),
JSON.stringify(manifest),
);
expect(() =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from ${JSON.stringify(resolve("scripts/check-build.mjs"))}; verifyBuild("dist");`,
],
{ cwd: temporary, stdio: "pipe" },
),
).toThrow();
} finally {
rmSync(temporary, { recursive: true, force: true });
}
},
);
// 사본 비교만으로는 양쪽 고지가 함께 축약되는 것을 감지할 수 없다.
it.each([
"street",
"mall",
"theater",
"department",
"market",
"subway",
"cafe",
"rain",
])(
"%s 고지는 각 항목에 저작자·원문·다운로드·라이선스·파생권리·변경을 모두 포함한다",
(id) => {
mkdirSync(".cache", { recursive: true });
const temporary = mkdtempSync(".cache/notice-fields-");
try {
for (const dir of ["public", "dist"])
cpSync("public", join(temporary, dir), { recursive: true });
for (const file of ["THIRD_PARTY_NOTICES.md", "package-lock.json"])
cpSync(file, join(temporary, file));
mkdirSync(join(temporary, ".agent"));
cpSync(
".agent/AMBIENCE-SOURCES-v0.2.3.json",
join(temporary, ".agent/AMBIENCE-SOURCES-v0.2.3.json"),
);
symlinkSync(resolve("node_modules"), join(temporary, "node_modules"));
const noticePath = existsSync("public/ambience/AMBIENCE_NOTICES.md")
? "ambience/AMBIENCE_NOTICES.md"
: "AMBIENCE_NOTICES.md";
const manifest = JSON.parse(
readFileSync("public/ambience/manifest.json", "utf8"),
);
// 완전한 기준 고지에서 한 항목의 필드만 지운다. 다른 항목에 같은 URL이 남아도 거부해야 한다.
let complete = readFileSync(join("public", noticePath), "utf8");
for (const entry of manifest.files) {
if (!complete.includes(`Creator: ${entry.creator}\n`))
complete = complete.replace(
`Source: ${entry.pageUrl}`,
`Creator: ${entry.creator}\nOutput license: ${entry.outputLicense}\nSource: ${entry.pageUrl}`,
);
}
const writeNotice = (value: string) => {
for (const dir of ["public", "dist"])
writeFileSync(join(temporary, dir, noticePath), value);
};
const verify = () =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from ${JSON.stringify(resolve("scripts/check-build.mjs"))}; verifyBuild("dist");`,
],
{ cwd: temporary, stdio: "pipe" },
);
writeNotice(complete);
expect(verify).not.toThrow();
const entry = manifest.files.find(
(e: { environmentId: string }) => e.environmentId === id,
);
const sections = complete.split("## ");
const index = sections.findIndex((s) =>
s.startsWith(`${entry.displayName} — ${id}\n`),
);
for (const field of [
"Creator:",
"Source:",
"Download:",
"License:",
"Output license:",
"Changes:",
]) {
const modified = [...sections];
modified[index] = modified[index]!.split("\n")
.filter((line) => !line.startsWith(field))
.join("\n");
writeNotice(modified.join("## "));
expect.soft(verify, `${id}: ${field}`).toThrow();
}
} finally {
rmSync(temporary, { recursive: true, force: true });
}
},
);
it.each([
"sourceSha256",
"expectedSourceSha256",
"sourceBytes",
"directMediaDurationSeconds",
"selectedSegmentStartSeconds",
"startSeconds",
"durationSeconds",
"outputLicense",
"operations",
"targetIntegratedLUFS",
"truePeakCeilingDBTP",
"finalLoudness",
"scalarFilter",
"measurementFilter",
"internalTruePeakCeilingDBTP",
"gainDB",
"iterations",
"version",
])(
"현장음 manifest 사본을 함께 바꿔도 승인 %s 계약 위반을 거부한다",
(field) => {
const temporary = mkdtempSync(".cache/ambience-plan-");
try {
for (const dir of ["public", "dist"])
cpSync("public", join(temporary, dir), { recursive: true });
for (const file of ["THIRD_PARTY_NOTICES.md", "package-lock.json"])
cpSync(file, join(temporary, file));
mkdirSync(join(temporary, ".agent"));
cpSync(
".agent/AMBIENCE-SOURCES-v0.2.3.json",
join(temporary, ".agent/AMBIENCE-SOURCES-v0.2.3.json"),
);
symlinkSync(resolve("node_modules"), join(temporary, "node_modules"));
const verify = () =>
execFileSync(
"node",
[
"--input-type=module",
"-e",
`import {verifyBuild} from ${JSON.stringify(resolve("scripts/check-build.mjs"))}; verifyBuild("dist");`,
],
{ cwd: temporary, stdio: "pipe" },
);
expect(verify).not.toThrow();
const manifest = JSON.parse(
readFileSync("public/ambience/manifest.json", "utf8"),
);
if (field === "targetIntegratedLUFS" || field === "truePeakCeilingDBTP")
manifest.normalization[field] = -1;
else if (field === "finalLoudness")
manifest.files[0].conversion.loudness.final.integratedLUFS = -12;
else if (field === "scalarFilter")
manifest.files[0].conversion.loudness.iterations[0].filter =
"loudnorm=I=-32.5";
else if (field === "measurementFilter")
manifest.normalization.measurementFilter = "loudnorm";
else if (field === "internalTruePeakCeilingDBTP")
manifest.normalization.internalTruePeakCeilingDBTP = -6;
else if (field === "gainDB")
manifest.files[0].conversion.loudness.iterations[0].gainDB += 1;
else if (field === "iterations")
manifest.files[0].conversion.loudness.iterations = [];
else if (field === "version") manifest.version = "0.2.3";
else if (field === "operations")
manifest.files[0].conversion.operations = "";
else if (field === "startSeconds" || field === "durationSeconds") {
const entry = manifest.files[0];
const previous = `source seconds ${entry.conversion.startSeconds}–${entry.conversion.startSeconds + entry.conversion.durationSeconds}.`;
entry.conversion[field] += 1;
const updated = `source seconds ${entry.conversion.startSeconds}–${entry.conversion.startSeconds + entry.conversion.durationSeconds}.`;
for (const dir of ["public", "dist"]) {
const path = join(temporary, dir, "ambience/AMBIENCE_NOTICES.md");
writeFileSync(
path,
readFileSync(path, "utf8").replace(previous, updated),
);
}
} else manifest.files[0][field] = "invalid";
for (const dir of ["public", "dist"])
writeFileSync(
join(temporary, dir, "ambience/manifest.json"),
JSON.stringify(manifest),
);
expect(verify).toThrow();
} finally {
rmSync(temporary, { recursive: true, force: true });
}
},
);