docs: localize browser evidence boundary comments
This commit is contained in:
@@ -15,11 +15,11 @@ if (
|
||||
throw new Error("usage: server.ts <port> <build-root> [section-count: 3..100]");
|
||||
}
|
||||
|
||||
// Opt-in keeps the existing Memory scenarios and their pagination unchanged.
|
||||
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
|
||||
const permissionsMode = Deno.args[3] === "permissions";
|
||||
if (permissionsMode) {
|
||||
// Permission evidence must serve a parent-supplied actual production build.
|
||||
// Do not make /health succeed for a missing build or substitute fixture HTML.
|
||||
// 権限の証拠は親から渡された実製品buildを使って採取する。
|
||||
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
|
||||
await Deno.stat(join(buildRoot, "index.html"));
|
||||
await Deno.stat(join(buildRoot, "_app/immutable"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env -S deno run -A
|
||||
/** Capture the actual built Subject detail before UI changes; never builds Web. */
|
||||
/** 変更前の実Web buildでSubject詳細を撮影する。Web自体はbuildしない。 */
|
||||
import { dirname, fromFileUrl, join, resolve } from "@std/path";
|
||||
import { capture } from "../src/capture.ts";
|
||||
import type { Scenario } from "../src/types.ts";
|
||||
@@ -10,7 +10,7 @@ const outputRoot = join(repositoryRoot, "target/web-ux/t-802");
|
||||
const buildRoot = resolve(Deno.args[0] ?? join(repositoryRoot, "web/workspace/build"));
|
||||
const runPrefix = Deno.args[1] ?? `before-${new Date().toISOString().replaceAll(/[:.]/g, "-")}`;
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(runPrefix)) throw new Error("run id must be a safe filename");
|
||||
// Preflight is mandatory. A missing build must not be replaced by fixture HTML.
|
||||
// Preflightは必須。buildの欠落をfixture HTMLで代替しない。
|
||||
for (const filename of ["index.html", "_app/immutable"]) {
|
||||
await Deno.stat(join(buildRoot, filename)).catch(() => {
|
||||
throw new Error(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Synthetic HTTP state for T-802 browser UX only. NOT the real Backend,
|
||||
* authentication, authorization, operation catalog, or cancellation implementation.
|
||||
* Keep real Backend integration evidence separate from these browser fixtures.
|
||||
* Browser UXだけの合成HTTP状態。実Backend、認証、認可、操作catalog、取消の実装ではない。
|
||||
* 実Backendの結合証拠は、このbrowser fixtureと分けて保持する。
|
||||
*/
|
||||
export const permissionFixtureSubjects = [
|
||||
["permissions-empty", "New Subject — permissions not configured"],
|
||||
@@ -31,7 +30,7 @@ export const fixtureLegacyPermission: FixturePermission = {
|
||||
operation: "worker.notify",
|
||||
resource: { kind: "worker", runtime_id: "legacy-runtime", worker_id: "legacy-recipient" },
|
||||
};
|
||||
// Deliberately a small synthetic catalog, not a copy/second authority for production.
|
||||
// 小さな合成catalogに限定する。製品catalogの複製や第二正本ではない。
|
||||
export const fixturePermissionOperations: {
|
||||
operation: string;
|
||||
label: string;
|
||||
@@ -105,8 +104,8 @@ export function createSubjectPermissionsFixture() {
|
||||
return async (request: Request): Promise<Response | null> => {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/api/auth/whoami") {
|
||||
// Exercise the real Web's admission gate with a synthetic HTTP session projection.
|
||||
// No cookies/tokens or real Backend session are created or bypassed.
|
||||
// 合成HTTP session projectionで実Webの画面入場判定を通す。
|
||||
// 実Backendのsessionやcookie/tokenは作成・迂回しない。
|
||||
return json({
|
||||
actor: {
|
||||
user_id: "synthetic-owner",
|
||||
@@ -156,7 +155,7 @@ export function createSubjectPermissionsFixture() {
|
||||
requests.push({ subject_id: id, request: payload, outcome: "conflict" });
|
||||
return error("Policy changed in another browser; reload and review", 409);
|
||||
}
|
||||
// Reject only the combinations used by this fixture. This is NOT Backend validation evidence.
|
||||
// fixtureで使う組合せだけを拒否する。Backend検証の証拠にはしない。
|
||||
for (const p of [...payload.execution, ...payload.delegation]) {
|
||||
const legacy = JSON.stringify(p) === JSON.stringify(fixtureLegacyPermission) &&
|
||||
id === "permissions-legacy";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env -S deno run -A
|
||||
/** Actual built Web + synthetic HTTP. No real Backend auth/Worker success claim. */
|
||||
/** 実Web buildを合成HTTPで操作する。実Backend認証やWorker動作の証拠ではない。 */
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { dirname, fromFileUrl, join, resolve } from "@std/path";
|
||||
import { chromium, type Page } from "playwright";
|
||||
@@ -11,7 +11,7 @@ const root = resolve(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||
const buildRoot = resolve(Deno.args[0] ?? join(root, "web/workspace/build"));
|
||||
const runId = Deno.args[1] ?? `after-${new Date().toISOString().replaceAll(/[:.]/g, "-")}`;
|
||||
if (!/^[a-zA-Z0-9._-]+$/.test(runId)) throw new Error("run id must be a safe filename");
|
||||
// Fail fast; the parent owns building Web. Never replace it with a synthetic shell.
|
||||
// Web buildは親の責務。欠落時は即失敗させ、合成shellで代替しない。
|
||||
for (const name of ["index.html", "_app/immutable"]) {
|
||||
await Deno.stat(join(buildRoot, name)).catch(() => {
|
||||
throw new Error(`Actual Web build missing: ${join(buildRoot, name)}; parent must supply it.`);
|
||||
@@ -19,7 +19,7 @@ for (const name of ["index.html", "_app/immutable"]) {
|
||||
}
|
||||
const output = join(root, "target/web-ux/t-802", runId);
|
||||
await Deno.mkdir(dirname(output), { recursive: true });
|
||||
await Deno.mkdir(output); // A repeated run ID must not overwrite prior evidence.
|
||||
await Deno.mkdir(output); // 同じrun IDで既存の証拠を上書きしない。
|
||||
const sourceCommit = await new Deno.Command("git", {
|
||||
args: ["rev-parse", "HEAD"],
|
||||
cwd: root,
|
||||
@@ -113,12 +113,12 @@ function observe(page: Page) {
|
||||
});
|
||||
}
|
||||
async function capturePoint(page: Page, id: string, expectedErrors: string[] = []) {
|
||||
// Show the decision being captured, without changing product CSS or layout.
|
||||
// 製品CSSやlayoutを変えず、確認対象の判断が見える位置へ移動する。
|
||||
if (id.includes("preview")) await page.locator(".diff").scrollIntoViewIfNeeded();
|
||||
else if (/saved|re-read|unknown|recovered|conflict/.test(id)) {
|
||||
await page.locator(".permissions > header").scrollIntoViewIfNeeded();
|
||||
}
|
||||
// Always retain the image first, even when an overflow assertion will fail.
|
||||
// overflow検査が失敗する場合も、先に画像を保存して根拠を残す。
|
||||
const image = join(output, `${id}.png`);
|
||||
const snapshot = join(output, `${id}.aria.txt`);
|
||||
await page.screenshot({ path: image });
|
||||
@@ -169,7 +169,7 @@ async function capturePoint(page: Page, id: string, expectedErrors: string[] = [
|
||||
imageSha256: await sha256File(image),
|
||||
accessibilitySnapshot: snapshot,
|
||||
expectedErrors,
|
||||
// Per-page errors are cumulative so reload does not erase recovery evidence.
|
||||
// 再読込みで復旧の証拠を消さないよう、pageごとにerrorを累積する。
|
||||
errors: [...diagnostics.get(page) ?? []],
|
||||
errorSummary: {
|
||||
observed: errorCounts.get(page) ?? 0,
|
||||
@@ -206,7 +206,7 @@ async function add(
|
||||
) {
|
||||
await page.getByRole("combobox", { name: "Permission scope", exact: true }).selectOption(lane);
|
||||
await page.getByRole("combobox", { name: "Operation", exact: true }).selectOption(operation);
|
||||
// Exercise the actual name/description/resource input UI, not internal JSON entry.
|
||||
// 内部JSON入力ではなく、実UIの名称・説明・資源入力を操作する。
|
||||
await page.getByText(
|
||||
operation === "ticket.read"
|
||||
? "Read the selected ticket. Does not allow editing or closing it."
|
||||
@@ -267,7 +267,7 @@ try {
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
observe(page);
|
||||
// Enter through the ordinary Subject detail navigation.
|
||||
// 通常のSubject詳細から管理導線へ進む。
|
||||
await page.goto(`${base}/w/memory-review/memory/permissions-empty`);
|
||||
await page.locator(".subject-overview").waitFor();
|
||||
await page.locator('main a[href$="/permissions"]').click();
|
||||
@@ -282,7 +282,7 @@ try {
|
||||
`${prefix}-operation-description-resource-input`,
|
||||
);
|
||||
await add(page, "delegation", "ticket.comment", "T-802");
|
||||
// Use named lane scopes so one grant cannot silently appear in both lanes.
|
||||
// 名前付きlaneで検査し、一つのgrantが両方へ黙って追加されないことを確認する。
|
||||
assertEquals(
|
||||
await page.locator('.lane[aria-label="Direct operations"]').getByText("Read ticket", {
|
||||
exact: true,
|
||||
@@ -310,7 +310,7 @@ try {
|
||||
await page.locator("[data-permission-state=ready]").waitFor();
|
||||
await capturePoint(page, `${prefix}-re-read`);
|
||||
|
||||
// Real browser transport loss AFTER the synthetic HTTP fixture accepts the POST.
|
||||
// 合成HTTP fixtureがPOSTを受け付けた後、実browserで応答を消失させる。
|
||||
await add(page, "execution", "ticket.comment", "T-803");
|
||||
let lost = false;
|
||||
await page.route("**/subjektiv/subjects/permissions-empty/permissions", async (route) => {
|
||||
@@ -323,7 +323,7 @@ try {
|
||||
});
|
||||
await save(page, "unknown");
|
||||
assert(lost);
|
||||
// Fault injection is one-shot; restore ordinary browser networking before evidence capture.
|
||||
// 障害注入は一度だけとし、撮影前に通常のbrowser通信へ戻す。
|
||||
await page.unroute("**/subjektiv/subjects/permissions-empty/permissions");
|
||||
await capturePoint(page, `${prefix}-unknown`, [
|
||||
"Intentional POST response loss after synthetic save",
|
||||
@@ -343,7 +343,7 @@ try {
|
||||
"Intentional earlier POST response loss",
|
||||
]);
|
||||
|
||||
// Two browser pages observe the same synthetic change identity before either save.
|
||||
// 二つのbrowser画面で同じ合成change identityを観測してから保存する。
|
||||
const other = await context.newPage();
|
||||
observe(other);
|
||||
await open(page, base, "permissions-edit");
|
||||
@@ -397,7 +397,7 @@ try {
|
||||
await capturePoint(page, `${prefix}-legacy-preserved`, [
|
||||
"Intentional earlier service unavailable GET",
|
||||
]);
|
||||
// Revoke a supported direct permission; this fixture proves UI intent, not real cancellation.
|
||||
// 直接操作の失効を確認する。ここで証明するのはUIの入力であり、実取消ではない。
|
||||
await page.getByRole("button", {
|
||||
name: "Remove Read ticket from direct operations",
|
||||
exact: true,
|
||||
|
||||
Reference in New Issue
Block a user