Files

114 lines
4.3 KiB
TypeScript

import { assert, assertEquals } from "@std/assert";
import { resolve } from "@std/path";
import type { ReviewContext } from "../../../../tools/web-ux/src/types.ts";
const root = resolve(import.meta.dirname!, "../../../..");
const output = `${root}/target/web-ux/hosting`;
async function sha(bytes: Uint8Array<ArrayBuffer>) {
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((value) =>
value.toString(16).padStart(2, "0")
).join("");
}
const results = [];
for (const theme of ["light", "dark"]) {
for (const prefix of ["before", "after", "navigation-before", "navigation-after"]) {
const run = `${prefix}-final-${theme}`;
const context: ReviewContext = JSON.parse(
await Deno.readTextFile(`${output}/${run}/review-context.json`),
);
assertEquals(context.diagnostics, []);
assert(!context.diagnosticSummary.truncated);
let screenshots = 0, expectedFailureCaptures = 0;
for (const capture of context.captures) {
assertEquals(capture.document.status, 200);
assert(!capture.errorSummary.truncated);
const expectedFailure = ["admin-load-error", "admin-save-error", "account-error"].includes(
capture.route.id,
);
if (expectedFailure) {
expectedFailureCaptures++;
// Non-OK bodies are cancelled by production client; ERR_ABORTED is the
// same deliberately failed response, not an unrelated network failure.
assertEquals(capture.errors.length, 3);
assertEquals(
capture.errors.filter((error) => error.status === 503).length,
1,
);
assertEquals(
capture.errors.filter((error) =>
error.kind === "console" && error.message.includes("503")
).length,
1,
);
assertEquals(
capture.errors.filter((error) =>
error.kind === "request" && error.message === "net::ERR_ABORTED"
).length,
1,
);
} else assertEquals(capture.errors, []);
assert(capture.snapshot !== null);
const snapshot = await Deno.readTextFile(`${output}/${run}/${capture.snapshot.bundlePath}`);
if (["ordinary", "anonymous"].includes(capture.route.id)) {
assert(!snapshot.includes('button "Edit registration policy"'));
assert(!snapshot.includes('link "Hosting settings"'));
}
if (capture.route.id === "ordinary-account-off") {
assert(snapshot.includes('button "Add passkey to alice"'));
assert(!snapshot.includes('link "Hosting settings"'));
}
for (const screenshot of capture.screenshots) {
assertEquals(
await sha(await Deno.readFile(`${output}/${run}/${screenshot.bundlePath}`)),
screenshot.sha256,
);
screenshots++;
}
}
assertEquals(expectedFailureCaptures, prefix === "after" ? 9 : 0);
results.push({
run,
captures: context.captures.length,
screenshots,
expectedFailureCaptures,
allDocuments200: true,
unexpectedErrors: 0,
imageHashesVerified: true,
diagnosticTruncation: false,
});
}
}
async function inventory(directory: string): Promise<{ path: string; sha256: string }[]> {
const files = [];
for await (const entry of Deno.readDir(directory)) {
const path = `${directory}/${entry.name}`;
if (entry.isDirectory) files.push(...await inventory(path));
else if (entry.isFile) {
files.push({
path: path.slice(root.length + 1),
sha256: await sha(await Deno.readFile(path)),
});
}
}
return files.sort((a, b) => a.path.localeCompare(b.path));
}
const baselineAssets = await inventory(`${root}/target/web-ux/baseline-source/web/workspace/build`);
const currentAssets = await inventory(`${root}/web/workspace/build`);
await Deno.writeTextFile(
`${output}/evidence-audit.json`,
JSON.stringify(
{
boundary:
"Actual production static shells; synthetic HTTP only. No Backend authorization/persistence/WebAuthn evidence.",
baselineSource: "git archive 7042ec7d50ca8b03b8034c33e9792f4b6ac9d48d web/workspace",
currentSource:
"Dirty shared worktree at same HEAD; exact served build bytes identified by inventory. Parent owns source/build provenance and final visual verdict.",
results,
baselineAssets,
currentAssets,
},
null,
2,
),
);
console.log(JSON.stringify(results, null, 2));