test: block stale workbench live smoke

This commit is contained in:
Code Queue Review
2026-05-22 23:58:19 +00:00
parent ff4b3f9167
commit 8b2521d817
4 changed files with 496 additions and 4 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
+102
View File
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
classifyLiveDeploymentIdentity,
classifyLiveWebAssetIdentity
} from "./src/dev-cloud-workbench-smoke-lib.mjs";
const sourceIdentity = Object.freeze({
status: "observed",
commitId: "ff4b3f928f76a0b46cd7998a2a865f3e1caf02c8",
shortCommitId: "ff4b3f928f76",
reportCommitId: "ff4b3f928f76",
worktreeState: "clean",
dirty: false
});
const expectedRuntimeIdentity = Object.freeze({
status: "observed",
serviceId: "hwlab-cloud-api",
source: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
commitId: "7de6edd",
imageTag: "7de6edd",
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd"
});
test("live workbench identity passes only when runtime commit or image tag matches current source", () => {
assert.equal(
classifyLiveDeploymentIdentity(sourceIdentity, {
status: "observed",
commitId: "7de6edd2c41f",
imageTag: "c7de474"
}, expectedRuntimeIdentity).status,
"pass"
);
assert.equal(
classifyLiveDeploymentIdentity(sourceIdentity, {
status: "observed",
commitId: "unknown",
imageTag: "7de6edd"
}, expectedRuntimeIdentity).status,
"pass"
);
const stale = classifyLiveDeploymentIdentity(sourceIdentity, {
status: "observed",
commitId: "c7de474",
imageTag: "c7de474"
}, expectedRuntimeIdentity);
assert.equal(stale.status, "blocked");
assert.match(stale.summary, /Deployment drift/u);
assert.match(stale.reason, /does not match current source desired runtime identity/u);
});
test("live workbench identity blocks when source identity is dirty or runtime identity is absent", () => {
const dirty = classifyLiveDeploymentIdentity(
{ ...sourceIdentity, worktreeState: "dirty", dirty: true, reportCommitId: "unknown" },
{ status: "observed", commitId: "7de6edd", imageTag: "7de6edd" },
expectedRuntimeIdentity
);
assert.equal(dirty.status, "blocked");
assert.match(dirty.reason, /not cleanly attributable/u);
const missingRuntime = classifyLiveDeploymentIdentity(sourceIdentity, {
status: "not_observed",
commitId: "not_observed",
imageTag: "not_observed"
}, expectedRuntimeIdentity);
assert.equal(missingRuntime.status, "blocked");
assert.match(missingRuntime.reason, /not observed/u);
});
test("live workbench identity uses deploy desired runtime identity instead of git HEAD", () => {
const deployed = classifyLiveDeploymentIdentity(sourceIdentity, {
status: "observed",
commitId: "7de6edd",
imageTag: "7de6edd"
}, expectedRuntimeIdentity);
assert.equal(deployed.status, "pass");
assert.equal(deployed.expectedCommit, "7de6edd");
assert.equal(deployed.expectedReportCommitId, "ff4b3f928f76");
assert.equal(deployed.expectedSource, "deploy/artifact-catalog.dev.json+deploy/deploy.json");
});
test("live web asset identity blocks stale deployed primary assets", () => {
const pass = classifyLiveWebAssetIdentity([
{ path: "index.html", status: "match" },
{ path: "styles.css", status: "match" },
{ path: "app.mjs", status: "match" }
]);
assert.equal(pass.status, "pass");
const stale = classifyLiveWebAssetIdentity([
{ path: "index.html", status: "match" },
{ path: "styles.css", status: "mismatch" },
{ path: "app.mjs", status: "mismatch" }
]);
assert.equal(stale.status, "blocked");
assert.deepEqual(stale.mismatches, ["styles.css", "app.mjs"]);
assert.match(stale.summary, /Deployment drift/u);
});
+243 -2
View File
@@ -1,5 +1,6 @@
import fs from "node:fs";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -31,6 +32,8 @@ const requiredWebAssets = Object.freeze([
"third_party/marked/LICENSE"
]);
const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs"]);
const chineseWorkbenchLabels = Object.freeze([
"HWLAB 云工作台",
"硬件资源",
@@ -369,13 +372,64 @@ function runStaticSmoke() {
async function runLiveSmoke(args) {
const checks = [];
const blockers = [];
const sourceIdentity = observeSourceIdentity();
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
blocker: "observability_blocker",
evidence: deploymentIdentity.evidence,
observations: deploymentIdentity
});
const http = await fetchText(args.url);
addCheck(checks, blockers, "live-http-html", http.ok && http.status === 200 && /text\/html/u.test(http.contentType) && liveHtmlLooksLikeWorkbench(http.body), "Live URL serves the Cloud Workbench HTML.", {
blocker: "runtime_blocker",
evidence: [args.url, `HTTP ${http.status ?? "none"}`, http.contentType ?? "no content-type"]
});
const webAssetIdentity = http.ok
? await inspectLiveWebAssetIdentity(args.url, http.body)
: {
status: "blocked",
summary: "Live web asset identity could not be checked because the 16666 HTML fetch failed.",
assets: []
};
addCheck(checks, blockers, "live-web-assets-current-main", webAssetIdentity.status, webAssetIdentity.summary, {
blocker: "observability_blocker",
evidence: webAssetIdentity.assets.map((asset) => `${asset.path}: ${asset.status}`),
observations: webAssetIdentity
});
const identityBlocked = deploymentIdentity.status !== "pass" || webAssetIdentity.status !== "pass";
if (identityBlocked) {
return baseReport({
mode: "live",
url: args.url,
status: "blocked",
checks,
blockers,
evidenceLevel: "BLOCKED",
devLive: false,
runtimeIdentity,
sourceIdentity,
deploymentIdentity,
expectedRuntimeIdentity,
webAssetIdentity,
safety: {
prodTouched: false,
servicesRestarted: false,
heavyE2E: false,
hardwareWriteApis: false,
sourceIsDevLive: false,
liveMode: "deployment-identity-preflight",
codeAgentBrowserJourneySkipped: true,
retainedApiFields: ["runtime.commitId", "runtime.imageTag", "web asset hash/status"],
statement: "Live smoke blocked before the browser chat journey because deployed runtime/assets do not match the current source identity; it did not post Code Agent chat, call hardware write APIs, restart services, or read secret values."
}
});
}
const dom = http.ok ? await inspectLiveDom(args.url) : { status: "skip", summary: "Browser DOM check skipped because HTTP fetch failed.", evidence: [] };
addCheck(checks, blockers, "live-browser-dom", dom.status, dom.summary, {
blocker: dom.status === "pass" ? null : "observability_blocker",
@@ -418,6 +472,10 @@ async function runLiveSmoke(args) {
evidenceLevel: status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED",
devLive: status === "pass",
runtimeIdentity,
sourceIdentity,
deploymentIdentity,
expectedRuntimeIdentity,
webAssetIdentity,
safety: {
prodTouched: false,
servicesRestarted: false,
@@ -431,8 +489,22 @@ async function runLiveSmoke(args) {
});
}
function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, help = undefined, safety, url = null, runtimeIdentity = null }) {
const sourceIdentity = observeSourceIdentity();
function baseReport({
mode,
status,
checks,
blockers,
evidenceLevel,
devLive,
help = undefined,
safety,
url = null,
runtimeIdentity = null,
sourceIdentity = observeSourceIdentity(),
deploymentIdentity = null,
expectedRuntimeIdentity = null,
webAssetIdentity = null
}) {
const liveJourneyPassed = mode === "live" && status === "pass";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
@@ -473,6 +545,9 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he
devLive,
sourceIdentity,
runtimeIdentity: runtimeIdentity ?? notObservedRuntimeIdentity("live runtime identity was not observed in this smoke mode"),
...(expectedRuntimeIdentity ? { expectedRuntimeIdentity } : {}),
...(deploymentIdentity ? { deploymentIdentity } : {}),
...(webAssetIdentity ? { webAssetIdentity } : {}),
sourceContract: {
status: "pass",
documents: [
@@ -630,6 +705,172 @@ function runGit(args) {
}
}
export function classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity = null) {
const expectedCommit = sanitizeRevision(expectedRuntimeIdentity?.commitId ?? sourceIdentity?.commitId);
const expectedShort = expectedCommit === "unknown" ? "unknown" : expectedCommit.slice(0, 7);
const expectedReport = sourceIdentity?.reportCommitId ?? "unknown";
const expectedImageTag = sanitizeRuntimeString(expectedRuntimeIdentity?.imageTag ?? expectedShort).toLowerCase();
const observedRuntimeCommit = sanitizeRevision(runtimeIdentity?.commitId);
const observedImageTag = sanitizeRuntimeString(runtimeIdentity?.imageTag).toLowerCase();
const runtimeObserved = runtimeIdentity?.status === "observed";
const expectedObserved = expectedRuntimeIdentity === null || expectedRuntimeIdentity?.status === "observed";
const sourceClean =
sourceIdentity?.status === "observed" &&
sourceIdentity?.worktreeState === "clean" &&
expectedReport !== "unknown";
const accepted = new Set([...commitIdentityVariants(expectedCommit), ...commitIdentityVariants(expectedImageTag)]);
const runtimeMatches =
runtimeObserved &&
sourceClean &&
expectedObserved &&
expectedCommit !== "unknown" &&
(identityVariantMatches(accepted, observedRuntimeCommit) || identityVariantMatches(accepted, observedImageTag));
if (runtimeMatches) {
return {
status: "pass",
expectedCommit,
expectedReportCommitId: expectedReport,
expectedImageTag,
expectedSource: expectedRuntimeIdentity?.source ?? "source-git-head",
observedRuntimeCommit,
observedImageTag,
evidence: [
`expected=${expectedShort}`,
`expectedImageTag=${expectedImageTag}`,
`runtimeCommit=${observedRuntimeCommit}`,
`imageTag=${observedImageTag}`
],
summary: "Live API runtime identity matches the current source desired runtime identity."
};
}
const reason = !sourceClean
? "current source identity is not cleanly attributable"
: !expectedObserved || expectedCommit === "unknown"
? "source desired runtime identity was not observed"
: !runtimeObserved
? "live API runtime identity was not observed"
: "live API runtime commit/image tag does not match current source desired runtime identity";
return {
status: "blocked",
expectedCommit,
expectedReportCommitId: expectedReport,
expectedImageTag,
expectedSource: expectedRuntimeIdentity?.source ?? "source-git-head",
observedRuntimeCommit,
observedImageTag,
reason,
evidence: [
`expected=${expectedShort}`,
`expectedImageTag=${expectedImageTag}`,
`runtimeCommit=${observedRuntimeCommit}`,
`imageTag=${observedImageTag}`
],
summary: `Deployment drift: ${reason}.`
};
}
function observeExpectedRuntimeIdentity(serviceId) {
const deploy = readJsonOrNull("deploy/deploy.json");
const catalog = readJsonOrNull("deploy/artifact-catalog.dev.json");
const deployService = Array.isArray(deploy?.services)
? deploy.services.find((service) => service?.serviceId === serviceId)
: null;
const catalogService = Array.isArray(catalog?.services)
? catalog.services.find((service) => service?.serviceId === serviceId)
: null;
const image = sanitizeRuntimeString(catalogService?.image ?? deployService?.image);
const imageTag = sanitizeRuntimeString(catalogService?.imageTag ?? deployService?.env?.HWLAB_IMAGE_TAG ?? imageTagFromImage(image));
const commitId = sanitizeRevision(catalogService?.commitId ?? deployService?.env?.HWLAB_COMMIT_ID ?? deploy?.commitId ?? catalog?.commitId ?? imageTag);
const sources = [];
if (catalogService) sources.push("deploy/artifact-catalog.dev.json");
if (deployService || deploy?.commitId) sources.push("deploy/deploy.json");
const observed = commitId !== "unknown" || imageTag !== "unknown";
return {
status: observed ? "observed" : "not_observed",
serviceId,
source: sources.length > 0 ? sources.join("+") : "not_observed",
commitId,
imageTag,
image,
summary: observed
? "Expected DEV runtime identity was derived from current source deploy desired state, not from live health."
: "Expected DEV runtime identity was not found in current source deploy desired state."
};
}
function readJsonOrNull(relativePath) {
try {
return JSON.parse(readText(relativePath));
} catch {
return null;
}
}
function imageTagFromImage(image) {
if (typeof image !== "string") return "unknown";
const match = image.match(/:([^:/]+)$/u);
return match?.[1] ?? "unknown";
}
function commitIdentityVariants(commitId) {
if (typeof commitId !== "string" || !/^[a-f0-9]{7,40}$/u.test(commitId)) return new Set();
return new Set([commitId, commitId.slice(0, 12), commitId.slice(0, 7)]);
}
function identityVariantMatches(accepted, observed) {
if (typeof observed !== "string" || observed === "unknown" || observed === "not_observed") return false;
if (accepted.has(observed)) return true;
return [...accepted].some((expected) => expected.length >= 7 && observed.startsWith(expected));
}
async function inspectLiveWebAssetIdentity(baseUrl, fetchedIndexHtml) {
const assets = [];
for (const assetPath of liveIdentityWebAssets) {
const sourceText = readText(`web/hwlab-cloud-web/${assetPath}`);
const live = assetPath === "index.html"
? { ok: true, status: 200, contentType: "text/html; charset=utf-8", body: fetchedIndexHtml }
: await fetchText(new URL(assetPath, baseUrl).toString());
assets.push(compareLiveAsset(assetPath, live, sourceText));
}
return classifyLiveWebAssetIdentity(assets);
}
export function classifyLiveWebAssetIdentity(assets) {
const mismatches = assets.filter((asset) => asset.status !== "match");
return {
status: mismatches.length === 0 ? "pass" : "blocked",
assets,
mismatches: mismatches.map((asset) => asset.path),
summary: mismatches.length === 0
? "Live 16666 primary web assets match the current clean source files."
: `Deployment drift: live 16666 primary web assets differ from current source (${mismatches.map((asset) => asset.path).join(", ")}).`
};
}
function compareLiveAsset(assetPath, live, sourceText) {
const liveText = typeof live?.body === "string" ? live.body : "";
const sourceHash = hashText(sourceText);
const liveHash = hashText(liveText);
const fetched = live?.ok === true && live?.status === 200;
const matches = fetched && liveText === sourceText;
return {
path: assetPath,
status: matches ? "match" : fetched ? "mismatch" : "fetch_failed",
httpStatus: live?.status ?? null,
contentType: live?.contentType ?? "",
sourceHash,
liveHash,
sourceLength: sourceText.length,
liveLength: liveText.length
};
}
function hashText(value) {
return createHash("sha256").update(value).digest("hex").slice(0, 16);
}
function assetsExist() {
return requiredWebAssets.every((file) => fs.existsSync(path.join(webRoot, file)));
}
+150 -1
View File
@@ -1381,7 +1381,7 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
});
assertArray(report.checks, `${label}.checks`);
assert.ok(report.checks.length >= 3, `${label}.checks must include HTTP, DOM, and Code Agent journey checks`);
assert.ok(report.checks.length >= 3, `${label}.checks must include HTTP plus either deployment identity or browser journey checks`);
assertUnique(report.checks.map((check) => check.id), `${label}.checks ids`);
const checks = new Map(report.checks.map((check, index) => {
const checkLabel = `${label}.checks[${index}]`;
@@ -1400,6 +1400,16 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
}
return [check.id, check];
}));
const deploymentIdentityPreflight =
report.status === "blocked" &&
report.safety?.liveMode === "deployment-identity-preflight" &&
checks.has("live-runtime-current-main") &&
checks.has("live-web-assets-current-main");
if (deploymentIdentityPreflight) {
assertDevCloudWorkbenchDeploymentPreflight(report, label, checks);
return;
}
for (const requiredCheck of [
"live-http-html",
"live-browser-dom",
@@ -1431,6 +1441,10 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`);
assert.equal(journey.observations.request.urlPath, "/v1/agent/chat", `${label}.live-code-agent-browser-journey.request.urlPath`);
assertObject(journey.observations.response, `${label}.live-code-agent-browser-journey.response`);
if (report.status === "blocked" && journey.status === "blocked") {
assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey);
return;
}
assert.equal(journey.observations.response.status, "completed", `${label}.live-code-agent-browser-journey.response.status`);
for (const field of ["provider", "model", "backend", "traceId"]) {
assertString(journey.observations.response[field], `${label}.live-code-agent-browser-journey.response.${field}`);
@@ -1465,6 +1479,141 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey);
}
function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) {
const response = journey.observations.response;
assert.equal(response.status, "failed", `${label}.live-code-agent-browser-journey.response.status`);
assert.equal(response.hasReply, false, `${label}.live-code-agent-browser-journey.response.hasReply`);
assertObject(response.error, `${label}.live-code-agent-browser-journey.response.error`);
assert.ok(
["provider_unavailable", "untrusted_completion"].includes(response.error.code),
`${label}.live-code-agent-browser-journey.response.error.code`
);
for (const field of ["provider", "model", "backend", "traceId"]) {
assertString(response[field], `${label}.live-code-agent-browser-journey.response.${field}`);
}
for (const forbiddenField of ["conversationId", "sessionId", "messageId", "reply"]) {
assert.equal(
Object.hasOwn(response, forbiddenField),
false,
`${label}.live-code-agent-browser-journey.response must not retain ${forbiddenField}`
);
}
assertObject(journey.observations.ui, `${label}.live-code-agent-browser-journey.ui`);
assert.equal(journey.observations.ui.agentChatStatus, "发送失败", `${label}.live-code-agent-browser-journey.ui.agentChatStatus`);
assert.equal(journey.observations.ui.completedMessageVisible, false, `${label}.live-code-agent-browser-journey.ui.completedMessageVisible`);
assert.equal(journey.observations.ui.failedMessageVisible, true, `${label}.live-code-agent-browser-journey.ui.failedMessageVisible`);
assertBlockers(report.blockers, `${label}.blockers`);
assert.ok(
report.blockers.some((blocker) => blocker.scope === "live-code-agent-browser-journey"),
`${label}.blockers must include live-code-agent-browser-journey`
);
assertObject(report.safety, `${label}.safety`);
for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive"]) {
assert.equal(report.safety[field], false, `${label}.safety.${field}`);
}
assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`);
assert.deepEqual(
report.safety.retainedApiFields,
["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv"],
`${label}.safety.retainedApiFields`
);
assertString(report.safety.statement, `${label}.safety.statement`);
}
function assertDevCloudWorkbenchDeploymentPreflight(report, label, checks) {
for (const requiredCheck of [
"live-runtime-current-main",
"live-http-html",
"live-web-assets-current-main"
]) {
assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`);
}
const runtimeCheck = checks.get("live-runtime-current-main");
const webAssetCheck = checks.get("live-web-assets-current-main");
assert.ok(["pass", "blocked"].includes(runtimeCheck.status), `${label}.live-runtime-current-main.status`);
assert.ok(["pass", "blocked"].includes(webAssetCheck.status), `${label}.live-web-assets-current-main.status`);
assert.ok(
runtimeCheck.status === "blocked" || webAssetCheck.status === "blocked",
`${label}.deployment preflight requires runtime or web asset drift`
);
assertObject(runtimeCheck.observations, `${label}.live-runtime-current-main.observations`);
assertObject(webAssetCheck.observations, `${label}.live-web-assets-current-main.observations`);
assertObject(report.expectedRuntimeIdentity, `${label}.expectedRuntimeIdentity`);
for (const field of ["status", "serviceId", "source", "commitId", "imageTag", "image", "summary"]) {
assertString(report.expectedRuntimeIdentity[field], `${label}.expectedRuntimeIdentity.${field}`);
}
assert.ok(["observed", "not_observed"].includes(report.expectedRuntimeIdentity.status), `${label}.expectedRuntimeIdentity.status`);
assertObject(report.deploymentIdentity, `${label}.deploymentIdentity`);
assert.equal(report.deploymentIdentity.status, runtimeCheck.status, `${label}.deploymentIdentity.status`);
assertString(report.deploymentIdentity.expectedCommit, `${label}.deploymentIdentity.expectedCommit`);
assertString(report.deploymentIdentity.expectedImageTag, `${label}.deploymentIdentity.expectedImageTag`);
assertString(report.deploymentIdentity.expectedSource, `${label}.deploymentIdentity.expectedSource`);
assertString(report.deploymentIdentity.observedRuntimeCommit, `${label}.deploymentIdentity.observedRuntimeCommit`);
assertString(report.deploymentIdentity.observedImageTag, `${label}.deploymentIdentity.observedImageTag`);
if (report.deploymentIdentity.status === "blocked") {
assertString(report.deploymentIdentity.reason, `${label}.deploymentIdentity.reason`);
}
assertString(report.deploymentIdentity.summary, `${label}.deploymentIdentity.summary`);
assertStringArray(report.deploymentIdentity.evidence, `${label}.deploymentIdentity.evidence`, { minLength: 4 });
assert.deepEqual(
runtimeCheck.observations,
report.deploymentIdentity,
`${label}.live-runtime-current-main.observations`
);
assertObject(report.webAssetIdentity, `${label}.webAssetIdentity`);
assert.equal(report.webAssetIdentity.status, webAssetCheck.status, `${label}.webAssetIdentity.status`);
assertArray(report.webAssetIdentity.assets, `${label}.webAssetIdentity.assets`);
assertStringArray(report.webAssetIdentity.mismatches, `${label}.webAssetIdentity.mismatches`, {
minLength: report.webAssetIdentity.status === "blocked" ? 1 : 0
});
assertString(report.webAssetIdentity.summary, `${label}.webAssetIdentity.summary`);
for (const [index, asset] of report.webAssetIdentity.assets.entries()) {
const assetLabel = `${label}.webAssetIdentity.assets[${index}]`;
assertObject(asset, assetLabel);
for (const field of ["path", "status", "contentType", "sourceHash", "liveHash"]) {
assertString(asset[field], `${assetLabel}.${field}`);
}
assert.ok(["match", "mismatch", "fetch_failed"].includes(asset.status), `${assetLabel}.status`);
assert.ok(Number.isInteger(asset.sourceLength), `${assetLabel}.sourceLength`);
assert.ok(Number.isInteger(asset.liveLength), `${assetLabel}.liveLength`);
}
assert.deepEqual(
webAssetCheck.observations,
report.webAssetIdentity,
`${label}.live-web-assets-current-main.observations`
);
assertBlockers(report.blockers, `${label}.blockers`);
if (runtimeCheck.status === "blocked") {
assert.ok(
report.blockers.some((blocker) => blocker.scope === "live-runtime-current-main"),
`${label}.blockers must include runtime identity drift`
);
}
if (webAssetCheck.status === "blocked") {
assert.ok(
report.blockers.some((blocker) => blocker.scope === "live-web-assets-current-main"),
`${label}.blockers must include web asset drift`
);
}
assertObject(report.safety, `${label}.safety`);
for (const field of ["prodTouched", "servicesRestarted", "heavyE2E", "hardwareWriteApis", "sourceIsDevLive", "codeAgentBrowserJourneySkipped"]) {
assert.equal(report.safety[field], field === "codeAgentBrowserJourneySkipped", `${label}.safety.${field}`);
}
assert.equal(report.safety.liveMode, "deployment-identity-preflight", `${label}.safety.liveMode`);
assert.deepEqual(
report.safety.retainedApiFields,
["runtime.commitId", "runtime.imageTag", "web asset hash/status"],
`${label}.safety.retainedApiFields`
);
assertString(report.safety.statement, `${label}.safety.statement`);
}
function assertDevCloudWorkbenchIdentity(report, label) {
assertObject(report.sourceIdentity, `${label}.sourceIdentity`);
for (const field of ["status", "source", "commitId", "shortCommitId", "ref", "worktreeState", "reportCommitId", "summary"]) {