From 29d41d883cfb7a2e980b8977de9276225d2e089e Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 6 Nov 2026 11:20:13 +0900 Subject: [PATCH] fix(test): connect captured startup HTTP to the Web contract --- web/workspace/deno.json | 3 +- .../worker-startup-backend-contract.test.ts | 55 ++++++++ .../test/worker-startup-backend-contract.ts | 124 ++++++++++++++++++ 3 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 web/workspace/test/worker-startup-backend-contract.test.ts create mode 100644 web/workspace/test/worker-startup-backend-contract.ts diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 9f72fda46..faeaf193f 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,8 +6,9 @@ "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,../../crates/error-contract/fixtures --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV src/lib/workspace/feature-permissions.test.ts 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/auth/login.test.ts src/lib/workspace/auth/routes.test.ts src/lib/workspace/auth/hosting.test.ts src/lib/workspace/auth/hosting-enrollment.test.ts src/lib/workspace/auth/activation.test.ts src/lib/workspace/auth/hosting-accounts.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/failure/receive.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/subjektiv/routes.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/alert-order.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 src/lib/workspace/workers/startup-failure.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 test/config-source/adoption-api.test.ts tests/memory-api.test.ts src/lib/workspace/memory/permissions.test.ts src/lib/workspace/memory/subject-body.test.ts src/lib/workspace/memory/conversation-attempt.test.ts src/lib/workspace/memory/conversation-api.test.ts tests/subject-delete.test.ts tests/journal-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 src/lib/workspace/api/ticket-parent.test.ts src/lib/workspace/tickets/ticket-parent-controller.test.ts", + "test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema,../../crates/error-contract/fixtures --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV src/lib/workspace/feature-permissions.test.ts 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/auth/login.test.ts src/lib/workspace/auth/routes.test.ts src/lib/workspace/auth/hosting.test.ts src/lib/workspace/auth/hosting-enrollment.test.ts src/lib/workspace/auth/activation.test.ts src/lib/workspace/auth/hosting-accounts.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/failure/receive.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/subjektiv/routes.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/alert-order.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 src/lib/workspace/workers/startup-failure.test.ts test/worker-startup-backend-contract.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 test/config-source/adoption-api.test.ts tests/memory-api.test.ts src/lib/workspace/memory/permissions.test.ts src/lib/workspace/memory/subject-body.test.ts src/lib/workspace/memory/conversation-attempt.test.ts src/lib/workspace/memory/conversation-api.test.ts tests/subject-delete.test.ts tests/journal-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 src/lib/workspace/api/ticket-parent.test.ts src/lib/workspace/tickets/ticket-parent-controller.test.ts", "test:journal:backend-contract": "cd ../.. && mkdir -p target && cargo test -p yoi-workspace-server --lib journal_browser_web_contract -- --nocapture > target/journal-browser-contract.log && cd web/workspace && deno run --allow-read=../../target/journal-browser-contract.log test/journal-backend-contract.ts ../../target/journal-browser-contract.log", + "test:worker:backend-contract": "cd ../.. && mkdir -p target && cargo test -p yoi-workspace-server --lib session_registration_connected_tests::browser_http_ -- --nocapture > target/worker-browser-contract.log 2>&1 && cd web/workspace && deno run --allow-read=../../target/worker-browser-contract.log test/worker-startup-backend-contract.ts ../../target/worker-browser-contract.log", "test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts", "test:browser:activation": "mkdir -p ../../target/web-ux/tmp && TMPDIR=../../target/web-ux/tmp deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/activation-browser/activation-shell.browser.test.ts", "test:browser:hosting": "mkdir -p ../../target/web-ux/tmp && TMPDIR=../../target/web-ux/tmp deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/hosting-browser/hosting-shell.browser.test.ts test/hosting-browser/setup-shell.browser.test.ts", diff --git a/web/workspace/test/worker-startup-backend-contract.test.ts b/web/workspace/test/worker-startup-backend-contract.test.ts new file mode 100644 index 000000000..1d2428c04 --- /dev/null +++ b/web/workspace/test/worker-startup-backend-contract.test.ts @@ -0,0 +1,55 @@ +import { verifyStartupHttpCaptures } from "./worker-startup-backend-contract.ts"; +import { startupFixture } from "../src/lib/workspace/workers/startup-failure.test-fixtures.ts"; + +// bridge自身の試験。合成markerを実handler実行の証拠とは扱わない。 +function capture(name: "denial" | "lost") { + const body = startupFixture(name); + return `T812_BROWSER_STARTUP_PROBLEM status=${body.status} ignored reason phrase content_type=application/problem+json body=${ + JSON.stringify(body) + }`; +} +async function rejects(log: string) { + try { + await verifyStartupHttpCaptures(log); + } catch (error) { + if (String(error).includes("test-private-body")) { + throw new Error("Capture body disclosed"); + } + return; + } + throw new Error( + "Expected the capture bridge to reject incomplete/invalid evidence", + ); +} + +Deno.test("Startup HTTP bridge requires both captured cases and passes real response shape through the production Web caller", async () => { + const result = await verifyStartupHttpCaptures( + `unrelated test output\n${capture("denial")}\n${capture("lost")}\n`, + ); + if (result.records !== 2 || result.cases !== 2) { + throw new Error("Unexpected capture coverage"); + } +}); +Deno.test("Startup HTTP bridge refuses missing, malformed, oversized, mismatched and legacy captures without exposing raw body", async () => { + const denial = capture("denial"); + const lost = capture("lost"); + for ( + const log of [ + "", + denial, + `${denial}\n${denial}`, + `${denial}\n${lost.replace("status=502", "status=500")}`, + `${denial}\n${ + lost.replace( + "content_type=application/problem+json", + "content_type=application/json", + ) + }`, + `${denial}\nT812_BROWSER_STARTUP_PROBLEM status=502 content_type=application/problem+json body=test-private-body`, + `${denial}\nT812_BROWSER_STARTUP_PROBLEM status=502 content_type=application/problem+json body=${ + "x".repeat(32769) + }`, + `${denial}\nT812_BROWSER_STARTUP_PROBLEM test-private-body`, + ] + ) await rejects(log); +}); diff --git a/web/workspace/test/worker-startup-backend-contract.ts b/web/workspace/test/worker-startup-backend-contract.ts new file mode 100644 index 000000000..2d25108bc --- /dev/null +++ b/web/workspace/test/worker-startup-backend-contract.ts @@ -0,0 +1,124 @@ +// 実認可・Runtime・保存を通したRust connected試験の公開HTTP捕捉だけを受ける。 +// Web送信/受信/表示modelへの接続であり、このprocess自体は認可や保存を代替証明しない。 +import { buildCreateWorkspaceWorkerRequest } from "../src/lib/workspace/sidebar/worker-launch.ts"; +import { + submitWorkerLaunch, + workerLaunchNeedsReconciliation, + workerStartupNotice, +} from "../src/lib/workspace/workers/startup-failure.ts"; + +const marker = "T812_BROWSER_STARTUP_PROBLEM "; +const maxLogBytes = 16 * 1024 * 1024; + +export async function verifyStartupHttpCaptures(log: string) { + if (new TextEncoder().encode(log).length > maxLogBytes) { + throw new Error("Startup capture log exceeds the test input limit"); + } + const request = buildCreateWorkspaceWorkerRequest({ + runtime_id: "embedded", + display_name: "HTTP contract check", + profile: "", + subjektiv_subject_id: "", + initial_text: "", + workdir_attachments: [], + working_directory_repository_key: "", + working_directory_selector: "HEAD", + }); + const cases = new Set(); + let records = 0; + for (const line of log.split("\n")) { + if (!line.includes(marker)) continue; + const metadata = line.slice(line.indexOf(marker) + marker.length); + const match = + /^status=(\d{3})(?: [A-Za-z ]+)? content_type=([^ ]+) body=(.*)$/u.exec( + metadata, + ); + if (!match) throw new Error("Malformed Startup HTTP capture metadata"); + const [, status, contentType, body] = match; + let calls = 0; + const path = "/api/w/contract/workers"; + const result = await submitWorkerLaunch( + ((url: unknown, init?: { method?: string; body?: unknown }) => { + calls++; + if ( + url !== path || init?.method !== "POST" || + init.body !== JSON.stringify(request) + ) { + throw new Error("Startup caller did not preserve the launch request"); + } + return Promise.resolve( + new Response(body, { + status: Number(status), + headers: { "content-type": contentType }, + }), + ); + }) as typeof fetch, + path, + request, + ); + if ( + calls !== 1 || result.kind !== "failed" || + result.receipt.kind !== "problem" + ) { + // 受信bodyや他のRust log行を例外へ転載しない。 + throw new Error( + "Actual Startup HTTP capture was not accepted by the Web contract", + ); + } + const receipt = result.receipt; + const failure = receipt.problem.yoi_failure; + const cause = failure.details; + if ( + failure.domain !== "session_registration" || + !("required_operation" in cause) || + cause.required_operation !== "session.record" + ) { + throw new Error("Expected connected Session registration HTTP captures"); + } + if ( + cause.reason === "permission_denied" && cause.principal === "subject" && + Number(status) === 409 + ) cases.add("denial"); + else if (cause.reason === "outcome_unknown" && Number(status) === 502) { + cases.add("response_lost"); + } else throw new Error("Unexpected connected Session registration result"); + if ( + !receipt.extension || + receipt.extension.operation.execution !== "errored" || + receipt.extension.backend_result_persistence !== "persisted" + ) { + throw new Error( + "Connected owner execution and saved result observations were not retained", + ); + } + if ( + workerStartupNotice(receipt).explanation !== receipt.problem.detail || + !workerLaunchNeedsReconciliation(receipt) + ) { + throw new Error( + "Web cause presentation or uncertain-result gating differs from the connected HTTP result", + ); + } + records++; + } + if (!cases.has("denial") || !cases.has("response_lost")) { + throw new Error( + "Missing real denial or response-loss Startup HTTP capture", + ); + } + return { records, cases: cases.size }; +} + +if (import.meta.main) { + const path = Deno.args[0]; + if (!path) { + throw new Error("Expected the connected browser_http test output path"); + } + if ((await Deno.stat(path)).size > maxLogBytes) { + throw new Error("Startup capture log exceeds the test input limit"); + } + const result = await verifyStartupHttpCaptures(await Deno.readTextFile(path)); + console.log( + `Actual Backend Startup responses accepted by production Web caller, decoder, notice and gating: ${result.records} records, ${result.cases} cases`, + ); +}