feat: release Find-Word v0.2.3
This commit is contained in:
+263
-2
@@ -1,7 +1,16 @@
|
||||
// @vitest-environment node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
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 자산만 복사한 뒤
|
||||
@@ -60,3 +69,255 @@ describe("production 자산 검증기", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// 새 배경 경로는 음성과 독립적이므로 실제 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",
|
||||
])(
|
||||
"현장음 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 === "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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user