feat: migrate device pod cli to rest authority

This commit is contained in:
Codex
2026-05-29 11:30:17 +08:00
parent 6e1a63da91
commit f740fc9dbc
12 changed files with 623 additions and 1093 deletions
+55
View File
@@ -173,6 +173,61 @@ test("device pod executor dispatches internal jobs through cloud-api gateway ada
}
});
test("device-pod executor maps v0.1 CLI options to device-host-cli argv", async () => {
let dispatchedBody = null;
const cloudApi = createServer(async (request, response) => {
dispatchedBody = await requestJson(request);
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
ok: true,
result: {
status: "completed",
dispatch: { shellExecuted: true, dispatchStatus: "succeeded", stdout: "ok", exitCode: 0 },
auditId: "aud_options",
evidenceId: "ev_options"
}
}));
});
await new Promise((resolve) => cloudApi.listen(0, "127.0.0.1", resolve));
const cloudPort = cloudApi.address().port;
const service = await startDevicePod("device-pod-test", {
HWLAB_CLOUD_API_INTERNAL_URL: `http://127.0.0.1:${cloudPort}`
});
try {
const jobResponse = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
method: "POST",
headers: { "content-type": "application/json", "x-hwlab-internal-service": "hwlab-cloud-api" },
body: JSON.stringify({
jobId: "job_options_test",
intent: "debug.download",
args: { action: "start", captureUart: "uart/1", captureDurationMs: 8000, port: "COM4", baudRate: 921600 },
profileHash: "sha256:profile",
profile: {
devicePodId: "device-pod-test",
target: { id: "target-test" },
route: { gatewaySessionId: "gws_test", resourceId: "res_test", capabilityId: "cap_test", hostCli: "node tools/device-host-cli.mjs" }
}
})
});
assert.equal(jobResponse.status, 202);
await waitForJobStatus(service.port, "device-pod-test", "job_options_test", "completed");
const command = dispatchedBody.params.input.command;
const encoded = command.match(/"([A-Za-z0-9+/=]+)"$/u)[1];
const decoded = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
assert.deepEqual(decoded.args, [
"debug-probe", "download", "start",
"--capture-uart", "uart/1",
"--capture-duration-ms", "8000",
"--port", "COM4",
"--baud-rate", "921600"
]);
} finally {
await service.stop();
await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve())));
}
});
async function startDevicePod(devicePodId, extraEnv = {}) {
const port = await freePort();
const child = spawn(bunCommand, ["run", "cmd/hwlab-device-pod/main.ts"], {
+23 -5
View File
@@ -399,20 +399,38 @@ function deviceHostArgs(intent, args = {}) {
}
if (intent === "workspace.rg") {
const pattern = textOr(args.pattern ?? args.query, "");
return pattern ? ["workspace", "rg", pattern, textOr(args.path, ".")] : null;
return pattern ? ["workspace", "rg", pattern, textOr(args.path, "."), ...hostOptionArgs(args, ["glob", "g", "filesWithMatches", "l", "ignoreCase", "i", "maxCount"])] : null;
}
if (intent === "workspace.apply-patch") return ["workspace", "apply-patch", textOr(args.base, "."), "--patch-b64", patchBase64(args)];
if (intent === "workspace.build") return ["workspace", "build", textOr(args.action, "start")];
if (intent === "workspace.build") return ["workspace", "build", textOr(args.action, "start"), ...hostOptionArgs(args, ["target", "timeoutMs"]), ...jobIdArgs(args)];
if (intent === "debug.status") return ["debug-probe", "status"];
if (intent === "debug.chip-id") return ["debug-probe", "chip-id"];
if (intent === "debug.download") return ["debug-probe", "download", textOr(args.action, "start")];
if (intent === "debug.download") return ["debug-probe", "download", textOr(args.action, "start"), ...hostOptionArgs(args, ["target", "timeoutMs", "captureUart", "captureDurationMs", "durationMs", "port", "baudRate"]), ...jobIdArgs(args)];
if (intent === "debug.reset") return ["debug-probe", "reset"];
if (intent === "io.ports") return ["io-probe", textOr(args.uartId, "uart/1"), "ports"];
if (intent === "io.uart.read") return ["io-probe", textOr(args.uartId, "uart/1"), "read", "--duration-ms", String(numberOr(args.durationMs ?? args["duration-ms"], 1000))];
if (intent === "io.uart.write") return ["io-probe", textOr(args.uartId, "uart/1"), "write", ...(args.hex ? ["--hex"] : []), textOr(args.message ?? args.text ?? args.data, "")];
if (intent === "io.uart.read") return ["io-probe", textOr(args.uartId, "uart/1"), "read", "--duration-ms", String(numberOr(args.durationMs ?? args["duration-ms"], 1000)), ...hostOptionArgs(args, ["port", "baudRate"])] ;
if (intent === "io.uart.read-after-launch-flash") return ["io-probe", textOr(args.uartId, "uart/1"), "read-after-launch-flash", ...hostOptionArgs(args, ["durationMs", "port", "baudRate", "flashBase", "skipHardwareReset", "connectMode", "timeoutMs"])] ;
if (intent === "io.uart.write") return ["io-probe", textOr(args.uartId, "uart/1"), "write", ...(args.hex ? ["--hex"] : []), ...hostOptionArgs(args, ["port", "baudRate"]), textOr(args.message ?? args.text ?? args.data, "")];
return null;
}
function jobIdArgs(args = {}) {
const jobId = textOr(args.jobId, "");
return jobId ? [jobId] : [];
}
function hostOptionArgs(args = {}, keys = []) {
const out = [];
for (const key of keys) {
const value = args[key];
if (value === undefined || value === null || value === "" || value === false) continue;
const name = `--${key.replace(/[A-Z]/gu, (match) => `-${match.toLowerCase()}`)}`;
if (value === true) out.push(name);
else out.push(name, String(value));
}
return out;
}
function patchBase64(args = {}) {
if (typeof args.patchB64 === "string") return args.patchB64;
if (typeof args["patch-b64"] === "string") return args["patch-b64"];