fix(test): connect captured startup HTTP to the Web contract

This commit is contained in:
2026-11-06 11:20:13 +09:00
parent bf796f8db2
commit 29d41d883c
3 changed files with 181 additions and 1 deletions
+2 -1
View File
@@ -6,8 +6,9 @@
"dev": "deno run -A npm:vite@8.3.2 dev", "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", "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", "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: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: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: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", "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",
@@ -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);
});
@@ -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<string>();
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`,
);
}