diff --git a/internal/cloud/m3-io-control.mjs b/internal/cloud/m3-io-control.mjs index 248be311..e4e68782 100644 --- a/internal/cloud/m3-io-control.mjs +++ b/internal/cloud/m3-io-control.mjs @@ -2072,12 +2072,13 @@ function recordWriteState(write, runtime) { } function durableStatus(runtime) { + const ready = isDurableRuntimeReady(runtime); return { status: runtime?.status ?? "unknown", durable: runtime?.durable === true, ready: runtime?.ready === true, liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true, - blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, + blocker: ready ? null : runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, redacted: true }; } diff --git a/internal/cloud/m3-io-control.test.mjs b/internal/cloud/m3-io-control.test.mjs index bd74c07d..dc8277e5 100644 --- a/internal/cloud/m3-io-control.test.mjs +++ b/internal/cloud/m3-io-control.test.mjs @@ -146,6 +146,7 @@ test("M3 status live descriptor aggregates gateways, boxes, patch-panel, IO valu assert.equal(status.patchPanel.lastError, null); assert.equal(status.trust.durableStatus, "blocked"); assert.equal(status.trust.blocker, "runtime_durable_adapter_auth_blocked"); + assert.equal(status.trust.runtime.blocker, "runtime_durable_adapter_auth_blocked"); assert.equal(status.trust.evidenceId, null); assert.equal(status.blocker.code, "runtime_durable_adapter_auth_blocked"); assert.deepEqual(fixture.calls.map((call) => call.path), [ @@ -158,6 +159,36 @@ test("M3 status live descriptor aggregates gateways, boxes, patch-panel, IO valu ]); }); +test("M3 status clears nested runtime blocker when trusted durable evidence is green", async () => { + const fixture = createM3ControlFixture(); + const status = await describeM3StatusLive({ + runtimeStore: createGreenDurableRuntimeStore({ blocker: M3_IO_BLOCKER_CODES.runtimeDurableBlocked }), + env: { + HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", + HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", + HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" + }, + now: () => fixedNow, + m3IoRequestJson: fixture.requestJson + }); + + assert.equal(status.status, "live"); + assert.equal(status.sourceKind, "DEV-LIVE"); + assert.equal(status.patchPanel.connectionActive, true); + assert.equal(status.trust.durableStatus, "green"); + assert.equal(status.trust.blocker, null); + assert.equal(status.trust.runtime.status, "ready"); + assert.equal(status.trust.runtime.durable, true); + assert.equal(status.trust.runtime.ready, true); + assert.equal(status.trust.runtime.liveRuntimeEvidence, true); + assert.equal(status.trust.runtime.blocker, null); + assert.equal(status.blocker, null); + assert.equal(status.trust.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f"); + assert.equal(status.trust.traceId, "trc_hotfix_307_verify_1779527747043_read_false"); + assert.equal(status.trust.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded"); + assert.equal(status.trust.evidenceId, "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded"); +}); + test("M3 status live descriptor keeps SOURCE fallback unverified when patch-panel route is missing", async () => { const fixture = createM3ControlFixture({ patchPanelStatusBody: { @@ -749,6 +780,73 @@ function createBlockedDurableRuntimeStore({ blocker = "runtime_durable_adapter_q }; } +function createGreenDurableRuntimeStore({ blocker = null } = {}) { + return { + async readiness() { + return { + adapter: "postgres", + durable: true, + durableRequested: true, + durableCapable: true, + ready: true, + status: "ready", + blocker, + reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries", + liveRuntimeEvidence: true, + fixtureEvidence: false + }; + }, + async queryAuditEvents() { + return { + events: [ + { + auditId: "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded", + traceId: "trc_hotfix_307_verify_1779527747043_read_false", + actorType: "user", + actorId: "usr_m3_operator", + action: "m3.io.di.read", + targetType: "hardware_operation", + targetId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + projectId: M3_IO_CHAIN.projectId, + gatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId, + operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + serviceId: "hwlab-cloud-api", + outcome: "succeeded", + metadata: { + route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1" + }, + environment: "dev", + occurredAt: fixedNow + } + ], + count: 1 + }; + }, + async queryEvidenceRecords() { + return { + records: [ + { + evidenceId: "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded", + projectId: M3_IO_CHAIN.projectId, + operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + kind: "trace", + uri: "memory://hwlab-cloud-api/op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/m3-io-control.json", + sha256: "0".repeat(64), + serviceId: "hwlab-cloud-api", + environment: "dev", + metadata: { + traceId: "trc_hotfix_307_verify_1779527747043_read_false", + route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1" + }, + createdAt: fixedNow + } + ], + count: 1 + }; + } + }; +} + function createM3ControlFixture({ patchPanelBody, patchPanelStatusBody, patchPanelWiringBody, gateway1Id = "gwsimu_1", gateway2Id = "gwsimu_2" } = {}) { const calls = []; const box1 = createBoxState({ diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index f63af7f8..27568c1a 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -1650,6 +1650,7 @@ test("cloud api /v1 exposes M3 IO control contract without generic frontend hard assert.equal(statusPayload.patchPanel.connectionActive, true); assert.equal(statusPayload.trust.durableStatus, "blocked"); assert.notEqual(statusPayload.trust.durableStatus, "green"); + assert.equal(statusPayload.trust.runtime.blocker, "runtime_durable_adapter_missing"); assert.equal(statusPayload.boundaries.frontendCallsOnly, "/v1/m3/status"); assert.equal(statusPayload.boundaries.directFrontendGatewayOrBoxAccess, false); } finally { @@ -1659,6 +1660,43 @@ test("cloud api /v1 exposes M3 IO control contract without generic frontend hard } }); +test("cloud api /v1/m3/status clears nested runtime blocker when trusted durable evidence is green", async () => { + const server = createCloudApiServer({ + runtimeStore: createGreenM3StatusRuntimeStore({ blocker: "runtime_durable_not_green" }), + env: { + HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1", + HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2", + HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel" + }, + m3IoRequestJson: m3ReadinessRequestJson + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`); + assert.equal(statusResponse.status, 200); + const statusPayload = await statusResponse.json(); + + assert.equal(statusPayload.contractVersion, "m3-status-v1"); + assert.equal(statusPayload.status, "live"); + assert.equal(statusPayload.sourceKind, "DEV-LIVE"); + assert.equal(statusPayload.patchPanel.connectionActive, true); + assert.equal(statusPayload.trust.durableStatus, "green"); + assert.equal(statusPayload.trust.blocker, null); + assert.equal(statusPayload.trust.runtime.status, "ready"); + assert.equal(statusPayload.trust.runtime.blocker, null); + assert.equal(statusPayload.trust.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f"); + assert.equal(statusPayload.trust.traceId, "trc_hotfix_307_verify_1779527747043_read_false"); + assert.equal(statusPayload.trust.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded"); + assert.equal(statusPayload.trust.evidenceId, "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded"); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("cloud api /v1/diagnostics/gate exposes Chinese single-table rows from live backend aggregation", async () => { const runtimeStore = createCloudRuntimeStore({ now: () => "2026-05-23T00:00:00.000Z" @@ -1788,3 +1826,70 @@ async function postAgent(port, body) { assert.equal(response.status, 200); return response.json(); } + +function createGreenM3StatusRuntimeStore({ blocker = null } = {}) { + return { + async readiness() { + return { + adapter: "postgres", + durable: true, + durableRequested: true, + durableCapable: true, + ready: true, + status: "ready", + blocker, + reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries", + liveRuntimeEvidence: true, + fixtureEvidence: false + }; + }, + async queryAuditEvents() { + return { + events: [ + { + auditId: "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded", + traceId: "trc_hotfix_307_verify_1779527747043_read_false", + actorType: "user", + actorId: "usr_m3_operator", + action: "m3.io.di.read", + targetType: "hardware_operation", + targetId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + projectId: "prj_mvp_topology", + gatewaySessionId: "gws_gwsimu_2", + operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + serviceId: "hwlab-cloud-api", + outcome: "succeeded", + metadata: { + route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1" + }, + environment: "dev", + occurredAt: "2026-05-23T00:00:00.000Z" + } + ], + count: 1 + }; + }, + async queryEvidenceRecords() { + return { + records: [ + { + evidenceId: "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded", + projectId: "prj_mvp_topology", + operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + kind: "trace", + uri: "memory://hwlab-cloud-api/op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/m3-io-control.json", + sha256: "0".repeat(64), + serviceId: "hwlab-cloud-api", + environment: "dev", + metadata: { + traceId: "trc_hotfix_307_verify_1779527747043_read_false", + route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1" + }, + createdAt: "2026-05-23T00:00:00.000Z" + } + ], + count: 1 + }; + } + }; +} diff --git a/package.json b/package.json index 91971fad..78664d84 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/wiring-status.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 2c2f6e32..30e5e445 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -49,13 +49,14 @@ const requiredWebAssets = Object.freeze([ "live-status.mjs", "runtime.mjs", "gate-summary.mjs", + "wiring-status.mjs", "workbench-hardware-panel.mjs", "help.md", "third_party/marked/marked.esm.js", "third_party/marked/LICENSE" ]); -const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "live-status.mjs"]); +const liveIdentityWebAssets = Object.freeze(["index.html", "styles.css", "app.mjs", "live-status.mjs", "wiring-status.mjs"]); const chineseWorkbenchLabels = Object.freeze([ "HWLAB 云工作台", @@ -1550,6 +1551,7 @@ function readStaticFiles() { styles: readText("web/hwlab-cloud-web/styles.css"), app: readText("web/hwlab-cloud-web/app.mjs"), liveStatus: readText("web/hwlab-cloud-web/live-status.mjs"), + wiringStatus: readText("web/hwlab-cloud-web/wiring-status.mjs"), runtime: readText("web/hwlab-cloud-web/runtime.mjs"), panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs"), help: readText("web/hwlab-cloud-web/help.md") @@ -1843,7 +1845,7 @@ function defaultHomepageHidesBackstage({ html }) { return defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(firstViewportText, term)); } -function defaultWorkspaceIsSanitized({ html, app }) { +function defaultWorkspaceIsSanitized({ html, app, wiringStatus }) { const defaultShellText = visibleTextFromHtml(defaultFirstViewportHtml(html)); const userFacingBodies = [ functionBody(app, "codeAgentStatusMessage"), @@ -1858,7 +1860,8 @@ function defaultWorkspaceIsSanitized({ html, app }) { !/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口|SOURCE fixture|DEV-LIVE/u.test(userFacingBodies) && /服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) && /Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) && - /等待可信记录/u.test(functionBody(app, "renderWiringList")) + /wiringPresentationFromSummary/u.test(functionBody(app, "renderWiringList")) && + /等待可信记录/u.test(functionBody(wiringStatus, "wiringPresentationFromSummary")) ); } diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index b49f0651..a13519d6 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -1,6 +1,7 @@ import { gateSummary } from "./gate-summary.mjs"; import { classifyWorkbenchLiveStatus } from "./live-status.mjs"; import { marked } from "./third_party/marked/marked.esm.js"; +import { wiringPresentationFromSummary } from "./wiring-status.mjs"; const DEFAULT_API_TIMEOUT_MS = 4500; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 150000; @@ -1648,18 +1649,9 @@ function renderDrafts() { } function renderWiringList(summary = null) { - const m3Status = summary?.contractVersion === "m3-status-v1" ? summary : null; - const liveM3Evidence = m3Status ? null : trustedM3LiveEvidenceFrom(summary); - const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence); - const connectionActive = m3Status?.patchPanel?.connectionActive === true; - const blockedTrust = m3Status?.trust?.blocker; - const traceEvidence = m3Live - ? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ") - : m3Status - ? [m3Status.trust?.operationId, m3Status.trust?.traceId, m3Status.trust?.auditId, m3Status.trust?.evidenceId].filter(Boolean).join(" / ") || `durable blocked: ${blockedTrust ?? "unverified"}` - : "等待可信记录"; - const stateLabel = m3Live ? "实况已验证" : connectionActive ? "接线实况 / 持久化受阻" : "待可信记录"; - const sourceLabel = m3Live ? "实况可信记录" : m3Status ? "/v1/m3/status 聚合" : "来源拓扑"; + const presentation = wiringPresentationFromSummary(summary, { + liveM3Evidence: trustedM3LiveEvidenceFrom(summary) + }); const sourceHeader = byId("wiring-source-device"); const targetHeader = byId("wiring-target-device"); @@ -1667,16 +1659,16 @@ function renderWiringList(summary = null) { if (targetHeader) targetHeader.textContent = M3_TRUSTED_ROUTE.toResourceId; const tr = document.createElement("tr"); - tr.dataset.sourceKind = m3Live ? "DEV-LIVE" : "SOURCE"; + tr.dataset.sourceKind = presentation.rowSourceKind; tr.dataset.wiringRow = "io"; const sourceCell = cell("", ""); sourceCell.append(textSpan(M3_TRUSTED_ROUTE.fromPort, "wiring-port mono")); - sourceCell.append(textSpan(`源端口;经 ${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:${stateLabel}`, "wiring-detail")); + sourceCell.append(textSpan(`源端口;经 ${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:${presentation.stateLabel}`, "wiring-detail")); const targetCell = cell("", ""); targetCell.append(textSpan(M3_TRUSTED_ROUTE.toPort, "wiring-port mono")); - targetCell.append(textSpan(`目标端口;证据来源:${sourceLabel};轨迹/证据:${traceEvidence}`, "wiring-detail mono wrap")); + targetCell.append(textSpan(`目标端口;证据来源:${presentation.sourceLabel};轨迹/证据:${presentation.traceEvidence}`, "wiring-detail mono wrap")); tr.append(sourceCell, targetCell); @@ -1687,8 +1679,8 @@ function renderWiringList(summary = null) { replaceChildren( summaryElement, document.createTextNode(`接线盘:${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:`), - badge(stateLabel, m3Live ? "dev-live" : "blocked"), - document.createTextNode(`;证据来源:${sourceLabel};轨迹/证据:${traceEvidence}。`) + badge(presentation.stateLabel, presentation.badgeTone), + document.createTextNode(`;证据来源:${presentation.sourceLabel};轨迹/证据:${presentation.traceEvidence}。`) ); } } diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index 4a71b412..4a8bb1a5 100644 --- a/web/hwlab-cloud-web/package.json +++ b/web/hwlab-cloud-web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "m3-readonly": "node scripts/m3-readonly-contract.mjs", - "check": "node scripts/check.mjs", + "check": "node --test wiring-status.test.mjs && node scripts/check.mjs", "build": "node scripts/build.mjs", "layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report ../../reports/dev-gate/dev-cloud-workbench-layout.json", "layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report ../../reports/dev-gate/dev-cloud-workbench-layout-build.json", diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 4f4597ae..f12aa23b 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -24,6 +24,7 @@ const requiredFiles = [ "live-status.mjs", "gate-summary.mjs", "runtime.mjs", + "wiring-status.mjs", "workbench-hardware-panel.mjs", "help.md", "third_party/marked/marked.esm.js", @@ -41,6 +42,7 @@ const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8"); const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8"); const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8"); const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8"); +const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-status.mjs"), "utf8"); const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8"); const buildScript = fs.readFileSync(path.resolve(rootDir, "scripts/build.mjs"), "utf8"); const distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-contract.mjs"), "utf8"); @@ -199,6 +201,18 @@ assert.match(html, /M3 实况状态/); assert.match(html, /硬件状态工作区/); assert.match(html, /id="hardware-source-status"/); assert.match(app, /function renderHardwareStatus/); +assert.match(app, /wiringPresentationFromSummary/); +assert.match(wiringStatus, /function wiringPresentationFromSummary/); +assert.match(wiringStatus, /function trustedM3StatusEvidenceFrom/); +assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /contractVersion !== "m3-status-v1"/); +assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /sourceKind !== "DEV-LIVE"/); +assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /trust\.durableStatus !== "green"/); +assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /trust\.blocker != null/); +assert.match(functionBody(wiringStatus, "trustedM3StatusEvidenceFrom"), /LIVE_M3_ID_FIELDS\.every/); +assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /接线实况 \/ 持久化受阻/); +assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /durableStatus !== "green"/); +assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /blockedTrust != null/); +assert.match(functionBody(wiringStatus, "wiringPresentationFromSummary"), /\/v1\/m3\/status 聚合 \/ 实况可信记录/); assert.match(app, /function initHardwareTabs/); assert.match(app, /function hardwareKvGroup/); assert.match(app, /function sourceFallbackM3Status/); diff --git a/web/hwlab-cloud-web/scripts/dist-contract.mjs b/web/hwlab-cloud-web/scripts/dist-contract.mjs index efa58aac..65e453e5 100644 --- a/web/hwlab-cloud-web/scripts/dist-contract.mjs +++ b/web/hwlab-cloud-web/scripts/dist-contract.mjs @@ -12,6 +12,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([ "live-status.mjs", "gate-summary.mjs", "runtime.mjs", + "wiring-status.mjs", "workbench-hardware-panel.mjs", "help.md", "third_party/marked/marked.esm.js", diff --git a/web/hwlab-cloud-web/wiring-status.mjs b/web/hwlab-cloud-web/wiring-status.mjs new file mode 100644 index 00000000..d69761fd --- /dev/null +++ b/web/hwlab-cloud-web/wiring-status.mjs @@ -0,0 +1,126 @@ +const M3_TRUSTED_ROUTE = Object.freeze({ + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" +}); + +const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]); +const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]); +const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]); + +export function wiringPresentationFromSummary(summary = null, { liveM3Evidence = null } = {}) { + const m3Status = summary?.contractVersion === "m3-status-v1" ? summary : null; + const evidence = m3Status ? trustedM3StatusEvidenceFrom(m3Status) : liveM3Evidence; + const m3Live = hasTrustedM3LiveEvidence(evidence); + const connectionActive = m3Status?.patchPanel?.connectionActive === true; + const blockedTrust = m3Status?.trust?.blocker; + const durableBlocked = m3Status && (m3Status.trust?.durableStatus !== "green" || blockedTrust != null); + const traceEvidence = m3Live + ? [evidence.operationId, evidence.traceId, evidence.auditId, evidence.evidenceId].join(" / ") + : m3Status + ? [m3Status.trust?.operationId, m3Status.trust?.traceId, m3Status.trust?.auditId, m3Status.trust?.evidenceId].filter(Boolean).join(" / ") || `durable blocked: ${blockedTrust ?? "unverified"}` + : "等待可信记录"; + const stateLabel = m3Live ? "实况已验证" : connectionActive && durableBlocked ? "接线实况 / 持久化受阻" : "待可信记录"; + const sourceLabel = m3Live ? "/v1/m3/status 聚合 / 实况可信记录" : m3Status ? "/v1/m3/status 聚合" : "来源拓扑"; + + return { + m3Live, + liveM3Evidence: evidence, + connectionActive, + stateLabel, + sourceLabel, + traceEvidence, + rowSourceKind: m3Live ? "DEV-LIVE" : "SOURCE", + badgeTone: m3Live ? "dev-live" : "blocked" + }; +} + +export function trustedM3StatusEvidenceFrom(status) { + if (status?.contractVersion !== "m3-status-v1") return null; + const trust = status.trust ?? {}; + const sourceKind = String(status.sourceKind ?? "").toUpperCase(); + if ( + status.status !== "live" || + sourceKind !== "DEV-LIVE" || + status.patchPanel?.connectionActive !== true || + status.patchPanel?.serviceId !== M3_TRUSTED_ROUTE.patchPanelServiceId || + trust.durableStatus !== "green" || + trust.blocker != null || + !LIVE_M3_ID_FIELDS.every((field) => isLiveIdentifier(trust[field])) + ) { + return null; + } + return { + status: "verified", + sourceKind: "DEV-LIVE", + live: true, + operationId: trust.operationId, + traceId: trust.traceId, + auditId: trust.auditId, + evidenceId: trust.evidenceId, + patchPanelLiveReport: { + serviceId: M3_TRUSTED_ROUTE.patchPanelServiceId, + sourceKind: "DEV-LIVE", + live: true, + reportId: trust.evidenceId, + activeConnections: [ + { + fromResourceId: M3_TRUSTED_ROUTE.fromResourceId, + fromPort: M3_TRUSTED_ROUTE.fromPort, + toResourceId: M3_TRUSTED_ROUTE.toResourceId, + toPort: M3_TRUSTED_ROUTE.toPort, + patchPanelServiceId: M3_TRUSTED_ROUTE.patchPanelServiceId + } + ] + } + }; +} + +function hasTrustedM3LiveEvidence(candidate) { + if (!candidate || typeof candidate !== "object") return false; + if (candidate.dryRun === true || candidate.fixture === true || candidate.sourceKind === "SOURCE" || candidate.sourceKind === "DRY-RUN") return false; + if (!hasLiveSourceKind(candidate)) return false; + if (!LIVE_M3_PASS_STATUSES.includes(String(candidate.status ?? candidate.outcome ?? "verified").toLowerCase())) return false; + if (!LIVE_M3_ID_FIELDS.every((field) => isLiveIdentifier(candidate[field] ?? candidate.operation?.[field] ?? candidate.identifiers?.[field]))) return false; + const patchPanelLiveReport = candidate.patchPanelLiveReport ?? candidate.patchPanelReport; + return hasTrustedPatchPanelLiveReport(patchPanelLiveReport); +} + +function hasTrustedPatchPanelLiveReport(report) { + if (!report || typeof report !== "object") return false; + if (report.dryRun === true || report.fixture === true || report.sourceKind === "SOURCE" || report.sourceKind === "DRY-RUN") return false; + if (!hasLiveSourceKind(report)) return false; + if (report.serviceId !== M3_TRUSTED_ROUTE.patchPanelServiceId) return false; + if (!isLiveIdentifier(report.reportId ?? report.patchPanelStatusId ?? report.statusId ?? report.evidenceId)) return false; + return (report.activeConnections ?? report.connections ?? []).some((link) => trustedM3Link([link])); +} + +function trustedM3Link(connections) { + return (connections ?? []).find( + (link) => + String(link.fromResourceId ?? "") === M3_TRUSTED_ROUTE.fromResourceId && + normalizePort(link.fromPort) === M3_TRUSTED_ROUTE.fromPort && + String(link.toResourceId ?? "") === M3_TRUSTED_ROUTE.toResourceId && + normalizePort(link.toPort) === M3_TRUSTED_ROUTE.toPort && + connectionPatchPanelServiceId(link) === M3_TRUSTED_ROUTE.patchPanelServiceId + ); +} + +function connectionPatchPanelServiceId(link) { + return link.patchPanelServiceId ?? link.patchPanel?.serviceId ?? link.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId; +} + +function hasLiveSourceKind(value) { + return value.live === true || value.sourceKind === "DEV-LIVE" || value.evidenceLevel === "DEV-LIVE"; +} + +function isLiveIdentifier(value) { + const text = String(value ?? "").trim(); + return text !== "" && !NON_LIVE_ID_VALUES.includes(text.toLowerCase()); +} + +function normalizePort(port) { + return String(port ?? "").trim().toUpperCase(); +} diff --git a/web/hwlab-cloud-web/wiring-status.test.mjs b/web/hwlab-cloud-web/wiring-status.test.mjs new file mode 100644 index 00000000..9bb5722a --- /dev/null +++ b/web/hwlab-cloud-web/wiring-status.test.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + trustedM3StatusEvidenceFrom, + wiringPresentationFromSummary +} from "./wiring-status.mjs"; + +const trustedGreenStatus = Object.freeze({ + contractVersion: "m3-status-v1", + status: "live", + sourceKind: "DEV-LIVE", + patchPanel: { + serviceId: "hwlab-patch-panel", + connectionActive: true + }, + trust: { + durableStatus: "green", + blocker: null, + operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f", + traceId: "trc_hotfix_307_verify_1779527747043_read_false", + auditId: "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded", + evidenceId: "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded" + } +}); + +test("m3-status-v1 trusted green renders verified wiring and preserves operation trace audit evidence IDs", () => { + const presentation = wiringPresentationFromSummary(trustedGreenStatus); + + assert.equal(presentation.m3Live, true); + assert.equal(presentation.stateLabel, "实况已验证"); + assert.equal(presentation.sourceLabel, "/v1/m3/status 聚合 / 实况可信记录"); + assert.equal(presentation.rowSourceKind, "DEV-LIVE"); + assert.equal(presentation.badgeTone, "dev-live"); + assert.doesNotMatch(presentation.stateLabel, /持久化受阻/u); + assert.match(presentation.traceEvidence, /op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/u); + assert.match(presentation.traceEvidence, /trc_hotfix_307_verify_1779527747043_read_false/u); + assert.match(presentation.traceEvidence, /aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded/u); + assert.match(presentation.traceEvidence, /evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded/u); +}); + +test("m3-status-v1 only reports persistence blocked when durable is not green or blocker exists", () => { + for (const trustPatch of [ + { durableStatus: "blocked", blocker: "runtime_durable_not_green" }, + { durableStatus: "green", blocker: "runtime_durable_not_green" } + ]) { + const presentation = wiringPresentationFromSummary({ + ...trustedGreenStatus, + trust: { + ...trustedGreenStatus.trust, + ...trustPatch + } + }); + + assert.equal(presentation.m3Live, false); + assert.equal(presentation.stateLabel, "接线实况 / 持久化受阻"); + assert.equal(presentation.sourceLabel, "/v1/m3/status 聚合"); + assert.equal(presentation.rowSourceKind, "SOURCE"); + assert.equal(presentation.badgeTone, "blocked"); + assert.match(presentation.traceEvidence, /op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/u); + } +}); + +test("SOURCE and DRY-RUN status inputs cannot be promoted to DEV-LIVE wiring", () => { + for (const sourceKind of ["SOURCE", "DRY-RUN"]) { + const evidence = trustedM3StatusEvidenceFrom({ + ...trustedGreenStatus, + sourceKind + }); + const presentation = wiringPresentationFromSummary({ + ...trustedGreenStatus, + sourceKind + }); + + assert.equal(evidence, null); + assert.equal(presentation.m3Live, false); + assert.equal(presentation.rowSourceKind, "SOURCE"); + assert.equal(presentation.badgeTone, "blocked"); + assert.equal(presentation.stateLabel, "待可信记录"); + } +});