feat: 实现 CaseRun registry 离线诊断

This commit is contained in:
root
2026-07-16 16:01:54 +02:00
parent 324bb9d2e4
commit 6af6711fc3
4 changed files with 413 additions and 9 deletions
@@ -0,0 +1,128 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { runHwlabCli } from "../src/hwlab-cli-lib.ts";
test("case aggregate remains registry-only even when runtime credentials exist", async () => {
const fixture = await createRegistryFixture("aggregate-offline");
try {
let runtimeCalls = 0;
const result = await runHwlabCli(["case", "aggregate", fixture.caseId, "--run-id", fixture.runId, "--case-repo", fixture.caseRepo, "--no-case-repo-git-sync"], {
cwd: fixture.root,
env: { HWLAB_API_KEY: "hwl_live_should_not_be_used" },
runProcess: async () => {
runtimeCalls += 1;
throw new Error("offline aggregate must not execute runtime commands");
},
});
assert.equal(result.exitCode, 0, JSON.stringify(result.payload, null, 2));
assert.equal(runtimeCalls, 0);
assert.equal(result.payload.action, "case.aggregate");
assert.match(await readFile(path.join(fixture.runDir, "aggregate.md"), "utf8"), /registry artifacts/u);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("case refresh is explicit, registry-backed, and semantically idempotent", async () => {
const fixture = await createRegistryFixture("refresh-local");
try {
const args = ["case", "refresh", fixture.caseId, "--run-id", fixture.runId, "--case-repo", fixture.caseRepo, "--source", "local", "--no-case-repo-git-sync"];
const first = await runHwlabCli(args, { cwd: fixture.root, now: () => "2026-07-16T14:00:00.000Z", env: { HWLAB_API_KEY: "hwl_live_should_not_be_used" }, runProcess: forbiddenRuntime });
assert.equal(first.exitCode, 0, JSON.stringify(first.payload, null, 2));
assert.deepEqual(first.payload.refreshed.provenance, { source: "local", authority: "registry", runtimeAccess: false });
assert.equal(first.payload.refreshed.commandCount, 2);
assert.equal(first.payload.refreshed.hwpodCommandCount, 1);
assert.equal(first.payload.refreshed.hwpodBuildCommandCount, 1);
const before = await fileHashes(fixture.runDir, ["evidence.json", "agent-messages.json", "artifact-manifest.json", "aggregate.md"]);
const second = await runHwlabCli(args, { cwd: fixture.root, now: () => "2026-07-16T15:00:00.000Z", env: { HWLAB_API_KEY: "hwl_live_should_not_be_used" }, runProcess: forbiddenRuntime });
assert.equal(second.exitCode, 0, JSON.stringify(second.payload, null, 2));
assert.deepEqual(await fileHashes(fixture.runDir, Object.keys(before)), before);
const manifest = JSON.parse(await readFile(path.join(fixture.runDir, "artifact-manifest.json"), "utf8"));
assert.deepEqual(manifest.refresh, { source: "local", authority: "registry", runtimeAccess: false });
assert.equal(manifest.trace.toolCallSummary.count, 2);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("case run offline diagnostics inspect trace and skill usage without runtime access", async () => {
const fixture = await createRegistryFixture("inspect-offline");
try {
const options = { cwd: fixture.root, env: { HWLAB_API_KEY: "hwl_live_should_not_be_used" }, runProcess: forbiddenRuntime };
const inspect = await runHwlabCli(["case", "run", "inspect", fixture.runId, "--case-repo", fixture.caseRepo], options);
assert.equal(inspect.exitCode, 0, JSON.stringify(inspect.payload, null, 2));
assert.equal(inspect.payload.serviceRuntime, false);
assert.equal(inspect.payload.traceId, "trc_offline");
assert.equal(inspect.payload.providerProfile, "deepseek");
assert.equal(inspect.payload.terminalStatus, "completed");
assert.equal(inspect.payload.toolCallSummary.count, 2);
const trace = await runHwlabCli(["case", "run", "trace-summary", fixture.runId, "--case-repo", fixture.caseRepo], options);
assert.equal(trace.exitCode, 0, JSON.stringify(trace.payload, null, 2));
assert.equal(trace.payload.commandCount, 2);
assert.equal(trace.payload.traceCommands.length, 2);
const skill = await runHwlabCli(["case", "run", "skill-usage", fixture.runId, "--skill", "keil", "--case-repo", fixture.caseRepo], options);
assert.equal(skill.exitCode, 0, JSON.stringify(skill.payload, null, 2));
assert.equal(skill.payload.serviceRuntime, false);
assert.equal(skill.payload.trace.readSkillFile, true);
assert.equal(skill.payload.resourceBundle.materialized, true);
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
test("case refresh accesses runtime only with explicit runtime source", async () => {
const fixture = await createRegistryFixture("refresh-runtime");
try {
let runtimeCalls = 0;
const result = await runHwlabCli(["case", "refresh", fixture.caseId, "--run-id", fixture.runId, "--case-repo", fixture.caseRepo, "--source", "runtime", "--no-case-repo-git-sync"], {
cwd: fixture.root,
env: { HWLAB_API_KEY: "hwl_live_runtime_test" },
runProcess: async () => {
runtimeCalls += 1;
return { command: [], exitCode: 0, stderr: "", stdout: JSON.stringify({ body: { traceId: "trc_offline", source: "hwlab-cli.client.agent.trace", status: "completed", terminalStatus: "completed", rows: [{ rowId: "tool:runtime", seq: 1, header: "工具调用", body: "hwpod build --spec .hwlab/hwpod-spec.yaml" }, { rowId: "final", seq: 2, header: "助手最终消息", terminal: true, body: "runtime refreshed" }] } }) };
},
});
assert.equal(result.exitCode, 0, JSON.stringify(result.payload, null, 2));
assert.equal(runtimeCalls, 1);
assert.deepEqual(result.payload.refreshed.provenance, { source: "runtime", authority: "registry", runtimeAccess: true });
} finally {
await rm(fixture.root, { recursive: true, force: true });
}
});
async function createRegistryFixture(suffix: string) {
const root = await mkdtemp(path.join(os.tmpdir(), `hwlab-caserun-${suffix}-`));
const caseRepo = path.join(root, "hwlab-case-registry");
const caseId = `case-${suffix}`;
const runId = `run-${suffix}`;
const runDir = path.join(caseRepo, "runs", caseId, runId);
await mkdir(path.join(caseRepo, ".git"), { recursive: true });
await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8");
await mkdir(path.join(runDir, ".hwlab"), { recursive: true });
const agent = { stageStatus: "completed", baseUrl: "http://web.test", traceId: "trc_offline", sessionId: "ses_offline", conversationId: "cnv_offline", threadId: "thr_offline", providerProfile: "deepseek", requestedProviderProfile: "deepseek", resolvedBackendProfile: "deepseek-runtime", provider: "deepseek", model: "deepseek-chat", terminalStatus: "completed", commandStatus: "completed", agentRunStatus: "completed", promptSha256: "prompt-sha" };
const toolCallSummary = { source: "agentrun-result.toolCallSummary", count: 2, statusCounts: { completed: 2 }, exitCodeCounts: { "0": 2 }, terminalStatus: "completed", commandStatus: "completed", agentRunStatus: "completed", items: [{ kind: "build", toolName: "commandExecution", status: "completed", exitCode: 0, command: "hwpod build --spec .hwlab/hwpod-spec.yaml" }, { kind: "other", toolName: "commandExecution", status: "completed", exitCode: 0, command: "cat ~/.agents/skills/keil/SKILL.md" }] };
await writeJson(path.join(runDir, "run.json"), { caseId, runId, runDir, caseRepo, specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), compileOnly: true, createdAt: "2026-07-16T13:00:00.000Z", updatedAt: "2026-07-16T13:00:00.000Z", definition: {}, subject: { repoLocalPath: "C:/work/fw", commitId: "0".repeat(40), subdir: "", worktreePath: "C:/work/fw/.worktree/case", workspacePath: "C:/work/fw/.worktree/case", setup: {} }, agent });
await writeJson(path.join(runDir, "evidence.json"), { contractVersion: "hwpod-case-run-evidence-v1", caseId, runId, compileOnly: true, autoEvaluation: false, status: "recorded", subject: { repoLocalPath: "C:/work/fw", commitId: "0".repeat(40), worktreePath: "C:/work/fw/.worktree/case" }, agent, agentTrace: { traceId: "trc_offline", terminalStatus: "completed", toolCallSummary, commandCount: 2, hwpodCommandCount: 1, hwpodBuildCommandCount: 1 }, agentFinal: { status: "completed", terminalStatus: "completed" }, postValidation: { source: "case-run-runner-post-agent-compile-check", status: "completed", returnCode: 0 }, decisions: { autoEvaluation: false, runnerPostAgentCompileCheck: "recorded" } });
await writeJson(path.join(runDir, "agent-trace.json"), { contractVersion: "hwpod-case-run-agent-trace-identity-v1", traceId: "trc_offline", status: "completed", terminalStatus: "completed", commandStatus: "completed", toolCallSummary });
await writeJson(path.join(runDir, "agent-messages.json"), { renderer: "tools/src/hwlab-cli/trace-renderer:traceDisplayRows", sourceEventCount: 2, renderedRowCount: 2, rows: [{ rowId: "tool:build", seq: 1, header: "工具调用", body: "hwpod build --spec .hwlab/hwpod-spec.yaml" }, { rowId: "tool:skill", seq: 2, header: "工具调用", body: "cat ~/.agents/skills/keil/SKILL.md" }] });
await writeJson(path.join(runDir, "artifact-manifest.json"), { contractVersion: "hwpod-case-run-artifact-manifest-v1", caseId, runId, agent, trace: { traceId: "trc_offline", terminalStatus: "completed", toolCallSummary, commandCount: 2, hwpodCommandCount: 1, hwpodBuildCommandCount: 1 }, agentFinal: { status: "completed" }, postValidation: { status: "completed", returnCode: 0 }, files: [] });
await writeFile(path.join(runDir, "agent-prompt.md"), "请读取 keil skill 后执行编译。\n", "utf8");
await writeFile(path.join(runDir, "final-response.md"), "# CaseRun Final Response\n\ncompleted\n", "utf8");
await writeFile(path.join(runDir, "agent-diff.patch"), "", "utf8");
await writeFile(path.join(runDir, ".hwlab", "hwpod-spec.yaml"), "metadata:\n name: offline\n", "utf8");
return { root, caseRepo, caseId, runId, runDir };
}
async function writeJson(file: string, value: unknown) { await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8"); }
async function fileHashes(root: string, files: string[]) { return Object.fromEntries(await Promise.all(files.map(async (file) => [file, createHash("sha256").update(await readFile(path.join(root, file))).digest("hex")]))); }
async function forbiddenRuntime() { throw new Error("offline diagnostics must not execute runtime commands"); }