From 74177c8cc44e12f204e148f97e9204884eeebbf3 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 03:18:25 +0000 Subject: [PATCH 1/2] test: guard degraded dev gate acceptance --- docs/dev-evidence-blocker-aggregator.md | 10 +- .../dev-gate/dev-cloud-workbench-live.json | 75 +++++- .../dev-gate/dev-m5-gate-aggregator-v2.json | 72 ++++-- reports/dev-gate/dev-m5-gate-aggregator-v2.md | 19 +- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 151 ++++++++++- .../src/dev-evidence-blocker-aggregator.mjs | 14 +- .../validate-dev-gate-report-guard.test.mjs | 115 +++++++++ scripts/validate-dev-gate-report.mjs | 238 ++++++++++++++++-- 8 files changed, 615 insertions(+), 79 deletions(-) diff --git a/docs/dev-evidence-blocker-aggregator.md b/docs/dev-evidence-blocker-aggregator.md index 3aeafc2c..288e9a70 100644 --- a/docs/dev-evidence-blocker-aggregator.md +++ b/docs/dev-evidence-blocker-aggregator.md @@ -48,7 +48,7 @@ without promoting the full gate: | Layer | Meaning | | --- | --- | -| `Frontend DEV revision` | Latest accepted Cloud Workbench frontend revision on the active `http://74.48.78.17:16666/` browser endpoint. Frontend revision evidence is browser-only and cannot green DB, M3, M4, or M5. | +| `Frontend DEV revision` | Latest Cloud Workbench frontend revision visible on the active `http://74.48.78.17:16666/` browser endpoint. While API health or runtime durability is degraded, this may only be summarized as deployed UI usable in degraded/read-only mode; frontend evidence cannot green DB, M3, M4, or M5. | | `EDGE/ROUTE live` | `16666` browser and `16667` health route evidence from read-only probes. | | `DB live/degraded` | Cloud API DB readiness from redacted health evidence; route success and env presence do not imply DB live. A `dns_error`/`dns_resolution_failed` DB probe is a DB live blocker. | | `Code Agent provider Secret` | #143 provider-backed chat readiness from redacted runtime error and Secret key-presence evidence only; missing provider Secret is independent from DB live. | @@ -66,9 +66,9 @@ Legacy public `http://74.48.78.17:6666` and `http://74.48.78.17:6667` are historical/deprecated only and are not active green evidence. -`latestFrontendDevFact` records the current #99/#108 frontend fact: revision -`1e8805664970839b72be40c34636b08f6d18b131` is accepted on DEV, with -`promotesM3M4M5=false`. +`latestFrontendDevFact` records the current #99/#108 frontend visibility fact: +revision `1e8805664970839b72be40c34636b08f6d18b131` is visible on DEV in +degraded/read-only mode, with `promotesM3M4M5=false`. ## Current Contract Map @@ -76,7 +76,7 @@ green evidence. | --- | --- | --- | | M0 | `SOURCE` | `docs/m0-contract-audit.md`, protocol schemas, evidence chain examples, M0 validator. | | M1 | `LOCAL` | `docs/m1-local-smoke.md`, `fixtures/mvp/runtime.json`, `scripts/m1-contract-smoke.mjs`. | -| M2 | `DEV-LIVE` for frontend/route only | DEV deploy smoke fixture plus active read-only `16666`/`16667` endpoint smoke and accepted frontend revision. | +| M2 | `DEV-LIVE` for frontend/route only | DEV deploy smoke fixture plus active read-only `16666`/`16667` endpoint smoke and degraded/read-only frontend visibility. | | M3 | `BLOCKED` | Hardware trusted loop topology fixture, local patch-panel smoke, and the no-write DEV plan exist, but DEV-LIVE operation/trace/audit/evidence IDs for `res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` are missing. | | M4 | `BLOCKED` | Agent automation loop fixture and local smoke exist, but live preflight is blocked at DB live readiness. | | M5 | `BLOCKED` | MVP e2e dry-run plan is green, but bounded DEV-LIVE acceptance is blocked by DB/M3/M4 and source/artifact coverage. | diff --git a/reports/dev-gate/dev-cloud-workbench-live.json b/reports/dev-gate/dev-cloud-workbench-live.json index 40578dbb..0464ff2c 100644 --- a/reports/dev-gate/dev-cloud-workbench-live.json +++ b/reports/dev-gate/dev-cloud-workbench-live.json @@ -2,7 +2,7 @@ "$schema": "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json", "$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json", "reportVersion": "v1", - "status": "pass", + "status": "degraded", "issue": "pikasTech/HWLAB#7", "taskId": "dev-cloud-workbench-live", "commitId": "unknown", @@ -15,7 +15,7 @@ "activeEndpoint": "http://74.48.78.17:16667", "activeBrowserEndpoint": "http://74.48.78.17:16666", "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": "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." }, "task": "DC-DCSN-P0-2026-003", "refs": [ @@ -33,8 +33,8 @@ "mode": "live", "url": "http://74.48.78.17:16666/", "generatedAt": "2026-05-22T18:11:35.955Z", - "evidenceLevel": "DEV-LIVE-BROWSER", - "devLive": true, + "evidenceLevel": "DEV-LIVE-BROWSER-DEGRADED", + "devLive": false, "sourceIdentity": { "status": "observed", "source": "git-head", @@ -53,10 +53,35 @@ "serviceId": "hwlab-cloud-api", "environment": "dev", "healthStatus": "degraded", + "ready": false, "commitId": "c7de474", "commitSource": "runtime-env", "imageTag": "c7de474", "observedAt": "2026-05-22T18:11:24.844Z", + "runtime": { + "durable": false, + "ready": false, + "status": "degraded", + "blocker": "runtime_durable_adapter_query_blocked", + "connection": { + "queryAttempted": true, + "queryResult": "query_blocked" + } + }, + "readiness": { + "ready": false, + "status": "degraded", + "durability": { + "ready": false, + "status": "blocked", + "blocker": "runtime_durable_adapter_query_blocked", + "blockedLayer": "durability_query", + "queryResult": "query_blocked" + } + }, + "blockerCodes": [ + "runtime_durable_adapter_query_blocked" + ], "summary": "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD." }, "sourceContract": { @@ -97,7 +122,7 @@ "summary": "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture." }, "devPreconditions": { - "status": "pass", + "status": "degraded", "requirements": [ "GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.", "Browser DOM exposes the default workbench and core controls.", @@ -107,7 +132,7 @@ "commands": [ "node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json" ], - "summary": "Deployed 16666 browser journey and same-origin Code Agent chat completed." + "summary": "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance." }, "endpoints": { "frontend": "http://74.48.78.17:16666", @@ -154,6 +179,42 @@ } } }, + { + "id": "live-api-runtime-readiness", + "status": "degraded", + "summary": "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance.", + "evidence": [ + "api.status=degraded", + "ready=false", + "runtime.durable=false", + "runtime.ready=false", + "durability.ready=false", + "runtime_durable_adapter_query_blocked", + "blocker=runtime_durable_adapter_query_blocked" + ], + "observations": { + "status": "degraded", + "healthStatus": "degraded", + "ready": false, + "runtimeDurable": false, + "runtimeReady": false, + "durabilityReady": false, + "durableBlocked": true, + "blockerCodes": [ + "runtime_durable_adapter_query_blocked" + ], + "evidence": [ + "api.status=degraded", + "ready=false", + "runtime.durable=false", + "runtime.ready=false", + "durability.ready=false", + "runtime_durable_adapter_query_blocked", + "blocker=runtime_durable_adapter_query_blocked" + ], + "summary": "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance." + } + }, { "id": "live-code-agent-browser-journey", "status": "pass", @@ -241,7 +302,7 @@ "error.code", "error.missingEnv" ], - "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": "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." }, "artifacts": { "reportPath": "/workspace/hwlab/.worktrees/deployed-workbench-journey-evidence/reports/dev-gate/dev-cloud-workbench-live.json" diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.json b/reports/dev-gate/dev-m5-gate-aggregator-v2.json index 11a07293..d2380001 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.json +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.json @@ -20,8 +20,8 @@ "pikasTech/HWLAB#46", "pikasTech/HWLAB#64" ], - "generatedAt": "2026-05-22T16:59:52.864Z", - "generatedFromCommit": "f64182b54fe6", + "generatedAt": "2026-05-23T03:16:52.953Z", + "generatedFromCommit": "96e802b2a22c", "environment": "dev", "endpoint": "http://74.48.78.17:16667", "frontendEndpoint": "http://74.48.78.17:16666", @@ -84,7 +84,7 @@ "taskId": "dev-artifact-publish", "lifecycleState": "active", "status": "published", - "commitId": "7e29522" + "commitId": "7de6edd" }, "devEdgeHealth": { "path": "reports/dev-gate/dev-edge-health.json", @@ -138,7 +138,7 @@ "overall": { "status": "blocked", "green": false, - "reason": "Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 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." + "reason": "Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 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": { "revision": "1e8805664970839b72be40c34636b08f6d18b131", @@ -149,15 +149,15 @@ "web/hwlab-cloud-web/styles.css" ], "evidence": [ - "Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131", - "Cloud Workbench public browser endpoint is the active frontend route only", + "Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131", + "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 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence.", + "summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only.", "issue": "pikasTech/HWLAB#99", "supports": [ "pikasTech/HWLAB#99", @@ -206,7 +206,7 @@ "id": "cloud-api-db-ready", "status": "blocked", "evidenceLevel": "BLOCKED", - "summary": "cloud-api DB status=degraded; ready=false; connected=false; liveDbEvidence=false." + "summary": "cloud-api DB status=degraded; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false." }, { "id": "m3-hardware-trusted-loop", @@ -233,10 +233,10 @@ "label": "Frontend DEV revision", "status": "pass", "evidenceLevel": "DEV-LIVE", - "summary": "http://74.48.78.17:16666/ serves the accepted Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; this is browser/frontend evidence only.", + "summary": "http://74.48.78.17:16666/ serves the Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only.", "evidence": [ - "Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131", - "Cloud Workbench public browser endpoint is the active frontend route only", + "Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131", + "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" ], "nextRequired": "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance." @@ -245,7 +245,7 @@ "label": "EDGE/ROUTE live", "status": "pass", "evidenceLevel": "DEV-LIVE", - "summary": "http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned accepted HWLAB DEV responses in the active M2 read-only smoke.", + "summary": "http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned observed HWLAB DEV route responses in the active M2 read-only smoke.", "evidence": [ "http://74.48.78.17:16667/health -> HTTP 200 identity=hwlab-edge-proxy status=ok", "http://74.48.78.17:16667/health/live -> HTTP 200 identity=hwlab-cloud-api status=degraded", @@ -257,7 +257,7 @@ "label": "DB live/degraded", "status": "blocked", "evidenceLevel": "BLOCKED", - "summary": "cloud-api DB status=degraded; configReady=true; ready=false; connected=false; liveDbEvidence=false.", + "summary": "cloud-api DB status=degraded; configReady=true; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false.", "evidence": [ "http://74.48.78.17:16667/health/live -> HTTP 200 serviceId=hwlab-cloud-api status=degraded db.ready=false db.connected=false db.connectionChecked=false", "dev-edge-health active report -> blocked by DB live readiness, while public 16667 TCP/HTTP and frp control/tunnel health are reachable", @@ -405,11 +405,12 @@ "status": "blocked", "currentLevel": "BLOCKED", "blockerClass": "db-live-readiness", - "dependency": "Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure.", + "dependency": "Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure.", "evidence": [ "db.status=degraded", "db.ready=false", "db.connected=false", + "db.endpointSource=unknown", "db.liveDbEvidence=false" ], "nextRequired": "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.", @@ -483,7 +484,7 @@ "highestVisibleLevel": "DRY-RUN", "liveEvidence": "missing_or_blocked", "evidenceCount": 2, - "blockerCount": 15, + "blockerCount": 16, "summary": "dry-run is green but live MVP gate is blocked; highest visible level is DRY-RUN; status is blocked." } ], @@ -545,11 +546,11 @@ "node scripts/dev-cloud-workbench-smoke.mjs --static" ], "evidence": [ - "Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131", - "Cloud Workbench public browser endpoint is the active frontend route only", + "Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131", + "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" ], - "summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence." + "summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only." }, { "milestone": "M2", @@ -607,7 +608,7 @@ "issue": "pikasTech/HWLAB#35", "taskId": "dev-artifact-publish", "reportPath": "reports/dev-gate/dev-artifacts.json", - "commitId": "7e29522", + "commitId": "7de6edd", "lifecycleState": "active", "level": "DRY-RUN", "status": "published", @@ -751,7 +752,8 @@ "status": "not_run", "category": "hardware-loop-live", "commands": [ - "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production" + "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run", + "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod" ], "evidence": [ "operationId=not_observed", @@ -996,7 +998,7 @@ "lifecycleState": "active", "status": "pass", "category": "frontend-dev-revision", - "summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence." + "summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only." }, { "milestone": "M2", @@ -1192,6 +1194,15 @@ "source": "reports/dev-gate/dev-edge-health.json", "summary": "Code Agent provider Secret hwlab-code-agent-provider/openai-api-key is not present as key-presence evidence; #143 real provider-backed chat remains blocked" }, + { + "priority": "P3", + "order": 99, + "type": "safety_blocker", + "scope": "m3-live-write-authorization", + "sourceIssue": "pikasTech/HWLAB#38", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "summary": "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets." + }, { "priority": "P3", "order": 99, @@ -1456,6 +1467,21 @@ ], "rationale": "Residual blocker that must be classified before claiming a green DEV gate." }, + { + "id": "dev-m3-hardware-loop:m3-live-write-authorization", + "priority": "P3", + "type": "safety_blocker", + "scope": "m3-live-write-authorization", + "status": "open", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "sourceIssue": "pikasTech/HWLAB#38", + "summary": "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets.", + "unblockOrder": 99, + "unblocks": [ + "pikasTech/HWLAB#39" + ], + "rationale": "Residual blocker that must be classified before claiming a green DEV gate." + }, { "id": "dev-m4-agent-loop:agent-mgr-health", "priority": "P3", @@ -1544,7 +1570,7 @@ ], "rationale": "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding.", "action": "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value.", - "evidenceRequired": "Cloud API health/live output showing DB env ready and redacted secret references, without secret material." + "evidenceRequired": "Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material." }, { "order": 4, @@ -1571,12 +1597,14 @@ "priority": "P3", "scopes": [ "code-agent-provider-secret", + "m3-live-write-authorization", "agent-mgr-health", "skills-commit-version-injection" ], "sourceIssues": [ "pikasTech/HWLAB#36", "pikasTech/HWLAB#37", + "pikasTech/HWLAB#38", "pikasTech/HWLAB#39" ], "rationale": "Residual blocker that must be classified before claiming a green DEV gate.", diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.md b/reports/dev-gate/dev-m5-gate-aggregator-v2.md index 08a2d202..437c9874 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.md +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.md @@ -1,7 +1,7 @@ # HWLAB M5 DEV Gate Aggregator v2 Status: blocked -Generated from: `f64182b54fe6` +Generated from: `96e802b2a22c` Scope: DEV only, report-only Active frontend: `http://74.48.78.17:16666/` Active API/live: `http://74.48.78.17:16667/health/live` @@ -9,19 +9,19 @@ Deprecated public endpoints: `http://74.48.78.17:6666`, `http://74.48.78.17:6667 ## Summary -Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 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 1e8805664970839b72be40c34636b08f6d18b131 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. ## Frontend DEV Fact -The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be40c34636b08f6d18b131` at `http://74.48.78.17:16666/`. This is DEV-LIVE browser/frontend evidence only and does not promote M3, M4, or M5. +The latest #99/#108 frontend DEV visibility fact is revision `1e8805664970839b72be40c34636b08f6d18b131` at `http://74.48.78.17:16666/`. It means the deployed UI is usable in degraded/read-only mode only; this is DEV-LIVE browser/frontend evidence and does not promote M3, M4, or M5. ## Current DEV Layering | Layer | Status | Evidence level | Current conclusion | Required next proof | | --- | --- | --- | --- | --- | -| Frontend DEV revision | pass | DEV-LIVE | http://74.48.78.17:16666/ serves the accepted Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; this is browser/frontend evidence only. | Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance. | -| EDGE/ROUTE live | pass | DEV-LIVE | http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned accepted HWLAB DEV responses in the active M2 read-only smoke. | Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance. | -| DB live/degraded | blocked | BLOCKED | cloud-api DB status=degraded; configReady=true; ready=false; connected=false; liveDbEvidence=false. | Provide live DB connection evidence through redacted health output; route reachability alone is insufficient. | +| Frontend DEV revision | pass | DEV-LIVE | http://74.48.78.17:16666/ serves the Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only. | Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance. | +| EDGE/ROUTE live | pass | DEV-LIVE | http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned observed HWLAB DEV route responses in the active M2 read-only smoke. | Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance. | +| DB live/degraded | blocked | BLOCKED | cloud-api DB status=degraded; configReady=true; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false. | Provide live DB connection evidence through redacted health output; route reachability alone is insufficient. | | D601 runner observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. | Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired. | | M3 hardware trusted loop | blocked | BLOCKED | No live DEV operation was attempted because read-only direct target checks did not prove the required patch-panel-owned M3 route. Blocker: DEV exposes two box-simu endpoints, but live identities are not the required distinct resources res_boxsimu_1 and res_boxsimu_2; observed resources=res_boxsimu_1. | Only a real DEV res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 observation with operation/trace/audit/evidence can clear M3. | | M4 agent loop | blocked | BLOCKED | Blocked at DB live readiness before scheduling a DEV agent task. | Do not schedule or claim the agent loop as live until DB live and required runtime/evidence preconditions pass. | @@ -58,7 +58,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | artifact-publish-digests | blocked | BLOCKED | artifactState=contract-skeleton, ciPublished=false, registryVerified=false, sha256=0, not_published=13, targetCovered=false | | d601-k3s-observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. | | dev-edge-frp-16667 | pass | DEV-LIVE | Committed edge report proves read-only public HTTP on :16667 /health and /health/live; this is route evidence, not DB/M3/M4/M5 acceptance. | -| cloud-api-db-ready | blocked | BLOCKED | cloud-api DB status=degraded; ready=false; connected=false; liveDbEvidence=false. | +| cloud-api-db-ready | blocked | BLOCKED | cloud-api DB status=degraded; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false. | | m3-hardware-trusted-loop | blocked | BLOCKED | M3 trusted loop is blocked until res_boxsimu_1:DO1 -> patch-panel -> res_boxsimu_2:DI1 is proven with operation/trace/audit/evidence. | | m4-agent-loop-live | blocked | BLOCKED | M4 agent loop live path is blocked before accepted agent scheduling/evidence closure. | | m5-mvp-dev-live | blocked | BLOCKED | M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed. | @@ -68,7 +68,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | Milestone | Status | Current level | Blocker class | Dependency | Required next proof | | --- | --- | --- | --- | --- | --- | | M3 | blocked | BLOCKED | direct-target-identity-gap | Two distinct live box-simu resources and two distinct gateway-simu identities are required before M3 can run. | Fix DEV simulator instance identity so two box-simu pods report res_boxsimu_1/res_boxsimu_2 and two gateway-simu pods report distinct gateway identities. | -| M4 | blocked | BLOCKED | db-live-readiness | Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure. | Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass. | +| M4 | blocked | BLOCKED | db-live-readiness | Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure. | Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass. | | M5 | blocked | BLOCKED | composite-db-m3-m4-live | M5 needs DB live readiness, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage. | After DB/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations. | ## Blockers @@ -89,6 +89,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | P0 | 6 | runtime_blocker | m3-patch-panel-wiring | DEV patch-panel is callable, but live wiring does not contain res_boxsimu_1:DO1 -> res_boxsimu_2:DI1; active=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0; configured=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0. | | P0 | 6 | runtime_blocker | m3-hardware-loop-runtime | Live M3 smoke reached DEV simulators, but patch-panel active DO1 -> DI1 connection is missing. | | P3 | 99 | agent_blocker | code-agent-provider-secret | Code Agent provider Secret hwlab-code-agent-provider/openai-api-key is not present as key-presence evidence; #143 real provider-backed chat remains blocked | +| P3 | 99 | safety_blocker | m3-live-write-authorization | Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets. | | P3 | 99 | agent_blocker | agent-mgr-health | hwlab-agent-mgr /health/live is degraded. | | P3 | 99 | agent_blocker | skills-commit-version-injection | DEV skills injection is incomplete: missing hwlab-agent-skills.HWLAB_SKILLS_VERSION, worker-dry-run.HWLAB_SKILL_VERSION_FROM_DEV. | @@ -99,7 +100,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be 2. Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report. Evidence required: Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write. 3. Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value. - Evidence required: Cloud API health/live output showing DB env ready and redacted secret references, without secret material. + Evidence required: Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material. 4. Fix DEV box-simu instance identity so direct endpoints expose distinct res_boxsimu_1 and res_boxsimu_2 resources. Evidence required: Read-only direct box-simu /health/live and /status output showing distinct res_boxsimu_1 and res_boxsimu_2 resources. 5. Resolve the blocker and attach source/local/dry-run/DEV-live evidence at the correct level. diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 20467fe8..1007e6fa 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -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(); diff --git a/scripts/src/dev-evidence-blocker-aggregator.mjs b/scripts/src/dev-evidence-blocker-aggregator.mjs index d76f8fcf..226e2612 100644 --- a/scripts/src/dev-evidence-blocker-aggregator.mjs +++ b/scripts/src/dev-evidence-blocker-aggregator.mjs @@ -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 diff --git a/scripts/validate-dev-gate-report-guard.test.mjs b/scripts/validate-dev-gate-report-guard.test.mjs index 0b25c06a..52e1e005 100644 --- a/scripts/validate-dev-gate-report-guard.test.mjs +++ b/scripts/validate-dev-gate-report-guard.test.mjs @@ -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); diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 7daf3e18..59b777e4 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -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, From 0b26610e662c86051656e7af16a83da8568100c9 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 03:57:09 +0000 Subject: [PATCH 2/2] test: accept sanitized blocked code agent evidence --- scripts/dev-cloud-workbench-smoke.test.mjs | 30 +++++++ scripts/src/code-agent-response-contract.mjs | 3 +- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 9 ++- .../validate-dev-gate-report-guard.test.mjs | 81 ++++++++++++++++--- scripts/validate-dev-gate-report.mjs | 3 +- 5 files changed, 111 insertions(+), 15 deletions(-) diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 04982a53..038458f9 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -143,6 +143,36 @@ test("Code Agent browser classifier blocks provider 502 response shapes", () => assert.equal(classification.blocker, "provider-upstream"); }); +test("Code Agent browser classifier treats sanitized 服务受阻 UI as blocked provider evidence", () => { + const summary = sanitizeAgentChatBody({ + status: "failed", + provider: "openai-responses", + model: "gpt-5.5", + backend: "hwlab-cloud-api/openai-responses", + traceId: "trc_provider_502", + error: { + code: "provider_unavailable", + message: "OpenAI Responses returned HTTP 502: request rejected", + providerStatus: 502, + missingEnv: [] + } + }, { httpStatus: 200 }); + + const classification = classifyCodeAgentBrowserJourney({ + responseSummary: summary, + httpOk: true, + httpStatus: 200, + ui: { + agentChatStatus: "服务受阻", + completedMessageVisible: false, + failedMessageVisible: true + } + }); + + assert.equal(classification.status, "blocked"); + assert.equal(classification.blocker, "provider-upstream"); +}); + test("Code Agent readiness classifier blocks completed payloads over any non-2xx HTTP status", () => { const completedPayload = { conversationId: "cnv_completed_non_2xx", diff --git a/scripts/src/code-agent-response-contract.mjs b/scripts/src/code-agent-response-contract.mjs index a24e2976..9eedae3c 100644 --- a/scripts/src/code-agent-response-contract.mjs +++ b/scripts/src/code-agent-response-contract.mjs @@ -1,5 +1,6 @@ const trustedLiveProviders = new Set(["openai-responses", "codex-cli"]); const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu; +const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]); export function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStatus = null } = {}) { const providerBlock = classifyCodeAgentProviderBlock(payload, { httpStatus }); @@ -193,7 +194,7 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = false, httpStatus = null, ui = null } = {}) { const providerBlock = classifyCodeAgentProviderBlock(responseSummary, { httpStatus }); const uiCompleted = ui?.agentChatStatus === "DEV-LIVE 回复" || ui?.agentChatStatus === "已回复" || ui?.completedMessageVisible === true; - const uiFailed = ui?.failedMessageVisible === true || ui?.agentChatStatus === "发送失败" || ui?.agentChatStatus === "BLOCKED 凭证缺口"; + const uiFailed = ui?.failedMessageVisible === true || blockedCodeAgentUiLabels.has(ui?.agentChatStatus); const evidence = inspectSanitizedCompletionEvidence(responseSummary); const backendCompleted = httpOk && diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 1007e6fa..29a00671 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -148,6 +148,8 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([ "message-evidence-chip" ]); +const blockedCodeAgentUiLabels = Object.freeze(["服务受阻", "BLOCKED 凭证缺口"]); + const forbiddenWritePatterns = Object.freeze([ /callRpc\(\s*["']hardware\./u, /hardware\.operation\.request/u, @@ -317,7 +319,7 @@ function runStaticSmoke() { addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider credential blockers and only real completed replies can become DEV-LIVE.", { blocker: "observability_blocker", - evidence: ["BLOCKED 凭证缺口", "provider_unavailable", "OPENAI_API_KEY", "hwlab-code-agent-provider/openai-api-key", "completed -> dev-live guard"] + evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard"] }); addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed Code Agent replies expose provider/model/trace/conversation evidence and reject echo/mock/stub completions.", { @@ -331,7 +333,7 @@ function runStaticSmoke() { "DEV-LIVE 回复", "SOURCE 回复", "发送失败", - "BLOCKED 凭证缺口", + "服务受阻 or legacy BLOCKED 凭证缺口", "message-evidence/details/chips" ] }); @@ -1207,10 +1209,9 @@ function hasCodeAgentReadinessVisibility({ html, app }) { /codeAgentAvailability/u.test(app) && /codeAgentAvailabilityFrom/u.test(app) && /latestCompletedAgentMessage/u.test(app) && - /BLOCKED 凭证缺口/u.test(app) && + blockedCodeAgentUiLabels.some((label) => app.includes(label)) && /provider_unavailable/u.test(app) && /OPENAI_API_KEY/u.test(app) && - /hwlab-code-agent-provider\/openai-api-key/u.test(app) && /只有真实 completed 回复才标 DEV-LIVE/u.test(app) && /不能因为只有 conversationId 标为开发实况/u.test(app) && /isTrustedCodeAgentProvider/u.test(app) && diff --git a/scripts/validate-dev-gate-report-guard.test.mjs b/scripts/validate-dev-gate-report-guard.test.mjs index 52e1e005..b6334fb8 100644 --- a/scripts/validate-dev-gate-report-guard.test.mjs +++ b/scripts/validate-dev-gate-report-guard.test.mjs @@ -357,8 +357,7 @@ test("rejects accepted workbench evidence that mixes provider_unavailable blocke await assertRejected(report, /provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/); }); -test("accepts blocked workbench evidence for provider HTTP 502 without promoting completion", async () => { - const report = cloneBaseReport(); +function applyProviderBlockedJourney(report, { uiLabel = "发送失败" } = {}) { const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey"); report.status = "blocked"; report.evidenceLevel = "BLOCKED"; @@ -392,7 +391,7 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting "provider=openai-responses", "model=gpt-5.5", "traceId=trc_provider_502", - "uiStatus=发送失败", + `uiStatus=${uiLabel}`, "blocker=provider-upstream" ]; journey.observations.request.status = 200; @@ -415,7 +414,75 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting providerStatus: 502, reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed" }; - journey.observations.ui.agentChatStatus = "发送失败"; + journey.observations.ui.agentChatStatus = uiLabel; + journey.observations.ui.inputCleared = false; + journey.observations.ui.completedMessageVisible = false; + journey.observations.ui.failedMessageVisible = true; + journey.observations.networkEvents = [ + { + method: "POST", + status: 200, + urlPath: "/v1/agent/chat", + body: journey.observations.response + } + ]; + return report; +} + +test("accepts blocked workbench evidence for provider HTTP 502 without promoting completion", async () => { + const report = applyProviderBlockedJourney(cloneBaseReport()); + + await withReport(report, 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("accepts sanitized 服务受阻 provider blocker wording only as blocked evidence", async () => { + const report = applyProviderBlockedJourney(cloneBaseReport(), { uiLabel: "服务受阻" }); + + await withReport(report, 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 sanitized 服务受阻 provider blocker evidence promoted to full workbench acceptance", async () => { + const report = fullAcceptanceReport(); + const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey"); + journey.status = "pass"; + journey.summary = "Incorrectly promoted provider blocker evidence."; + journey.evidence = [ + "POST /v1/agent/chat HTTP 200", + "status=failed", + "provider=openai-responses", + "model=gpt-5.5", + "traceId=trc_provider_502", + "uiStatus=服务受阻", + "blocker=provider-upstream" + ]; + journey.observations.response = { + status: "failed", + provider: "openai-responses", + model: "gpt-5.5", + backend: "hwlab-cloud-api/openai-responses", + traceId: "trc_provider_502", + hasReply: false, + error: { + code: "provider_unavailable", + missingEnv: [], + providerStatus: 502 + } + }; + journey.observations.classification = { + status: "blocked", + blocker: "provider-upstream", + providerStatus: 502, + reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed" + }; + journey.observations.ui.agentChatStatus = "服务受阻"; journey.observations.ui.inputCleared = false; journey.observations.ui.completedMessageVisible = false; journey.observations.ui.failedMessageVisible = true; @@ -428,11 +495,7 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting } ]; - await withReport(report, async (reportPath) => { - const result = runValidator(reportPath); - assert.equal(result.status, 0, result.stderr); - assert.match(result.stdout, /validated 1 dev-gate report JSON file/); - }); + await assertRejected(report, /response\.status|provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/); }); test("rejects accepted workbench evidence that hides providerStatus 502 in network events", async () => { diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 59b777e4..66f70fe4 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -42,6 +42,7 @@ const evidenceLevels = new Set(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCK const devCloudWorkbenchRuntimeIdentityMaxSkewMs = 15 * 60 * 1000; const syntheticAcceptedCodeAgentModels = new Set(["gpt-5"]); const deploymentPreflightCodeAgentErrorCode = "deployment_identity_preflight"; +const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]); const requiredValidationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" @@ -1548,7 +1549,7 @@ function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) { assertObject(journey.observations.ui, `${label}.live-code-agent-browser-journey.ui`); assert.ok( - ["发送失败", "BLOCKED 凭证缺口"].includes(journey.observations.ui.agentChatStatus), + blockedCodeAgentUiLabels.has(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`);