From a65875fd669b7b2bad50e3cb8cfca6ef6b940290 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 6 Nov 2026 12:14:20 +0900 Subject: [PATCH] fix(web): validate corrected startup HTTP and align generated schemas --- openapi/failure.schema.json | 40 ++++++++--------- openapi/problem-details.schema.json | 40 ++++++++--------- .../src/lib/workspace/api/workdirs.ts | 2 +- .../worker-failure.browser.test.ts | 44 +++++++++++++++++-- .../test/worker-startup-backend-contract.ts | 20 +++++---- 5 files changed, 92 insertions(+), 54 deletions(-) diff --git a/openapi/failure.schema.json b/openapi/failure.schema.json index e0226145c..1bbd2b424 100644 --- a/openapi/failure.schema.json +++ b/openapi/failure.schema.json @@ -99,25 +99,6 @@ "maxLength": 128, "minLength": 1 }, - "Diagnostic": { - "type": "object", - "properties": { - "code": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "explanation": { - "type": "string", - "maxLength": 2048, - "minLength": 1 - } - }, - "required": [ - "code", - "explanation" - ] - }, "FailureContext": { "description": "Common metadata is untrusted after deserialization. No audience label confers trust.", "type": "object", @@ -143,7 +124,7 @@ "diagnostics": { "type": "array", "items": { - "$ref": "#/$defs/Diagnostic" + "$ref": "#/$defs/FailureDiagnostic" }, "maxItems": 8 }, @@ -165,6 +146,25 @@ "supplementary_omitted" ] }, + "FailureDiagnostic": { + "type": "object", + "properties": { + "code": { + "type": "string", + "maxLength": 128, + "minLength": 1 + }, + "explanation": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + } + }, + "required": [ + "code", + "explanation" + ] + }, "OperationContext": { "type": "object", "properties": { diff --git a/openapi/problem-details.schema.json b/openapi/problem-details.schema.json index 91788cbfb..768cc1330 100644 --- a/openapi/problem-details.schema.json +++ b/openapi/problem-details.schema.json @@ -82,25 +82,6 @@ "maxLength": 128, "minLength": 1 }, - "Diagnostic": { - "type": "object", - "properties": { - "code": { - "type": "string", - "maxLength": 128, - "minLength": 1 - }, - "explanation": { - "type": "string", - "maxLength": 2048, - "minLength": 1 - } - }, - "required": [ - "code", - "explanation" - ] - }, "FailureContext": { "description": "Common metadata is untrusted after deserialization. No audience label confers trust.", "type": "object", @@ -126,7 +107,7 @@ "diagnostics": { "type": "array", "items": { - "$ref": "#/$defs/Diagnostic" + "$ref": "#/$defs/FailureDiagnostic" }, "maxItems": 8 }, @@ -148,6 +129,25 @@ "supplementary_omitted" ] }, + "FailureDiagnostic": { + "type": "object", + "properties": { + "code": { + "type": "string", + "maxLength": 128, + "minLength": 1 + }, + "explanation": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + } + }, + "required": [ + "code", + "explanation" + ] + }, "FailureEnvelope": { "description": "DTO/schema source only. Deserialization does not produce a ProjectedFailure.", "type": "object", diff --git a/web/workspace/src/lib/workspace/api/workdirs.ts b/web/workspace/src/lib/workspace/api/workdirs.ts index 15619fe84..12df3d81d 100644 --- a/web/workspace/src/lib/workspace/api/workdirs.ts +++ b/web/workspace/src/lib/workspace/api/workdirs.ts @@ -10,7 +10,7 @@ import type { WorkingDirectoryRemovalResponse, WorkingDirectorySource, WorkingDirectorySummary, -} from "../../generated/workdir-api"; +} from "../../generated/workdir-api.ts"; const SUMMARY_KEYS = new Set([ "working_directory_id", diff --git a/web/workspace/test/visual-review/worker-failure.browser.test.ts b/web/workspace/test/visual-review/worker-failure.browser.test.ts index ffa0b9d9a..7c563121f 100644 --- a/web/workspace/test/visual-review/worker-failure.browser.test.ts +++ b/web/workspace/test/visual-review/worker-failure.browser.test.ts @@ -6,12 +6,44 @@ import { stopOwnedProcesses, } from "../../../../tools/web-ux/src/processes.ts"; import { startupFixture } from "../../src/lib/workspace/workers/startup-failure.test-fixtures.ts"; +import { verifyStartupHttpCaptures } from "../worker-startup-backend-contract.ts"; // production build + exact T812 HTTP fixtures。派生故障・操作結果はWeb試験用でありServer保証ではない。 // beforeは旧production buildで同fixtureを撮影するためだけに明示選択し、afterの成功と混同しない。 Deno.test("Worker failure cause, independent outcomes, keyboard details and safe reconciliation", async () => { const before = Deno.env.get("T813_PHASE") === "before"; - const phase = before ? "before-real-contract" : "after"; + const captureInput = Deno.env.get("T813_HTTP_CAPTURE_LOG"); + const captureLog = captureInput + ? await Deno.readTextFile(captureInput) + : null; + const captureSha256 = captureLog + ? Array.from( + new Uint8Array( + await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(captureLog), + ), + ), + ).map((b) => b.toString(16).padStart(2, "0")).join("") + : null; + const captures = captureLog + ? (await verifyStartupHttpCaptures(captureLog)).captures + : null; + function fixture(name: Parameters[0]) { + if (captures && (name === "denial" || name === "lost")) { + const capture = captures.find((item) => + item.status === (name === "denial" ? 409 : 502) + ); + assert(capture, "Expected both validated real HTTP captures"); + return JSON.parse(capture.body); + } + return startupFixture(name); + } + const phase = before + ? "before-real-contract" + : captureInput + ? "after-connected" + : "after"; const scenarioPath = new URL("./optional-repository.json", import.meta.url).pathname; const scenario = await loadScenario(scenarioPath); @@ -115,7 +147,7 @@ Deno.test("Worker failure cause, independent outcomes, keyboard details and safe }, }); }); - let body = JSON.parse(JSON.stringify(startupFixture("denial"))); + let body = JSON.parse(JSON.stringify(fixture("denial"))); await page.route("**/api/w/home-empty/workers", async (route) => { if (route.request().method() !== "POST") return route.continue(); submissions++; @@ -144,7 +176,7 @@ Deno.test("Worker failure cause, independent outcomes, keyboard details and safe ] ) { body = JSON.parse( - JSON.stringify(startupFixture( + JSON.stringify(fixture( name === "lost" ? "lost" : name === "rejected" @@ -294,8 +326,12 @@ Deno.test("Worker failure cause, independent outcomes, keyboard details and safe route: "/w/home-empty/workers/new", persona: "synthetic owner", name, + captureInput, + captureSha256, fixture: ["denial", "lost"].includes(name) - ? "T812 exact real HTTP body" + ? captureInput + ? "forwarded exact T812 corrected-source HTTP capture" + : "T812 exact real HTTP body" : "synthetic derivative/fault", width, theme: colorScheme, diff --git a/web/workspace/test/worker-startup-backend-contract.ts b/web/workspace/test/worker-startup-backend-contract.ts index 2d25108bc..83313f563 100644 --- a/web/workspace/test/worker-startup-backend-contract.ts +++ b/web/workspace/test/worker-startup-backend-contract.ts @@ -1,6 +1,6 @@ // 実認可・Runtime・保存を通したRust connected試験の公開HTTP捕捉だけを受ける。 // Web送信/受信/表示modelへの接続であり、このprocess自体は認可や保存を代替証明しない。 -import { buildCreateWorkspaceWorkerRequest } from "../src/lib/workspace/sidebar/worker-launch.ts"; +import type { CreateWorkspaceWorkerRequest } from "../src/lib/generated/worker-launch-api.ts"; import { submitWorkerLaunch, workerLaunchNeedsReconciliation, @@ -14,17 +14,18 @@ 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({ + const request: CreateWorkspaceWorkerRequest = { runtime_id: "embedded", display_name: "HTTP contract check", - profile: "", - subjektiv_subject_id: "", - initial_text: "", + profile: null, + ticket_assignment: null, + initial_submit: [], workdir_attachments: [], - working_directory_repository_key: "", - working_directory_selector: "HEAD", - }); + feature_connections: {}, + control_operation_id: null, + }; const cases = new Set(); + const captures: { status: number; contentType: string; body: string }[] = []; let records = 0; for (const line of log.split("\n")) { if (!line.includes(marker)) continue; @@ -99,6 +100,7 @@ export async function verifyStartupHttpCaptures(log: string) { "Web cause presentation or uncertain-result gating differs from the connected HTTP result", ); } + captures.push({ status: Number(status), contentType, body }); records++; } if (!cases.has("denial") || !cases.has("response_lost")) { @@ -106,7 +108,7 @@ export async function verifyStartupHttpCaptures(log: string) { "Missing real denial or response-loss Startup HTTP capture", ); } - return { records, cases: cases.size }; + return { records, cases: cases.size, captures }; } if (import.meta.main) {