fix(POSTV1-06): 恢复贴纸与字体运行时资源链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { loadConfiguredRuntimeAssets } from "../../apps/api/src/runtime-assets.js";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
if (resolve(directory).startsWith(resolve(tmpdir()))) rmSync(directory, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
function fixture() {
|
||||
const base = mkdtempSync(join(tmpdir(), "dada-postv1-assets-"));
|
||||
temporaryDirectories.push(base);
|
||||
const assetRoot = join(base, "assets");
|
||||
const dataRoot = join(base, "data");
|
||||
const configFile = join(base, "instance.json");
|
||||
const trustedManifestPath = join(base, "trusted-manifest.json");
|
||||
const bytes = Buffer.from("synthetic sticker bytes");
|
||||
const entry = {
|
||||
assetId: "STK001",
|
||||
mimeType: "image/png",
|
||||
relativePath: "p0a-static-v1/STK001.png",
|
||||
resourceVersion: "p0a-static-v1",
|
||||
rootRef: "p0a_runtime_assets",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
const manifest = {
|
||||
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 },
|
||||
entries: [entry],
|
||||
root_ref: "p0a_runtime_assets",
|
||||
schema_version: "DadaRuntimeAssets/v1",
|
||||
source: "external_read_only",
|
||||
};
|
||||
mkdirSync(join(assetRoot, "p0a-static-v1"), { recursive: true });
|
||||
writeFileSync(join(assetRoot, entry.relativePath), bytes);
|
||||
writeFileSync(join(assetRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
writeFileSync(trustedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
writeFileSync(configFile, JSON.stringify({ asset_root: assetRoot }));
|
||||
return { assetRoot, configFile, dataRoot, trustedManifestPath };
|
||||
}
|
||||
|
||||
describe("POSTV1-06 portable runtime assets", () => {
|
||||
it("activates a validated external asset root without exposing its path", () => {
|
||||
const input = fixture();
|
||||
const loaded = loadConfiguredRuntimeAssets(input);
|
||||
|
||||
expect(loaded.state).toMatchObject({ configured: true, pause_reason: null, status: "active" });
|
||||
expect(loaded.publicAssets?.read("p0a-static-v1", "STK001")?.bytes.toString()).toBe("synthetic sticker bytes");
|
||||
expect(JSON.stringify(loaded.state)).not.toContain(input.assetRoot);
|
||||
});
|
||||
|
||||
it("rejects a changed external manifest and leaves unrelated API features available", () => {
|
||||
const input = fixture();
|
||||
writeFileSync(join(input.assetRoot, "manifest.json"), "{}\n");
|
||||
const loaded = loadConfiguredRuntimeAssets(input);
|
||||
|
||||
expect(loaded.publicAssets).toBeUndefined();
|
||||
expect(loaded.state).toMatchObject({ configured: true, pause_reason: "asset_manifest_invalid", status: "unavailable" });
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ describe("TDD-WP0-DATA-001-root-validation resource boundary", () => {
|
||||
const assetRoot = join(base, "read-only-assets");
|
||||
const relativePath = "images/source.png";
|
||||
const bytes = Buffer.from("synthetic png fixture");
|
||||
const assetId = randomUUID();
|
||||
const assetId = "STK001";
|
||||
mkdirSync(join(assetRoot, "images"), { recursive: true });
|
||||
writeFileSync(join(assetRoot, relativePath), bytes);
|
||||
const manifest = JSON.stringify({ assets: [{ asset_id: assetId, relative_path: relativePath }] });
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
defaultLocalDataRoot,
|
||||
initializeLocalDataRoot,
|
||||
inspectInitializedLocalDataRoot,
|
||||
readConfiguredAssetRoot,
|
||||
resolvePathWithinRoot,
|
||||
validateLocalDataRoot,
|
||||
validateReadOnlyAssetRoot,
|
||||
@@ -66,6 +67,17 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("TDD-WP0-DATA-001-root-validation", () => {
|
||||
it("reads only an absolute configured read-only asset root", () => {
|
||||
const base = temporaryDirectory();
|
||||
const configFile = join(base, "instance.json");
|
||||
const assetRoot = join(base, "runtime-assets");
|
||||
writeFileSync(configFile, JSON.stringify({ asset_root: assetRoot }));
|
||||
|
||||
expect(readConfiguredAssetRoot(configFile)).toBe(resolve(assetRoot));
|
||||
writeFileSync(configFile, JSON.stringify({ asset_root: "relative-assets" }));
|
||||
expect(() => readConfiguredAssetRoot(configFile)).toThrow("asset_root_configuration_invalid");
|
||||
});
|
||||
|
||||
it("derives default data and configuration paths from LOCALAPPDATA without a hardcoded user", () => {
|
||||
const localAppData = join(temporaryDirectory(), "LocalAppData");
|
||||
expect(defaultLocalDataRoot({ LOCALAPPDATA: localAppData })).toBe(join(localAppData, "Dada", "P0A", "data"));
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
createRuntimeAssetManifest,
|
||||
deployRuntimeAssetPlan,
|
||||
readRuntimeAssetManifest,
|
||||
serializeRuntimeAssetManifest,
|
||||
} from "../../scripts/lib/runtime-assets.mjs";
|
||||
|
||||
test("committed P0-A runtime manifest covers the frozen first-version binary assets", () => {
|
||||
const manifest = readRuntimeAssetManifest("config/runtime-assets-manifest.json");
|
||||
assert.deepEqual(manifest.counts, {
|
||||
dynamic_fonts: 7,
|
||||
dynamic_images: 8,
|
||||
font_panel_items: 11,
|
||||
static_stickers: 1407,
|
||||
});
|
||||
assert.equal(manifest.entries.length, 1433);
|
||||
assert.doesNotMatch(serializeRuntimeAssetManifest(manifest), /[A-Za-z]:[\\/]/);
|
||||
});
|
||||
|
||||
test("runtime asset deployment creates verified hardlinks and a path-free manifest", async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "dada-runtime-assets-"));
|
||||
t.after(() => rm(root, { force: true, recursive: true }));
|
||||
const sourceRoot = join(root, "source");
|
||||
const assetRoot = join(root, "assets");
|
||||
const sourcePath = join(sourceRoot, "sticker.png");
|
||||
const bytes = Buffer.from("runtime asset fixture");
|
||||
await mkdir(sourceRoot);
|
||||
await writeFile(sourcePath, bytes);
|
||||
const entry = {
|
||||
assetId: "STK001",
|
||||
mimeType: "image/png",
|
||||
relativePath: "p0a-static-v1/STK001.png",
|
||||
resourceVersion: "p0a-static-v1",
|
||||
rootRef: "p0a_runtime_assets",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
const manifest = createRuntimeAssetManifest({
|
||||
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 },
|
||||
entries: [entry],
|
||||
});
|
||||
|
||||
await deployRuntimeAssetPlan({ assetRoot, manifest, resources: [{ entry, sourcePath }] });
|
||||
|
||||
const targetPath = join(assetRoot, entry.relativePath);
|
||||
const [sourceStat, targetStat] = await Promise.all([stat(sourcePath), stat(targetPath)]);
|
||||
assert.equal(sourceStat.ino, targetStat.ino);
|
||||
assert.deepEqual(await readFile(targetPath), bytes);
|
||||
const writtenManifest = await readFile(join(assetRoot, "manifest.json"), "utf8");
|
||||
assert.deepEqual(JSON.parse(writtenManifest), manifest);
|
||||
assert.doesNotMatch(writtenManifest, /[A-Za-z]:[\\/]/);
|
||||
});
|
||||
|
||||
test("runtime asset deployment refuses a mismatched existing target", async (t) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "dada-runtime-assets-conflict-"));
|
||||
t.after(() => rm(root, { force: true, recursive: true }));
|
||||
const sourcePath = join(root, "source.png");
|
||||
const assetRoot = join(root, "assets");
|
||||
const targetPath = join(assetRoot, "p0a-static-v1", "STK001.png");
|
||||
await mkdir(join(assetRoot, "p0a-static-v1"), { recursive: true });
|
||||
await writeFile(sourcePath, "expected");
|
||||
await writeFile(targetPath, "unexpected");
|
||||
const entry = {
|
||||
assetId: "STK001",
|
||||
mimeType: "image/png",
|
||||
relativePath: "p0a-static-v1/STK001.png",
|
||||
resourceVersion: "p0a-static-v1",
|
||||
rootRef: "p0a_runtime_assets",
|
||||
sha256: createHash("sha256").update("expected").digest("hex"),
|
||||
};
|
||||
const manifest = createRuntimeAssetManifest({
|
||||
counts: { dynamic_fonts: 0, dynamic_images: 0, font_panel_items: 0, static_stickers: 1 },
|
||||
entries: [entry],
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => deployRuntimeAssetPlan({ assetRoot, manifest, resources: [{ entry, sourcePath }] }),
|
||||
/asset_target_conflict/,
|
||||
);
|
||||
});
|
||||
@@ -112,6 +112,14 @@ async function verifyWorkerStartup(configPath) {
|
||||
test("portable package serves the product and keeps SQLite data across API restart", async () => {
|
||||
assert.ok(existsSync(join(packageRoot, "Dada.exe")));
|
||||
assert.ok(existsSync(join(packageRoot, "web", "index.html")));
|
||||
const runtimeAssetManifest = JSON.parse(await readFile(join(packageRoot, "asset-metadata", "manifest.json"), "utf8"));
|
||||
assert.deepEqual(runtimeAssetManifest.counts, {
|
||||
dynamic_fonts: 7,
|
||||
dynamic_images: 8,
|
||||
font_panel_items: 11,
|
||||
static_stickers: 1407,
|
||||
});
|
||||
assert.equal(runtimeAssetManifest.entries.length, 1433);
|
||||
const packagedWorker = await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8");
|
||||
const packagedOneApiAdapter = await readFile(join(packageRoot, "server", "worker", "dist", "oneapi-generation-adapter.js"), "utf8");
|
||||
assert.match(packagedWorker, /GenerationProcessor/);
|
||||
|
||||
Reference in New Issue
Block a user