test: complete WP4-07 visual and performance budgets
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m34s
Dada P0-A isolated Windows CI / validate-and-package (push) Failing after 2m34s
This commit is contained in:
@@ -76,7 +76,7 @@ async function routeEditor(page: Page, projectId: string, backend: Backend) {
|
||||
backend.recent = [item, ...backend.recent.filter((entry) => entry.asset_id !== item.asset_id)];
|
||||
await route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 }));
|
||||
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 }));
|
||||
}
|
||||
|
||||
test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and account recent use", async ({ page }) => {
|
||||
@@ -100,7 +100,7 @@ test("TDD-WP4-TXT-003 exposes the frozen catalog, display-name search and accoun
|
||||
expect(page.getByText("添加普通文字", { exact: true })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: /FLOWER001 春日计划/ }).click();
|
||||
await expect(page.getByText("对象 1 / 50")).toBeVisible();
|
||||
await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "wp4-fixture-v1" }]);
|
||||
await expect.poll(() => backend.recent).toEqual([{ asset_id: "FLOWER001", asset_kind: "text_template", resource_version: "p0a-complex-v1" }]);
|
||||
await page.reload();
|
||||
await page.getByRole("button", { name: "文字模板", exact: true }).click();
|
||||
await expect(page.getByLabel("最近使用文字模板").getByText("FLOWER001", { exact: true })).toBeVisible();
|
||||
@@ -158,7 +158,14 @@ test("TDD-WP4-TXT-001 preserves multiline content and transforms across a templa
|
||||
expect(pixelEvidence.line_one_ink_pixels).toBeGreaterThan(100);
|
||||
expect(pixelEvidence.line_two_ink_pixels).toBeGreaterThan(100);
|
||||
await page.reload();
|
||||
await page.getByLabel("编辑画布").click({ position: { x: 270, y: 360 } });
|
||||
const reopenedStage = page.getByLabel("编辑画布");
|
||||
const reopenedBounds = await reopenedStage.boundingBox();
|
||||
const reopenedText = backend.canvas.elements[0];
|
||||
if (!reopenedBounds || !reopenedText) throw new Error("Reopened text geometry is unavailable.");
|
||||
await reopenedStage.click({ position: {
|
||||
x: reopenedBounds.width * reopenedText.position.x,
|
||||
y: reopenedBounds.height * reopenedText.position.y,
|
||||
} });
|
||||
await expect(page.getByLabel("文字内容")).toHaveValue("第一行\n第二行");
|
||||
writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "canvas-state.json", backend.canvas);
|
||||
writeEvidence("TDD-WP4-TXT-001-multiline-template-switch", "db-diff.json", { content: backend.canvas.elements[0]?.content, saves: backend.saves, template: backend.canvas.elements[0]?.template_or_asset_id });
|
||||
|
||||
@@ -99,10 +99,9 @@ async function routeEditor(page: Page, projectId: string, backend: Backend) {
|
||||
return route.fulfill({ body: rawSvg(colors), contentType: "image/svg+xml", status: 200 });
|
||||
});
|
||||
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/public/wp4-fixture-v1/FONT081", (route) => route.fulfill({ body: readFileSync(font081Path), contentType: "font/ttf", status: 200 }));
|
||||
await page.route("**/api/v1/assets/public/wp4-dynamic-source-v1/*", (route) => {
|
||||
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => {
|
||||
const assetId = decodeURIComponent(route.request().url().split("/").at(-1)!);
|
||||
const asset = dynamicSourceAssets[assetId];
|
||||
const asset = assetId === "FONT081" ? { contentType: "font/ttf", path: font081Path } : dynamicSourceAssets[assetId];
|
||||
if (!asset) return route.fulfill({ status: 404 });
|
||||
return route.fulfill({ body: readFileSync(asset.path), contentType: asset.contentType, status: 200 });
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ async function routeEditor(page: Page, projectId: string, backend: Backend, opti
|
||||
}));
|
||||
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
|
||||
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: readFileSync(windowsFont), contentType: "font/ttf" }));
|
||||
}
|
||||
|
||||
test("TDD-WP4-EXP-001 cancel keeps a pending text edit outside history and export", async ({ page }) => {
|
||||
|
||||
@@ -7,28 +7,29 @@ import { resolve } from "node:path";
|
||||
// @ts-expect-error no declaration file is needed for the test-only ESM fixture.
|
||||
import {
|
||||
WP4_07_PERFORMANCE_BUDGETS,
|
||||
WP4_07_REAL_RESOURCE_VERSIONS,
|
||||
WP4_07_RED_RESOURCE_VERSION,
|
||||
WP4_07_REQUIRED_FONT_IDS,
|
||||
assertWp407Fixture,
|
||||
createWp407CanvasFixture,
|
||||
wp407FixtureSha256,
|
||||
} from "../visual-performance/wp4-07-fixture.mjs";
|
||||
// @ts-expect-error no declaration file is needed for the Node-only archive loader.
|
||||
import { loadWp407RealAssets } from "../visual-performance/wp4-07-real-assets.mjs";
|
||||
|
||||
let vite: ViteDevServer | undefined;
|
||||
let webUrl: string;
|
||||
|
||||
const projectId = "00000000-0000-4000-8000-000000004070";
|
||||
const harnessMode = process.env.DADA_WP4_07_HARNESS_MODE;
|
||||
const fixedCanvas = createWp407CanvasFixture(WP4_07_RED_RESOURCE_VERSION);
|
||||
if (!new Set(["red_contract", "real_archive"]).has(harnessMode ?? "")) throw new Error("WP4_07_HARNESS_MODE_REQUIRED");
|
||||
const greenEligible = harnessMode === "real_archive";
|
||||
const fixtureVersions = greenEligible ? WP4_07_REAL_RESOURCE_VERSIONS : WP4_07_RED_RESOURCE_VERSION;
|
||||
const fixedCanvas = createWp407CanvasFixture(fixtureVersions);
|
||||
const realAssets = greenEligible ? loadWp407RealAssets() : undefined;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
assertWp407Fixture();
|
||||
if (harnessMode !== "red_contract") {
|
||||
const realUrl = process.env.DADA_WP4_07_APP_URL;
|
||||
if (!realUrl) throw new Error("WP4_07_REAL_APP_URL_REQUIRED");
|
||||
webUrl = realUrl;
|
||||
return;
|
||||
}
|
||||
assertWp407Fixture(fixtureVersions);
|
||||
vite = await createServer({
|
||||
configFile: resolve("apps/web/vite.config.ts"),
|
||||
root: resolve("apps/web"),
|
||||
@@ -49,7 +50,7 @@ const session = {
|
||||
csrf_token: "csrf-wp4-07-red-contract-000000000000000000000000",
|
||||
expires_at: "2026-09-03T08:00:00.000Z",
|
||||
user: {
|
||||
creator_name: "WP4-07 Fixture",
|
||||
creator_name: "WP4-07 Archive",
|
||||
role: "user",
|
||||
social_id: "@dada_fixture",
|
||||
status: "active",
|
||||
@@ -87,14 +88,14 @@ function svgForAsset(assetId: string) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" fill="${fill}"/><path d="M20 120L80 20l60 100z" fill="${accent}"/><text x="80" y="145" text-anchor="middle" font-family="Arial" font-size="12" fill="#fff">${assetId.replaceAll("&", "")}</text></svg>`;
|
||||
}
|
||||
|
||||
async function routeRedContractEditor(page: Page, backend: BackendState) {
|
||||
const fontBytes = readFileSync("C:\\Windows\\Fonts\\arial.ttf");
|
||||
async function routeEditor(page: Page, backend: BackendState) {
|
||||
const fontBytes = greenEligible ? undefined : readFileSync("C:\\Windows\\Fonts\\arial.ttf");
|
||||
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(session), contentType: "application/json", status: 200 }));
|
||||
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({
|
||||
body: JSON.stringify({
|
||||
canvas_state: backend.canvas,
|
||||
created_at: "2026-07-27T04:00:00.000Z",
|
||||
current_image_id: null,
|
||||
current_image_id: fixedCanvas.background.asset_id,
|
||||
draft_prompt: "WP4-07 fixed visual and performance fixture",
|
||||
generations: [],
|
||||
images: [],
|
||||
@@ -122,29 +123,38 @@ async function routeRedContractEditor(page: Page, backend: BackendState) {
|
||||
backend.latestSaves += 1;
|
||||
await route.fulfill({ body: JSON.stringify({ status: "saved" }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => route.fulfill({
|
||||
body: `<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="1080" height="1920" fill="#30343b"/><rect x="72" y="80" width="936" height="1760" fill="#f7f7f5"/><path d="M72 1520L430 960l260 290 318-480v1070H72z" fill="#1769aa"/><circle cx="790" cy="420" r="210" fill="#f2f400"/></svg>`,
|
||||
contentType: "image/svg+xml",
|
||||
status: 200,
|
||||
}));
|
||||
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => {
|
||||
if (realAssets) return route.fulfill({ body: readFileSync(realAssets.background.path), contentType: realAssets.background.content_type, status: 200 });
|
||||
return route.fulfill({
|
||||
body: `<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920"><rect width="1080" height="1920" fill="#30343b"/><rect x="72" y="80" width="936" height="1760" fill="#f7f7f5"/><path d="M72 1520L430 960l260 290 318-480v1070H72z" fill="#1769aa"/><circle cx="790" cy="420" r="210" fill="#f2f400"/></svg>`,
|
||||
contentType: "image/svg+xml",
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/public/**", (route) => {
|
||||
const url = decodeURIComponent(route.request().url());
|
||||
if (url.includes("/missing-fixture/")) return route.fulfill({ status: 404 });
|
||||
const assetId = url.split("/").filter(Boolean).at(-1) ?? "asset";
|
||||
if (WP4_07_REQUIRED_FONT_IDS.some((fontId: string) => url.endsWith(`/${fontId}`))) {
|
||||
return route.fulfill({ body: fontBytes, contentType: "font/ttf", status: 200 });
|
||||
const url = new URL(route.request().url());
|
||||
const parts = decodeURIComponent(url.pathname).split("/").filter(Boolean);
|
||||
const assetId = parts.at(-1) ?? "asset";
|
||||
const resourceVersion = parts.at(-2) ?? "missing";
|
||||
if (realAssets) {
|
||||
const expectedVersion = assetId.startsWith("STK") ? WP4_07_REAL_RESOURCE_VERSIONS.static : WP4_07_REAL_RESOURCE_VERSIONS.complex;
|
||||
const asset = realAssets.publicAssets.get(assetId);
|
||||
if (resourceVersion !== expectedVersion || !asset) return route.fulfill({ status: 404 });
|
||||
return route.fulfill({ body: readFileSync(asset.path), contentType: asset.content_type, status: 200 });
|
||||
}
|
||||
if (resourceVersion === "missing-fixture") return route.fulfill({ status: 404 });
|
||||
if (WP4_07_REQUIRED_FONT_IDS.some((fontId: string) => url.pathname.endsWith(`/${fontId}`))) {
|
||||
return route.fulfill({ body: fontBytes!, contentType: "font/ttf", status: 200 });
|
||||
}
|
||||
return route.fulfill({ body: svgForAsset(assetId), contentType: "image/svg+xml", status: 200 });
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareEditor(page: Page, backend: BackendState) {
|
||||
if (harnessMode === "red_contract") await routeRedContractEditor(page, backend);
|
||||
const targetProject = harnessMode === "red_contract" ? projectId : process.env.DADA_WP4_07_PROJECT_ID;
|
||||
if (!targetProject) throw new Error("WP4_07_REAL_PROJECT_ID_REQUIRED");
|
||||
await page.goto(`${webUrl}/app/projects/${targetProject}/editor`, { waitUntil: "domcontentloaded" });
|
||||
await routeEditor(page, backend);
|
||||
await page.goto(`${webUrl}/app/projects/${projectId}/editor`, { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByText("对象 50 / 50")).toBeVisible();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.waitForTimeout(250);
|
||||
@@ -156,23 +166,25 @@ function percentile(values: readonly number[], ratio: number) {
|
||||
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))]!;
|
||||
}
|
||||
|
||||
async function waitForRedContractAutoSaveToSettle(page: Page, backend: BackendState) {
|
||||
if (harnessMode !== "red_contract") return;
|
||||
async function waitForAutoSaveToSettle(page: Page, backend: BackendState) {
|
||||
let priorSaves = -1;
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
await page.waitForTimeout(1_100);
|
||||
if (backend.projectSaves === priorSaves) return;
|
||||
priorSaves = backend.projectSaves;
|
||||
}
|
||||
throw new Error("red contract autosave queue did not settle before export failure isolation");
|
||||
throw new Error("autosave queue did not settle before export failure isolation");
|
||||
}
|
||||
|
||||
test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", async ({ page }, testInfo) => {
|
||||
test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", async ({ context, page }, testInfo) => {
|
||||
test.skip(process.env.DADA_WP4_07_LAYER === "performance", "visual layer not requested");
|
||||
const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 7 };
|
||||
await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
|
||||
await prepareEditor(page, backend);
|
||||
await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
|
||||
const layoutSelectors = {
|
||||
canvas: ".editor-canvas-frame",
|
||||
canvas_frame: ".editor-canvas-frame",
|
||||
canvas_surface: ".editor-canvas",
|
||||
footer: ".editor-statusbar",
|
||||
left_panel: ".editor-assets-panel",
|
||||
right_panel: ".editor-inspector",
|
||||
@@ -181,6 +193,13 @@ test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", as
|
||||
};
|
||||
const layoutBoxes: Record<string, unknown> = {};
|
||||
for (const [name, selector] of Object.entries(layoutSelectors)) layoutBoxes[name] = await page.locator(selector).boundingBox();
|
||||
const frameBox = await page.locator(layoutSelectors.canvas_frame).boundingBox();
|
||||
const surfaceBox = await page.locator(layoutSelectors.canvas_surface).boundingBox();
|
||||
expect(frameBox).not.toBeNull();
|
||||
expect(surfaceBox).not.toBeNull();
|
||||
expect(Math.abs(frameBox!.width - surfaceBox!.width)).toBeLessThanOrEqual(2);
|
||||
expect(Math.abs(frameBox!.height - surfaceBox!.height)).toBeLessThanOrEqual(2);
|
||||
expect(Math.abs(surfaceBox!.width / surfaceBox!.height - fixedCanvas.pixel_width / fixedCanvas.pixel_height)).toBeLessThan(0.002);
|
||||
await page.screenshot({ animations: "disabled", path: evidencePath(testInfo, "editor.png") });
|
||||
await page.getByLabel("编辑画布").screenshot({ animations: "disabled", path: evidencePath(testInfo, "canvas.png") });
|
||||
await page.getByRole("button", { name: "导出", exact: true }).click();
|
||||
@@ -192,20 +211,23 @@ test("TDD-WP4-VIS-001 captures fixed Chrome and Edge editor/export evidence", as
|
||||
});
|
||||
writeEvidence(testInfo, "layout-boxes.json", {
|
||||
browser,
|
||||
eligible_for_green: false,
|
||||
fixture_sha256: wp407FixtureSha256(),
|
||||
eligible_for_green: greenEligible,
|
||||
fixture_sha256: wp407FixtureSha256(fixtureVersions),
|
||||
harness_mode: harnessMode,
|
||||
layout_boxes: layoutBoxes,
|
||||
viewport: { device_scale_factor: 1, height: 1080, width: 1920 },
|
||||
});
|
||||
if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
|
||||
});
|
||||
|
||||
test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", async ({ page }, testInfo) => {
|
||||
test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", async ({ context, page }, testInfo) => {
|
||||
test.skip(process.env.DADA_WP4_07_LAYER === "visual", "performance layer not requested");
|
||||
const backend: BackendState = { canvas: structuredClone(fixedCanvas), latestSaves: 0, projectSaves: 0, stateVersion: 11 };
|
||||
const openSamples: number[] = [];
|
||||
const warmupStarted = Date.now();
|
||||
await context.tracing.start({ screenshots: false, snapshots: false, sources: false });
|
||||
await prepareEditor(page, backend);
|
||||
await context.tracing.stop({ path: evidencePath(testInfo, "trace.zip") });
|
||||
const warmupOpenMs = Date.now() - warmupStarted;
|
||||
for (let index = 0; index < WP4_07_PERFORMANCE_BUDGETS.measured_runs; index += 1) {
|
||||
const started = Date.now();
|
||||
@@ -319,12 +341,12 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
|
||||
if (run > 0) exportRuns.push(result);
|
||||
}
|
||||
|
||||
await waitForRedContractAutoSaveToSettle(page, backend);
|
||||
await waitForAutoSaveToSettle(page, backend);
|
||||
const savesBeforeFailure = { latest: backend.latestSaves, project: backend.projectSaves };
|
||||
const exportFailure = await page.evaluate(async ({ canvas, fontIds, targetProjectId }) => {
|
||||
const exportFailure = await page.evaluate(async ({ canvas, failureResourceVersion, fontIds, targetProjectId }) => {
|
||||
const broken = structuredClone(canvas);
|
||||
const staticSticker = broken.elements.find((element: { type: string }) => element.type === "static_sticker");
|
||||
staticSticker.resource_version = "missing-fixture";
|
||||
staticSticker.resource_version = failureResourceVersion;
|
||||
const statuses = Object.fromEntries(fontIds.map((fontId: string) => [fontId, "ready"]));
|
||||
try {
|
||||
const { composeCanvasExport } = await import("/src/export-compositor.ts");
|
||||
@@ -333,17 +355,22 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}, { canvas: fixedCanvas, fontIds: WP4_07_REQUIRED_FONT_IDS, targetProjectId: projectId });
|
||||
}, {
|
||||
canvas: fixedCanvas,
|
||||
failureResourceVersion: greenEligible ? "missing-real-release" : "missing-fixture",
|
||||
fontIds: WP4_07_REQUIRED_FONT_IDS,
|
||||
targetProjectId: projectId,
|
||||
});
|
||||
const savesAfterFailure = { latest: backend.latestSaves, project: backend.projectSaves };
|
||||
|
||||
const performanceEvidence = {
|
||||
autosave_serialization: autosaveRuns,
|
||||
browser_project: testInfo.project.name,
|
||||
editor_reopen: { max_ms: Math.max(...openSamples), p50_ms: percentile(openSamples, 0.5), p95_ms: percentile(openSamples, 0.95), samples_ms: openSamples, warmup_ms: warmupOpenMs },
|
||||
eligible_for_green: false,
|
||||
eligible_for_green: greenEligible,
|
||||
export_1080x1920: exportRuns,
|
||||
export_failure: { observed_error: exportFailure, saves_after: savesAfterFailure, saves_before: savesBeforeFailure },
|
||||
fixture_sha256: wp407FixtureSha256(),
|
||||
fixture_sha256: wp407FixtureSha256(fixtureVersions),
|
||||
harness_mode: harnessMode,
|
||||
interaction: interactionRuns,
|
||||
normative_budgets: WP4_07_PERFORMANCE_BUDGETS,
|
||||
@@ -359,4 +386,5 @@ test("TDD-WP4-PERF-001 measures the fixed 50-element budget without dilution", a
|
||||
top_count: topDomCount,
|
||||
});
|
||||
writeEvidence(testInfo, "environment.json", await page.evaluate(() => ({ device_pixel_ratio: devicePixelRatio, user_agent: navigator.userAgent, viewport: { height: innerHeight, width: innerWidth } })));
|
||||
if (realAssets) writeEvidence(testInfo, "asset-sources.json", realAssets.evidence);
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ test("TDD-WP5-CAT-001 keeps the 1,407 sticker directory virtual and loads origin
|
||||
const backend = { saves: 0, version: 1 };
|
||||
const requests = { original: 0, thumbnails: 0, thumbnailIds: new Set<string>() };
|
||||
await routeEditor(page, backend);
|
||||
await page.route("**/api/v1/assets/public/fixture-v1/*", async (route) => {
|
||||
await page.route("**/api/v1/assets/public/p0a-static-v1/*", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const assetId = decodeURIComponent(url.pathname.split("/").at(-1) ?? "");
|
||||
if (url.searchParams.get("variant") === "thumbnail") {
|
||||
|
||||
@@ -64,8 +64,8 @@ async function routeEditor(page: Page, backend: Backend) {
|
||||
});
|
||||
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/recent", (route) => route.fulfill({ body: JSON.stringify({ status: "recorded" }), contentType: "application/json" }));
|
||||
await page.route("**/api/v1/assets/public/wp4-fixture-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf" }));
|
||||
await page.route("**/api/v1/assets/public/fixture-v1/*", (route) => route.fulfill({ body: png, contentType: "image/png" }));
|
||||
await page.route("**/api/v1/assets/public/p0a-complex-v1/*", (route) => route.fulfill({ body: fontBytes, contentType: "font/ttf" }));
|
||||
await page.route("**/api/v1/assets/public/p0a-static-v1/*", (route) => route.fulfill({ body: png, contentType: "image/png" }));
|
||||
await page.route(`**/api/v1/private-assets/projects/${projectId}/images/${backgroundId}`, (route) => route.fulfill({ body: rawSvg(), contentType: "image/svg+xml" }));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ describe("TASK-WP4-03 archived FontFace gate", () => {
|
||||
const loader = new ArchivedFontLoader({
|
||||
createFace: (family, source) => {
|
||||
expect(family).toBe("Dada_FONT081");
|
||||
expect(source).toBe("url(\"/api/v1/assets/public/wp4-fixture-v1/FONT081\")");
|
||||
expect(source).toBe("url(\"/api/v1/assets/public/p0a-complex-v1/FONT081\")");
|
||||
return { load };
|
||||
},
|
||||
fontSet: { add, check: () => true, ready: Promise.resolve() },
|
||||
});
|
||||
await expect(loader.ensure({ fontId: "FONT081", url: "/api/v1/assets/public/wp4-fixture-v1/FONT081" })).resolves.toBe("ready");
|
||||
await expect(loader.ensure({ fontId: "FONT081", url: "/api/v1/assets/public/p0a-complex-v1/FONT081" })).resolves.toBe("ready");
|
||||
expect(load).toHaveBeenCalledOnce();
|
||||
expect(add).toHaveBeenCalledOnce();
|
||||
expect(loader.status("FONT081")).toBe("ready");
|
||||
|
||||
@@ -53,6 +53,10 @@ export const WP4_07_DYNAMIC_VALUES = Object.freeze({
|
||||
|
||||
const timestamp = "2026-07-27T04:00:00.000Z";
|
||||
const redResourceVersion = "wp4-07-red-contract-v1";
|
||||
export const WP4_07_REAL_RESOURCE_VERSIONS = Object.freeze({
|
||||
complex: "p0a-complex-v1",
|
||||
static: "p0a-static-v1",
|
||||
});
|
||||
const palette = ["#111111", "#F2F400", "#1769AA", "#C92A24", "#FFFFFF"];
|
||||
const textTemplateIds = [
|
||||
"FLOWER001", "FLOWER003", "FLOWER005", "FLOWER008", "H003", "H004",
|
||||
@@ -100,9 +104,14 @@ function common(index, type, templateOrAssetId, resourceVersion) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
function releaseVersions(input = redResourceVersion) {
|
||||
return typeof input === "string" ? { complex: input, static: input } : input;
|
||||
}
|
||||
|
||||
export function createWp407CanvasFixture(resourceVersions = redResourceVersion) {
|
||||
const versions = releaseVersions(resourceVersions);
|
||||
const text = textTemplateIds.map((templateId, offset) => ({
|
||||
...common(offset + 1, "text_template", templateId, resourceVersion),
|
||||
...common(offset + 1, "text_template", templateId, versions.complex),
|
||||
content: offset === 0 ? "DADA\n视觉预算" : `固定文字 ${String(offset + 1).padStart(2, "0")}`,
|
||||
font_size: 48,
|
||||
position: offset === 0 ? { x: 0.5, y: 0.5 } : position(offset, 4, 0.09),
|
||||
@@ -123,7 +132,7 @@ export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
},
|
||||
}));
|
||||
const stickers = Array.from({ length: 24 }, (_, offset) => ({
|
||||
...common(offset + 13, "static_sticker", `STK${String(offset + 1).padStart(3, "0")}`, resourceVersion),
|
||||
...common(offset + 13, "static_sticker", `STK${String(offset + 1).padStart(3, "0")}`, versions.static),
|
||||
opacity: 0.84 + (offset % 4) * 0.04,
|
||||
position: position(offset, 6, 0.39),
|
||||
rotation: (offset % 5 - 2) * 6,
|
||||
@@ -131,7 +140,7 @@ export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
style_parameters: { flip_horizontal: offset % 7 === 0 },
|
||||
}));
|
||||
const colors = colorCards.map(([cardId, styleId], offset) => ({
|
||||
...common(offset + 37, "color_card", cardId, resourceVersion),
|
||||
...common(offset + 37, "color_card", cardId, versions.complex),
|
||||
colors: [...palette],
|
||||
position: { x: 0.16 + offset * 0.22, y: 0.83 },
|
||||
scale: { x: 1.25, y: 1.25 },
|
||||
@@ -139,7 +148,7 @@ export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
style_parameters: { palette_algorithm_version: "mmcq-v1" },
|
||||
}));
|
||||
const dynamics = dynamicIds.map((dynamicId, offset) => ({
|
||||
...common(offset + 41, "dynamic_sticker", dynamicId, resourceVersion),
|
||||
...common(offset + 41, "dynamic_sticker", dynamicId, versions.complex),
|
||||
dynamic_fields: { ...WP4_07_DYNAMIC_VALUES },
|
||||
formatted_value: dynamicId === "DYN012" ? "12:00 PM" : "@dada_fixture",
|
||||
position: { x: 0.09 + (offset % 5) * 0.205, y: 0.9 + Math.floor(offset / 5) * 0.06 },
|
||||
@@ -162,8 +171,8 @@ export function createWp407CanvasFixture(resourceVersion = redResourceVersion) {
|
||||
};
|
||||
}
|
||||
|
||||
export function wp407FixtureContract() {
|
||||
const canvas = createWp407CanvasFixture();
|
||||
export function wp407FixtureContract(resourceVersions = redResourceVersion) {
|
||||
const canvas = createWp407CanvasFixture(resourceVersions);
|
||||
return {
|
||||
canvas,
|
||||
dynamic_values: WP4_07_DYNAMIC_VALUES,
|
||||
@@ -174,12 +183,12 @@ export function wp407FixtureContract() {
|
||||
};
|
||||
}
|
||||
|
||||
export function wp407FixtureSha256() {
|
||||
return createHash("sha256").update(JSON.stringify(wp407FixtureContract())).digest("hex").toUpperCase();
|
||||
export function wp407FixtureSha256(resourceVersions = redResourceVersion) {
|
||||
return createHash("sha256").update(JSON.stringify(wp407FixtureContract(resourceVersions))).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
export function assertWp407Fixture() {
|
||||
const fixture = wp407FixtureContract();
|
||||
export function assertWp407Fixture(resourceVersions = redResourceVersion) {
|
||||
const fixture = wp407FixtureContract(resourceVersions);
|
||||
const counts = Object.fromEntries(["text_template", "static_sticker", "color_card", "dynamic_sticker"].map((type) => [
|
||||
type,
|
||||
fixture.canvas.elements.filter((element) => element.type === type).length,
|
||||
@@ -195,12 +204,12 @@ export function assertWp407Fixture() {
|
||||
if (fixture.environment.viewport.width !== 1920 || fixture.environment.viewport.height !== 1080 || fixture.environment.device_scale_factor !== 1) {
|
||||
throw new Error("candidate viewport or DPR changed");
|
||||
}
|
||||
return { counts, fixture_sha256: wp407FixtureSha256() };
|
||||
return { counts, fixture_sha256: wp407FixtureSha256(resourceVersions) };
|
||||
}
|
||||
|
||||
export const WP4_07_RED_RESOURCE_VERSION = redResourceVersion;
|
||||
export const WP4_07_REQUIRED_FONT_IDS = Object.freeze([
|
||||
"FONT005", "FONT008", "FONT011", "FONT022", "FONT027", "FONT039", "FONT043", "FONT046", "FONT052", "FONT081",
|
||||
"FONT005", "FONT008", "FONT011", "FONT021", "FONT022", "FONT027", "FONT039", "FONT043", "FONT046", "FONT052", "FONT081",
|
||||
"15974853bc3294ef68e7e6d58fe74fd7", "46f8336813e4c48d06a1aef294fdccf6",
|
||||
"53ca6b704728520da50c145eabb2e635", "cca5efc0e02fb1bf62349bd68ef30fc1",
|
||||
"dd25b35dcb7ba4476cbaa9a9592e39e2", "e4210c9872f0c279b35273f230809821",
|
||||
|
||||
@@ -10,7 +10,10 @@ test("WP5 lineage recognizes tasks already merged into later remote heads", () =
|
||||
const gate = inspectWp5TaskLineage({
|
||||
"codex/wp5-03": sha("3"),
|
||||
"codex/wp5-04": sha("4"),
|
||||
}, [commit(4), commit(3), commit(2), commit(1)]);
|
||||
}, {
|
||||
"codex/wp5-03": [commit(3), commit(2), commit(1)],
|
||||
"codex/wp5-04": [commit(4), commit(2), commit(1)],
|
||||
});
|
||||
|
||||
assert.equal(gate.complete, false);
|
||||
assert.equal(gate.candidate_baseline_branch, "codex/wp5-04");
|
||||
@@ -19,13 +22,21 @@ test("WP5 lineage recognizes tasks already merged into later remote heads", () =
|
||||
assert.equal(gate.task_shas["TASK-WP5-02"], sha("2"));
|
||||
});
|
||||
|
||||
test("WP5 lineage becomes complete only at a wp5-07 head containing every task", () => {
|
||||
const heads = { "codex/wp5-07": sha("7") };
|
||||
const complete = inspectWp5TaskLineage(heads, Array.from({ length: 7 }, (_, index) => commit(7 - index)));
|
||||
const incomplete = inspectWp5TaskLineage(heads, [commit(7), commit(6), commit(5), commit(4), commit(3), commit(2)]);
|
||||
test("WP5 lineage accepts independently pushed terminal branches without rewriting their heads", () => {
|
||||
const heads = Object.fromEntries(Array.from({ length: 5 }, (_, index) => [`codex/wp5-0${index + 3}`, sha(String(index + 3))]));
|
||||
const histories = {
|
||||
"codex/wp5-03": [commit(3), commit(2), commit(1)],
|
||||
"codex/wp5-04": [commit(4), commit(2), commit(1)],
|
||||
"codex/wp5-05": [commit(5), commit(4), commit(2), commit(1)],
|
||||
"codex/wp5-06": [commit(6), commit(5), commit(4), commit(2), commit(1)],
|
||||
"codex/wp5-07": [commit(7), commit(4), commit(2), commit(1)],
|
||||
};
|
||||
const complete = inspectWp5TaskLineage(heads, histories);
|
||||
const incomplete = inspectWp5TaskLineage(heads, { ...histories, "codex/wp5-03": [commit(2), commit(1)] });
|
||||
|
||||
assert.equal(complete.complete, true);
|
||||
assert.deepEqual(complete.missing_tasks, []);
|
||||
assert.deepEqual(complete.terminal_branch_shas, heads);
|
||||
assert.equal(incomplete.complete, false);
|
||||
assert.deepEqual(incomplete.missing_tasks, ["TASK-WP5-01"]);
|
||||
assert.deepEqual(incomplete.missing_tasks, ["TASK-WP5-03"]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
import { WP4_07_REAL_RESOURCE_VERSIONS } from "./wp4-07-fixture.mjs";
|
||||
|
||||
const dynamicFontTemplates = Object.freeze({
|
||||
"15974853bc3294ef68e7e6d58fe74fd7": "DYN002",
|
||||
"46f8336813e4c48d06a1aef294fdccf6": "DYN016",
|
||||
"53ca6b704728520da50c145eabb2e635": "DYN007",
|
||||
cca5efc0e02fb1bf62349bd68ef30fc1: "DYN015",
|
||||
dd25b35dcb7ba4476cbaa9a9592e39e2: "DYN001",
|
||||
e4210c9872f0c279b35273f230809821: "DYN011",
|
||||
f4bfd4132df2d6be97ceabadf3853505: "DYN008",
|
||||
});
|
||||
|
||||
const dynamicImages = Object.freeze({
|
||||
"DYN001-image28": ["DYN001", "image28.png"],
|
||||
"DYN002-image29": ["DYN002", "image29.png"],
|
||||
"DYN003-image30": ["DYN003", "image30.png"],
|
||||
"DYN004-image32": ["DYN004", "image32.png"],
|
||||
"DYN008-backendui0": ["DYN008", "backendui0.png"],
|
||||
"DYN011-backendui0": ["DYN011", "backendui0.png"],
|
||||
"DYN015-imager2": ["DYN015", "imager2_2.png"],
|
||||
"DYN016-image21": ["DYN016", "image21.png"],
|
||||
});
|
||||
|
||||
function sha256(path) {
|
||||
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function walkFiles(directory) {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(directory, entry.name);
|
||||
return entry.isDirectory() ? walkFiles(path) : [path];
|
||||
});
|
||||
}
|
||||
|
||||
function firstFile(directory, predicate) {
|
||||
const path = walkFiles(directory).find(predicate);
|
||||
if (!path) throw new Error(`WP4_07_REAL_ASSET_FILE_MISSING:${basename(directory)}`);
|
||||
return path;
|
||||
}
|
||||
|
||||
function inside(root, path) {
|
||||
const delta = relative(resolve(root), resolve(path));
|
||||
return delta !== ".." && !delta.startsWith(`..${sep}`) && !isAbsolute(delta);
|
||||
}
|
||||
|
||||
function contentType(path) {
|
||||
const extension = extname(path).toLowerCase();
|
||||
if (extension === ".png") return "image/png";
|
||||
if (extension === ".otf") return "font/otf";
|
||||
if (extension === ".woff") return "font/woff";
|
||||
if (extension === ".woff2") return "font/woff2";
|
||||
return "font/ttf";
|
||||
}
|
||||
|
||||
function assetRecord(assetId, path, sourceReference, expectedSha256) {
|
||||
if (!existsSync(path) || !statSync(path).isFile()) throw new Error(`WP4_07_REAL_ASSET_UNAVAILABLE:${assetId}`);
|
||||
const actualSha256 = sha256(path);
|
||||
if (expectedSha256 && actualSha256 !== expectedSha256) throw new Error(`WP4_07_REAL_ASSET_HASH_MISMATCH:${assetId}`);
|
||||
return {
|
||||
asset_id: assetId,
|
||||
bytes: statSync(path).size,
|
||||
content_type: contentType(path),
|
||||
path,
|
||||
sha256: actualSha256,
|
||||
source_reference: sourceReference,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadWp407RealAssets() {
|
||||
const manifestPath = resolve(process.env.DADA_WP4_07_FINAL_ASSET_MANIFEST ?? "");
|
||||
if (!process.env.DADA_WP4_07_FINAL_ASSET_MANIFEST || !existsSync(manifestPath)) throw new Error("WP4_07_FINAL_ASSET_MANIFEST_REQUIRED");
|
||||
const handoffPath = resolve(process.env.DADA_COMPLEX_ASSET_MANIFEST ?? join(homedir(), "Desktop", "sticker_web_handoff", "sticker_web_catalog_manifest.json"));
|
||||
const staticRoot = resolve(process.env.DADA_STATIC_STICKER_ROOT ?? join(homedir(), "Desktop", "贴纸素材"));
|
||||
const backgroundPath = resolve(process.env.DADA_WP4_07_BACKGROUND_PATH ?? join(homedir(), "Documents", "贴纸脚本", "time_01_input_20260716.png"));
|
||||
if (!existsSync(handoffPath) || !existsSync(staticRoot)) throw new Error("WP4_07_REAL_ARCHIVE_ROOT_REQUIRED");
|
||||
|
||||
const manifestRaw = readFileSync(manifestPath, "utf8");
|
||||
const manifest = JSON.parse(manifestRaw);
|
||||
const handoff = JSON.parse(readFileSync(handoffPath, "utf8"));
|
||||
const collectionRoots = Object.fromEntries(handoff.collections.map((collection) => [collection.id, resolve(collection.root)]));
|
||||
const fontRoot = collectionRoots.font_panel;
|
||||
const dynamicRoot = collectionRoots.interactive_stickers;
|
||||
if (!fontRoot || !dynamicRoot) throw new Error("WP4_07_REAL_ARCHIVE_COLLECTION_REQUIRED");
|
||||
|
||||
const publicAssets = new Map();
|
||||
for (const item of manifest.assets.font_panel_items) {
|
||||
const packageDirectory = resolve(fontRoot, item.canonical_resource_reference.path);
|
||||
if (!inside(fontRoot, packageDirectory)) throw new Error(`WP4_07_UNSAFE_FONT_REFERENCE:${item.canonical_id}`);
|
||||
const path = firstFile(resolve(packageDirectory, "font_files"), (candidate) => /\.(?:otf|ttf|woff2?|ztf)$/i.test(candidate));
|
||||
publicAssets.set(item.canonical_id, assetRecord(item.canonical_id, path, `font_panel/${item.canonical_id}/${basename(path)}`));
|
||||
}
|
||||
|
||||
for (const item of manifest.assets.static_stickers) {
|
||||
const path = resolve(staticRoot, item.relative_path);
|
||||
if (!inside(staticRoot, path)) throw new Error(`WP4_07_UNSAFE_STATIC_REFERENCE:${item.stable_id}`);
|
||||
publicAssets.set(item.stable_id, assetRecord(item.stable_id, path, `static/${item.stable_id}`, item.sha256));
|
||||
}
|
||||
|
||||
for (const [assetId, templateId] of Object.entries(dynamicFontTemplates)) {
|
||||
const directory = resolve(dynamicRoot, "templates", templateId, "fonts", assetId);
|
||||
const path = firstFile(directory, (candidate) => /\.(?:otf|ttf|woff2?|ztf)$/i.test(candidate));
|
||||
publicAssets.set(assetId, assetRecord(assetId, path, `interactive/${templateId}/fonts/${assetId}/${basename(path)}`));
|
||||
}
|
||||
for (const [assetId, [templateId, filename]] of Object.entries(dynamicImages)) {
|
||||
const path = resolve(dynamicRoot, "templates", templateId, "resource", filename);
|
||||
publicAssets.set(assetId, assetRecord(assetId, path, `interactive/${templateId}/resource/${filename}`));
|
||||
}
|
||||
|
||||
const background = assetRecord("private-background", backgroundPath, "private/background/time_01_input_20260716.png");
|
||||
const manifestSha256 = createHash("sha256").update(manifestRaw).digest("hex").toUpperCase();
|
||||
return {
|
||||
background,
|
||||
evidence: {
|
||||
asset_count: publicAssets.size,
|
||||
assets: [...publicAssets.values()].map(({ asset_id, bytes, content_type, sha256: hash, source_reference }) => ({
|
||||
asset_id, bytes, content_type, sha256: hash, source_reference,
|
||||
})),
|
||||
background: { bytes: background.bytes, content_type: background.content_type, sha256: background.sha256, source_reference: background.source_reference },
|
||||
manifest_sha256: manifestSha256,
|
||||
release_version: manifest.release_version,
|
||||
resource_versions: WP4_07_REAL_RESOURCE_VERSIONS,
|
||||
source_mutations: 0,
|
||||
},
|
||||
publicAssets,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user