diff --git a/internal/cloud/m3-io-control.mjs b/internal/cloud/m3-io-control.mjs index 0170ad1e..8ad97d6a 100644 --- a/internal/cloud/m3-io-control.mjs +++ b/internal/cloud/m3-io-control.mjs @@ -897,6 +897,14 @@ async function tryRuntimeWrite(label, write) { } try { const result = await write(); + if (result === undefined) { + return { + label, + written: false, + skipped: true, + reason: "runtimeStore method unavailable" + }; + } return { label, written: true, diff --git a/internal/cloud/m3-io-control.test.mjs b/internal/cloud/m3-io-control.test.mjs index 509462cc..5202f901 100644 --- a/internal/cloud/m3-io-control.test.mjs +++ b/internal/cloud/m3-io-control.test.mjs @@ -97,6 +97,48 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI, assert.equal(evidence.records[0].operationId, result.operationId); }); +test("M3 DO write can prove control reachability while durable trusted persistence stays blocked", async () => { + const fixture = createM3ControlFixture(); + const result = await handleM3IoControl( + { + action: "do.write", + gatewayId: "gwsimu_1", + resourceId: "res_boxsimu_1", + boxId: "boxsimu_1", + port: "DO1", + value: true, + traceId: "trc_m3_control_durable_blocked", + requestId: "req_m3_control_durable_blocked", + actorId: "usr_m3_operator" + }, + { + runtimeStore: createBlockedDurableRuntimeStore(), + now: () => fixedNow, + 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" + }, + requestJson: fixture.requestJson + } + ); + + assert.equal(result.status, "succeeded"); + assert.equal(result.accepted, true); + assert.equal(result.result.value, true); + assert.equal(result.result.targetReadback.value, true); + assert.equal(result.controlPath.status, "succeeded"); + assert.equal(result.controlPath.patchPanel, true); + assert.equal(result.evidenceState.status, "blocked"); + assert.equal(result.evidenceState.sourceKind, "BLOCKED"); + assert.equal(result.evidenceState.durable, false); + assert.equal(result.evidenceState.blocker, "runtime_durable_adapter_query_blocked"); + assert.equal(result.persistence.runtime.status, "blocked"); + assert.equal(result.persistence.runtime.liveRuntimeEvidence, false); + assert.equal(result.persistence.writes.every((write) => write.written !== true), true); + assert.match(result.evidenceState.reason, /操作链路可达性与可信持久化分离/u); +}); + test("M3 DI read returns structured value through gateway-simu without patch-panel mutation", async () => { const fixture = createM3ControlFixture(); writeBoxPort({ @@ -206,6 +248,38 @@ test("M3 IO control blocks invalid port direction instead of allowing generic wr assert.match(result.blocker.zh, /端口方向|资源越界/u); }); +test("M3 IO control blocks invalid DI target before calling gateway-simu", async () => { + const result = await handleM3IoControl( + { + action: "di.read", + gatewayId: "gwsimu_2", + resourceId: "res_boxsimu_1", + port: "DO1", + traceId: "trc_m3_invalid_di_target", + requestId: "req_m3_invalid_di_target", + actorId: "usr_m3_operator" + }, + { + runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }), + now: () => fixedNow, + 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" + }, + requestJson: async () => { + throw new Error("gateway must not be called for invalid DI target"); + } + } + ); + + assert.equal(result.status, "blocked"); + assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.portDirectionInvalid); + assert.match(result.blocker.zh, /DI read|端口方向|资源越界/u); + assert.equal(result.controlPath.gatewaySimu, false); + assert.equal(result.controlPath.frontendBypass, false); +}); + test("M3 IO control fails closed when indexed gateway identity drifts", async () => { const fixture = createM3ControlFixture({ gateway1Id: "gwsimu_2" @@ -283,6 +357,25 @@ test("M3 IO control blocks when patch-panel wiring does not deliver DO1 to DI1", assert.match(result.blocker.zh, /wiring 不存在|未路由/u); }); +function createBlockedDurableRuntimeStore() { + return { + async readiness() { + return { + adapter: "postgres", + durable: false, + durableRequested: true, + durableCapable: false, + ready: false, + status: "blocked", + blocker: "runtime_durable_adapter_query_blocked", + reason: "Postgres runtime adapter could not complete a live runtime schema query", + liveRuntimeEvidence: false, + fixtureEvidence: false + }; + } + }; +} + function createM3ControlFixture({ patchPanelBody, gateway1Id = "gwsimu_1", gateway2Id = "gwsimu_2" } = {}) { const calls = []; const box1 = createBoxState({