74 lines
3.8 KiB
JavaScript
74 lines
3.8 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { join } from "node:path";
|
|
import { chromium } from "playwright";
|
|
import { createServer } from "node:http";
|
|
|
|
const root = fileURLToPath(new URL(".", import.meta.url));
|
|
const manifest = JSON.parse(await readFile(join(root, "frame-manifest.json"), "utf8"));
|
|
const assets = JSON.parse(await readFile(join(root, "asset-manifest.json"), "utf8"));
|
|
const errors = [];
|
|
const hashes = {};
|
|
|
|
for (const asset of assets.assets) {
|
|
const local = await readFile(join(root, asset.path));
|
|
const source = await readFile(join(root, "../../../..", asset.source));
|
|
const localHash = createHash("sha256").update(local).digest("hex");
|
|
const sourceHash = createHash("sha256").update(source).digest("hex");
|
|
hashes[asset.path] = { source: asset.source, sha256: localHash };
|
|
if (localHash !== sourceHash) errors.push(`source hash mismatch: ${asset.path}`);
|
|
}
|
|
|
|
const mime = { ".html":"text/html", ".css":"text/css", ".js":"text/javascript", ".png":"image/png", ".ttf":"font/ttf" };
|
|
const server = createServer(async (request, response) => {
|
|
const rawPath = decodeURIComponent((request.url || "/").split("?")[0]);
|
|
const relativePath = rawPath === "/" ? "index.html" : rawPath.replace(/^\//, "");
|
|
try {
|
|
const { extname } = await import("node:path");
|
|
const body = await readFile(join(root, relativePath));
|
|
response.writeHead(200, { "content-type": mime[extname(relativePath)] || "application/octet-stream" });
|
|
response.end(body);
|
|
} catch { response.writeHead(404); response.end("Not found"); }
|
|
});
|
|
|
|
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address();
|
|
const browser = await chromium.launch({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 844, height: 390 }, deviceScaleFactor: 1 });
|
|
|
|
for (const frame of manifest.frames) {
|
|
await page.goto(`http://127.0.0.1:${port}/index.html?frame=${frame.id}`, { waitUntil: "networkidle" });
|
|
const state = await page.evaluate(() => {
|
|
const slice = document.querySelector("#slice");
|
|
const nodes = [...document.querySelectorAll("body *")];
|
|
const overflow = nodes.filter(node => {
|
|
const r = node.getBoundingClientRect();
|
|
return r.width > 0 && r.height > 0 && (r.left < -0.5 || r.top < -0.5 || r.right > 844.5 || r.bottom > 390.5);
|
|
}).map(node => `${node.tagName}.${node.className}`);
|
|
return {
|
|
ready: document.documentElement.dataset.ready,
|
|
frame: slice?.dataset.frame,
|
|
overflow,
|
|
actors: document.querySelectorAll(".layer-actors img").length,
|
|
text: document.body.innerText
|
|
};
|
|
});
|
|
if (state.ready !== "true" || state.frame !== frame.id) errors.push(`frame not ready: ${frame.id}`);
|
|
if (state.overflow.length) errors.push(`viewport overflow ${frame.id}: ${state.overflow.join(", ")}`);
|
|
if (state.actors < 2) errors.push(`missing actors: ${frame.id}`);
|
|
if (!state.text.includes("妮娅") || !state.text.includes("赛布尔")) errors.push(`identity copy missing: ${frame.id}`);
|
|
}
|
|
|
|
await browser.close();
|
|
server.close();
|
|
|
|
const generation = JSON.parse(await readFile(join(root, "generation-record.json"), "utf8"));
|
|
if (generation.paid_calls_used > generation.authorized_call_ceiling) errors.push("paid call ceiling exceeded");
|
|
if (generation.returned_cost_usd > generation.authorized_cost_ceiling_usd) errors.push("paid cost ceiling exceeded");
|
|
|
|
const report = { ok: errors.length === 0, viewport: manifest.viewport, frame_count: manifest.frames.length, asset_count: assets.assets.length, paid_calls_used: generation.paid_calls_used, paid_cost_usd: generation.returned_cost_usd, errors, hashes };
|
|
await writeFile(join(root, "evidence", "verification.json"), JSON.stringify(report, null, 2) + "\n");
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (errors.length) process.exitCode = 1;
|