diff --git a/docs/principle/ux-design.md b/docs/principle/ux-design.md
index 878ad0d64..12967bc81 100644
--- a/docs/principle/ux-design.md
+++ b/docs/principle/ux-design.md
@@ -16,6 +16,8 @@ Workspace Webは、Workspaceの確定した状態を確認し、注意が必要
存在しないWorkspace、閲覧できないWorkspace、読込みに失敗したWorkspaceは区別し、別のWorkspaceへ暗黙に切り替えない。読取り専用の場合も、利用できる閲覧機能を残す。
+ページ遷移はAPI応答を待たずに遷移先と読込み中表示を出す。旧ページを新しいURLの内容として残さず、遅れて届いた旧対象の応答で表示やcanonical遷移を上書きしない。同じ対象の再取得は別の更新中表示にし、Consoleの下書きなど編集中の内容を破棄しない。権限が必要な操作は確認済みのデータが届くまで表示しない。
+
## ページの目的
各routeの主要目的は、「Ticketを選ぶ」「Workerを確認する」「Repositoryのアクセスを設定する」のように、動詞と対象を一つずつ使って表現できるものにする。
diff --git a/tools/web-ux/browser-tests/deferred_navigation_test.ts b/tools/web-ux/browser-tests/deferred_navigation_test.ts
new file mode 100644
index 000000000..d945355f3
--- /dev/null
+++ b/tools/web-ux/browser-tests/deferred_navigation_test.ts
@@ -0,0 +1,496 @@
+///
+import { assert, assertEquals } from "jsr:@std/assert@1.0.19";
+import { type Browser, chromium, type Page, type Request, type Route } from "npm:playwright@1.59.1";
+import { loadScenario } from "../src/scenario.ts";
+import { startOwnedProcesses, stopOwnedProcesses } from "../src/processes.ts";
+
+// Explicit browser boundary: production static assets + synthetic loopback API.
+// No real Backend auth, Worker execution, or application test hooks.
+// The caller supplies the built workspace via the standard suite layout; for isolated
+// runs, copy this suite into a tree whose web/workspace/build points at isolated assets.
+// This suite NEVER runs a build, mutates assets, or falls back to another static root.
+const staticRoot = new URL("../../../web/workspace/build/", import.meta.url).pathname;
+const output = new URL("../../../.yoi/dev/navigation-visual/", import.meta.url).pathname;
+const phase = "after";
+const listPath = "/w/tickets/tickets";
+const detailPath = "/w/tickets/tickets/T-2-read-only-investigation";
+const otherDetailPath = "/w/tickets/tickets/T-3-independent-review-before-merge";
+
+async function withFixture(run: (browser: Browser, base: string) => Promise) {
+ // A missing build is an explicit failure; no alternate root or implicit build.
+ await Deno.stat(`${staticRoot}index.html`);
+ const scenarioPath = new URL("../scenarios/tickets.json", import.meta.url).pathname;
+ const scenario = await loadScenario(scenarioPath);
+ 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}`;
+ scenario.processes![0].args![4] = String(port);
+ scenario.processes![0].args![5] = staticRoot;
+ scenario.processes![0].readyUrl = `${base}/health`;
+ const processes = await startOwnedProcesses(
+ scenario.processes!,
+ scenarioPath,
+ `${output}${phase}/logs/${port}`,
+ [],
+ );
+ let browser: Browser | null = null;
+ try {
+ browser = await chromium.launch({ headless: true });
+ await run(browser, base);
+ } finally {
+ await browser?.close();
+ assertEquals(await stopOwnedProcesses(processes), [], "owned fixture cleanup");
+ }
+}
+
+async function bounded(promise: Promise): Promise {
+ let timer: number | undefined;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error("Response gate timed out")), 15_000);
+ }),
+ ]);
+ } finally {
+ clearTimeout(timer);
+ }
+}
+
+// Fetch the real synthetic response, but do not deliver it to the browser until release.
+// held means response bytes exist in Playwright, NOT requestfinished in the browser.
+async function responseGate(
+ page: Page,
+ pattern: string | RegExp,
+ matches: (request: Request) => boolean = () => true,
+ replacement?: { status: number; body: string; contentType: string },
+ transform?: (body: Record) => unknown,
+) {
+ const held = Promise.withResolvers();
+ const releaseSignal = Promise.withResolvers();
+ const delivered = Promise.withResolvers();
+ let closed = true;
+ let intercepted = false;
+ let browserFinished = false;
+ let gatedRequest: Request | undefined;
+ const onFinished = (request: Request) => {
+ if (request === gatedRequest) browserFinished = true;
+ };
+ page.on("requestfinished", onFinished);
+ const handler = async (route: Route) => {
+ if (intercepted || !matches(route.request())) return await route.fallback();
+ intercepted = true;
+ gatedRequest = route.request();
+ try {
+ const response = replacement ? undefined : await route.fetch();
+ const json = transform && response ? transform(await response.json()) : undefined;
+ held.resolve();
+ await releaseSignal.promise;
+ if (replacement) await route.fulfill(replacement);
+ else await route.fulfill({ response, ...(json === undefined ? {} : { json }) });
+ delivered.resolve();
+ } catch (error) {
+ held.reject(error);
+ delivered.reject(error);
+ }
+ };
+ await page.route(pattern, handler);
+ // Attach handlers immediately so cancellation during teardown isn't an unhandled rejection.
+ void held.promise.catch(() => {});
+ void delivered.promise.catch(() => {});
+ return {
+ held: () => bounded(held.promise),
+ assertClosed() {
+ assert(intercepted, "API response must be intercepted first");
+ assert(closed, "response gate must remain CLOSED during navigation assertions");
+ assertEquals(browserFinished, false, "browser must not have finished the held request");
+ },
+ release() {
+ closed = false;
+ releaseSignal.resolve();
+ },
+ delivered: () => bounded(delivered.promise),
+ async dispose() {
+ closed = false;
+ releaseSignal.resolve();
+ await page.unroute(pattern, handler);
+ page.off("requestfinished", onFinished);
+ },
+ };
+}
+type Gate = Awaited>;
+
+async function loading(page: Page, gate: Gate, path: string, title: RegExp, updating = false) {
+ await gate.held();
+ gate.assertClosed();
+ await page.waitForURL((url) => url.pathname + url.search === path, { timeout: 5_000 });
+ await page.getByRole("heading", { name: title, level: 1 }).waitFor({ timeout: 5_000 });
+ await page.getByRole("status").filter({ hasText: updating ? /^Updating…$/ : /Loading/ })
+ .first().waitFor({ timeout: 5_000 });
+ gate.assertClosed();
+}
+
+// Synthetic links exercise the public SvelteKit delegated link handler, not a full reload.
+// Used only where the fixture has no product link (cross-workspace and stale slug scenarios).
+async function navigate(page: Page, path: string) {
+ await page.evaluate((path) => {
+ const link = document.createElement("a");
+ link.href = path;
+ link.dataset.sveltekitPreloadData = "off";
+ document.body.append(link);
+ link.click();
+ link.remove();
+ }, path);
+}
+async function clickTickets(page: Page) {
+ // Actual product sidebar link, even when the mobile sidebar is folded.
+ await page.locator("a.sidebar-link").filter({ hasText: /^Tickets$/ })
+ .evaluate((link) => (link as HTMLAnchorElement).click());
+}
+async function paint(page: Page) {
+ await page.evaluate(() =>
+ new Promise((resolve) =>
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
+ )
+ );
+}
+async function capture(page: Page, name: string, gate?: Gate) {
+ gate?.assertClosed();
+ await paint(page);
+ const directory = `${output}${phase}`;
+ await Deno.mkdir(directory, { recursive: true });
+ await page.screenshot({ path: `${directory}/${name}.png`, fullPage: true });
+ // Safe, fixture-only evidence. Deliberately no cookies, storage, headers, trace or HAR.
+ const context = await page.evaluate(() => ({
+ path: location.pathname + location.search,
+ title: document.title,
+ headings: [...document.querySelectorAll("h1")].map((node) => node.textContent),
+ statuses: [...document.querySelectorAll('[role="status"]')].map((node) => node.textContent),
+ liveRegions: [...document.querySelectorAll('[aria-live="polite"]')].map((node) =>
+ node.textContent
+ ),
+ alerts: [...document.querySelectorAll('[role="alert"]')].map((node) => node.textContent),
+ viewport: { width: innerWidth, height: innerHeight },
+ theme: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
+ pageOverflow: document.documentElement.scrollWidth > innerWidth,
+ }));
+ assertEquals(context.pageOverflow, false, "navigation state must not overflow viewport");
+ await Deno.writeTextFile(
+ `${directory}/${name}.json`,
+ JSON.stringify(
+ {
+ ...context,
+ responseGate: gate ? "closed" : "resolved",
+ staticRoot,
+ boundary: "synthetic loopback fixture; no real Backend authentication",
+ },
+ null,
+ 2,
+ ),
+ );
+ gate?.assertClosed();
+}
+async function rows(page: Page) {
+ await page.locator("a.ticket-list-row").first().waitFor();
+}
+async function search(page: Page, query: string) {
+ await page.getByRole("searchbox", { name: "Search tickets", exact: true }).fill(query);
+ await page.getByRole("button", { name: "Search", exact: true }).click();
+}
+const queryPattern = "**/api/w/tickets/tickets/query";
+
+Deno.test("immediate navigation: Orchestrator -> Tickets -> detail before responses, visual matrix", async () => {
+ await withFixture(async (browser, base) => {
+ for (const colorScheme of ["light", "dark"] as const) {
+ for (const width of [1440, 768, 390, 320]) {
+ const context = await browser.newContext({
+ colorScheme,
+ viewport: { width, height: 900 },
+ reducedMotion: "reduce",
+ });
+ try {
+ const page = await context.newPage();
+ const errors: string[] = [];
+ page.on("pageerror", (error) => errors.push(String(error)));
+ await page.goto(`${base}/w/tickets/orchestrator`);
+ await page.locator(".orchestrator-assignment").first().waitFor();
+ const listGate = await responseGate(page, queryPattern);
+ try {
+ await clickTickets(page);
+ await loading(page, listGate, listPath, /^Tickets$/);
+ assertEquals(await page.locator(".orchestrator-assignment").count(), 0);
+ await capture(page, `${colorScheme}-${width}-tickets-gated`, listGate);
+ listGate.release();
+ await rows(page);
+ await capture(page, `${colorScheme}-${width}-tickets-resolved`);
+ } finally {
+ await listGate.dispose();
+ }
+ const detailGate = await responseGate(page, "**/api/w/tickets/tickets/T-2");
+ try {
+ await page.locator(`a.ticket-list-row[href="${detailPath}"]`).click();
+ await loading(page, detailGate, detailPath, /^Ticket$/);
+ assertEquals(await page.locator("a.ticket-list-row").count(), 0);
+ assertEquals(
+ await page.getByRole("button", { name: "Release assignment", exact: true }).count(),
+ 0,
+ );
+ await capture(page, `${colorScheme}-${width}-detail-gated`, detailGate);
+ detailGate.release();
+ await page.getByRole("heading", { name: "Read-only investigation", exact: true })
+ .waitFor();
+ await capture(page, `${colorScheme}-${width}-detail-resolved`);
+ } finally {
+ await detailGate.dispose();
+ }
+ assertEquals(errors, []);
+ } finally {
+ await context.close();
+ }
+ }
+ }
+ });
+});
+
+Deno.test("immediate navigation: query and back/forward commit URLs with response gates closed", async () => {
+ await withFixture(async (browser, base) => {
+ const page = await browser.newPage();
+ await page.goto(`${base}${listPath}`);
+ await rows(page);
+ const open = await responseGate(page, queryPattern);
+ try {
+ await search(page, "state:closed");
+ await loading(page, open, `${listPath}?q=state%3Aclosed`, /^Tickets$/, true);
+ open.release();
+ await page.locator(".ticket-list-key").filter({ hasText: /^T-4$/ }).waitFor();
+ } finally {
+ await open.dispose();
+ }
+ for (const direction of ["back", "forward"] as const) {
+ const gate = await responseGate(page, queryPattern);
+ try {
+ // Don't await goBack/goForward's optional load lifecycle while API is gated.
+ await page.evaluate(
+ (direction) => direction === "back" ? history.back() : history.forward(),
+ direction,
+ );
+ const path = direction === "back" ? listPath : `${listPath}?q=state%3Aclosed`;
+ await loading(page, gate, path, /^Tickets$/, true);
+ await capture(page, `query-${direction}-gated`, gate);
+ gate.release();
+ await page.getByRole("searchbox", { name: "Search tickets", exact: true })
+ .waitFor();
+ await page.waitForFunction(
+ (value) =>
+ (document.querySelector('input[type="search"]') as HTMLInputElement)?.value === value,
+ direction === "back" ? "state:open" : "state:closed",
+ );
+ await rows(page);
+ if (direction === "forward") {
+ assertEquals(await page.locator(".ticket-list-key").allTextContents(), ["T-4"]);
+ }
+ } finally {
+ await gate.dispose();
+ }
+ }
+ });
+});
+
+Deno.test("immediate navigation: rapid A -> B rejects late A data and stale canonical redirects", async () => {
+ await withFixture(async (browser, base) => {
+ const page = await browser.newPage();
+ await page.goto(`${base}${listPath}`);
+ await rows(page);
+ // First canonical A (late data), then an obsolete slug A (late canonical redirect).
+ for (const pathA of [detailPath, "/w/tickets/tickets/T-2-obsolete-title"]) {
+ const gateA = await responseGate(page, "**/api/w/tickets/tickets/T-2");
+ const gateB = await responseGate(page, "**/api/w/tickets/tickets/T-3");
+ try {
+ await navigate(page, pathA);
+ await loading(page, gateA, pathA, /^Ticket$/);
+ await navigate(page, otherDetailPath);
+ await loading(page, gateB, otherDetailPath, /^Ticket$/);
+ gateA.assertClosed();
+ gateB.release();
+ await page.getByRole("heading", { name: "Independent review before merge", exact: true })
+ .waitFor();
+ const response = page.waitForResponse((response) =>
+ new URL(response.url()).pathname === "/api/w/tickets/tickets/T-2"
+ );
+ gateA.release();
+ await response;
+ await gateA.delivered();
+ await paint(page);
+ assertEquals(new URL(page.url()).pathname, otherDetailPath);
+ assert(
+ await page.getByRole("heading", {
+ name: "Independent review before merge",
+ exact: true,
+ }).isVisible(),
+ );
+ assertEquals(
+ await page.getByRole("heading", { name: "Read-only investigation", exact: true })
+ .count(),
+ 0,
+ );
+ await capture(
+ page,
+ pathA === detailPath
+ ? "late-old-response-resolved"
+ : "stale-canonical-redirect-resolved",
+ );
+ } finally {
+ await gateA.dispose();
+ await gateB.dispose();
+ }
+ await navigate(page, listPath);
+ await rows(page);
+ }
+ });
+});
+
+Deno.test("immediate navigation: slow workspace switch clears old identity and permission projection", async () => {
+ await withFixture(async (browser, base) => {
+ const page = await browser.newPage();
+ // Same existing fixture server, distinct identities and explicit opposite permissions.
+ await page.route("**/api/w/*/workspace", async (route) => {
+ const response = await route.fetch();
+ const body = await response.json();
+ const old = body.workspace_id === "tickets";
+ body.display_name = old ? "Old privileged workspace" : "New read-only workspace";
+ body.permissions = {
+ manage_repositories: old,
+ manage_secrets: old,
+ manage_runtimes: old,
+ delete_workspace: old,
+ };
+ await route.fulfill({ response, json: body });
+ });
+ await page.goto(`${base}${detailPath}`);
+ await page.getByRole("heading", { name: "Read-only investigation", exact: true }).waitFor();
+ await page.getByText("Old privileged workspace", { exact: true }).first().waitFor();
+ assert(
+ await page.getByRole("button", { name: "Release assignment", exact: true }).isVisible(),
+ );
+ // route.fetch bypasses other Playwright handlers: transform this held response too.
+ const gate = await responseGate(
+ page,
+ "**/api/w/tickets-readonly/workspace",
+ () => true,
+ undefined,
+ (body) => ({ ...body, display_name: "New read-only workspace" }),
+ );
+ const target = detailPath.replace("/w/tickets/", "/w/tickets-readonly/");
+ try {
+ await navigate(page, target);
+ await loading(page, gate, target, /^Workspace$/);
+ assertEquals(await page.getByText("Old privileged workspace", { exact: true }).count(), 0);
+ assertEquals(
+ await page.getByRole("button", { name: "Release assignment", exact: true }).count(),
+ 0,
+ );
+ assertEquals(
+ await page.locator('a[href="/w/tickets/settings"], a[href="/w/tickets"]').count(),
+ 0,
+ );
+ await capture(page, "workspace-switch-gated", gate);
+ gate.release();
+ await page.getByRole("heading", { name: "Read-only investigation", exact: true }).waitFor();
+ await page.getByText("New read-only workspace", { exact: true }).first().waitFor();
+ assertEquals(
+ await page.getByRole("button", { name: "Release assignment", exact: true }).count(),
+ 0,
+ );
+ await capture(page, "workspace-switch-resolved");
+ } finally {
+ await gate.dispose();
+ }
+ });
+});
+
+Deno.test("immediate navigation: cold load and unrelated global catalog response cannot block destination", async () => {
+ await withFixture(async (browser, base) => {
+ const page = await browser.newPage();
+ const catalog = await responseGate(page, /\/api\/workspaces(?:\?|$)/);
+ const tickets = await responseGate(page, queryPattern);
+ try {
+ await page.goto(`${base}${listPath}`, { waitUntil: "domcontentloaded" });
+ await catalog.held();
+ await loading(page, tickets, listPath, /^Tickets$/);
+ catalog.assertClosed();
+ await capture(page, "cold-load-gated", tickets);
+ tickets.release();
+ await rows(page);
+ // Global catalog remains unavailable while the page is fully usable.
+ catalog.assertClosed();
+ const detail = await responseGate(page, "**/api/w/tickets/tickets/T-2");
+ try {
+ await page.locator(`a.ticket-list-row[href="${detailPath}"]`).click();
+ await loading(page, detail, detailPath, /^Ticket$/);
+ catalog.assertClosed();
+ detail.release();
+ await page.getByRole("heading", { name: "Read-only investigation", exact: true })
+ .waitFor();
+ catalog.assertClosed();
+ await capture(page, "global-catalog-delayed-detail-resolved");
+ } finally {
+ await detail.dispose();
+ }
+ catalog.release();
+ } finally {
+ await catalog.dispose();
+ await tickets.dispose();
+ }
+ });
+});
+
+Deno.test("immediate navigation: failed read stays at destination and gated Retry recovers", async () => {
+ await withFixture(async (browser, base) => {
+ const page = await browser.newPage();
+ await page.goto(`${base}/w/tickets/orchestrator`);
+ await page.locator(".orchestrator-assignment").first().waitFor();
+ const failure = await responseGate(page, queryPattern, () => true, {
+ status: 503,
+ contentType: "application/json",
+ body: JSON.stringify({ message: "Navigation fixture unavailable" }),
+ });
+ try {
+ await clickTickets(page);
+ await loading(page, failure, listPath, /^Tickets$/);
+ failure.release();
+ await page.getByRole("alert").filter({
+ hasText: /Ticket Browser API request failed with HTTP 503/,
+ })
+ .waitFor();
+ assertEquals(new URL(page.url()).pathname, listPath);
+ assertEquals(
+ await page.getByText("No tickets match this query.", { exact: true }).count(),
+ 0,
+ );
+ await capture(page, "read-error");
+ } finally {
+ await failure.dispose();
+ }
+ const retry = await responseGate(page, queryPattern);
+ try {
+ await page.getByRole("button", { name: "Retry", exact: true }).click();
+ // Product Retry performs a local query read, not SvelteKit invalidation.
+ // Its polite live-region status must be visible while that response stays CLOSED.
+ await retry.held();
+ retry.assertClosed();
+ assertEquals(new URL(page.url()).pathname, listPath);
+ await page.getByRole("heading", { name: "Tickets", level: 1, exact: true }).waitFor();
+ await page.locator('.ticket-list-page-state[aria-live="polite"]')
+ .filter({ hasText: /^Loading…$/ }).waitFor();
+ retry.assertClosed();
+ await capture(page, "read-retry-gated", retry);
+ retry.release();
+ await rows(page);
+ assertEquals(await page.getByRole("alert").count(), 0);
+ await capture(page, "read-retry-resolved");
+ } finally {
+ await retry.dispose();
+ }
+ });
+});
diff --git a/tools/web-ux/deno.json b/tools/web-ux/deno.json
index b2774d68e..7a01f2a29 100644
--- a/tools/web-ux/deno.json
+++ b/tools/web-ux/deno.json
@@ -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/ticket_shell_test.ts browser-tests/ticket_principal_test.ts 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/workdir_removal_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"
+ "test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/deferred_navigation_test.ts browser-tests/ticket_shell_test.ts browser-tests/ticket_principal_test.ts 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/workdir_removal_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,
diff --git a/web/workspace/deno.json b/web/workspace/deno.json
index 82c3b97ad..a1d4b1e2f 100644
--- a/web/workspace/deno.json
+++ b/web/workspace/deno.json
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@8.3.2 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json",
- "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
+ "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/navigation/deferred.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/api/ticket-case.test.ts src/lib/workspace/api/ticket-operations.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts src/lib/workspace/tickets/ticket-search.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
"test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts",
"test:browser:empty-workspace": "deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/visual-review/empty-workspace.browser.test.ts",
"test:browser:drive": "deno test --config test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/drive-browser/drive.browser.test.ts",
diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
index bd0e09663..756b10be9 100644
--- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
+++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts
@@ -220,11 +220,11 @@ Deno.test("design lab demonstrates nested sidebar slots without flattening level
Deno.test("workspace Worker list lives on the dedicated Workers page", async () => {
const workspacePage = await Deno.readTextFile(
- new URL("./../../../routes/w/[workspaceId]/+page.svelte", import.meta.url),
+ new URL("./../../../routes/w/[workspaceId]/PageContent.svelte", import.meta.url),
);
const workersPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/workers/+page.svelte",
+ "./../../../routes/w/[workspaceId]/workers/PageContent.svelte",
import.meta.url,
),
);
@@ -337,7 +337,7 @@ Deno.test("workspace Tickets surface provides query list and lifecycle controls"
);
const ticketsPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/tickets/+page.svelte",
+ "./../../../routes/w/[workspaceId]/tickets/PageContent.svelte",
import.meta.url,
),
);
@@ -349,7 +349,7 @@ Deno.test("workspace Tickets surface provides query list and lifecycle controls"
);
const ticketDetailPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte",
+ "./../../../routes/w/[workspaceId]/tickets/[ticketId]/PageContent.svelte",
import.meta.url,
),
);
@@ -364,7 +364,7 @@ Deno.test("workspace Tickets surface provides query list and lifecycle controls"
);
const repositoryPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/repositories/[repositoryKey]/+page.svelte",
+ "./../../../routes/w/[workspaceId]/repositories/[repositoryKey]/PageContent.svelte",
import.meta.url,
),
);
@@ -444,7 +444,7 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
);
const subjectIndexPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/memory/+page.svelte",
+ "./../../../routes/w/[workspaceId]/memory/PageContent.svelte",
import.meta.url,
),
);
@@ -456,7 +456,7 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
);
const subjectPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/memory/[subjectId]/+page.svelte",
+ "./../../../routes/w/[workspaceId]/memory/[subjectId]/PageContent.svelte",
import.meta.url,
),
);
@@ -468,7 +468,7 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
);
const memoryDetailPage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/+page.svelte",
+ "./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/PageContent.svelte",
import.meta.url,
),
);
@@ -544,7 +544,7 @@ Deno.test("root layout keeps Workspace selection explicit", async () => {
Deno.test("Worker Console uses protocol observation events without transcript fetch", async () => {
const consolePage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
+ "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/PageContent.svelte",
import.meta.url,
),
);
@@ -569,7 +569,7 @@ Deno.test("Worker Console uses protocol observation events without transcript fe
Deno.test("Worker Console models availability separately from projected lines", async () => {
const consolePage = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
+ "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/PageContent.svelte",
import.meta.url,
),
);
@@ -619,7 +619,7 @@ Deno.test("Worker Console models availability separately from projected lines",
Deno.test("Worker Console owns its narrower centered shell width", async () => {
const page = await Deno.readTextFile(
new URL(
- "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
+ "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/PageContent.svelte",
import.meta.url,
),
);
@@ -733,7 +733,7 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
Deno.test("Model information stays below Composer in one compact monospace row", async () => {
const page = await Deno.readTextFile(new URL(
- "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
+ "./../../../routes/w/[workspaceId]/workers/[workerRef]/console/PageContent.svelte",
import.meta.url,
));
const composer = page.indexOf('