Files
tyx_AI_xhs/tests/unit/wp5-01-asset-compiler.test.ts
T
suyx f4fabb66e5
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
fix(WP4-07): 兼容迁移后的字体资源路径
2026-08-04 18:29:17 +08:00

231 lines
12 KiB
TypeScript

import { createHash } from "node:crypto";
import {
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { compileAssetArchive } from "../../packages/asset-compiler/src/index.js";
const temporaryRoots: string[] = [];
afterEach(() => {
for (const root of temporaryRoots.splice(0)) rmSync(root, { force: true, recursive: true });
});
function json(path: string, value: unknown) {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}
function csv(path: string, rows: Array<Record<string, string>>) {
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))];
const encode = (value: string) => `"${value.replaceAll('"', '""')}"`;
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, `${headers.map(encode).join(",")}\n${rows.map((row) => headers.map((header) => encode(row[header] ?? "")).join(",")).join("\n")}\n`);
}
function hash(path: string) {
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}
function filesBelow(root: string): string[] {
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
const path = join(root, entry.name);
return entry.isDirectory() ? filesBelow(path) : [path];
});
}
function snapshot(roots: string[]) {
return roots.flatMap((root) => filesBelow(root).map((path) => {
const stats = statSync(path);
return { path: `${basename(root)}/${relative(root, path).replaceAll("\\", "/")}`, sha256: hash(path), size: stats.size, mtime_ms: stats.mtimeMs };
})).sort((left, right) => left.path.localeCompare(right.path));
}
interface Fixture {
evidenceTrap: string;
handoffRoot: string;
manifestPath: string;
outputRoot: string;
root: string;
sourceRoot: string;
}
function createFixture(): Fixture {
const root = mkdtempSync(join(tmpdir(), "dada-wp5-01-"));
temporaryRoots.push(root);
const handoffRoot = join(root, "handoff");
const sourceRoot = join(root, "sources");
const outputRoot = join(root, "output");
const evidenceTrap = join(root, "evidence", "executed.txt");
mkdirSync(handoffRoot, { recursive: true });
writeFileSync(join(handoffRoot, "HANDOFF.md"), "fixture handoff\n");
json(join(handoffRoot, "validation.json"), { all_valid: true, schema_version: 1 });
const textRoot = join(sourceRoot, "text");
const fixtureFontBytes = Buffer.from("dada-test-font-package");
const fixtureFontSha256 = createHash("sha256").update(fixtureFontBytes).digest("hex").toUpperCase();
csv(join(textRoot, "flower", "catalog.csv"), [{
canonical_dir: "templates/FLOWER001", canonical_id: "FLOWER001", category: "flower", default_text: "春日,计划",
display_name: "春日,计划", family: "text_template", font_paths: "fonts/original.ztf", preview_path: "preview.png",
resource_class: "zip_template",
}]);
json(join(textRoot, "flower", "templates", "FLOWER001", "metadata.json"), {
canonical_id: "FLOWER001", category: "flower", default_text: "春日,计划", display_name: "春日,计划",
dynamic_keys: [], family: "text_template",
files: { fonts: ["fonts/original.ztf"], layer: "layer.pb", package: "package", preview: "preview.png" },
resource_class: "zip_template", runtime: { editable_text: true }, schema_version: 1,
source: { archive: join(root, "evidence", "historical") },
});
mkdirSync(join(textRoot, "flower", "templates", "FLOWER001", "fonts"), { recursive: true });
writeFileSync(join(textRoot, "flower", "templates", "FLOWER001", "fonts", "original.ztf"), fixtureFontBytes);
writeFileSync(join(textRoot, "unreferenced-source.bin"), Buffer.alloc(128 * 1024, 7));
const fontRoot = join(sourceRoot, "fonts");
const fontDirectory = join(fontRoot, "resources", "font_packages", "FONT001_Test");
csv(join(fontRoot, "reports", "font_panel_catalog.csv"), [{
candidate_id: "FONT001", display_name: "Test Font", font_family: "Dada Test", local_sha256: fixtureFontSha256,
panel_order: "1", resource_dir: fontDirectory, resource_status: "verified_extracted",
}]);
json(join(fontDirectory, "metadata.json"), {
candidate_id: "FONT001", display_name: "Test Font", font_family: "Dada Test", font_file_count: 1,
local_sha256: fixtureFontSha256, panel_order: 1, resource_status: "verified_extracted",
});
const colorRoot = join(sourceRoot, "colors");
csv(join(colorRoot, "catalog.csv"), [{
canonical_dir: "styles/COLOR001", canonical_id: "COLOR001", category: "color", default_text: "",
display_name: "style_01", family: "color_card", preview_path: "preview.png", resource_class: "parameter_renderer",
}]);
json(join(colorRoot, "styles", "COLOR001", "metadata.json"), {
canonical_id: "COLOR001", category: "color", default_text: "", display_name: "style_01", dynamic_keys: [],
family: "color_card", files: { preview: "preview.png", renderer_source: "renderer_source.py" },
resource_class: "parameter_renderer", runtime: { stable_style_id: "style_01" }, schema_version: 1,
});
const dynamicRoot = join(sourceRoot, "dynamic");
csv(join(dynamicRoot, "catalog.csv"), [{
canonical_dir: "templates/DYN001", canonical_id: "DYN001", category: "location", default_text: "",
display_name: "location", dynamic_keys: "title", family: "interactive_sticker", resource_class: "dynamic_resource",
}]);
const dynamicDirectory = join(dynamicRoot, "templates", "DYN001");
json(join(dynamicDirectory, "metadata.json"), {
canonical_id: "DYN001", category: "location", default_text: "", display_name: "location", dynamic_keys: ["title"],
family: "interactive_sticker", files: { lua: ["resource/trap.lua"], prefab: ["resource/trap.prefab"] },
resource_class: "dynamic_resource", runtime: { has_dynamic_binding: true }, schema_version: 1,
});
mkdirSync(join(dynamicDirectory, "resource"), { recursive: true });
writeFileSync(join(dynamicDirectory, "resource", "trap.lua"), `io.open([[${evidenceTrap}]], "w")`);
writeFileSync(join(dynamicDirectory, "resource", "trap.prefab"), "must remain conversion evidence only");
const manifestPath = join(handoffRoot, "sticker_web_catalog_manifest.json");
json(manifestPath, {
collections: [
{ catalogs: { flower: relative(textRoot, join(textRoot, "flower", "catalog.csv")) }, id: "text_templates", root: relative(handoffRoot, textRoot) },
{ catalog: relative(fontRoot, join(fontRoot, "reports", "font_panel_catalog.csv")), id: "font_panel", root: relative(handoffRoot, fontRoot) },
{ catalog: "catalog.csv", id: "color_cards", root: relative(handoffRoot, colorRoot) },
{ catalog: "catalog.csv", id: "interactive_stickers", root: relative(handoffRoot, dynamicRoot) },
],
schema_version: 1,
validation: "validation.json",
web_handoff: "HANDOFF.md",
});
return { evidenceTrap, handoffRoot, manifestPath, outputRoot, root, sourceRoot };
}
describe("TDD-WP5-MAN-001 readonly asset compiler", () => {
it("produces declarative, redacted, content-addressed output without changing or copying source", () => {
const fixture = createFixture();
const before = snapshot([fixture.handoffRoot, fixture.sourceRoot]);
const result = compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
const after = snapshot([fixture.handoffRoot, fixture.sourceRoot]);
expect(after).toEqual(before);
expect(existsSync(fixture.evidenceTrap)).toBe(false);
expect(result.report).toMatchObject({ copied_source_files: 0, executed_source_files: 0, source_mutations: 0 });
const manifestText = readFileSync(join(fixture.outputRoot, "output-manifest.json"), "utf8");
const outputManifest = JSON.parse(manifestText) as { items: Array<Record<string, unknown>>; schema_version: string };
expect(outputManifest.schema_version).toBe("asset-release-compiler/v1");
expect(outputManifest.items.map((item) => item.canonical_id)).toEqual(["COLOR001", "DYN001", "FLOWER001", "FONT001"]);
expect(outputManifest.items.every((item) => item.release_status === "imported" && item.validation_status === "metadata_validated")).toBe(true);
const allOutput = filesBelow(fixture.outputRoot);
expect(allOutput.every((path) => path.endsWith(".json"))).toBe(true);
expect(allOutput.some((path) => path.endsWith(".lua") || path.endsWith(".prefab") || path.endsWith(".bin"))).toBe(false);
const serializedOutput = allOutput.map((path) => readFileSync(path, "utf8")).join("\n");
expect(serializedOutput).not.toContain(resolve(fixture.root));
expect(serializedOutput).not.toContain("file://");
expect(serializedOutput).not.toContain("historical");
expect(serializedOutput).not.toContain("unreferenced-source.bin");
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_ASSET_COMPILER;
if (evidenceDirectory) {
mkdirSync(evidenceDirectory, { recursive: true });
for (const file of ["compiler-report.json", "source-before.json", "source-after.json", "output-manifest.json"]) {
copyFileSync(join(fixture.outputRoot, file), join(evidenceDirectory, file));
}
}
});
it("is reproducible and reuses existing derived JSON by content hash", () => {
const fixture = createFixture();
const secondOutput = join(fixture.root, "second-output");
compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: secondOutput, releaseVersion: "fixture-v1" });
expect(readFileSync(join(fixture.outputRoot, "output-manifest.json"), "utf8")).toBe(readFileSync(join(secondOutput, "output-manifest.json"), "utf8"));
const repeated = compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" });
expect(repeated.report.derived_files).toEqual({ created: 0, reused: 4, total: 4 });
});
it("safely relocates legacy absolute font package paths after an archive move", () => {
const fixture = createFixture();
const catalogPath = join(fixture.sourceRoot, "fonts", "reports", "font_panel_catalog.csv");
const metadata = JSON.parse(readFileSync(join(fixture.sourceRoot, "fonts", "resources", "font_packages", "FONT001_Test", "metadata.json"), "utf8")) as { local_sha256: string };
csv(catalogPath, [{
candidate_id: "FONT001",
display_name: "Test Font",
font_family: "Dada Test",
local_sha256: metadata.local_sha256,
panel_order: "1",
resource_dir: "C:/Users/legacy/Desktop/sticker_text/fonts/resources/font_packages/FONT001_Test",
resource_status: "verified_extracted",
}]);
expect(() => compileAssetArchive({
manifestPath: fixture.manifestPath,
outputDirectory: fixture.outputRoot,
releaseVersion: "fixture-v1",
})).not.toThrow();
});
it("rejects evidence collections, traversal and output inside a source root", () => {
const fixture = createFixture();
const manifest = JSON.parse(readFileSync(fixture.manifestPath, "utf8")) as { collections: unknown[] };
manifest.collections.push({ catalog: "../evidence/catalog.csv", id: "evidence", root: "../evidence" });
json(fixture.manifestPath, manifest);
expect(() => compileAssetArchive({ manifestPath: fixture.manifestPath, outputDirectory: fixture.outputRoot, releaseVersion: "fixture-v1" })).toThrow(/unsupported collection/i);
const clean = createFixture();
const catalogPath = join(clean.sourceRoot, "dynamic", "catalog.csv");
const row = { canonical_dir: "../../evidence", canonical_id: "DYN001", category: "location", default_text: "", display_name: "location", dynamic_keys: "title", family: "interactive_sticker", resource_class: "dynamic_resource" };
csv(catalogPath, [row]);
expect(() => compileAssetArchive({ manifestPath: clean.manifestPath, outputDirectory: clean.outputRoot, releaseVersion: "fixture-v1" })).toThrow(/unsafe relative path|outside.*source root/i);
const nested = createFixture();
expect(() => compileAssetArchive({ manifestPath: nested.manifestPath, outputDirectory: join(nested.sourceRoot, "text", "derived"), releaseVersion: "fixture-v1" })).toThrow(/output.*source root/i);
});
});