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,
"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": {
+20 -20
View File
@@ -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",
@@ -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",
@@ -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<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 =
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,
@@ -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<string>();
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) {