143 lines
5.3 KiB
TypeScript
143 lines
5.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`;
|
|
const synthetic = `ui-projection-${crypto.randomUUID()}`;
|
|
// 合成値はこの子processのmemory/envに限定する。argv、URL、保存証拠へ出さない。
|
|
await Deno.mkdir(`${root}/target/web-ux/tmp`, { recursive: true });
|
|
const audit = [];
|
|
const includeExisting = Deno.args.includes("--existing");
|
|
const runLabel = Deno.args.find((arg) => arg.startsWith("--run-label="))?.slice(12) ??
|
|
String(Date.now());
|
|
assert(/^[a-z0-9-]+$/.test(runLabel), "run label must be a safe path component");
|
|
for (const theme of ["light", "dark"]) {
|
|
for (
|
|
const kind of includeExisting ? ["setup", "settings"] : ["setup"]
|
|
) {
|
|
const run = `${kind === "setup" ? "setup" : "after-setup"}-${runLabel}-${theme}`;
|
|
const child = await new Deno.Command(Deno.execPath(), {
|
|
cwd: root,
|
|
env: { TMPDIR: `${root}/target/web-ux/tmp`, WEB_UX_SYNTHETIC_SETUP_INPUT: synthetic },
|
|
args: [
|
|
"run",
|
|
"--config",
|
|
"tools/web-ux/deno.json",
|
|
"--allow-env",
|
|
"--allow-net",
|
|
"--allow-read",
|
|
"--allow-write=target/web-ux",
|
|
"--allow-run",
|
|
"--allow-sys",
|
|
"tools/web-ux/cli.ts",
|
|
"capture",
|
|
"--scenario",
|
|
`tools/web-ux/scenarios/hosting-${kind}-${theme}.json`,
|
|
"--output",
|
|
"target/web-ux/hosting",
|
|
"--run-id",
|
|
run,
|
|
],
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
}).output();
|
|
assertEquals(child.code, 2); // 両scenarioとも意図した拒否・障害表示を含む。
|
|
const context: ReviewContext = JSON.parse(
|
|
await Deno.readTextFile(`${output}/${run}/review-context.json`),
|
|
);
|
|
assertEquals(context.diagnostics, []);
|
|
assertEquals(context.status, "completed-with-errors");
|
|
assertEquals(context.captures.length, kind === "setup" ? 18 : 54);
|
|
let images = 0;
|
|
for (const capture of context.captures) {
|
|
assertEquals(capture.document.status, 200);
|
|
assertEquals(capture.errorSummary.truncated, false);
|
|
const rejected = capture.route.id === "setup-reject";
|
|
const expected = rejected ||
|
|
["admin-load-error", "admin-save-error", "account-error"].includes(capture.route.id);
|
|
if (expected) {
|
|
assert(capture.errors.every((error) =>
|
|
error.status === (rejected ? 403 : 503) ||
|
|
(error.kind === "console" && error.message.includes(String(rejected ? 403 : 503))) ||
|
|
(error.kind === "request" && error.message === "net::ERR_ABORTED")
|
|
));
|
|
assert(capture.errors.some((error) => error.status === (rejected ? 403 : 503)));
|
|
} else assertEquals(capture.errors, []);
|
|
for (const image of capture.screenshots) {
|
|
assertEquals(
|
|
await sha(await Deno.readFile(`${output}/${run}/${image.bundlePath}`)),
|
|
image.sha256,
|
|
);
|
|
images++;
|
|
}
|
|
}
|
|
let scannedTextFiles = 0;
|
|
for await (const path of files(`${output}/${run}`)) {
|
|
if (/\.(json|md|html|log)$/.test(path)) {
|
|
assert(
|
|
!(await Deno.readTextFile(path)).includes(synthetic),
|
|
"synthetic input must not appear in artifacts",
|
|
);
|
|
scannedTextFiles++;
|
|
}
|
|
}
|
|
audit.push({
|
|
run,
|
|
source: context.source,
|
|
captures: context.captures.length,
|
|
images,
|
|
unexpectedErrors: 0,
|
|
allDocuments200: true,
|
|
imageHashesVerified: true,
|
|
syntheticInputAbsent: true,
|
|
scannedTextFiles,
|
|
});
|
|
}
|
|
}
|
|
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("");
|
|
}
|
|
async function* files(dir: string): AsyncGenerator<string> {
|
|
for await (const entry of Deno.readDir(dir)) {
|
|
const path = `${dir}/${entry.name}`;
|
|
if (entry.isDirectory) yield* files(path);
|
|
else if (entry.isFile) yield path;
|
|
}
|
|
}
|
|
async function inventory(dir: string) {
|
|
const result = [];
|
|
for await (const path of files(dir)) {
|
|
result.push({
|
|
path: path.slice(root.length + 1),
|
|
sha256: await sha(await Deno.readFile(path)),
|
|
});
|
|
}
|
|
return result.sort((a, b) => a.path.localeCompare(b.path));
|
|
}
|
|
const diff = await new Deno.Command("git", {
|
|
cwd: root,
|
|
args: ["diff", "--binary", "HEAD", "--", "web/workspace/src", "web/workspace/deno.json"],
|
|
stdout: "piped",
|
|
stderr: "null",
|
|
}).output();
|
|
assert(diff.success);
|
|
await Deno.writeTextFile(
|
|
`${output}/setup-evidence-audit.json`,
|
|
JSON.stringify(
|
|
{
|
|
boundary:
|
|
"Production UI modules/styles unchanged. Setup-only bootstrap replaces navigator.credentials create/get; synthetic API responses. Not native WebAuthn, real token proof, authorization or persistence evidence. No request header/body logging.",
|
|
audit,
|
|
currentTrackedWebDiffSha256: await sha(diff.stdout),
|
|
currentSourceFiles: await inventory(`${root}/web/workspace/src`),
|
|
currentBuildFiles: await inventory(`${root}/web/workspace/build`),
|
|
fixtureFiles: await inventory(import.meta.dirname!),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
console.log("Capture diagnostics, image hashes and synthetic-input artifact scan PASS.");
|