fix(web): validate corrected startup HTTP and align generated schemas

This commit is contained in:
2026-11-06 12:14:20 +09:00
parent 9454f153fe
commit a65875fd66
5 changed files with 92 additions and 54 deletions
+20 -20
View File
@@ -99,25 +99,6 @@
"maxLength": 128, "maxLength": 128,
"minLength": 1 "minLength": 1
}, },
"Diagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"maxLength": 128,
"minLength": 1
},
"explanation": {
"type": "string",
"maxLength": 2048,
"minLength": 1
}
},
"required": [
"code",
"explanation"
]
},
"FailureContext": { "FailureContext": {
"description": "Common metadata is untrusted after deserialization. No audience label confers trust.", "description": "Common metadata is untrusted after deserialization. No audience label confers trust.",
"type": "object", "type": "object",
@@ -143,7 +124,7 @@
"diagnostics": { "diagnostics": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/$defs/Diagnostic" "$ref": "#/$defs/FailureDiagnostic"
}, },
"maxItems": 8 "maxItems": 8
}, },
@@ -165,6 +146,25 @@
"supplementary_omitted" "supplementary_omitted"
] ]
}, },
"FailureDiagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"maxLength": 128,
"minLength": 1
},
"explanation": {
"type": "string",
"maxLength": 2048,
"minLength": 1
}
},
"required": [
"code",
"explanation"
]
},
"OperationContext": { "OperationContext": {
"type": "object", "type": "object",
"properties": { "properties": {
+20 -20
View File
@@ -82,25 +82,6 @@
"maxLength": 128, "maxLength": 128,
"minLength": 1 "minLength": 1
}, },
"Diagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"maxLength": 128,
"minLength": 1
},
"explanation": {
"type": "string",
"maxLength": 2048,
"minLength": 1
}
},
"required": [
"code",
"explanation"
]
},
"FailureContext": { "FailureContext": {
"description": "Common metadata is untrusted after deserialization. No audience label confers trust.", "description": "Common metadata is untrusted after deserialization. No audience label confers trust.",
"type": "object", "type": "object",
@@ -126,7 +107,7 @@
"diagnostics": { "diagnostics": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/$defs/Diagnostic" "$ref": "#/$defs/FailureDiagnostic"
}, },
"maxItems": 8 "maxItems": 8
}, },
@@ -148,6 +129,25 @@
"supplementary_omitted" "supplementary_omitted"
] ]
}, },
"FailureDiagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"maxLength": 128,
"minLength": 1
},
"explanation": {
"type": "string",
"maxLength": 2048,
"minLength": 1
}
},
"required": [
"code",
"explanation"
]
},
"FailureEnvelope": { "FailureEnvelope": {
"description": "DTO/schema source only. Deserialization does not produce a ProjectedFailure.", "description": "DTO/schema source only. Deserialization does not produce a ProjectedFailure.",
"type": "object", "type": "object",
@@ -10,7 +10,7 @@ import type {
WorkingDirectoryRemovalResponse, WorkingDirectoryRemovalResponse,
WorkingDirectorySource, WorkingDirectorySource,
WorkingDirectorySummary, WorkingDirectorySummary,
} from "../../generated/workdir-api"; } from "../../generated/workdir-api.ts";
const SUMMARY_KEYS = new Set([ const SUMMARY_KEYS = new Set([
"working_directory_id", "working_directory_id",
@@ -6,12 +6,44 @@ import {
stopOwnedProcesses, stopOwnedProcesses,
} from "../../../../tools/web-ux/src/processes.ts"; } from "../../../../tools/web-ux/src/processes.ts";
import { startupFixture } from "../../src/lib/workspace/workers/startup-failure.test-fixtures.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保証ではない。 // production build + exact T812 HTTP fixtures。派生故障・操作結果はWeb試験用でありServer保証ではない。
// beforeは旧production buildで同fixtureを撮影するためだけに明示選択し、afterの成功と混同しない。 // beforeは旧production buildで同fixtureを撮影するためだけに明示選択し、afterの成功と混同しない。
Deno.test("Worker failure cause, independent outcomes, keyboard details and safe reconciliation", async () => { Deno.test("Worker failure cause, independent outcomes, keyboard details and safe reconciliation", async () => {
const before = Deno.env.get("T813_PHASE") === "before"; 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<typeof startupFixture>[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 = const scenarioPath =
new URL("./optional-repository.json", import.meta.url).pathname; new URL("./optional-repository.json", import.meta.url).pathname;
const scenario = await loadScenario(scenarioPath); 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) => { await page.route("**/api/w/home-empty/workers", async (route) => {
if (route.request().method() !== "POST") return route.continue(); if (route.request().method() !== "POST") return route.continue();
submissions++; submissions++;
@@ -144,7 +176,7 @@ Deno.test("Worker failure cause, independent outcomes, keyboard details and safe
] ]
) { ) {
body = JSON.parse( body = JSON.parse(
JSON.stringify(startupFixture( JSON.stringify(fixture(
name === "lost" name === "lost"
? "lost" ? "lost"
: name === "rejected" : name === "rejected"
@@ -294,8 +326,12 @@ Deno.test("Worker failure cause, independent outcomes, keyboard details and safe
route: "/w/home-empty/workers/new", route: "/w/home-empty/workers/new",
persona: "synthetic owner", persona: "synthetic owner",
name, name,
captureInput,
captureSha256,
fixture: ["denial", "lost"].includes(name) 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", : "synthetic derivative/fault",
width, width,
theme: colorScheme, theme: colorScheme,
@@ -1,6 +1,6 @@
// 実認可・Runtime・保存を通したRust connected試験の公開HTTP捕捉だけを受ける。 // 実認可・Runtime・保存を通したRust connected試験の公開HTTP捕捉だけを受ける。
// Web送信/受信/表示modelへの接続であり、このprocess自体は認可や保存を代替証明しない。 // 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 { import {
submitWorkerLaunch, submitWorkerLaunch,
workerLaunchNeedsReconciliation, workerLaunchNeedsReconciliation,
@@ -14,17 +14,18 @@ export async function verifyStartupHttpCaptures(log: string) {
if (new TextEncoder().encode(log).length > maxLogBytes) { if (new TextEncoder().encode(log).length > maxLogBytes) {
throw new Error("Startup capture log exceeds the test input limit"); throw new Error("Startup capture log exceeds the test input limit");
} }
const request = buildCreateWorkspaceWorkerRequest({ const request: CreateWorkspaceWorkerRequest = {
runtime_id: "embedded", runtime_id: "embedded",
display_name: "HTTP contract check", display_name: "HTTP contract check",
profile: "", profile: null,
subjektiv_subject_id: "", ticket_assignment: null,
initial_text: "", initial_submit: [],
workdir_attachments: [], workdir_attachments: [],
working_directory_repository_key: "", feature_connections: {},
working_directory_selector: "HEAD", control_operation_id: null,
}); };
const cases = new Set<string>(); const cases = new Set<string>();
const captures: { status: number; contentType: string; body: string }[] = [];
let records = 0; let records = 0;
for (const line of log.split("\n")) { for (const line of log.split("\n")) {
if (!line.includes(marker)) continue; 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", "Web cause presentation or uncertain-result gating differs from the connected HTTP result",
); );
} }
captures.push({ status: Number(status), contentType, body });
records++; records++;
} }
if (!cases.has("denial") || !cases.has("response_lost")) { 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", "Missing real denial or response-loss Startup HTTP capture",
); );
} }
return { records, cases: cases.size }; return { records, cases: cases.size, captures };
} }
if (import.meta.main) { if (import.meta.main) {