997b96dc65
Add structured Code Agent blocker taxonomy across /v1/agent/chat, runner/session/tool, and HWLAB M3 Skill CLI paths.\n\nValidated with npm run check and focused server/session/Skill CLI/web tests.
222 lines
7.5 KiB
JavaScript
222 lines
7.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
HWLAB_M3_IO_API_BASE_URL_ENV,
|
|
HWLAB_M3_IO_CAPABILITY_LEVELS,
|
|
HWLAB_M3_IO_DEV_SERVICE_BASE_URL,
|
|
HWLAB_M3_IO_API_ROUTE,
|
|
runM3IoSkillCommand,
|
|
validateCloudApiTarget
|
|
} from "./src/m3-io-skill-client.mjs";
|
|
|
|
test("M3 Skill CLI posts only to HWLAB API /v1/m3/io for DO1 writes", async () => {
|
|
const calls = [];
|
|
const result = await runM3IoSkillCommand(
|
|
[
|
|
"m3",
|
|
"io",
|
|
"--action",
|
|
"do.write",
|
|
"--value",
|
|
"false",
|
|
"--api-base-url",
|
|
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
|
"--trace-id",
|
|
"trc_skill_cli_write_false",
|
|
"--request-id",
|
|
"req_skill_cli_write_false"
|
|
],
|
|
{
|
|
now: () => "2026-05-23T00:08:00.000Z",
|
|
requestJson: async (url, request) => {
|
|
calls.push({ url, request });
|
|
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
|
|
assert.equal(request.method, "POST");
|
|
assert.equal(request.body.action, "do.write");
|
|
assert.equal(request.body.resourceId, "res_boxsimu_1");
|
|
assert.equal(request.body.boxId, "boxsimu_1");
|
|
assert.equal(request.body.port, "DO1");
|
|
assert.equal(request.body.value, false);
|
|
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
status: "succeeded",
|
|
accepted: true,
|
|
traceId: "trc_skill_cli_write_false",
|
|
operationId: "op_m3_do_write_cli",
|
|
auditId: "aud_m3_do_write_cli_succeeded",
|
|
evidenceId: "evd_m3_do_write_cli_succeeded",
|
|
auditState: {
|
|
status: "written_non_durable"
|
|
},
|
|
evidenceState: {
|
|
status: "blocked",
|
|
sourceKind: "BLOCKED",
|
|
blocker: "runtime_durable_not_green",
|
|
writeStatus: "written_non_durable"
|
|
},
|
|
durableStatus: {
|
|
status: "degraded",
|
|
durable: false,
|
|
blocker: "runtime_durable_not_green"
|
|
},
|
|
result: {
|
|
value: false,
|
|
targetReadback: {
|
|
value: false
|
|
}
|
|
},
|
|
controlPath: {
|
|
cloudApi: true,
|
|
gatewaySimu: true,
|
|
boxSimu: true,
|
|
patchPanel: true,
|
|
frontendBypass: false
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
);
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.route, HWLAB_M3_IO_API_ROUTE);
|
|
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
|
assert.equal(result.controlReady, true);
|
|
assert.equal(result.readiness.status, "ready");
|
|
assert.equal(result.accepted, true);
|
|
assert.equal(result.status, "succeeded");
|
|
assert.equal(result.operationId, "op_m3_do_write_cli");
|
|
assert.equal(result.audit.auditId, "aud_m3_do_write_cli_succeeded");
|
|
assert.equal(result.evidence.evidenceId, "evd_m3_do_write_cli_succeeded");
|
|
assert.equal(result.durable.blocker, "runtime_durable_not_green");
|
|
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
|
|
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
|
|
assert.equal(result.safety.directGatewayCalls, false);
|
|
assert.equal(result.safety.directBoxCalls, false);
|
|
assert.equal(result.safety.directPatchPanelCalls, false);
|
|
assert.equal(result.safety.fallbackUsed, false);
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test("M3 Skill CLI rejects direct gateway, box, and patch-panel targets before request", async () => {
|
|
for (const url of [
|
|
"http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
|
|
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
|
|
"http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301"
|
|
]) {
|
|
const result = await runM3IoSkillCommand(
|
|
[
|
|
"m3",
|
|
"io",
|
|
"--action",
|
|
"di.read",
|
|
"--api-base-url",
|
|
url,
|
|
"--trace-id",
|
|
"trc_skill_cli_direct_blocked"
|
|
],
|
|
{
|
|
requestJson: async () => {
|
|
throw new Error("direct hardware target must be blocked before network call");
|
|
}
|
|
}
|
|
);
|
|
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, "blocked");
|
|
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
|
assert.equal(result.controlReady, false);
|
|
assert.equal(result.readiness.status, "blocked");
|
|
assert.equal(result.accepted, false);
|
|
assert.equal(result.blocker.code, "direct_hardware_target_blocked");
|
|
assert.equal(result.capabilityBlocker.code, "direct_hardware_target_blocked");
|
|
assert.equal(result.operationId, null);
|
|
assert.equal(result.safety.directGatewayCalls, false);
|
|
assert.equal(result.safety.directBoxCalls, false);
|
|
assert.equal(result.safety.directPatchPanelCalls, false);
|
|
}
|
|
});
|
|
|
|
test("M3 Skill CLI validates exact API route contract", () => {
|
|
const invalid = validateCloudApiTarget({
|
|
url: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/rpc/m3.io.di.read"
|
|
});
|
|
assert.equal(invalid.code, "invalid_hwlab_api_route");
|
|
});
|
|
|
|
test("M3 Skill CLI reports missing API base as safe structured config blocker", async () => {
|
|
const result = await runM3IoSkillCommand(
|
|
[
|
|
"m3",
|
|
"io",
|
|
"--action",
|
|
"di.read",
|
|
"--trace-id",
|
|
"trc_skill_cli_api_base_missing"
|
|
],
|
|
{
|
|
env: {
|
|
PATH: process.env.PATH
|
|
},
|
|
requestJson: async () => {
|
|
throw new Error("missing API base must be blocked before request");
|
|
}
|
|
}
|
|
);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, "blocked");
|
|
assert.equal(result.blocker.code, "skill_cli_api_base_missing");
|
|
assert.equal(result.blocker.layer, "skill-cli-config");
|
|
assert.equal(result.blocker.retryable, false);
|
|
assert.equal(result.error.code, "skill_cli_api_base_missing");
|
|
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
|
assert.equal(result.accepted, false);
|
|
assert.equal(result.operationId, null);
|
|
assert.equal(result.audit.status, "not_written");
|
|
assert.equal(result.hwlabApi.source, "missing-config");
|
|
assert.equal(result.hwlabApi.redactedUrl, null);
|
|
assert.deepEqual(result.error.missingConfig, [
|
|
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL",
|
|
"HWLAB_API_BASE_URL",
|
|
"HWLAB_CLOUD_API_BASE_URL",
|
|
"contract:hwlab-agent-runtime.m3-io.apiBaseUrl"
|
|
]);
|
|
assert.equal(JSON.stringify(result).includes("://"), false);
|
|
});
|
|
|
|
test("M3 Skill CLI reports HWLAB API unavailable as retryable structured blocker", async () => {
|
|
const result = await runM3IoSkillCommand(
|
|
[
|
|
"m3",
|
|
"io",
|
|
"--action",
|
|
"di.read",
|
|
"--api-base-url",
|
|
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
|
"--trace-id",
|
|
"trc_skill_cli_hwlab_api_unavailable"
|
|
],
|
|
{
|
|
requestJson: async () => ({
|
|
ok: false,
|
|
status: 503,
|
|
body: null,
|
|
error: "service unavailable"
|
|
})
|
|
}
|
|
);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.status, "blocked");
|
|
assert.equal(result.blocker.code, "hwlab_api_unavailable");
|
|
assert.equal(result.blocker.layer, "hwlab-api");
|
|
assert.equal(result.blocker.retryable, true);
|
|
assert.equal(result.error.code, "hwlab_api_unavailable");
|
|
assert.equal(result.error.retryable, true);
|
|
assert.equal(result.error.route, HWLAB_M3_IO_API_ROUTE);
|
|
assert.equal(result.error.toolName, "hwlab-agent-runtime.m3-io");
|
|
});
|