test: guard degraded dev gate acceptance
This commit is contained in:
@@ -391,6 +391,13 @@ async function runLiveSmoke(args) {
|
||||
const blockers = [];
|
||||
const sourceIdentity = observeSourceIdentity();
|
||||
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
|
||||
const apiRuntimeReadiness = classifyLiveApiRuntimeReadiness(runtimeIdentity);
|
||||
addCheck(checks, blockers, "live-api-runtime-readiness", apiRuntimeReadiness.status, apiRuntimeReadiness.summary, {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: apiRuntimeReadiness.evidence,
|
||||
observations: apiRuntimeReadiness
|
||||
});
|
||||
|
||||
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
|
||||
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
|
||||
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
|
||||
@@ -523,14 +530,18 @@ async function runLiveSmoke(args) {
|
||||
});
|
||||
}
|
||||
|
||||
const status = blockers.length === 0 ? "pass" : "blocked";
|
||||
const status = blockers.length === 0
|
||||
? apiRuntimeReadiness.status === "degraded" ? "degraded" : "pass"
|
||||
: "blocked";
|
||||
return baseReport({
|
||||
mode: "live",
|
||||
url: args.url,
|
||||
status,
|
||||
checks,
|
||||
blockers,
|
||||
evidenceLevel: status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED",
|
||||
evidenceLevel: status === "pass"
|
||||
? "DEV-LIVE-BROWSER"
|
||||
: status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
|
||||
devLive: status === "pass",
|
||||
runtimeIdentity,
|
||||
sourceIdentity,
|
||||
@@ -545,7 +556,9 @@ async function runLiveSmoke(args) {
|
||||
sourceIsDevLive: false,
|
||||
liveMode: "browser-user-journey-with-code-agent-post",
|
||||
retainedApiFields: ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv", "error.providerStatus"],
|
||||
statement: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
|
||||
statement: status === "degraded"
|
||||
? "Live smoke records deployed UI usable in degraded/read-only mode only; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance and must not be used to infer M3/M4/M5 acceptance."
|
||||
: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -567,6 +580,7 @@ function baseReport({
|
||||
webAssetIdentity = null
|
||||
}) {
|
||||
const liveJourneyPassed = mode === "live" && status === "pass";
|
||||
const liveJourneyDegraded = mode === "live" && status === "degraded";
|
||||
return {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json",
|
||||
@@ -584,7 +598,9 @@ function baseReport({
|
||||
activeEndpoint: runtime.endpoints.api,
|
||||
activeBrowserEndpoint: runtime.endpoints.frontend,
|
||||
deprecatedEndpoint: null,
|
||||
summary: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
|
||||
summary: liveJourneyDegraded
|
||||
? "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance."
|
||||
: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
|
||||
},
|
||||
task: "DC-DCSN-P0-2026-003",
|
||||
refs: [
|
||||
@@ -643,7 +659,7 @@ function baseReport({
|
||||
summary: "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
|
||||
},
|
||||
devPreconditions: {
|
||||
status: liveJourneyPassed ? "pass" : "blocked",
|
||||
status: liveJourneyPassed ? "pass" : liveJourneyDegraded ? "degraded" : "blocked",
|
||||
requirements: [
|
||||
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
|
||||
"Browser DOM exposes the default workbench and core controls.",
|
||||
@@ -655,6 +671,8 @@ function baseReport({
|
||||
],
|
||||
summary: liveJourneyPassed
|
||||
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
|
||||
: liveJourneyDegraded
|
||||
? "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance."
|
||||
: mode === "live"
|
||||
? "Deployed browser journey is blocked; see checks and blockers."
|
||||
: "Static/local fixture evidence does not establish the deployed 16666 browser journey."
|
||||
@@ -1417,6 +1435,75 @@ function staticSafety() {
|
||||
};
|
||||
}
|
||||
|
||||
export function classifyLiveApiRuntimeReadiness(runtimeIdentity) {
|
||||
const healthStatus = sanitizeRuntimeString(runtimeIdentity?.healthStatus);
|
||||
const runtime = runtimeIdentity?.runtime ?? {};
|
||||
const readiness = runtimeIdentity?.readiness ?? {};
|
||||
const durability = readiness?.durability ?? {};
|
||||
const blockerCodes = Array.isArray(runtimeIdentity?.blockerCodes)
|
||||
? runtimeIdentity.blockerCodes.map((item) => sanitizeRuntimeString(item))
|
||||
: [];
|
||||
const ready = runtimeIdentity?.ready === true || readiness?.ready === true;
|
||||
const runtimeReady = runtime?.ready === true;
|
||||
const durabilityReady = durability?.ready === true;
|
||||
const runtimeDurable = runtime?.durable === true;
|
||||
const durableBlocked = runtime?.blocker === "runtime_durable_adapter_query_blocked" ||
|
||||
durability?.blocker === "runtime_durable_adapter_query_blocked" ||
|
||||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
|
||||
durability?.blockedLayer === "durability_query" ||
|
||||
runtime?.connection?.queryResult === "query_blocked" ||
|
||||
durability?.queryResult === "query_blocked";
|
||||
const degraded = healthStatus === "degraded" ||
|
||||
ready === false ||
|
||||
runtimeReady === false ||
|
||||
durabilityReady === false ||
|
||||
runtimeDurable === false ||
|
||||
durableBlocked;
|
||||
|
||||
if (!degraded) {
|
||||
return {
|
||||
status: "pass",
|
||||
healthStatus,
|
||||
ready,
|
||||
runtimeDurable,
|
||||
runtimeReady,
|
||||
durabilityReady,
|
||||
durableBlocked,
|
||||
blockerCodes,
|
||||
evidence: [
|
||||
`api.status=${healthStatus}`,
|
||||
`ready=${ready}`,
|
||||
`runtime.durable=${runtimeDurable}`,
|
||||
`runtime.ready=${runtimeReady}`,
|
||||
`durability.ready=${durabilityReady}`
|
||||
],
|
||||
summary: "Live API health and runtime durability are ready for browser journey evidence."
|
||||
};
|
||||
}
|
||||
|
||||
const blockedEvidence = [
|
||||
`api.status=${healthStatus}`,
|
||||
`ready=${ready}`,
|
||||
`runtime.durable=${runtimeDurable}`,
|
||||
`runtime.ready=${runtimeReady}`,
|
||||
`durability.ready=${durabilityReady}`,
|
||||
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
|
||||
...blockerCodes.map((code) => `blocker=${code}`)
|
||||
].filter(Boolean);
|
||||
return {
|
||||
status: "degraded",
|
||||
healthStatus,
|
||||
ready,
|
||||
runtimeDurable,
|
||||
runtimeReady,
|
||||
durabilityReady,
|
||||
durableBlocked,
|
||||
blockerCodes,
|
||||
evidence: blockedEvidence,
|
||||
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
|
||||
};
|
||||
}
|
||||
|
||||
async function observeLiveRuntimeIdentity(apiEndpoint) {
|
||||
const endpoint = new URL("/health/live", apiEndpoint).toString();
|
||||
const controller = new AbortController();
|
||||
@@ -1442,10 +1529,14 @@ async function observeLiveRuntimeIdentity(apiEndpoint) {
|
||||
serviceId: sanitizeRuntimeString(body.serviceId),
|
||||
environment: sanitizeRuntimeString(body.environment),
|
||||
healthStatus: sanitizeRuntimeString(body.status),
|
||||
ready: body.ready === true,
|
||||
commitId: sanitizeRevision(body.commit?.id ?? body.commitId),
|
||||
commitSource: sanitizeRuntimeString(body.commit?.source),
|
||||
imageTag: sanitizeRuntimeString(body.image?.tag),
|
||||
observedAt: sanitizeTimestamp(body.observedAt),
|
||||
runtime: summarizeRuntimeHealth(body.runtime),
|
||||
readiness: summarizeReadinessHealth(body.readiness),
|
||||
blockerCodes: sanitizeStringList(body.blockerCodes),
|
||||
summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -1466,15 +1557,65 @@ function notObservedRuntimeIdentity(reason, endpoint = new URL("/health/live", r
|
||||
serviceId: "not_observed",
|
||||
environment: "not_observed",
|
||||
healthStatus: "not_observed",
|
||||
ready: false,
|
||||
commitId: "not_observed",
|
||||
commitSource: "not_observed",
|
||||
imageTag: "not_observed",
|
||||
observedAt: new Date().toISOString(),
|
||||
runtime: {
|
||||
durable: false,
|
||||
ready: false,
|
||||
status: "not_observed",
|
||||
blocker: "not_observed"
|
||||
},
|
||||
readiness: {
|
||||
ready: false,
|
||||
status: "not_observed",
|
||||
durability: {
|
||||
ready: false,
|
||||
status: "not_observed",
|
||||
blocker: "not_observed",
|
||||
blockedLayer: "not_observed",
|
||||
queryResult: "not_observed"
|
||||
}
|
||||
},
|
||||
blockerCodes: [],
|
||||
reason: oneLine(reason),
|
||||
summary: "Live runtime identity is recorded as not_observed; source identity must not be treated as the deployed runtime revision."
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeRuntimeHealth(runtimeHealth) {
|
||||
return {
|
||||
durable: runtimeHealth?.durable === true,
|
||||
ready: runtimeHealth?.ready === true,
|
||||
status: sanitizeRuntimeString(runtimeHealth?.status),
|
||||
blocker: sanitizeRuntimeString(runtimeHealth?.blocker),
|
||||
connection: {
|
||||
queryAttempted: runtimeHealth?.connection?.queryAttempted === true,
|
||||
queryResult: sanitizeRuntimeString(runtimeHealth?.connection?.queryResult)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeReadinessHealth(readiness) {
|
||||
return {
|
||||
ready: readiness?.ready === true,
|
||||
status: sanitizeRuntimeString(readiness?.status),
|
||||
durability: {
|
||||
ready: readiness?.durability?.ready === true,
|
||||
status: sanitizeRuntimeString(readiness?.durability?.status),
|
||||
blocker: sanitizeRuntimeString(readiness?.durability?.blocker),
|
||||
blockedLayer: sanitizeRuntimeString(readiness?.durability?.blockedLayer),
|
||||
queryResult: sanitizeRuntimeString(readiness?.durability?.queryResult)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeStringList(value) {
|
||||
return Array.isArray(value) ? value.map((item) => sanitizeRuntimeString(item)) : [];
|
||||
}
|
||||
|
||||
function sanitizeRevision(value) {
|
||||
if (typeof value !== "string") return "unknown";
|
||||
const trimmed = value.trim().toLowerCase();
|
||||
|
||||
@@ -109,15 +109,15 @@ const frontendDevFact = Object.freeze({
|
||||
"web/hwlab-cloud-web/styles.css"
|
||||
],
|
||||
evidence: [
|
||||
`Cloud Web /health/live accepted revision ${latestFrontendRevision}`,
|
||||
"Cloud Workbench public browser endpoint is the active frontend route only",
|
||||
`Cloud Web /health/live observed revision ${latestFrontendRevision}`,
|
||||
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
|
||||
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
|
||||
],
|
||||
commands: [
|
||||
"node web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"node scripts/dev-cloud-workbench-smoke.mjs --static"
|
||||
],
|
||||
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest accepted DEV frontend fact, but it is frontend-only evidence.`
|
||||
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only.`
|
||||
});
|
||||
|
||||
function issue(id) {
|
||||
@@ -949,7 +949,7 @@ function buildCurrentDevLayering(reports, blockers) {
|
||||
label: "Frontend DEV revision",
|
||||
status: "pass",
|
||||
evidenceLevel: "DEV-LIVE",
|
||||
summary: `${activeBrowserRoot} serves the accepted Cloud Workbench frontend revision ${latestFrontendRevision}; this is browser/frontend evidence only.`,
|
||||
summary: `${activeBrowserRoot} serves the Cloud Workbench frontend revision ${latestFrontendRevision}; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only.`,
|
||||
evidence: frontendDevFact.evidence,
|
||||
nextRequired: "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance."
|
||||
},
|
||||
@@ -958,7 +958,7 @@ function buildCurrentDevLayering(reports, blockers) {
|
||||
status: m2EndpointLive || edgeLive ? "pass" : "blocked",
|
||||
evidenceLevel: m2EndpointLive || edgeLive ? "DEV-LIVE" : "BLOCKED",
|
||||
summary: m2EndpointLive
|
||||
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned accepted HWLAB DEV responses in the active M2 read-only smoke.`
|
||||
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned observed HWLAB DEV route responses in the active M2 read-only smoke.`
|
||||
: `No active read-only public endpoint report proves both ${activeBrowserRoot} and ${activeApiLiveEndpoint}.`,
|
||||
evidence: publicEndpointEvidence(reports.devM2Smoke),
|
||||
nextRequired: "Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance."
|
||||
@@ -1299,7 +1299,7 @@ export async function buildReport() {
|
||||
green: dod.green,
|
||||
reason: dod.green
|
||||
? "All #9 DEV DoD checks are green."
|
||||
: `Frontend revision ${latestFrontendRevision} and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
|
||||
: `Frontend revision ${latestFrontendRevision} is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
|
||||
},
|
||||
latestFrontendDevFact: {
|
||||
...frontendDevFact,
|
||||
@@ -1382,7 +1382,7 @@ ${report.overall.reason}
|
||||
|
||||
## Frontend DEV Fact
|
||||
|
||||
The latest accepted #99/#108 frontend DEV fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. This is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence only and does not promote M3, M4, or M5.
|
||||
The latest #99/#108 frontend DEV visibility fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. It means the deployed UI is usable in degraded/read-only mode only; this is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence and does not promote M3, M4, or M5.
|
||||
|
||||
## Current DEV Layering
|
||||
|
||||
|
||||
@@ -21,6 +21,76 @@ function cloneBaseAggregatorReport() {
|
||||
return JSON.parse(JSON.stringify(baseAggregatorReport));
|
||||
}
|
||||
|
||||
function fullAcceptanceReport() {
|
||||
const report = cloneBaseReport();
|
||||
report.status = "pass";
|
||||
report.evidenceLevel = "DEV-LIVE-BROWSER";
|
||||
report.devLive = true;
|
||||
report.devPreconditions.status = "pass";
|
||||
report.devPreconditions.summary = "Deployed 16666 browser journey and same-origin Code Agent chat completed.";
|
||||
report.reportLifecycle.summary = "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance.";
|
||||
report.safety.statement = "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance.";
|
||||
Object.assign(report.runtimeIdentity, {
|
||||
healthStatus: "ok",
|
||||
ready: true,
|
||||
runtime: {
|
||||
durable: true,
|
||||
ready: true,
|
||||
status: "ok",
|
||||
blocker: "none",
|
||||
connection: {
|
||||
queryAttempted: true,
|
||||
queryResult: "ok"
|
||||
}
|
||||
},
|
||||
readiness: {
|
||||
ready: true,
|
||||
status: "ok",
|
||||
durability: {
|
||||
ready: true,
|
||||
status: "ready",
|
||||
blocker: "none",
|
||||
blockedLayer: "none",
|
||||
queryResult: "ok"
|
||||
}
|
||||
},
|
||||
blockerCodes: []
|
||||
});
|
||||
const readiness = report.checks.find((check) => check.id === "live-api-runtime-readiness");
|
||||
if (readiness) {
|
||||
Object.assign(readiness, {
|
||||
status: "pass",
|
||||
summary: "Live API health and runtime durability are ready for browser journey evidence.",
|
||||
evidence: [
|
||||
"api.status=ok",
|
||||
"ready=true",
|
||||
"runtime.durable=true",
|
||||
"runtime.ready=true",
|
||||
"durability.ready=true"
|
||||
],
|
||||
observations: {
|
||||
status: "pass",
|
||||
healthStatus: "ok",
|
||||
ready: true,
|
||||
runtimeDurable: true,
|
||||
runtimeReady: true,
|
||||
durabilityReady: true,
|
||||
durableBlocked: false,
|
||||
blockerCodes: [],
|
||||
evidence: [
|
||||
"api.status=ok",
|
||||
"ready=true",
|
||||
"runtime.durable=true",
|
||||
"runtime.ready=true",
|
||||
"durability.ready=true"
|
||||
],
|
||||
summary: "Live API health and runtime durability are ready for browser journey evidence."
|
||||
}
|
||||
});
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
function preflightBlockedReport() {
|
||||
const report = cloneBaseReport();
|
||||
report.status = "blocked";
|
||||
@@ -177,6 +247,51 @@ test("accepts the sanitized deployed workbench evidence contract", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts full workbench acceptance only when API health and runtime durability are ready", async () => {
|
||||
await withReport(fullAcceptanceReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects full workbench acceptance when API health is degraded", async () => {
|
||||
const report = fullAcceptanceReport();
|
||||
report.runtimeIdentity.healthStatus = "degraded";
|
||||
report.checks.find((check) => check.id === "live-api-runtime-readiness").observations.healthStatus = "degraded";
|
||||
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
|
||||
});
|
||||
|
||||
test("rejects full workbench acceptance when runtime durability query is blocked", async () => {
|
||||
const report = fullAcceptanceReport();
|
||||
Object.assign(report.runtimeIdentity.runtime, {
|
||||
durable: false,
|
||||
ready: false,
|
||||
status: "degraded",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
connection: {
|
||||
queryAttempted: true,
|
||||
queryResult: "query_blocked"
|
||||
}
|
||||
});
|
||||
Object.assign(report.runtimeIdentity.readiness.durability, {
|
||||
ready: false,
|
||||
status: "blocked",
|
||||
blocker: "runtime_durable_adapter_query_blocked",
|
||||
blockedLayer: "durability_query",
|
||||
queryResult: "query_blocked"
|
||||
});
|
||||
report.runtimeIdentity.blockerCodes = ["runtime_durable_adapter_query_blocked"];
|
||||
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
|
||||
});
|
||||
|
||||
test("rejects degraded workbench evidence with full-acceptance wording", async () => {
|
||||
const report = cloneBaseReport();
|
||||
report.devPreconditions.summary = "M3/M4/M5 DEV-LIVE accepted.";
|
||||
report.safety.statement = "Full DEV-LIVE accepted.";
|
||||
await assertRejected(report, /degraded API\/runtime report must not claim full M3\/M4\/M5 DEV-LIVE acceptance/);
|
||||
});
|
||||
|
||||
test("accepts preflight-blocked workbench evidence when Code Agent POST is marked not_sent", async () => {
|
||||
await withReport(preflightBlockedReport(), async (reportPath) => {
|
||||
const result = runValidator(reportPath);
|
||||
|
||||
@@ -1351,13 +1351,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
|
||||
assert.equal(report.devOnly, true, `${label}.devOnly`);
|
||||
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
|
||||
assertStatus(report.status, `${label}.status`);
|
||||
assert.equal(report.status, report.blockers.length === 0 ? "pass" : "blocked", `${label}.status must match blockers`);
|
||||
assert.equal(
|
||||
report.blockers.length === 0 ? ["pass", "degraded"].includes(report.status) : report.status === "blocked",
|
||||
true,
|
||||
`${label}.status must match blockers`
|
||||
);
|
||||
assertTimestamp(report.generatedAt, `${label}.generatedAt`);
|
||||
assert.equal(report.mode, "live", `${label}.mode`);
|
||||
assert.equal(report.url, "http://74.48.78.17:16666/", `${label}.url`);
|
||||
assert.equal(report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED", `${label}.evidenceLevel`);
|
||||
assert.equal(
|
||||
report.evidenceLevel,
|
||||
report.status === "pass"
|
||||
? "DEV-LIVE-BROWSER"
|
||||
: report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
|
||||
`${label}.evidenceLevel`
|
||||
);
|
||||
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
|
||||
assertDevCloudWorkbenchIdentity(report, label);
|
||||
const apiRuntimeReadiness = assertDevCloudWorkbenchApiRuntimeReadiness(report, label);
|
||||
assertDevCloudWorkbenchEndpoints(report, label);
|
||||
|
||||
assertObject(report.sourceContract, `${label}.sourceContract`);
|
||||
@@ -1424,6 +1435,7 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
|
||||
for (const requiredCheck of [
|
||||
"live-http-html",
|
||||
"live-browser-dom",
|
||||
"live-api-runtime-readiness",
|
||||
"live-code-agent-browser-journey"
|
||||
]) {
|
||||
assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`);
|
||||
@@ -1445,8 +1457,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
|
||||
assert.equal(dom.coreControlsVisible[control], true, `${label}.live-browser-dom.coreControlsVisible.${control}`);
|
||||
}
|
||||
|
||||
const apiRuntimeReadinessCheck = checks.get("live-api-runtime-readiness");
|
||||
assert.equal(
|
||||
apiRuntimeReadinessCheck.status,
|
||||
apiRuntimeReadiness.status,
|
||||
`${label}.live-api-runtime-readiness.status`
|
||||
);
|
||||
assert.deepEqual(
|
||||
apiRuntimeReadinessCheck.observations,
|
||||
apiRuntimeReadiness,
|
||||
`${label}.live-api-runtime-readiness.observations`
|
||||
);
|
||||
|
||||
const journey = checks.get("live-code-agent-browser-journey");
|
||||
assert.equal(journey.status, report.status, `${label}.live-code-agent-browser-journey.status`);
|
||||
assert.equal(
|
||||
journey.status,
|
||||
report.status === "degraded" ? "pass" : report.status,
|
||||
`${label}.live-code-agent-browser-journey.status`
|
||||
);
|
||||
assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`);
|
||||
assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`);
|
||||
assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`);
|
||||
@@ -1486,6 +1514,9 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
|
||||
assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`);
|
||||
assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`);
|
||||
assertString(report.safety.statement, `${label}.safety.statement`);
|
||||
if (report.status === "degraded") {
|
||||
assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness);
|
||||
}
|
||||
assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey);
|
||||
}
|
||||
|
||||
@@ -1537,6 +1568,157 @@ function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) {
|
||||
assertString(report.safety.statement, `${label}.safety.statement`);
|
||||
}
|
||||
|
||||
function assertDevCloudWorkbenchApiRuntimeReadiness(report, label) {
|
||||
assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`);
|
||||
if (Object.hasOwn(report.runtimeIdentity, "ready")) {
|
||||
assertBoolean(report.runtimeIdentity.ready, `${label}.runtimeIdentity.ready`);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(report.runtimeIdentity, "runtime")) {
|
||||
assertObject(report.runtimeIdentity.runtime, `${label}.runtimeIdentity.runtime`);
|
||||
for (const field of ["durable", "ready"]) {
|
||||
if (Object.hasOwn(report.runtimeIdentity.runtime, field)) {
|
||||
assertBoolean(report.runtimeIdentity.runtime[field], `${label}.runtimeIdentity.runtime.${field}`);
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(report.runtimeIdentity.runtime, "status")) {
|
||||
assertString(report.runtimeIdentity.runtime.status, `${label}.runtimeIdentity.runtime.status`);
|
||||
}
|
||||
if (Object.hasOwn(report.runtimeIdentity.runtime, "blocker")) {
|
||||
assertString(report.runtimeIdentity.runtime.blocker, `${label}.runtimeIdentity.runtime.blocker`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(report.runtimeIdentity, "readiness")) {
|
||||
assertObject(report.runtimeIdentity.readiness, `${label}.runtimeIdentity.readiness`);
|
||||
if (Object.hasOwn(report.runtimeIdentity.readiness, "ready")) {
|
||||
assertBoolean(report.runtimeIdentity.readiness.ready, `${label}.runtimeIdentity.readiness.ready`);
|
||||
}
|
||||
if (Object.hasOwn(report.runtimeIdentity.readiness, "status")) {
|
||||
assertString(report.runtimeIdentity.readiness.status, `${label}.runtimeIdentity.readiness.status`);
|
||||
}
|
||||
if (Object.hasOwn(report.runtimeIdentity.readiness, "durability")) {
|
||||
assertObject(report.runtimeIdentity.readiness.durability, `${label}.runtimeIdentity.readiness.durability`);
|
||||
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, "ready")) {
|
||||
assertBoolean(report.runtimeIdentity.readiness.durability.ready, `${label}.runtimeIdentity.readiness.durability.ready`);
|
||||
}
|
||||
for (const field of ["status", "blocker", "blockedLayer", "queryResult"]) {
|
||||
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, field)) {
|
||||
assertString(report.runtimeIdentity.readiness.durability[field], `${label}.runtimeIdentity.readiness.durability.${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.hasOwn(report.runtimeIdentity, "blockerCodes")) {
|
||||
assertStringArray(report.runtimeIdentity.blockerCodes, `${label}.runtimeIdentity.blockerCodes`);
|
||||
}
|
||||
|
||||
const runtime = report.runtimeIdentity.runtime ?? {};
|
||||
const readiness = report.runtimeIdentity.readiness ?? {};
|
||||
const durability = readiness.durability ?? {};
|
||||
const blockerCodes = Array.isArray(report.runtimeIdentity.blockerCodes) ? report.runtimeIdentity.blockerCodes : [];
|
||||
const durableBlocked = runtime.blocker === "runtime_durable_adapter_query_blocked" ||
|
||||
durability.blocker === "runtime_durable_adapter_query_blocked" ||
|
||||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
|
||||
durability.blockedLayer === "durability_query" ||
|
||||
runtime.connection?.queryResult === "query_blocked" ||
|
||||
durability.queryResult === "query_blocked";
|
||||
const degraded = report.runtimeIdentity.healthStatus === "degraded" ||
|
||||
report.runtimeIdentity.ready === false ||
|
||||
runtime.ready === false ||
|
||||
durability.ready === false ||
|
||||
runtime.durable === false ||
|
||||
durableBlocked;
|
||||
|
||||
const status = degraded ? "degraded" : "pass";
|
||||
const result = {
|
||||
status,
|
||||
healthStatus: report.runtimeIdentity.healthStatus,
|
||||
ready: report.runtimeIdentity.ready === true,
|
||||
runtimeDurable: runtime.durable === true,
|
||||
runtimeReady: runtime.ready === true,
|
||||
durabilityReady: durability.ready === true,
|
||||
durableBlocked,
|
||||
blockerCodes,
|
||||
evidence: [
|
||||
`api.status=${report.runtimeIdentity.healthStatus}`,
|
||||
`ready=${report.runtimeIdentity.ready === true}`,
|
||||
`runtime.durable=${runtime.durable === true}`,
|
||||
`runtime.ready=${runtime.ready === true}`,
|
||||
`durability.ready=${durability.ready === true}`,
|
||||
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
|
||||
...blockerCodes.map((code) => `blocker=${code}`)
|
||||
].filter(Boolean),
|
||||
summary: degraded
|
||||
? "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
|
||||
: "Live API health and runtime durability are ready for browser journey evidence."
|
||||
};
|
||||
|
||||
if (["pass", "degraded"].includes(report.status)) {
|
||||
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
|
||||
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
|
||||
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
|
||||
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
|
||||
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
|
||||
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
|
||||
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
|
||||
|
||||
const generatedAtMs = Date.parse(report.generatedAt);
|
||||
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
|
||||
assert.ok(
|
||||
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
|
||||
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
|
||||
);
|
||||
}
|
||||
|
||||
if (report.status === "pass") {
|
||||
assert.equal(
|
||||
degraded,
|
||||
false,
|
||||
`${label}.runtimeIdentity degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted`
|
||||
);
|
||||
}
|
||||
|
||||
if (degraded) {
|
||||
assert.ok(
|
||||
["degraded", "blocked"].includes(report.status),
|
||||
`${label}.status must be degraded or blocked when API health or runtime durability is degraded`
|
||||
);
|
||||
assert.equal(report.devLive, false, `${label}.devLive must remain false for degraded/read-only workbench evidence`);
|
||||
assert.equal(
|
||||
report.evidenceLevel,
|
||||
report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
|
||||
`${label}.evidenceLevel`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness) {
|
||||
assert.equal(apiRuntimeReadiness.status, "degraded", `${label}.live-api-runtime-readiness.status`);
|
||||
assert.equal(report.devPreconditions.status, "degraded", `${label}.devPreconditions.status`);
|
||||
const summaryText = [
|
||||
report.reportLifecycle.summary,
|
||||
report.devPreconditions.summary,
|
||||
report.safety.statement
|
||||
].join(" ");
|
||||
assert.match(
|
||||
summaryText,
|
||||
/degraded\/read-only mode|degraded\/read-only|read-only mode/u,
|
||||
`${label} degraded API/runtime report must be summarized as deployed UI usable in degraded/read-only mode`
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
summaryText,
|
||||
/full DEV-LIVE accepted|full DEV-LIVE acceptance passed|M3\/M4\/M5 DEV-LIVE accepted|M[345]\s+(?:DEV-LIVE\s+)?(?:accepted|acceptance passed|green)/u,
|
||||
`${label} degraded API/runtime report must not claim full M3/M4/M5 DEV-LIVE acceptance`
|
||||
);
|
||||
}
|
||||
|
||||
function assertCodeAgentRetainedApiFields(value, label) {
|
||||
assertArray(value, label);
|
||||
for (const field of ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv"]) {
|
||||
@@ -1763,31 +1945,39 @@ function assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey) {
|
||||
`${label} deployed workbench report must not contain deprecated public endpoint evidence`
|
||||
);
|
||||
|
||||
if (report.status !== "pass") {
|
||||
if (!["pass", "degraded"].includes(report.status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(report.devLive, true, `${label}.devLive`);
|
||||
assert.equal(report.evidenceLevel, "DEV-LIVE-BROWSER", `${label}.evidenceLevel`);
|
||||
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
|
||||
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
|
||||
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
|
||||
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
|
||||
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
|
||||
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
|
||||
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
|
||||
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
|
||||
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
|
||||
|
||||
const generatedAtMs = Date.parse(report.generatedAt);
|
||||
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
|
||||
assert.ok(
|
||||
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
|
||||
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
|
||||
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
|
||||
assert.equal(
|
||||
report.evidenceLevel,
|
||||
report.status === "pass" ? "DEV-LIVE-BROWSER" : "DEV-LIVE-BROWSER-DEGRADED",
|
||||
`${label}.evidenceLevel`
|
||||
);
|
||||
|
||||
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
|
||||
if (report.status === "pass") {
|
||||
assert.notEqual(report.runtimeIdentity.healthStatus, "degraded", `${label}.runtimeIdentity.healthStatus`);
|
||||
assert.equal(report.runtimeIdentity.ready, true, `${label}.runtimeIdentity.ready`);
|
||||
assert.equal(report.runtimeIdentity.runtime?.durable, true, `${label}.runtimeIdentity.runtime.durable`);
|
||||
assert.equal(report.runtimeIdentity.runtime?.ready, true, `${label}.runtimeIdentity.runtime.ready`);
|
||||
assert.equal(report.runtimeIdentity.readiness?.durability?.ready, true, `${label}.runtimeIdentity.readiness.durability.ready`);
|
||||
assert.notEqual(
|
||||
report.runtimeIdentity.runtime?.blocker,
|
||||
"runtime_durable_adapter_query_blocked",
|
||||
`${label}.runtimeIdentity.runtime.blocker`
|
||||
);
|
||||
assert.notEqual(
|
||||
report.runtimeIdentity.readiness?.durability?.blocker,
|
||||
"runtime_durable_adapter_query_blocked",
|
||||
`${label}.runtimeIdentity.readiness.durability.blocker`
|
||||
);
|
||||
assert.equal(
|
||||
(report.runtimeIdentity.blockerCodes ?? []).includes("runtime_durable_adapter_query_blocked"),
|
||||
false,
|
||||
`${label}.runtimeIdentity.blockerCodes`
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
report.sourceIdentity.reportCommitId === "unknown" || report.sourceIdentity.worktreeState === "clean",
|
||||
true,
|
||||
|
||||
Reference in New Issue
Block a user