Merge pull request #244 from pikasTech/fix/dev-gate-degraded-acceptance-guard-pr
Guard degraded DEV gate acceptance summaries
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
});
|
||||
@@ -391,6 +393,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 +532,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 +558,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 +582,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 +600,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 +661,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 +673,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."
|
||||
@@ -1189,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) &&
|
||||
@@ -1417,6 +1436,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 +1530,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 +1558,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);
|
||||
@@ -242,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";
|
||||
@@ -277,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;
|
||||
@@ -300,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;
|
||||
@@ -313,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 () => {
|
||||
|
||||
@@ -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"
|
||||
@@ -1351,13 +1352,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 +1436,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 +1458,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 +1515,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);
|
||||
}
|
||||
|
||||
@@ -1517,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`);
|
||||
@@ -1537,6 +1569,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 +1946,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