import assert from "node:assert/strict"; import { mkdtemp, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { HWLAB_M3_IO_API_ROUTE, HWLAB_M3_STATUS_API_ROUTE } from "../../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; import { runCli } from "./cli.mjs"; async function captureCli(args, options = {}) { let stdout = ""; let stderr = ""; const exitCode = await runCli(args, { cwd: options.cwd ?? process.cwd(), env: options.env ?? { PATH: process.env.PATH }, requestJson: options.requestJson, spawnDetached: options.spawnDetached, stdout: { write(chunk) { stdout += chunk; } }, stderr: { write(chunk) { stderr += chunk; } } }); return { exitCode, stdout, stderr }; } test("repo hwlab-cli cicd submit is non-blocking and records follow-up commands", async () => { const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-")); const launched = []; const result = await captureCli( ["cicd", "submit", "--kind", "ci-publish", "--services", "hwlab-cloud-web", "--concurrency", "2"], { cwd, spawnDetached: async (job) => { launched.push(job); return { pid: 123456 }; } } ); assert.equal(result.exitCode, 0, result.stderr); const body = JSON.parse(result.stdout); assert.equal(body.ok, true); assert.equal(body.nonBlocking, true); assert.equal(body.kind, "ci-publish"); assert.match(body.jobId, /^ci-publish-/u); assert.equal(body.pid, 123456); assert.equal(body.followup.status, `hwlab-cli cicd status ${body.jobId}`); assert.equal(launched.length, 1); const spec = JSON.parse(await readFile(path.join(cwd, ".state", "hwlab-cicd", "jobs", body.jobId, "spec.json"), "utf8")); assert.deepEqual(spec.args.slice(0, 4), ["scripts/dev-artifact-publish.mjs", "--publish", "--report", body.reportPath]); assert.ok(spec.args.includes("--quiet-build")); assert.equal(spec.envPatch.HWLAB_CI_ARTIFACT_RUN_ID, body.jobId); }); test("repo hwlab-cli cicd status/logs/report disclose bounded job state", async () => { const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-")); const submit = await captureCli(["cicd", "submit", "--kind", "ci-publish"], { cwd, spawnDetached: async () => ({ pid: 123457 }) }); const submitted = JSON.parse(submit.stdout); const jobDir = path.join(cwd, ".state", "hwlab-cicd", "jobs", submitted.jobId); await writeFile(path.join(jobDir, "stdout.log"), "build line 1\nbuild line 2\n", "utf8"); await writeFile(path.join(jobDir, "stderr.log"), "warning line\n", "utf8"); await writeFile( path.join(jobDir, "report.json"), `${JSON.stringify({ sourceCommitId: "abc123", artifactPublish: { status: "published", serviceCount: 1, requiredServiceCount: 1, publishedCount: 1, timings: { durationMs: 42 } }, blockers: [] })}\n`, "utf8" ); const status = await captureCli(["cicd", "status", submitted.jobId], { cwd }); assert.equal(status.exitCode, 0, status.stderr); assert.equal(JSON.parse(status.stdout).status, "submitted"); const logs = await captureCli(["cicd", "logs", submitted.jobId, "--tail-bytes", "12"], { cwd }); assert.equal(logs.exitCode, 0, logs.stderr); const logBody = JSON.parse(logs.stdout); assert.equal(logBody.stdout.returnedBytes, 12); assert.equal(logBody.stdout.truncated, true); assert.ok(logBody.stdout.text.includes("line 2")); const report = await captureCli(["cicd", "report", submitted.jobId], { cwd }); assert.equal(report.exitCode, 0, report.stderr); const reportBody = JSON.parse(report.stdout); assert.equal(reportBody.summary.kind, "ci-publish"); assert.equal(reportBody.summary.status, "published"); assert.equal(reportBody.summary.publishedCount, 1); assert.equal(Object.hasOwn(reportBody, "report"), false); }); test("repo hwlab-cli m3 status uses Skill CLI and calls only HWLAB API /v1/m3/status", async () => { const calls = []; const result = await captureCli( [ "m3", "status", "--api-base-url", "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "--trace-id", "trc_hwlab_cli_status" ], { requestJson: async (url, request) => { calls.push({ url, request }); assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_STATUS_API_ROUTE}`); assert.equal(request.method, "GET"); return { ok: true, status: 200, body: { status: "live", sourceKind: "DEV-LIVE", traceId: "trc_hwlab_cli_status", boxes: [{ id: "boxsimu_2", resourceId: "res_boxsimu_2", online: true, ports: { DI1: { value: false } } }] } }; } } ); assert.equal(result.exitCode, 0, result.stderr); const body = JSON.parse(result.stdout); assert.equal(body.route, HWLAB_M3_STATUS_API_ROUTE); assert.equal(body.method, "GET"); assert.equal(body.readback.value, false); assert.equal(body.command.includes("hwlab-cloud-api.hwlab-dev"), false); assert.equal(calls.length, 1); }); test("repo hwlab-cli m3 io blocks direct hardware targets before request", async () => { const result = await captureCli( [ "m3", "io", "--action", "di.read", "--api-base-url", "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301", "--trace-id", "trc_hwlab_cli_direct_blocked" ], { requestJson: async () => { throw new Error("direct hardware target must not be called"); } } ); assert.equal(result.exitCode, 2); const body = JSON.parse(result.stderr); assert.equal(body.route, HWLAB_M3_IO_API_ROUTE); assert.equal(body.blocker.code, "direct_hardware_target_blocked"); assert.equal(body.hwlabApi.redactedUrl, null); assert.equal(JSON.stringify(body).includes("patch-panel.hwlab-dev"), false); });