Files
pikasTech-HWLAB/scripts/dev-m4-agent-loop-smoke.mjs
T
2026-05-22 11:16:29 +08:00

321 lines
11 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const fixedNow = () => "2026-05-21T00:00:00.000Z";
const DEV_ENDPOINT = "http://74.48.78.17:16667";
const requiredDocs = ["docs/dev-acceptance-matrix.md", "docs/dev-m4-agent-loop.md"];
const reportPath = "reports/dev-gate/dev-m4-agent-loop.json";
const preflightCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production";
const dryRunCommand = "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run";
function parseArgs(argv) {
const flags = new Set();
for (const arg of argv) {
if (arg.startsWith("--")) {
flags.add(arg);
}
}
return {
live: flags.has("--live") || flags.has("--allow-live"),
dryRun: flags.has("--dry-run"),
confirmDev: flags.has("--confirm-dev"),
confirmedNonProduction: flags.has("--confirmed-non-production")
};
}
async function readJSON(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
}
async function readText(relativePath) {
return readFile(path.join(repoRoot, relativePath), "utf8");
}
async function writeJSON(relativePath, value) {
const absolutePath = path.join(repoRoot, relativePath);
await mkdir(path.dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function currentOriginMainCommitId() {
return execFileSync("git", ["rev-parse", "--short=7", "origin/main"], {
cwd: repoRoot,
encoding: "utf8"
}).trim();
}
function requestJson(urlString, { method = "GET", headers = {}, body, timeoutMs = 5000 } = {}) {
const url = new URL(urlString);
const client = url.protocol === "https:" ? httpsRequest : httpRequest;
const payload = body === undefined ? null : Buffer.from(body);
return new Promise((resolve, reject) => {
const request = client(
{
protocol: url.protocol,
hostname: url.hostname,
port: url.port,
path: `${url.pathname}${url.search}`,
method,
headers: {
...headers
}
},
(response) => {
let raw = "";
response.setEncoding("utf8");
response.on("data", (chunk) => {
raw += chunk;
});
response.on("end", () => {
let parsed = null;
if (raw.length > 0) {
try {
parsed = JSON.parse(raw);
} catch (error) {
reject(new Error(`invalid JSON response from ${urlString}: ${error.message}`));
return;
}
}
resolve({
statusCode: response.statusCode ?? 0,
headers: response.headers,
body: parsed,
raw
});
});
}
);
request.on("error", reject);
request.setTimeout(timeoutMs, () => {
request.destroy(new Error(`request to ${urlString} timed out after ${timeoutMs}ms`));
});
if (payload) {
request.write(payload);
}
request.end();
});
}
function statusForResult(result) {
return result.blocked ? "blocked" : "pass";
}
function blockerForResult(result) {
return result.blocked
? [
{
type: result.blockerType,
scope: "devPreconditions",
status: "open",
summary: result.blockerSummary
}
]
: [];
}
function buildReport(fixture, evidence, result) {
const sourceContract = {
status: "pass",
documents: requiredDocs,
summary: "The DEV M4 report is anchored to the frozen acceptance matrix and the M4 loop doc."
};
const validationCommands = [
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs",
"node --check scripts/dev-m4-agent-loop-smoke.mjs",
dryRunCommand,
preflightCommand
];
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-m4-agent-loop.json",
reportVersion: "v1",
issue: "pikasTech/HWLAB#37",
taskId: "dev-m4-agent-loop",
commitId: currentOriginMainCommitId(),
acceptanceLevel: "dev_m4_agent_loop",
devOnly: true,
prodDisabled: true,
sourceContract,
validationCommands,
localSmoke: {
status: "pass",
commands: ["node scripts/m4-agent-loop-smoke.mjs"],
evidence: [
"Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.",
"Workspace isolation and explicit skills commit wiring are covered by fixture assertions."
],
summary: "Local contract smoke passes on the repo fixture."
},
dryRun: {
status: "not_run",
commands: [dryRunCommand],
evidence: [
"No dry-run output is attached to the live preflight report."
],
summary: "The live preflight report does not attempt a dry-run agent run."
},
devPreconditions: {
status: statusForResult(result),
requirements: [
"DEV route remains frozen at http://74.48.78.17:16667",
"No secret reads, real deployment, or long-running agent task is attempted",
"Agent manager, worker, and skills are reachable only through the cloud API boundary"
],
summary: result.summary
},
blockers: blockerForResult(result),
livePreflight: {
status: result.blocked ? "blocked" : "pass",
commands: [preflightCommand],
evidence: evidence.length ? evidence : ["No live DEV observation was recorded."],
summary: result.summary
},
notes: "DEV M4 agent loop live preflight report. No secret material is read."
};
}
async function main() {
const args = parseArgs(process.argv.slice(2));
assert.equal(args.dryRun, false, "--dry-run is not supported for this live preflight");
assert.equal(args.live, true, "live preflight requires --live");
assert.equal(args.confirmDev, true, "live preflight requires --confirm-dev");
assert.equal(args.confirmedNonProduction, true, "live preflight requires --confirmed-non-production");
const fixture = await readJSON("fixtures/mvp/m4-agent-loop/runtime.json");
const evidence = [];
const localEvidence = await readText(fixture.evidencePath);
assert.ok(localEvidence.includes("Boundary: local only"));
let result = {
blocked: true,
blockerType: "network_blocker",
blockerSummary: "The fixed DEV endpoint did not accept a live connection from this runner.",
summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is not reachable from this D601 runner.`
};
try {
const health = await requestJson(`${DEV_ENDPOINT}/health`);
if (health.statusCode < 200 || health.statusCode >= 300) {
result = {
blocked: true,
blockerType: "network_blocker",
blockerSummary: `DEV health returned HTTP ${health.statusCode}.`,
summary: `Blocked on DEV ingress health because ${DEV_ENDPOINT}/health did not return success.`
};
} else {
const body = health.body || {};
evidence.push(`health:${body.serviceId}:${body.environment}:${body.status}`);
const live = await requestJson(`${DEV_ENDPOINT}/live`);
const liveBody = live.body || {};
if (live.statusCode < 200 || live.statusCode >= 300 || liveBody?.serviceId !== "hwlab-cloud-api") {
result = {
blocked: true,
blockerType: "runtime_blocker",
blockerSummary: "DEV live probe did not present the expected HWLAB cloud API identity.",
summary: "Blocked because the live DEV boundary did not expose the expected cloud API identity."
};
} else {
evidence.push(`live:${liveBody.serviceId}:${liveBody.status}`);
const rpcHealth = await requestJson(`${DEV_ENDPOINT}/rpc`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "req_dev_m4_health",
method: "system.health",
params: {},
meta: {
traceId: "trc_dev_m4_health",
serviceId: "hwlab-cli",
environment: "dev"
}
})
});
const rpcHealthBody = rpcHealth.body || {};
evidence.push(`rpc-health:${rpcHealthBody.result?.serviceId ?? "unknown"}`);
const rpcHardware = await requestJson(`${DEV_ENDPOINT}/rpc`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "req_dev_m4_hw",
method: "hardware.operation.request",
params: {
projectId: fixture.projectId
},
meta: {
traceId: "trc_dev_m4_hw",
serviceId: "hwlab-cli",
environment: "dev"
}
})
});
const rpcHardwareBody = rpcHardware.body || {};
const accepted = rpcHardwareBody.result?.accepted === true;
const status = rpcHardwareBody.result?.status || rpcHardwareBody.error?.message || "unknown";
evidence.push(`rpc-hardware:${status}`);
if (!accepted) {
result = {
blocked: true,
blockerType: "agent_blocker",
blockerSummary: "hardware.operation.request remains a skeleton response in the current DEV runtime.",
summary: "Blocked by M3 readiness because the agent hardware dispatch path is still skeleton-only."
};
} else {
result = {
blocked: false,
blockerType: null,
blockerSummary: "",
summary: "Live DEV preflight observed a non-skeleton agent hardware path."
};
}
}
}
} catch (error) {
result = {
blocked: true,
blockerType: "network_blocker",
blockerSummary: error instanceof Error ? error.message : String(error),
summary: `Blocked before agent scheduling because ${DEV_ENDPOINT} is unreachable from this runner.`
};
}
const report = buildReport(fixture, evidence, result);
await writeJSON(reportPath, report);
process.stdout.write(JSON.stringify({
reportPath,
status: report.devPreconditions.status,
blocker: report.blockers[0] ?? null,
evidence: report.livePreflight.evidence,
summary: report.livePreflight.summary,
observedAt: fixedNow()
}, null, 2));
process.stdout.write("\n");
}
await main();