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 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. 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 ## Adding a scenario
1. Name the concrete user task and expected data state; do not write “looks correct”. 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", "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", "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": "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": { "fmt": {
"lineWidth": 100, "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"
}
]
}