fix: 补齐 CaseRun UART 权威观测
This commit is contained in:
@@ -25,6 +25,21 @@ const DOCUMENT = {
|
||||
}
|
||||
};
|
||||
|
||||
function executePythonNodePlan(plan: unknown) {
|
||||
const pythonScript = `
|
||||
import importlib.util, json, logging, sys
|
||||
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
plan = json.loads(sys.argv[1])
|
||||
executor = module.NodeOpsExecutor({"nodeId": plan["nodeId"]}, logging.getLogger("test"))
|
||||
print(json.dumps(executor.execute(plan)))
|
||||
`;
|
||||
const python = spawnSync("python3", ["-c", pythonScript, JSON.stringify(plan)], { cwd: process.cwd(), encoding: "utf8" });
|
||||
assert.equal(python.status, 0, python.stderr);
|
||||
return JSON.parse(python.stdout);
|
||||
}
|
||||
|
||||
test("CaseRun derives compile-only and runtime validation plans from case definition", () => {
|
||||
const compile = caseValidationPlanFromDefinition({});
|
||||
assert.equal(compile.mode, "compile-only");
|
||||
@@ -64,18 +79,7 @@ test("HWPOD node owns ioProbe command output normalization before CaseRun projec
|
||||
document.spec.ioProbe.endpoints.main41.command = `${process.execPath} ${fakeBoardComm}`;
|
||||
const plan = compileHwpodNodeOpsPlan({ document, intent: "io.probe.read", args: { probeId: "main41.ai0.current", count: 3 } });
|
||||
const nodeResult = await executeHwpodNodeOpsPlan(plan, { now: () => "2026-07-16T00:00:00.000Z" });
|
||||
const pythonScript = `
|
||||
import importlib.util, json, logging, sys
|
||||
spec = importlib.util.spec_from_file_location("hwlab_node", "tools/hwlab-node.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
plan = json.loads(sys.argv[1])
|
||||
executor = module.NodeOpsExecutor({"nodeId": plan["nodeId"]}, logging.getLogger("test"))
|
||||
print(json.dumps(executor.execute(plan)))
|
||||
`;
|
||||
const python = spawnSync("python3", ["-c", pythonScript, JSON.stringify(plan)], { cwd: process.cwd(), encoding: "utf8" });
|
||||
assert.equal(python.status, 0, python.stderr);
|
||||
const pythonResult = JSON.parse(python.stdout);
|
||||
const pythonResult = executePythonNodePlan(plan);
|
||||
|
||||
assert.equal(nodeResult.ok, true);
|
||||
assert.equal(nodeResult.results[0].output.observation.probeId, "main41.ai0.current");
|
||||
@@ -96,6 +100,46 @@ print(json.dumps(executor.execute(plan)))
|
||||
}
|
||||
});
|
||||
|
||||
test("HWPOD TS and Python nodes project identical UART observations before CaseRun normalization", async () => {
|
||||
const workspacePath = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-uart-observation-"));
|
||||
try {
|
||||
const serialMonitor = path.join(workspacePath, "serial-monitor-fixture.mjs");
|
||||
await writeFile(serialMonitor, `
|
||||
const args = process.argv.slice(2).join(" ");
|
||||
if (args === "monitor status") console.log(JSON.stringify({ success: true, data: { isMonitoring: true, port: "COM9", baudRate: 115200 } }));
|
||||
else if (args.startsWith("fetch")) console.log(JSON.stringify({ success: true, data: [{ timestamp: "2026-07-16T00:00:00Z", port: "COM9", data: "UART READY" }], count: 1, totalCount: 1, hasMore: false, truncated: false, sourceFile: "artifacts/uart/session.jsonl" }));
|
||||
else { console.error("unsupported " + args); process.exit(2); }
|
||||
`, "utf8");
|
||||
const document = structuredClone(DOCUMENT);
|
||||
document.spec.workspace.path = workspacePath;
|
||||
document.spec.ioProbe.uart = { id: "uart1", port: "COM9", baudrate: 115200 };
|
||||
document.spec.tooling = { serialMonitorDir: workspacePath, serialMonitorCommand: `${process.execPath} ${serialMonitor}` };
|
||||
const plan = compileHwpodNodeOpsPlan({ document, intent: "io.uart.read", args: { port: "uart1", maxBytes: 128 } });
|
||||
const nodeResult = await executeHwpodNodeOpsPlan(plan, { now: () => "2026-07-16T00:00:00.000Z" });
|
||||
const pythonResult = executePythonNodePlan(plan);
|
||||
|
||||
assert.equal(nodeResult.ok, true);
|
||||
assert.equal(pythonResult.ok, true);
|
||||
assert.deepEqual(pythonResult.results[0].output.observation, nodeResult.results[0].output.observation);
|
||||
assert.equal(nodeResult.results[0].output.observation.text, "UART READY");
|
||||
assert.equal(nodeResult.results[0].output.observation.rawArtifactRef, "artifacts/uart/session.jsonl");
|
||||
|
||||
const validationPlan = caseValidationPlanFromDefinition({ validation: { mode: "download-uart", uart: { port: "uart1" } } });
|
||||
const normalized = normalizeValidationStepResult({ step: validationPlan.steps[2], document, exitCode: 0, payload: { body: nodeResult } });
|
||||
assert.equal(normalized.blocker, undefined);
|
||||
assert.equal(normalized.observation.evidenceLevel, "serial-log");
|
||||
assert.equal(normalized.observation.rawArtifactRef, "artifacts/uart/session.jsonl");
|
||||
|
||||
const stdoutOnly = structuredClone(nodeResult);
|
||||
stdoutOnly.results[0].output = { workspacePath, stdout: JSON.stringify(nodeResult.results[0].output.observation) };
|
||||
const rejected = normalizeValidationStepResult({ step: validationPlan.steps[2], document, exitCode: 0, payload: { body: stdoutOnly } });
|
||||
assert.equal(rejected.observation, undefined);
|
||||
assert.equal(rejected.blocker.code, "hwpod_validation_observation_missing");
|
||||
} finally {
|
||||
await rm(workspacePath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("CaseRun normalizes authoritative ioProbe statistics and replay relationships", () => {
|
||||
const plan = caseValidationPlanFromDefinition({ validation: { mode: "io-probe", ioProbe: { probeId: "main41.ai0.current", quantity: "current", unit: "mA" } } });
|
||||
const step = plan.steps[2];
|
||||
|
||||
+8
-1
@@ -803,7 +803,14 @@ class NodeOpsExecutor:
|
||||
rows = fetch_body.get("data") if isinstance(fetch_body.get("data"), list) else []
|
||||
text = "\n".join(str(row.get("data")) for row in rows if isinstance(row, dict) and row.get("data"))
|
||||
max_bytes = safe_int(args.get("maxBytes"), 16384)
|
||||
return {"ok": True, "bindingSource": "serial-monitor-cli", "serialMonitorDir": str(serial_dir), "requestedPort": port or None, "resolvedPort": active_port or port or None, "baudRate": baud or active_baud, "command": [*command_base, *fetch_args], "data": rows, "count": fetch_body.get("count", len(rows)), "totalCount": fetch_body.get("totalCount"), "hasMore": fetch_body.get("hasMore", False), "text": text[:max_bytes], "truncated": len(text) > max_bytes or fetch_body.get("truncated") is True, "monitorStatus": status_body, "monitorStarted": not matches, "monitorStart": monitor_start, "sourceFile": fetch_body.get("sourceFile")}
|
||||
truncated_text = text[:max_bytes]
|
||||
resolved_port = active_port or port or None
|
||||
resolved_baud = baud or active_baud
|
||||
source_file = str(fetch_body.get("sourceFile") or "")
|
||||
observation = {"observationId": f"uart:{resolved_port or 'unknown'}:{source_file or 'session'}", "port": resolved_port, "baudRate": resolved_baud, "data": rows, "text": truncated_text, "truncated": len(text) > max_bytes or fetch_body.get("truncated") is True}
|
||||
if source_file:
|
||||
observation["rawArtifactRef"] = source_file
|
||||
return {"ok": True, "workspacePath": str(self._workspace_root(args)), "bindingSource": "serial-monitor-cli", "serialMonitorDir": str(serial_dir), "requestedPort": port or None, "resolvedPort": resolved_port, "baudRate": resolved_baud, "command": [*command_base, *fetch_args], "data": rows, "count": fetch_body.get("count", len(rows)), "totalCount": fetch_body.get("totalCount"), "hasMore": fetch_body.get("hasMore", False), "text": truncated_text, "truncated": len(text) > max_bytes or fetch_body.get("truncated") is True, "monitorStatus": status_body, "monitorStarted": not matches, "monitorStart": monitor_start, "sourceFile": source_file or None, "observation": observation}
|
||||
|
||||
def _parse_json(self, text: object) -> dict:
|
||||
try:
|
||||
|
||||
@@ -1408,13 +1408,26 @@ async function uartRead(args: any) {
|
||||
const rows = Array.isArray(fetchBody.data) ? fetchBody.data : [];
|
||||
const joined = rows.map((row: any) => text(row?.data)).filter(Boolean).join("\n");
|
||||
const truncatedText = joined.length > maxBytes ? joined.slice(0, maxBytes) : joined;
|
||||
const resolvedPort = text(statusData.port) || targetPort;
|
||||
const baudRate = numberValue(statusData.baudRate) || targetBaudRate || activeBaudRate || null;
|
||||
const sourceFile = text(fetchBody.sourceFile);
|
||||
const observation = {
|
||||
observationId: `uart:${resolvedPort || "unknown"}:${sourceFile || "session"}`,
|
||||
port: resolvedPort,
|
||||
baudRate,
|
||||
data: rows,
|
||||
text: truncatedText,
|
||||
truncated: joined.length > maxBytes || fetchBody.truncated === true,
|
||||
...(sourceFile ? { rawArtifactRef: sourceFile } : {})
|
||||
};
|
||||
return {
|
||||
ok: true,
|
||||
workspacePath: workspaceRoot(args),
|
||||
bindingSource: "serial-monitor-cli",
|
||||
serialMonitorDir,
|
||||
requestedPort: diagnostics.requestedPort,
|
||||
resolvedPort: text(statusData.port) || targetPort,
|
||||
baudRate: numberValue(statusData.baudRate) || targetBaudRate || activeBaudRate || null,
|
||||
resolvedPort,
|
||||
baudRate,
|
||||
command: [...commandBase, ...fetchArgs],
|
||||
data: rows,
|
||||
count: fetchBody.count ?? rows.length,
|
||||
@@ -1425,7 +1438,8 @@ async function uartRead(args: any) {
|
||||
monitorStatus: statusBody ?? monitorStatus,
|
||||
monitorStarted: !monitoringMatches,
|
||||
monitorStart,
|
||||
sourceFile: fetchBody.sourceFile ?? null
|
||||
sourceFile: sourceFile || null,
|
||||
observation
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user