fix: synchronize native picker interception in Feature browser tests

This commit is contained in:
2026-10-05 20:14:19 +09:00
parent 9146331450
commit 9812e68b4d
10 changed files with 2253 additions and 1 deletions
+44
View File
@@ -217,6 +217,50 @@ It captures 1440px, 768px, and 390px; `browser-tests/console_pending_shell_test.
monospace inheritance, truncation, and retained-session cancellation gating. Live revision-fenced
cancellation and state transitions are covered by Console component tests. No real Worker is used.
## Console live Feature invocation fixture
`scenarios/console-live-light.json` and `console-live-dark.json` capture the **production Console**
with a synthetic live Worker at 1440px, 768px, and 390px. The fixture provides metadata for `review`
(enum, integer, boolean, and Worker-file arguments) and `attach` (the generic attachment client
adapter), a real workspace protocol WebSocket, request-ID-preserving completion responses, and
synthetic upload staging. It never executes a Worker or Feature invocation. No fixture CSS, shell,
or editor overrides are used.
Build production sources first, optionally from an isolated copy under `target/web-ux`, then select
the build explicitly. From `tools/web-ux`:
```sh
export WEB_UX_BUILD_ROOT='<absolute-path-to-production-build>'
deno task web-ux capture --scenario scenarios/console-live-light.json --output ../../target/web-ux --run-id console-live-light
deno task web-ux capture --scenario scenarios/console-live-dark.json --output ../../target/web-ux --run-id console-live-dark
deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts
```
`WEB_UX_LIFECYCLE_OUTPUT` optionally places browser interaction screenshots and JSON evidence at an
explicit artifact path. Tests consume an existing build; they do not rebuild or mutate Web sources.
The lifecycle test covers argument completion, editing/removing/undoing typed chips, native picker
cancellation, generic-adapter staging, literal unselected slash prose, typed Submit values, and
abandoned-file DELETE. Admission tests verify rejection preserves drafts and staged files,
transport send alone cannot clear the draft, authoritative acceptance transfers ownership, and
failed/in-flight-cancelled uploads cannot Submit. A retry/late-success matrix verifies stable upload
IDs, deleted reservation non-resurrection, and abandoned resource cleanup at accepted draft boundaries.
Intentional upload HTTP503 browser errors are recorded separately from unexpected failures.
The Chromium picker helper awaits interception acknowledgement before real gestures (not a timeout
extension or a replacement input); diagnostics retain user activation, input connectivity, and DOM
lifecycle evidence.
The localhost-only fixture control endpoint sets deterministic `submit: accept | reject | hold`
and `upload: normal | fail | hold` modes; `accept_pending: true` releases held admission responses and
`release_uploads: true` delivers real successful responses for held uploads.
`/fixture-state` exposes actual received protocol methods, HTTP requests, emitted admission events,
upload attempts (including retry IDs), completed uploads, and DELETE IDs for assertions. Only synthetic data is recorded.
`console-live-baseline-light.json` and `console-live-baseline-dark.json` use the same production
shell/data and capture initial plus unselected literal slash text. They support a pre-Feature HEAD
build, where no implicit Feature completion or typed chip exists. Initial and `slash-plain` capture
keys are shared with the current scenarios for like-for-like comparison; Feature-specific states
are explicitly new behavior, not fabricated baseline controls.
## Adding a scenario
1. Name the concrete user task and expected data state; do not write “looks correct”.
@@ -0,0 +1,398 @@
/// <reference lib="dom" />
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join } from "@std/path";
import { chromium, type Page } from "playwright";
import { Buffer } from "node:buffer";
import {
observePicker,
pickerDiagnostics,
prepareNativePicker,
} from "./console_live_picker_diagnostics.ts";
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const route = "/w/console-live-review/workers/W-901-console-live-fixture/console";
const output = Deno.env.get("WEB_UX_LIFECYCLE_OUTPUT") ??
join(root, "target/web-ux/console-live-lifecycle");
type State = {
methods: {
method: string;
params?: {
input?: import("../../../web/workspace/src/lib/generated/protocol.ts").Segment[];
submission_request_id?: string;
};
}[];
events: { event: string }[];
requests: { method: string; path: string }[];
uploads: string[];
upload_attempts: string[];
deletes: string[];
held_uploads: string[];
};
const state = async (base: string): Promise<State> =>
await (await fetch(base + "/fixture-state")).json();
const control = async (base: string, value: unknown): Promise<void> => {
const response = await fetch(base + "/fixture-control", {
method: "POST",
body: JSON.stringify(value),
});
assert(response.ok);
await response.body?.cancel();
};
async function eventually(base: string, predicate: (value: State) => boolean): Promise<State> {
const deadline = Date.now() + 5000;
do {
const value = await state(base);
if (predicate(value)) return value;
await new Promise((resolve) => setTimeout(resolve, 25));
} while (Date.now() < deadline);
throw new Error("Fixture evidence did not reach expected state");
}
type FixtureBody = (
page: Page,
base: string,
errors: string[],
caseOutput: string,
) => Promise<void>;
async function fixture(body: FixtureBody, matrix = false): Promise<void> {
for (const theme of matrix ? ["light", "dark"] as const : ["light"] as const) {
for (const width of matrix ? [1440, 768, 390] : [1440]) await fixtureCase(body, theme, width);
}
}
async function fixtureCase(
body: FixtureBody,
theme: "light" | "dark",
width: number,
): Promise<void> {
const caseOutput = join(output, `${theme}-${width}`);
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
listener.close();
const base = `http://127.0.0.1:${port}`;
const server = new Deno.Command(Deno.execPath(), {
args: [
"run",
"--allow-net",
"--allow-read",
join(root, "tools/web-ux/browser-tests/console_live_fixture_server.ts"),
String(port),
Deno.env.get("WEB_UX_BUILD_ROOT") ?? join(root, "web/workspace/build"),
],
stdout: "null",
stderr: "inherit",
}).spawn();
try {
const deadline = Date.now() + 10000;
let started = false;
while (!started && Date.now() < deadline) {
try {
const response = await fetch(base + "/health");
started = response.ok;
await response.body?.cancel();
} catch { /* bounded owned startup */ }
if (!started) await new Promise((resolve) => setTimeout(resolve, 25));
}
assert(started);
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width, height: width === 390 ? 844 : 900 },
colorScheme: theme,
reducedMotion: "reduce",
});
page.setDefaultTimeout(5000);
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(String(error)));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
await prepareNativePicker(page);
await observePicker(page);
await page.goto(base + route);
await page.locator(".cm-content[contenteditable=true]").waitFor();
await page.getByText("Ready. Choose a Feature", { exact: false }).waitFor();
await Deno.mkdir(caseOutput, { recursive: true });
try {
await body(page, base, errors, caseOutput);
await Deno.writeTextFile(
join(caseOutput, `picker-${crypto.randomUUID()}.json`),
JSON.stringify(await pickerDiagnostics(page), null, 2),
);
} catch (error) {
const id = crypto.randomUUID();
await page.screenshot({ path: join(caseOutput, `admission-failure-${id}.png`) });
await Deno.writeTextFile(
join(caseOutput, `admission-failure-${id}.json`),
JSON.stringify(
{
error: String(error),
body: await page.locator("body").innerText(),
errors,
picker: await pickerDiagnostics(page),
fixture: await state(base),
},
null,
2,
),
);
throw error;
}
} finally {
await browser.close();
}
} finally {
server.kill("SIGTERM");
await server.status;
}
}
async function draft(page: Page, text: string): Promise<void> {
await page.locator(".cm-content").click();
await page.keyboard.press("Control+a");
await page.keyboard.press("Backspace");
await page.keyboard.type(text);
}
async function review(page: Page): Promise<void> {
await draft(page, "/rev");
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
await page.keyboard.press("Tab");
await page.keyboard.press("Escape");
await page.keyboard.type('mode="brief", count=2, enabled=true)');
await page.locator(".composer-typed-chip").waitFor();
}
async function attach(page: Page, name: string): Promise<void> {
await draft(page, "/att");
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
const chooser = page.waitForEvent("filechooser");
await page.keyboard.press("Enter");
const nativeChooser = await chooser;
const diagnostics = await pickerDiagnostics(page) as {
kind: string;
active: boolean;
connected: boolean;
disabled?: boolean;
}[];
const click = diagnostics.filter((record) => record.kind === "input.click").at(-1);
assert(
click?.active && click.connected && !click.disabled,
"native picker must run synchronously with active real gesture and connected enabled input",
);
await nativeChooser.setFiles({
name,
mimeType: "text/markdown",
buffer: Buffer.from("Synthetic fixture document"),
});
}
Deno.test("production leading slash remains responsive after loop fix", () =>
fixture(async (page, base, errors, caseOutput) => {
await draft(page, "/");
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
assertEquals(
(await state(base)).methods.filter((method) => method.method !== "list_completions"),
[],
);
assertEquals((await state(base)).uploads, []);
assertEquals(errors, []);
await Deno.writeTextFile(
join(caseOutput, "slash-fix-result.json"),
JSON.stringify({ status: "pass", fixture: await state(base), errors }, null, 2),
);
}));
Deno.test("production Submit rejection retains typed draft and only authoritative acceptance clears it", () =>
fixture(async (page, base, errors, caseOutput) => {
await review(page);
await attach(page, "admission.md");
await page.locator(".composer-typed-chip").waitFor();
// Keep the uploaded atom and add a completion-selected Feature next to it.
await page.keyboard.press("Control+End");
await page.keyboard.type(" /rev");
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
await page.keyboard.press("Tab");
await page.keyboard.press("Escape");
await page.keyboard.type('mode="brief", count=2, enabled=true)');
await page.waitForFunction(() =>
document.querySelectorAll(".composer-typed-chip").length === 2
);
await control(base, { submit: "reject" });
await page.keyboard.press("Control+Enter");
await page.getByText("Synthetic admission rejection; draft was not accepted.", { exact: false })
.waitFor();
const rejected = await eventually(
base,
(value) => value.events.some((event) => event.event === "submission_rejected"),
);
assertEquals(await page.locator(".composer-typed-chip").count(), 2);
assertEquals(rejected.deletes, [], "rejection must keep staged file ownership");
await page.screenshot({ path: join(caseOutput, "admission-rejected.png") });
await control(base, { submit: "hold" });
await page.locator(".cm-content[contenteditable=true]").waitFor();
await page.locator(".cm-content").focus();
await page.keyboard.press("Control+Enter");
const pending = await eventually(
base,
(value) => value.methods.filter((method) => method.method === "submit").length === 2,
);
assertEquals(
await page.locator(".composer-typed-chip").count(),
2,
"transport send is not admission",
);
assertEquals(pending.deletes, []);
await page.screenshot({ path: join(caseOutput, "admission-pending.png") });
await control(base, { accept_pending: true });
await page.waitForFunction(() =>
document.querySelectorAll(".composer-typed-chip").length === 0
);
const accepted = await eventually(
base,
(value) => value.events.some((event) => event.event === "submission_accepted"),
);
assertEquals(accepted.deletes, [], "accepted file ownership transfers; no staging DELETE");
const submissions = accepted.methods.filter((method) => method.method === "submit");
assertEquals(submissions.length, 2);
assert(
submissions.every((method) =>
method.params?.input?.some((segment) => segment.kind === "uploaded_file")
),
);
assert(
submissions.every((method) =>
method.params?.input?.some((segment) => segment.kind === "feature_invoke")
),
);
assertEquals(
submissions[0].params?.input,
submissions[1].params?.input,
"retry preserves structured invocation IDs and values",
);
assertEquals(errors, []);
await page.screenshot({ path: join(caseOutput, "admission-accepted.png") });
await Deno.writeTextFile(
join(caseOutput, "admission-result.json"),
JSON.stringify({ status: "pass", rejected, pending, accepted, errors }, null, 2),
);
}, true));
Deno.test("production attachment upload failure and in-flight cancellation never Submit and record DELETE", () =>
fixture(async (page, base, errors, caseOutput) => {
await control(base, { upload: "fail" });
await attach(page, "failure.md");
await page.getByText("Upload failed (503).", { exact: true }).waitFor();
await page.screenshot({ path: join(caseOutput, "upload-failed.png") });
await page.getByRole("button", { name: "Remove failure.md", exact: true }).click();
const failed = await state(base);
assertEquals(failed.uploads, []);
assertEquals(failed.methods.filter((method) => method.method === "submit"), []);
await control(base, { upload: "hold" });
await attach(page, "cancel.md");
await eventually(base, (value) => value.held_uploads.length === 1);
await page.screenshot({ path: join(caseOutput, "upload-pending.png") });
await page.getByRole("button", { name: "Remove cancel.md", exact: true }).click();
const cancelled = await eventually(
base,
(value) => value.deletes.length > failed.deletes.length && value.held_uploads.length === 0,
);
assertEquals(cancelled.uploads, []);
assertEquals(cancelled.methods.filter((method) => method.method === "submit"), []);
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
await page.screenshot({ path: join(caseOutput, "upload-cancelled.png") });
assert(
cancelled.requests.filter((request) => request.method === "DELETE").every((request) =>
request.path.includes("/attachment-uploads/")
),
);
// The fixture's deliberate HTTP503 emits a browser resource error, not a page exception.
assert(errors.every((error) => /503/.test(error)), JSON.stringify(errors));
await Deno.writeTextFile(
join(caseOutput, "upload-failure-cancellation-result.json"),
JSON.stringify({ status: "pass", failed, cancelled, expectedErrors: errors }, null, 2),
);
}, true));
Deno.test("production upload retry retains upload ID and late completion cannot resurrect deleted reservation", () =>
fixture(async (page, base, errors, caseOutput) => {
await control(base, { upload: "fail" });
await attach(page, "retry.md");
await page.getByText("Upload failed (503).", { exact: true }).waitFor();
const failed = await state(base);
assertEquals(failed.upload_attempts.length, 1);
await control(base, { upload: "normal" });
await page.getByRole("button", { name: "Retry", exact: true }).click();
await page.locator(".composer-typed-chip").filter({ hasText: "Attached retry.md" }).waitFor();
const retried = await state(base);
assertEquals(
retried.upload_attempts,
[failed.upload_attempts[0], failed.upload_attempts[0]],
"retry keeps staging identity",
);
assertEquals(retried.uploads, [failed.upload_attempts[0]]);
await page.screenshot({ path: join(caseOutput, "upload-retried.png") });
// Submit authoritative acceptance is covered separately; this draft is abandoned.
await draft(page, "deleted upload");
await page.keyboard.press("Escape");
// Clearing/accepting this text boundary releases the abandoned staged resource.
await page.keyboard.press("Control+Enter");
const deleted = await eventually(base, (value) => value.deletes.includes(retried.uploads[0]));
await control(base, { upload: "hold" });
await attach(page, "late.md");
const held = await eventually(base, (value) => value.held_uploads.length === 1);
const lateId = held.held_uploads[0];
await page.locator(".composer-typed-chip").filter({ hasText: "Uploading late.md" }).waitFor();
// Delete only the reservation, without aborting its real pending upload.
await page.locator(".cm-content").focus();
await page.keyboard.press("Control+End");
await page.keyboard.press("Backspace");
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
await page.screenshot({ path: join(caseOutput, "late-upload-deleted.png") });
const response = page.waitForResponse((response) =>
response.request().method() === "PUT" && response.url().endsWith(lateId)
);
await control(base, { release_uploads: true });
assertEquals((await response).status(), 200);
const late = await eventually(
base,
(value) => value.uploads.includes(lateId) && value.held_uploads.length === 0,
);
assertEquals(
await page.locator(".composer-typed-chip").count(),
0,
"late success cannot resurrect deleted atom",
);
assertEquals(
late.methods.filter((method) => method.method === "submit").length,
1,
"upload/retry/late completion never submits",
);
// Deleted resources remain undo-owned until the next accepted draft boundary.
await page.screenshot({ path: join(caseOutput, "late-upload-completed.png") });
await draft(page, "finish late deletion");
await page.keyboard.press("Control+Enter");
const completed = await eventually(base, (value) => value.deletes.includes(lateId));
assertEquals(completed.methods.filter((method) => method.method === "submit").length, 2);
assertEquals(
completed.methods.filter((method) => method.method === "submit").at(-1)?.params?.input,
[{ kind: "text", content: "finish late deletion" }],
);
assert(errors.every((error) => /503/.test(error)), JSON.stringify(errors));
await Deno.writeTextFile(
join(caseOutput, "retry-late-result.json"),
JSON.stringify(
{ status: "pass", failed, retried, deleted, held, late, completed, expectedErrors: errors },
null,
2,
),
);
}, true));
Deno.test("production selected invocation preserves multiple positional typed arguments", () =>
fixture(async (page, base, errors, caseOutput) => {
await draft(page, "/rev");
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
await page.keyboard.press("Tab");
await page.keyboard.press("Escape");
await page.keyboard.type('"brief", 2, true)');
await page.locator(".composer-typed-chip").waitFor({ timeout: 2000 });
assertEquals((await state(base)).methods.filter((method) => method.method === "submit"), []);
assertEquals(errors, []);
await Deno.writeTextFile(
join(caseOutput, "positional-result.json"),
JSON.stringify({ status: "pass", fixture: await state(base), errors }, null, 2),
);
}));
@@ -0,0 +1,470 @@
import { extname, join, normalize } from "jsr:@std/path@1.1.4";
// Synthetic authority, real production shell and protocol transport. No UI overrides.
const port = Number(Deno.args[0]);
const buildRoot = Deno.args[1] ?? Deno.env.get("WEB_UX_BUILD_ROOT");
if (!Number.isInteger(port) || !buildRoot) {
throw new Error("usage: console_live_fixture_server.ts <port> <build-root>");
}
const workspaceId = "console-live-review";
const runtimeId = "runtime-a";
const workerId = "worker-a";
const workerPath = `/api/w/${workspaceId}/runtimes/${runtimeId}/workers/${workerId}`;
const json = (value: unknown, status = 200) =>
new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json; charset=utf-8" },
});
const invocations:
import("../../../web/workspace/src/lib/generated/protocol.ts").FeatureInvocationDescriptor[] = [
{
identity: "fixture.review",
name: "review",
aliases: ["rv"],
display_name: "Review",
description: "Review a synthetic document without running a real Worker.",
syntax: "parenthesized",
arguments: [
{
name: "mode",
position: 0,
required: true,
value_type: { kind: "enum", values: ["brief", "thorough"] },
completion: { kind: "static", values: ["brief", "thorough"] },
description: "Review detail",
},
{
name: "count",
position: 1,
required: false,
value_type: { kind: "integer" },
completion: { kind: "none" },
description: "Number of examples",
},
{
name: "enabled",
position: 2,
required: false,
value_type: { kind: "boolean" },
completion: { kind: "static", values: ["true", "false"] },
description: "Enable review",
},
{
name: "path",
position: 3,
required: false,
value_type: { kind: "worker_file" },
completion: { kind: "worker_file" },
description: "Worker document",
},
],
},
{
identity: "fixture.attach",
name: "attach",
aliases: [],
display_name: "Attach file",
description: "Stage a client file using the attachment adapter.",
syntax: "parenthesized",
arguments: [{
name: "file",
position: 0,
required: true,
value_type: { kind: "client_file" },
completion: { kind: "client_file" },
}],
client_adapter: "attachment",
},
];
const entries = [
{
kind: "message",
entry_id: "user-1",
timestamp: 1,
provenance: "human_input",
role: "user",
content: [{ kind: "text", text: "Review the release notes before sending." }],
},
{
kind: "message",
entry_id: "assistant-1",
timestamp: 2,
provenance: "model_output",
role: "assistant",
content: [{
kind: "text",
text:
"Ready. Choose a Feature and inspect its arguments, or attach a document. Nothing runs until Submit.",
}],
},
];
const session = {
pending_submissions: { revision: 0, notification_count: 0, head_id: null, submissions: [] },
entries,
};
const state = { last_command_id: 0, state: { kind: "idle" } };
const worker = {
worker_id: workerId,
runtime_id: runtimeId,
resource_key: "W-901",
display_name: "Console Live Fixture",
label: "Console Live Fixture",
availability: "observed",
state: "idle",
worker_state: state,
retention_state: "active",
host_id: "fixture-host",
profile: "default",
tags: [],
diagnostics: [],
workspace: { workspace_id: workspaceId, visibility: "workspace", identity: workspaceId },
implementation: { kind: "builtin", display_hint: "fixture" },
workdir_attachments: [],
};
const snapshot = {
event: "snapshot",
data: {
session,
state,
greeting: {
worker_name: worker.label,
cwd: "/synthetic/workdir",
provider: "fixture",
model: "synthetic",
scope_summary: "synthetic read-only files",
tools: [],
context_window: 200000,
context_tokens: 2400,
},
},
};
const methods: unknown[] = [];
const requests: { method: string; path: string }[] = [];
const events: unknown[] = [];
let submitMode: "accept" | "reject" | "hold" = "accept";
let uploadMode: "normal" | "fail" | "hold" = "normal";
const pendingAcceptances: (() => void)[] = [];
const cancelledUploads = new Set<string>();
const heldUploads = new Map<string, (response: Response) => void>();
const releaseHeldUploads = new Map<string, () => void>();
const grants = new Map<string, { file_name: string; media_type: string }>();
const uploads: string[] = [];
const uploadAttempts: string[] = [];
const deletes: string[] = [];
let historyRequests = 0;
let nextSubscription = 0;
const mime: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".woff2": "font/woff2",
".wasm": "application/wasm",
};
async function staticResponse(pathname: string): Promise<Response> {
const relative = normalize(pathname.replace(/^\/+/, "") || "index.html");
if (relative.startsWith("..")) return new Response("not found", { status: 404 });
let path = join(buildRoot, relative);
try {
if ((await Deno.stat(path)).isDirectory) path = join(path, "index.html");
return new Response(await Deno.readFile(path), {
headers: { "content-type": mime[extname(path)] ?? "application/octet-stream" },
});
} catch {
return new Response(await Deno.readFile(join(buildRoot, "index.html")), {
headers: { "content-type": mime[".html"] },
});
}
}
Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
const url = new URL(request.url);
if (url.pathname === "/health") return new Response("ok");
if (url.pathname === "/fixture-state") {
return json({
methods,
events,
requests,
uploads,
upload_attempts: uploadAttempts,
deletes,
held_uploads: [...heldUploads.keys()],
history_requests: historyRequests,
});
}
if (url.pathname === "/fixture-control" && request.method === "POST") {
const control = await request.json();
if (["accept", "reject", "hold"].includes(control.submit)) submitMode = control.submit;
if (["normal", "fail", "hold"].includes(control.upload)) uploadMode = control.upload;
if (control.accept_pending) { for (const accept of pendingAcceptances.splice(0)) accept(); }
if (control.release_uploads) {
for (const release of [...releaseHeldUploads.values()]) release();
}
return json({ submitMode, uploadMode });
}
if (url.pathname.startsWith("/api/")) {
requests.push({ method: request.method, path: url.pathname });
}
if (url.pathname === "/api/workspaces") {
return json([{
workspace_id: workspaceId,
owner_account_id: "owner",
display_name: "Console Live Review",
state: "active",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T00:00:00Z",
}]);
}
if (url.pathname === `/api/w/${workspaceId}/workspace`) {
return json({
workspace_id: workspaceId,
display_name: "Console Live Review",
record_authority: "fixture",
schema_version: 1,
auth: {
Passkey: {
rp_id: "127.0.0.1",
origin: url.origin,
public_base_url: url.origin,
cookie_name: "fixture",
},
},
permissions: {
manage_repositories: false,
manage_secrets: false,
manage_runtimes: false,
delete_workspace: false,
},
extension_points: {
store: "fixture",
event_stream: { status: "ready", note: "fixture", diagnostics: [] },
host_worker_bridge: { status: "ready", note: "fixture", diagnostics: [] },
companion_console: { status: "ready", note: "fixture", diagnostics: [] },
},
});
}
if (url.pathname === `/api/w/${workspaceId}/repositories`) {
return json({ workspace_id: workspaceId, items: [], source: "fixture", diagnostics: [] });
}
if (url.pathname === `/api/w/${workspaceId}/working-directories`) {
return json({ workspace_id: workspaceId, items: [], diagnostics: [] });
}
if (url.pathname === `/api/w/${workspaceId}/workers/W-901` || url.pathname === workerPath) {
return json(worker);
}
if (url.pathname === `${workerPath}/session`) return json({ availability: "live_protocol" });
if (url.pathname === `${workerPath}/session/history`) {
historyRequests++;
return json({
availability: "page",
page: {
session_id: "session-a",
lineage_id: "lineage-a",
compact_ancestor_lineage_ids: [],
turns: [{ turn_id: "user-1", entries }],
next_cursor: null,
has_more: false,
},
});
}
if (url.pathname === `${workerPath}/attachment-upload-grants` && request.method === "POST") {
const id = url.searchParams.get("upload_id")!;
grants.set(id, {
file_name: url.searchParams.get("file_name")!,
media_type: url.searchParams.get("media_type")!,
});
return json({ upload_id: id, expires_at_ms: Date.now() + 60000 });
}
if (url.pathname.startsWith(`${workerPath}/attachment-uploads/`)) {
const id = url.pathname.split("/").at(-1)!;
if (request.method === "DELETE") {
deletes.push(id);
grants.delete(id);
cancelledUploads.add(id);
heldUploads.get(id)?.(new Response(null, { status: 204 }));
heldUploads.delete(id);
releaseHeldUploads.delete(id);
return new Response(null, { status: 204 });
}
const grant = grants.get(id);
if (!grant || request.method !== "PUT") return json({ error: "unknown_upload" }, 404);
uploadAttempts.push(id);
if (uploadMode === "fail") {
await request.body?.cancel();
return json({ error: "synthetic_upload_failure" }, 503);
}
const bytes = await request.arrayBuffer();
const hash = Array.from(
new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
const complete = () => {
uploads.push(id);
return json({
file: {
artifact_id: id,
...grant,
created_at_ms: Date.now(),
availability: "available",
byte_len: bytes.byteLength,
sha256: hash,
},
});
};
if (uploadMode === "hold") {
if (cancelledUploads.has(id)) return new Response(null, { status: 204 });
return await new Promise<Response>((resolve) => {
heldUploads.set(id, resolve);
releaseHeldUploads.set(id, () => {
heldUploads.delete(id);
releaseHeldUploads.delete(id);
resolve(complete());
});
});
}
return complete();
}
if (url.pathname.startsWith(`${workerPath}/attachments/`) && request.method === "DELETE") {
deletes.push(url.pathname.split("/").at(-1)!);
return new Response(null, { status: 204 });
}
if (
url.pathname === `/api/w/${workspaceId}/protocol/ws` &&
request.headers.get("upgrade") === "websocket"
) {
const { socket, response } = Deno.upgradeWebSocket(request);
const subscriptions = new Map<string, string>();
const send = (frame: unknown) => socket.send(JSON.stringify(frame));
socket.onmessage = (event) => {
const frame = JSON.parse(String(event.data));
if (frame.frame === "request" && frame.message.method === "subscribe_events") {
const { request_id, selector } = frame.message.params;
const subscription_id = `fixture-${++nextSubscription}`;
subscriptions.set(subscription_id, selector.topic);
send({
protocol_version: 2,
frame: "response",
message: {
result: "subscribed",
payload: {
request_id,
subscription_id,
selector,
snapshot: selector.topic === "worker_protocol"
? { topic: "worker_protocol", data: { worker_id: workerId, events: [snapshot] } }
: { topic: "workers", data: { workers: [] } },
},
},
});
} else if (frame.frame === "worker_protocol") {
const method = frame.message.method;
methods.push(method);
const subscription_id = frame.message.subscription_id;
if (subscriptions.get(subscription_id) !== "worker_protocol") return;
if (method.method === "list_completions") {
const { kind, prefix, request_id, context } = method.params;
let candidates: unknown[] = [];
if (kind === "feature") {
candidates = invocations.filter((descriptor) =>
descriptor.name.startsWith(prefix) ||
descriptor.aliases.some((alias) => alias.startsWith(prefix))
).map((invocation) => ({
value: invocation.name,
is_dir: false,
description: invocation.description,
invocation,
}));
} else if (kind === "file") {
candidates = ["docs/", "docs/release-notes.md"].filter((value) =>
value.startsWith(prefix)
).map((value) => ({ value, is_dir: value.endsWith("/") }));
} else if (context?.invocation === "fixture.review") {
const descriptor = invocations[0].arguments.find((argument) =>
argument.name === context.argument
);
const values = !context.argument
? invocations[0].arguments.map((argument) => argument.name + "=")
: descriptor?.completion.kind === "static"
? descriptor.completion.values ?? []
: context.argument === "path"
? ["docs/release-notes.md", "docs/long-release-notes-for-responsive-validation.md"]
: [];
candidates = values.filter((value) => value.startsWith(prefix)).map((value) => ({
value,
is_dir: false,
}));
}
send({
protocol_version: 2,
frame: "event",
message: {
event: "event",
data: {
subscription_id,
payload: {
event: "worker_protocol",
data: {
worker_id: workerId,
event: {
event: "completions",
data: {
kind,
prefix,
...(request_id ? { request_id } : {}),
...(context ? { context } : {}),
entries: candidates,
},
},
},
},
},
},
});
} else if (method.method === "submit") {
const emit = (event: unknown) => {
events.push(event);
if (socket.readyState !== WebSocket.OPEN) return;
send({
protocol_version: 2,
frame: "event",
message: {
event: "event",
data: {
subscription_id,
payload: { event: "worker_protocol", data: { worker_id: workerId, event } },
},
},
});
};
const submission_request_id = method.params.submission_request_id;
const accept = () =>
emit({
event: "submission_accepted",
data: {
submission_request_id,
submission_id: `accepted-${submission_request_id}`,
disposition: "started",
},
});
if (submitMode === "reject") {
emit({
event: "submission_rejected",
data: {
submission_request_id,
message: "Synthetic admission rejection; draft was not accepted.",
},
});
} else if (submitMode === "hold") pendingAcceptances.push(accept);
else accept();
}
// Admission responses are synthetic authority only. Never execute a Feature or Worker.
}
};
return response;
}
if (url.pathname.startsWith("/api/")) {
return json({ error: "unhandled_fixture_request", path: url.pathname }, 404);
}
return await staticResponse(url.pathname);
});
@@ -0,0 +1,72 @@
/// <reference lib="dom" />
import type { Page } from "playwright";
// Playwright's listener subscription enables Chromium interception asynchronously.
// Keep it enabled for the Page lifetime and await the browser acknowledgement
// before any real gesture; otherwise keyboard.press can overtake interception.
export async function prepareNativePicker(page: Page): Promise<void> {
page.on("filechooser", () => {});
const session = await page.context().newCDPSession(page);
await session.send("Page.setInterceptFileChooserDialog", { enabled: true });
}
// Observe native controls without substituting them or synthesizing activation.
export async function observePicker(page: Page): Promise<void> {
await page.addInitScript(() => {
const records: unknown[] = [];
Object.assign(window, { __pickerDiagnostics: records });
const record = (kind: string, element: Element | null, extra = {}) =>
records.push({
kind,
time: performance.now(),
active: navigator.userActivation.isActive,
hasBeenActive: navigator.userActivation.hasBeenActive,
connected: element?.isConnected,
tag: element?.tagName,
className: element?.className,
editor: document.querySelector(".cm-content")?.textContent,
focused: document.activeElement?.className,
...extra,
});
document.addEventListener("keydown", (event) => {
if (["Enter", "Tab", "Escape"].includes(event.key)) {
record("keydown", event.target as Element, { key: event.key, trusted: event.isTrusted });
queueMicrotask(() => record("after-keydown", event.target as Element, { key: event.key }));
}
}, true);
const nativeClick = HTMLInputElement.prototype.click;
HTMLInputElement.prototype.click = function () {
if (this.type === "file") record("input.click", this, { disabled: this.disabled });
nativeClick.call(this);
if (this.type === "file") record("after-input.click", this);
};
document.addEventListener("click", (event) => {
if ((event.target as HTMLInputElement)?.type === "file") {
record("input-click-event", event.target as Element, { trusted: event.isTrusted });
}
}, true);
const observer = new MutationObserver((changes) => {
for (const change of changes) {
for (
const [kind, nodes] of [
["added", change.addedNodes],
["removed", change.removedNodes],
] as const
) {
for (const node of Array.from(nodes)) {
if (
node instanceof Element && (node.matches("input[type=file], .cm-editor") ||
node.querySelector("input[type=file], .cm-editor"))
) record(kind, node);
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
});
}
export async function pickerDiagnostics(page: Page): Promise<unknown> {
return await page.evaluate(() =>
(window as unknown as { __pickerDiagnostics: unknown }).__pickerDiagnostics
);
}
@@ -0,0 +1,290 @@
/// <reference lib="dom" />
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join } from "@std/path";
import { chromium, type Page } from "playwright";
import { Buffer } from "node:buffer";
import {
observePicker,
pickerDiagnostics,
prepareNativePicker,
} from "./console_live_picker_diagnostics.ts";
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const route = "/w/console-live-review/workers/W-901-console-live-fixture/console";
// Build outside this test. Allows isolated snapshots without writes to production sources.
const buildRoot = Deno.env.get("WEB_UX_BUILD_ROOT") ?? join(root, "web/workspace/build");
const outputRoot = Deno.env.get("WEB_UX_LIFECYCLE_OUTPUT") ??
join(root, "target/web-ux/console-live-lifecycle");
async function ready(url: string): Promise<void> {
const deadline = Date.now() + 10000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
await response.body?.cancel();
if (response.ok) return;
} catch { /* owned server startup */ }
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error("Owned live fixture did not start");
}
type Method = {
method: string;
params?: {
input?: Array<{ kind: string; invocation?: { identity: string; arguments: unknown[] } }>;
};
};
type FixtureState = { methods: Method[]; uploads: string[]; deletes: string[] };
async function fixtureState(base: string): Promise<FixtureState> {
return await (await fetch(`${base}/fixture-state`)).json();
}
async function replaceDraft(page: Page, text: string): Promise<void> {
await page.locator(".cm-content").click();
await page.keyboard.press("Control+a");
await page.keyboard.press("Backspace");
await page.keyboard.type(text);
}
async function chooseReview(page: Page): Promise<void> {
await replaceDraft(page, "/rev");
await page.getByRole("option").filter({ hasText: "review" }).waitFor();
await page.keyboard.press("Tab");
}
Deno.test("production live Console completes, edits/removes typed drafts and stages/cancels attachment adapter", async () => {
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
listener.close();
const base = `http://127.0.0.1:${port}`;
const server = new Deno.Command(Deno.execPath(), {
args: [
"run",
"--allow-net",
"--allow-read",
join(root, "tools/web-ux/browser-tests/console_live_fixture_server.ts"),
String(port),
buildRoot,
],
stdout: "null",
stderr: "inherit",
}).spawn();
await Deno.mkdir(outputRoot, { recursive: true });
try {
await ready(`${base}/health`);
const browser = await chromium.launch({ headless: true });
try {
for (const theme of ["light", "dark"] as const) {
for (const width of [1440, 768, 390]) {
const context = await browser.newContext({
viewport: { width, height: width === 390 ? 844 : 900 },
colorScheme: theme,
reducedMotion: "reduce",
});
const page = await context.newPage();
await prepareNativePicker(page);
await observePicker(page);
page.setDefaultTimeout(5000);
const errors: string[] = [];
const acknowledgedDeletes = new Set<string>();
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
page.on("pageerror", (error) => errors.push(String(error)));
page.on(
"requestfailed",
(request) =>
errors.push(
`failed ${request.method()} ${request.url()} ${request.failure()?.errorText}`,
),
);
page.on("response", (response) => {
if (response.request().method() === "DELETE" && response.status() === 204) {
acknowledgedDeletes.add(response.url());
}
if (response.status() >= 400) {
errors.push(`HTTP ${response.status()} ${response.url()}`);
}
});
try {
assertEquals((await page.goto(base + route))?.status(), 200);
await page.locator(".cm-content[contenteditable=true]").waitFor();
await page.getByText("Ready. Choose a Feature", { exact: false }).waitFor();
const start = await fixtureState(base);
await chooseReview(page);
await page.getByRole("option").filter({ hasText: "brief" }).waitFor();
await page.keyboard.type("m");
await page.getByRole("option").filter({ hasText: "mode=" }).click();
await page.getByRole("option").filter({ hasText: "brief" }).click();
await page.keyboard.type(", count=2, enabled=");
await page.getByRole("option").filter({ hasText: "true" }).click();
await page.keyboard.type(', path="docs/');
await page.getByRole("option").filter({ hasText: "docs/release-notes.md" }).click();
await page.keyboard.type(")");
await page.locator(".composer-typed-chip").waitFor();
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-invocation.png`) });
let current = await fixtureState(base);
assertEquals(current.uploads, start.uploads, "Feature discovery must not stage files");
assertEquals(
current.methods.filter((method) => method.method !== "list_completions"),
start.methods.filter((method) => method.method !== "list_completions"),
"completion must not submit, notify or execute controls",
);
// Pointer chip edit opens readable syntax, keyboard editing stays unexecuted.
await page.locator(".composer-typed-chip").click();
await page.waitForFunction(() =>
document.querySelector(".cm-content")?.textContent?.includes("count=2")
);
for (
let index = 0;
index < ', enabled=true, path="docs/release-notes.md"'.length;
index++
) {
await page.keyboard.press("ArrowLeft");
}
await page.keyboard.press("Backspace");
await page.keyboard.type("3");
await page.waitForFunction(() =>
document.querySelector(".cm-content")?.textContent?.includes("count=3")
);
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-editing.png`) });
await page.keyboard.press("Escape");
await page.keyboard.press("Control+End");
await page.keyboard.type(" ");
await page.locator(".composer-typed-chip").waitFor();
await page.keyboard.press("Backspace");
await page.keyboard.press("Backspace");
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
await page.keyboard.press("Control+z");
await page.locator(".composer-typed-chip").waitFor();
// Cancel a native picker; no upload and no accidental invocation remains.
await replaceDraft(page, "before /att");
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
const picker = page.waitForEvent("filechooser");
await page.keyboard.press("Enter");
await (await picker).setFiles([]);
assertEquals((await fixtureState(base)).uploads, start.uploads);
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
assertEquals((await page.locator(".cm-content").innerText()).trim(), "before");
await page.screenshot({
path: join(outputRoot, `${theme}-${width}-picker-cancelled.png`),
});
// Same generic adapter, this time choose a synthetic local file.
await page.keyboard.type("/att");
await page.getByRole("option").filter({ hasText: "attach" }).waitFor();
const uploadPicker = page.waitForEvent("filechooser");
await page.keyboard.press("Enter");
await (await uploadPicker).setFiles({
name: "release-notes.md",
mimeType: "text/markdown",
buffer: Buffer.from("Synthetic release notes"),
});
await page.locator(".composer-typed-chip").waitFor();
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-attachment.png`) });
current = await fixtureState(base);
assertEquals(current.uploads.length, start.uploads.length + 1);
await page.keyboard.press("Control+End");
await page.keyboard.press("Backspace");
assertEquals(await page.locator(".composer-typed-chip").count(), 0);
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-deleted.png`) });
// Staged resource is retained through undo, then released when draft is reset.
await page.keyboard.press("Control+z");
await page.locator(".composer-typed-chip").waitFor();
await replaceDraft(page, "literal /review(mode=brief)");
assertEquals(
await page.locator(".composer-typed-chip").count(),
0,
"unselected slash prose stays literal",
);
await page.keyboard.press("Escape");
// Explicit selection becomes the sole structured Feature payload on Submit.
await chooseReview(page);
await page.keyboard.press("Escape");
await page.keyboard.type('mode="thorough", count=3, enabled=false)');
await page.locator(".composer-typed-chip").waitFor();
const release = page.waitForResponse((response) =>
response.request().method() === "DELETE" && response.url().includes("/attachments/")
);
await page.keyboard.press("Control+Enter");
await page.waitForFunction(() => !document.querySelector(".composer-typed-chip"));
const releasedResponse = await release;
assertEquals(releasedResponse.status(), 204);
current = await fixtureState(base);
assert(
current.deletes.includes(current.uploads.at(-1)!),
"accepted draft boundary releases the abandoned uploaded atom",
);
const submissions = current.methods.slice(start.methods.length).filter((method) =>
method.method === "submit"
);
assertEquals(submissions.length, 1);
assertEquals(submissions[0].params?.input?.[0].kind, "feature_invoke");
assertEquals(submissions[0].params?.input?.[0].invocation?.identity, "fixture.review");
assertEquals(submissions[0].params?.input?.[0].invocation?.arguments, [
{ name: "mode", value: { kind: "string", value: "thorough" } },
{ name: "count", value: { kind: "integer", value: 3 } },
{ name: "enabled", value: { kind: "boolean", value: false } },
]);
// Chromium may report ERR_ABORTED after receiving the empty 204 cleanup
// response. Retain that diagnostic, but verify delivery by both response
// status and server DELETE evidence instead of claiming a lost operation.
assert(
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
"no page horizontal overflow",
);
const pickerEvidence = await pickerDiagnostics(page);
// Snapshot diagnostics only after the final browser awaits; classification
// and serialization must use the same immutable set of observed errors.
const recordedErrors = [...errors];
const acknowledgedDeleteDiagnostics = recordedErrors.filter((error) =>
[...acknowledgedDeletes].some((url) =>
error === `failed DELETE ${url} net::ERR_ABORTED`
)
);
assertEquals(
recordedErrors.filter((error) => !acknowledgedDeleteDiagnostics.includes(error)),
[],
);
await Deno.writeTextFile(
join(outputRoot, `${theme}-${width}-result.json`),
JSON.stringify(
{
theme,
width,
errors: recordedErrors,
acknowledgedDeleteDiagnostics,
acknowledgedDeleteResponseStatus: releasedResponse.status(),
picker: pickerEvidence,
status: "pass",
before: start,
after: current,
},
null,
2,
),
);
} catch (error) {
await page.screenshot({ path: join(outputRoot, `${theme}-${width}-failure.png`) });
await Deno.writeTextFile(
join(outputRoot, `${theme}-${width}-failure.json`),
JSON.stringify(
{
error: String(error),
errors,
body: await page.locator("body").innerText(),
fixture: await fixtureState(base),
},
null,
2,
),
);
throw error;
} finally {
await context.close();
}
}
}
} finally {
await browser.close();
}
} finally {
server.kill("SIGTERM");
await server.status;
}
});
+1 -1
View File
@@ -11,7 +11,7 @@
"web-ux": "deno run --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys cli.ts",
"check": "deno check cli.ts src/*.ts tests/*.ts browser-tests/*.ts",
"test": "deno test --allow-env --allow-read --allow-write --allow-run --allow-sys tests",
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts browser-tests/console_pending_shell_test.ts"
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts browser-tests/console_pending_shell_test.ts browser-tests/console_live_shell_test.ts browser-tests/console_live_admission_shell_test.ts"
},
"fmt": {
"lineWidth": 100,
@@ -0,0 +1,171 @@
{
"schemaVersion": 1,
"id": "console-live-baseline-dark",
"title": "Production Console Feature invocation draft lifecycle",
"baseUrl": "http://127.0.0.1:15286",
"colorScheme": "dark",
"reducedMotion": "reduce",
"personas": [
{
"id": "fixture",
"label": "Live synthetic Worker",
"auth": {
"kind": "anonymous"
}
}
],
"viewports": [
{
"label": "desktop",
"width": 1440,
"height": 900
},
{
"label": "narrow",
"width": 768,
"height": 900
},
{
"label": "mobile",
"width": 390,
"height": 844
}
],
"routes": [
{
"id": "console",
"label": "Live Console",
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
"goal": "Inspect the unchanged production Console shell and literal slash text before Feature completion existed.",
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
},
"capturePoints": [
{
"id": "initial",
"label": "Idle live Console"
},
{
"id": "slash-plain",
"label": "Literal unselected slash text",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "w"
},
{
"action": "press",
"selector": ".cm-content",
"key": "("
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
}
],
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
}
}
]
}
],
"processes": [
{
"id": "console-live-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"--allow-env=WEB_UX_BUILD_ROOT",
"browser-tests/console_live_fixture_server.ts",
"15286"
],
"env": {
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
},
"readyUrl": "http://127.0.0.1:15286/health"
}
]
}
@@ -0,0 +1,171 @@
{
"schemaVersion": 1,
"id": "console-live-baseline-light",
"title": "Production Console Feature invocation draft lifecycle",
"baseUrl": "http://127.0.0.1:15286",
"colorScheme": "light",
"reducedMotion": "reduce",
"personas": [
{
"id": "fixture",
"label": "Live synthetic Worker",
"auth": {
"kind": "anonymous"
}
}
],
"viewports": [
{
"label": "desktop",
"width": 1440,
"height": 900
},
{
"label": "narrow",
"width": 768,
"height": 900
},
{
"label": "mobile",
"width": 390,
"height": 844
}
],
"routes": [
{
"id": "console",
"label": "Live Console",
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
"goal": "Inspect the unchanged production Console shell and literal slash text before Feature completion existed.",
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
},
"capturePoints": [
{
"id": "initial",
"label": "Idle live Console"
},
{
"id": "slash-plain",
"label": "Literal unselected slash text",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "w"
},
{
"action": "press",
"selector": ".cm-content",
"key": "("
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
}
],
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
}
}
]
}
],
"processes": [
{
"id": "console-live-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"--allow-env=WEB_UX_BUILD_ROOT",
"browser-tests/console_live_fixture_server.ts",
"15286"
],
"env": {
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
},
"readyUrl": "http://127.0.0.1:15286/health"
}
]
}
@@ -0,0 +1,318 @@
{
"schemaVersion": 1,
"id": "console-live-dark",
"title": "Production Console Feature invocation draft lifecycle",
"baseUrl": "http://127.0.0.1:15286",
"colorScheme": "dark",
"reducedMotion": "reduce",
"personas": [
{
"id": "fixture",
"label": "Live synthetic Worker",
"auth": {
"kind": "anonymous"
}
}
],
"viewports": [
{
"label": "desktop",
"width": 1440,
"height": 900
},
{
"label": "narrow",
"width": 768,
"height": 900
},
{
"label": "mobile",
"width": 390,
"height": 844
}
],
"routes": [
{
"id": "console",
"label": "Live Console",
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
"goal": "Discover Feature metadata, complete typed arguments and inspect a draft chip without submitting or staging files.",
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
},
"capturePoints": [
{
"id": "initial",
"label": "Idle live Console"
},
{
"id": "slash-plain",
"label": "Literal unselected slash text",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "w"
},
{
"action": "press",
"selector": ".cm-content",
"key": "("
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
}
],
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
}
},
{
"id": "feature-menu",
"label": "Feature names",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+Space"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
}
]
},
{
"id": "argument-menu",
"label": "Argument values",
"interaction": [
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+Space"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
},
{
"action": "press",
"selector": ".cm-content",
"key": "Tab"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
}
]
},
{
"id": "typed-chip",
"label": "Selected typed invocation",
"interaction": [
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+End"
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
}
],
"ready": {
"kind": "selector",
"selector": ".composer-typed-chip"
}
}
]
}
],
"processes": [
{
"id": "console-live-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"--allow-env=WEB_UX_BUILD_ROOT",
"browser-tests/console_live_fixture_server.ts",
"15286"
],
"env": {
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
},
"readyUrl": "http://127.0.0.1:15286/health"
}
]
}
@@ -0,0 +1,318 @@
{
"schemaVersion": 1,
"id": "console-live-light",
"title": "Production Console Feature invocation draft lifecycle",
"baseUrl": "http://127.0.0.1:15286",
"colorScheme": "light",
"reducedMotion": "reduce",
"personas": [
{
"id": "fixture",
"label": "Live synthetic Worker",
"auth": {
"kind": "anonymous"
}
}
],
"viewports": [
{
"label": "desktop",
"width": 1440,
"height": 900
},
{
"label": "narrow",
"width": 768,
"height": 900
},
{
"label": "mobile",
"width": 390,
"height": 844
}
],
"routes": [
{
"id": "console",
"label": "Live Console",
"path": "/w/console-live-review/workers/W-901-console-live-fixture/console",
"goal": "Discover Feature metadata, complete typed arguments and inspect a draft chip without submitting or staging files.",
"dataState": "Live synthetic idle Worker; review enum/integer/boolean/worker-file arguments plus generic attachment adapter. No real account, file, or invocation execution.",
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
},
"capturePoints": [
{
"id": "initial",
"label": "Idle live Console"
},
{
"id": "slash-plain",
"label": "Literal unselected slash text",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "w"
},
{
"action": "press",
"selector": ".cm-content",
"key": "("
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
}
],
"ready": {
"kind": "selector",
"selector": ".cm-content[contenteditable=true]"
}
},
{
"id": "feature-menu",
"label": "Feature names",
"interaction": [
{
"action": "click",
"selector": ".cm-content"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+Space"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
}
]
},
{
"id": "argument-menu",
"label": "Argument values",
"interaction": [
{
"action": "press",
"selector": ".cm-content",
"key": "Control+a"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Backspace"
},
{
"action": "press",
"selector": ".cm-content",
"key": "/"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "v"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+Space"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
},
{
"action": "press",
"selector": ".cm-content",
"key": "Tab"
},
{
"action": "wait",
"ready": {
"kind": "selector",
"selector": "[role=option]"
}
}
]
},
{
"id": "typed-chip",
"label": "Selected typed invocation",
"interaction": [
{
"action": "press",
"selector": ".cm-content",
"key": "Escape"
},
{
"action": "press",
"selector": ".cm-content",
"key": "Control+End"
},
{
"action": "press",
"selector": ".cm-content",
"key": "b"
},
{
"action": "press",
"selector": ".cm-content",
"key": "r"
},
{
"action": "press",
"selector": ".cm-content",
"key": "i"
},
{
"action": "press",
"selector": ".cm-content",
"key": "e"
},
{
"action": "press",
"selector": ".cm-content",
"key": "f"
},
{
"action": "press",
"selector": ".cm-content",
"key": ")"
}
],
"ready": {
"kind": "selector",
"selector": ".composer-typed-chip"
}
}
]
}
],
"processes": [
{
"id": "console-live-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"--allow-env=WEB_UX_BUILD_ROOT",
"browser-tests/console_live_fixture_server.ts",
"15286"
],
"env": {
"WEB_UX_BUILD_ROOT": "${WEB_UX_BUILD_ROOT}"
},
"readyUrl": "http://127.0.0.1:15286/health"
}
]
}