fix: orchestrate caserun agent flow without grading
This commit is contained in:
@@ -214,9 +214,9 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case collect d601-f103-v2-compile --case-re
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run d601-f103-v2-compile --case-repo /root/hwlab-case-registry
|
||||
```
|
||||
|
||||
`case run` 默认按 compile-only 验收:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree 并写入 run-local spec,然后调用 `hwpod-cli build`,再通过 `cmd.run` 查询 Keil 异步 job-status,收集 `evidence.json` 到 run state,并把审计副本写入 case registry repo 的 `runs/<caseId>/<runId>/`。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载验收时,应作为独立 action 显式加入 case 定义和验收口径。
|
||||
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 采集 subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再调用 `hwpod-cli build`,通过 `cmd.run` 查询 Keil 异步 job-status,最后收集 `evidence.json` 到 run state,并把审计副本写入 case registry repo 的 `runs/<caseId>/<runId>/`。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。
|
||||
|
||||
当前标准 smoke case 是 `d601-f103-v2-compile`。它绑定已发现并预装的 `D601-F103-V2` HWPOD spec,当前只作为 HWPOD/Keil 编译底座 smoke;它可以由 Code Agent、AgentRun runner 或操作员从 `hwlab-cli case run d601-f103-v2-compile` 入口发起,但通过结果只证明 CaseRun runner 和 Keil 编译链路可用,不证明 Code Agent 已经收到 prompt 或完成源码任务。通过结果必须至少包含:`status=succeeded`、Keil build job terminal success、`returnCode=0`、`compileOnly=true`、`downloadSkipped=true`,并记录 `.hex` 和 `.axf` artifact 路径。
|
||||
当前标准 smoke case 是 `d601-f103-v2-compile`。它绑定已发现并预装的 `D601-F103-V2` HWPOD spec,目标是让 CaseRun 通过 Web 等价入口或 AgentRun runner 布置 Code Agent 舞台,再调用 `hwlab-cli case run d601-f103-v2-compile` 跑完整流程,而不是由操作员手工编译后代替运行。当前版本的结果只要求记录:subject provenance、agent prompt/session/trace/conversation/provider、subject worktree diff 文件、HWPOD build invocation、Keil job/status/artifact 字段、`compileOnly=true` 和 `downloadSkipped=true`;不得把这些 evidence 自动归纳为 Code Agent 通过/失败。
|
||||
|
||||
产物归属按两条线分开:
|
||||
|
||||
@@ -234,6 +234,6 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run d601-f103-v2-compile --case-repo /
|
||||
3. `hwpod-cli --dry-run` 输出的 plan 不需要云端 spec,也不读取其他本地旧 profile authority。
|
||||
4. `hwlab-api` 的 node-ops 入口能接收 plan,返回 JSON result;无可用 node 时必须返回结构化 blocker,而不是静默伪造 DEV-LIVE。
|
||||
5. 真实 CLI 验收必须证明 `hwpod-cli -> hwpod-compiler-cli -> hwlab-api /v1/hwpod-node-ops -> hwpod-node` 全链路走通。
|
||||
6. CaseRun 验收必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath` 和 `subject.commitId` 创建隔离 subject worktree,产出 isolated run state 和 case registry repo evidence;compile-only case 的通过条件是 Keil build job terminal success,并明确记录 downloadSkipped。
|
||||
7. `d601-f103-v2-compile` 的真实验收只覆盖 compile-only smoke;操作员直接运行 `hwpod-cli build`、直接调用 Keil 或只检查 node 侧 job,不等同于 CaseRun 通过,compile-only CaseRun 通过也不等同于 Code Agent 评测通过。
|
||||
8. agent-task CaseRun 的真实验收必须证明 `case run` 已创建或调用 Code Agent / AgentRun session,任务 prompt 已进入 agent 上下文,agent 在隔离 subject worktree 内产生可审计 diff,并由 CaseRun 对该 diff 执行编译、下载或 I/O smoke 等验证动作。
|
||||
6. CaseRun 流程必须能用 `hwlab-cli case run <caseId>` 从 case registry repo 读取 case 定义,按 `subject.repoLocalPath` 和 `subject.commitId` 创建隔离 subject worktree,生成 Code Agent prompt/context,记录 agent session/trace/conversation/provider,采集 subject worktree diff,执行 compile-only smoke,并产出 isolated run state 和 case registry repo evidence;当前版本只记录流程证据,不设置通过条件、不自动评价 evidence,并明确记录 downloadSkipped。
|
||||
7. `d601-f103-v2-compile` 的真实流程只覆盖当前编排链路和 compile-only smoke;操作员直接运行 `hwpod-cli build`、直接调用 Keil 或只检查 node 侧 job,不等同于 CaseRun 流程跑完。
|
||||
8. agent-task CaseRun 的真实流程必须证明 `case run` 已创建或调用 Code Agent / AgentRun session,任务 prompt 已进入 agent 上下文,CaseRun 已采集隔离 subject worktree diff,并已继续执行编译、下载或 I/O smoke 等后续动作;当前版本只记录这些事实,不自动判断 agent 是否完成任务。
|
||||
|
||||
@@ -113,6 +113,72 @@ test("case run exposes Keil hex and axf artifacts in evidence summary", () => {
|
||||
assert.deepEqual(artifactsFromKeilStatusForTest({ result: { hex_file: "F:\\out.hex", axf_file: "F:\\out.axf" } }), ["F:\\out.hex", "F:\\out.axf"]);
|
||||
});
|
||||
|
||||
test("case run orchestrates agent prompt, diff capture, and compile evidence without auto evaluation", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const caseRepo = path.join(root, "hwlab-case-registry");
|
||||
const cwd = path.join(root, "hwlab");
|
||||
const caseDir = path.join(caseRepo, "cases", "d601-f103-v2-compile");
|
||||
try {
|
||||
await mkdir(path.join(caseRepo, ".git"), { recursive: true });
|
||||
await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8");
|
||||
await mkdir(caseDir, { recursive: true });
|
||||
await mkdir(cwd, { recursive: true });
|
||||
await writeFile(path.join(caseDir, "case.json"), JSON.stringify({
|
||||
contractVersion: "hwpod-case-v1",
|
||||
caseId: "d601-f103-v2-compile",
|
||||
title: "D601-F103-V2 Keil compile-only CaseRun",
|
||||
hwpodSpec: "hwpod-spec.yaml",
|
||||
subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" },
|
||||
runtime: { apiUrl: "http://api.test", webUrl: "http://web.test" }
|
||||
}, null, 2), "utf8");
|
||||
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
|
||||
|
||||
const requests: any[] = [];
|
||||
const result = await runHwlabCli([
|
||||
"case",
|
||||
"run",
|
||||
"d601-f103-v2-compile",
|
||||
"--case-repo",
|
||||
caseRepo,
|
||||
"--run-id",
|
||||
"run-agent-flow",
|
||||
"--poll-interval-ms",
|
||||
"10",
|
||||
"--agent-poll-interval-ms",
|
||||
"10"
|
||||
], {
|
||||
cwd,
|
||||
now: () => "2026-06-05T00:00:00.000Z",
|
||||
sleep: async () => undefined,
|
||||
env: { HWLAB_API_KEY: "hwl_live_test_case_agent_key" },
|
||||
runProcess: async () => ({ command: [], exitCode: 0, stdout: JSON.stringify({ body: { results: [{ output: { stdout: JSON.stringify({ job_id: "job-compile-1" }) } }] } }), stderr: "" }),
|
||||
fetchImpl: caseRunFlowFetch(requests)
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "case.run");
|
||||
assert.equal(result.payload.status, "completed");
|
||||
assert.equal(result.payload.evidence.status, "recorded");
|
||||
assert.equal(result.payload.evidence.autoEvaluation, false);
|
||||
assert.equal(result.payload.evidence.ok, undefined);
|
||||
assert.equal(result.payload.agent.stageStatus, "completed");
|
||||
assert.equal(result.payload.agent.traceId.startsWith("trc_case_"), true);
|
||||
assert.match(result.payload.agentDiff.statusShort, /projects\/01_baseline\/main\.c/u);
|
||||
assert.equal(result.payload.build.hwpodExitCode, 0);
|
||||
assert.equal(result.payload.build.keilStatus, "completed");
|
||||
|
||||
const prompt = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-prompt.md"), "utf8");
|
||||
assert.match(prompt, /without auto-grading/u);
|
||||
const diff = await readFile(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-agent-flow", "agent-diff.patch"), "utf8");
|
||||
assert.match(diff, /printf\("hello"\)/u);
|
||||
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/sessions"), true);
|
||||
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat"), true);
|
||||
assert.equal(requests.some((item) => item.url === "http://web.test/v1/agent/chat/result/" + result.payload.agent.traceId), true);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function mkdtempCaseRoot() {
|
||||
return await import("node:fs/promises").then(({ mkdtemp }) => mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-")));
|
||||
}
|
||||
@@ -141,3 +207,39 @@ function subjectWorktreeFetch(requests: any[]) {
|
||||
] } }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
}
|
||||
|
||||
function caseRunFlowFetch(requests: any[]) {
|
||||
let resultPolls = 0;
|
||||
return async (url: RequestInfo | URL, init?: RequestInit) => {
|
||||
const rawBody = String(init?.body ?? "{}");
|
||||
const body = rawBody ? JSON.parse(rawBody) : {};
|
||||
requests.push({ url: String(url), body });
|
||||
if (String(url) === "http://web.test/v1/agent/sessions") {
|
||||
return new Response(JSON.stringify({ ok: true, session: { sessionId: "ses_case_run", conversationId: body.conversationId, threadId: "thread-case", providerProfile: body.providerProfile } }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url) === "http://web.test/v1/agent/chat") {
|
||||
return new Response(JSON.stringify({ ok: true, accepted: true, traceId: body.traceId, resultUrl: `/v1/agent/chat/result/${body.traceId}` }), { status: 202, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).startsWith("http://web.test/v1/agent/chat/result/")) {
|
||||
resultPolls += 1;
|
||||
return new Response(JSON.stringify({ ok: true, status: resultPolls > 1 ? "completed" : "running", reply: { content: "done" } }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (body.ops?.[0]?.args?.argv?.includes("status") && body.ops?.length === 1) {
|
||||
return hwpodResponse([{ exitCode: 0, stdout: body.ops[0].args.argv.includes("--short") ? " M projects/01_baseline/main.c\n" : "" }]);
|
||||
}
|
||||
if (body.ops?.[0]?.args?.argv?.includes("--stat")) {
|
||||
return hwpodResponse([{ exitCode: 0, stdout: " projects/01_baseline/main.c | 1 +\n" }]);
|
||||
}
|
||||
if (body.ops?.[0]?.args?.argv?.includes("--binary")) {
|
||||
return hwpodResponse([{ exitCode: 0, stdout: "+printf(\"hello\");\n" }]);
|
||||
}
|
||||
if (body.ops?.[0]?.args?.argv?.includes("job-status")) {
|
||||
return hwpodResponse([{ exitCode: 0, stdout: JSON.stringify({ status: "completed", return_code: 0, result: { hex_file: "F:\\out.hex", axf_file: "F:\\out.axf" } }) }]);
|
||||
}
|
||||
return subjectWorktreeFetch(requests)(url, init);
|
||||
};
|
||||
}
|
||||
|
||||
function hwpodResponse(outputs: Array<{ exitCode: number; stdout?: string; stderr?: string }>) {
|
||||
return new Response(JSON.stringify({ body: { results: outputs.map((output) => ({ ok: output.exitCode === 0, output })) } }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
|
||||
+395
-22
@@ -1,19 +1,23 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
|
||||
|
||||
const DEFAULT_API_URL = "http://74.48.78.17:19667";
|
||||
const DEFAULT_WEB_URL = "http://74.48.78.17:19666";
|
||||
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
||||
const DEFAULT_JOB_TIMEOUT_MS = 50000;
|
||||
const MAX_JOB_TIMEOUT_MS = 50000;
|
||||
const DEFAULT_AGENT_TIMEOUT_MS = 120000;
|
||||
const HWLAB_API_KEY_PREFIX = "hwl_live_";
|
||||
const STANDARD_CASE_REPO_PATH = "/root/hwlab-case-registry";
|
||||
const REMOVED_CASE_REPO_PATH = "/root/hwpod-cases";
|
||||
|
||||
type EnvLike = Record<string, string | undefined>;
|
||||
type ParsedArgs = Record<string, unknown> & { _: string[] };
|
||||
type FetchLike = typeof fetch;
|
||||
type RunProcessLike = (command: string[], cwd: string, env: Record<string, string | undefined>) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>;
|
||||
|
||||
type CaseContext = {
|
||||
parsed: ParsedArgs;
|
||||
@@ -22,6 +26,7 @@ type CaseContext = {
|
||||
cwd: string;
|
||||
now: () => string;
|
||||
sleep: (ms: number) => Promise<void>;
|
||||
runProcess?: RunProcessLike;
|
||||
rest: string[];
|
||||
};
|
||||
|
||||
@@ -40,6 +45,9 @@ type PreparedCaseRun = {
|
||||
updatedAt: string;
|
||||
definition: Record<string, unknown>;
|
||||
subject: PreparedSubjectRun;
|
||||
agentTask?: PreparedAgentTask | null;
|
||||
agent?: AgentRunStage | null;
|
||||
agentDiff?: AgentDiffStage | null;
|
||||
};
|
||||
|
||||
type PreparedSubjectRun = {
|
||||
@@ -51,6 +59,42 @@ type PreparedSubjectRun = {
|
||||
setup: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type PreparedAgentTask = {
|
||||
prompt: string;
|
||||
workspace: "subjectWorktree";
|
||||
constraints: string[];
|
||||
providerProfile: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
type AgentRunStage = {
|
||||
stageStatus: string;
|
||||
baseUrl: string;
|
||||
projectId: string;
|
||||
providerProfile: string;
|
||||
conversationId: string;
|
||||
sessionId: string;
|
||||
threadId: string;
|
||||
traceId: string;
|
||||
promptPath: string;
|
||||
promptSha256: string;
|
||||
accepted: Record<string, unknown> | null;
|
||||
result: Record<string, unknown> | null;
|
||||
polls: number;
|
||||
timedOut: boolean;
|
||||
sessionResponse: Record<string, unknown> | null;
|
||||
error?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
type AgentDiffStage = {
|
||||
statusShort: string;
|
||||
diffStat: string;
|
||||
diffPatchPath: string;
|
||||
diffPatchSha256: string;
|
||||
sourceRootStatusShort: string;
|
||||
requests: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export async function caseCommand(context: CaseContext) {
|
||||
const subcommand = context.rest[0] || "help";
|
||||
if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp();
|
||||
@@ -65,6 +109,8 @@ export function caseHelp() {
|
||||
return ok("case.help", {
|
||||
serviceRuntime: false,
|
||||
compileOnlyDefault: true,
|
||||
agentTaskRequiredForRun: false,
|
||||
autoEvaluation: false,
|
||||
stateRoot: ".state/hwlab-cli/caserun",
|
||||
standardCaseRepo: STANDARD_CASE_REPO_PATH,
|
||||
subjectRequired: {
|
||||
@@ -75,14 +121,21 @@ export function caseHelp() {
|
||||
`case prepare CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-id RUN_ID]`,
|
||||
`case build CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-dir DIR]`,
|
||||
`case collect CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} --run-dir DIR`,
|
||||
`case run CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH}`
|
||||
`case run CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--provider-profile deepseek|codex-api|minimax-m3]`
|
||||
],
|
||||
notes: [
|
||||
"case build remains a compile-only HWPOD smoke stage.",
|
||||
"case run prepares the subject worktree, submits a prompt to HWLAB Code Agent, records agent provenance and workspace diff, then runs compile validation.",
|
||||
"This version records raw stage evidence only; it does not auto-grade agent output, diff contents, or Keil evidence."
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export async function runCaseRun(context: CaseContext) {
|
||||
const prepared = await prepareCaseRun(context, "run");
|
||||
const built = await buildCaseRun(context, prepared.run);
|
||||
const agentStage = await runAgentTaskStage(context, prepared.run);
|
||||
const diffStage = await collectAgentDiff(context, agentStage.run);
|
||||
const built = await buildCaseRun(context, diffStage.run);
|
||||
const collected = await collectCaseRun(context, built.run, built.evidence);
|
||||
return ok("case.run", {
|
||||
caseId: collected.run.caseId,
|
||||
@@ -90,10 +143,12 @@ export async function runCaseRun(context: CaseContext) {
|
||||
runDir: collected.run.runDir,
|
||||
compileOnly: true,
|
||||
prepare: prepared.summary,
|
||||
agent: agentSummary(agentStage.agent),
|
||||
agentDiff: diffSummary(diffStage.diff),
|
||||
build: built.summary,
|
||||
collect: collected.summary,
|
||||
evidence: collected.evidence
|
||||
}, collected.evidence.ok === true ? "succeeded" : "failed");
|
||||
}, "completed");
|
||||
}
|
||||
|
||||
export async function prepareCaseRun(context: CaseContext, action = "prepare") {
|
||||
@@ -106,6 +161,7 @@ export async function prepareCaseRun(context: CaseContext, action = "prepare") {
|
||||
const now = context.now();
|
||||
const apiUrl = apiUrlFrom(context, loaded.definition);
|
||||
const subject = await prepareSubjectWorktree(context, { caseId, runId, runDir, apiUrl, definition: loaded.definition, sourceSpecPath: loaded.specPath });
|
||||
const agentTask = agentTaskFromDefinition(loaded.definition, caseId);
|
||||
const run: PreparedCaseRun = {
|
||||
caseId,
|
||||
runId,
|
||||
@@ -120,7 +176,8 @@ export async function prepareCaseRun(context: CaseContext, action = "prepare") {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
definition: loaded.definition,
|
||||
subject
|
||||
subject,
|
||||
agentTask
|
||||
};
|
||||
await mkdir(path.dirname(specPath), { recursive: true });
|
||||
await writeFile(specPath, rewriteHwpodSpecWorkspacePath(loaded.specText, subject.workspacePath), "utf8");
|
||||
@@ -134,6 +191,7 @@ export async function prepareCaseRun(context: CaseContext, action = "prepare") {
|
||||
sourceSpecPath: loaded.specPath,
|
||||
apiUrl,
|
||||
compileOnly: true,
|
||||
agentTask: agentTaskSummary(agentTask),
|
||||
subject,
|
||||
summary: prepareSummary(run),
|
||||
run
|
||||
@@ -143,7 +201,7 @@ export async function prepareCaseRun(context: CaseContext, action = "prepare") {
|
||||
export async function buildCaseRun(context: CaseContext, prepared?: PreparedCaseRun) {
|
||||
const run = prepared ?? await loadOrPrepareCaseRun(context);
|
||||
const command = [process.execPath, path.join(context.cwd, "tools/hwpod-cli.ts"), "build", "--spec", run.specPath, "--reason", `case-run ${run.caseId} ${run.runId} compile-only`];
|
||||
const invoked = await runProcess(command, context.cwd, {
|
||||
const invoked = await (context.runProcess ?? runProcess)(command, context.cwd, {
|
||||
...process.env,
|
||||
...context.env,
|
||||
HWLAB_RUNTIME_API_URL: run.apiUrl,
|
||||
@@ -152,19 +210,19 @@ export async function buildCaseRun(context: CaseContext, prepared?: PreparedCase
|
||||
const hwpodPayload = parseJsonMaybe(invoked.stdout);
|
||||
const jobId = extractKeilJobId(hwpodPayload);
|
||||
const job = jobId ? await pollKeilJobStatus(context, run, jobId) : null;
|
||||
const nodeOk = invoked.exitCode === 0 && hwpodPayload?.ok !== false && firstHwpodResultOk(hwpodPayload) !== false;
|
||||
const jobOk = job ? job.ok === true : false;
|
||||
const okStatus = nodeOk && jobOk;
|
||||
const evidence = clean({
|
||||
contractVersion: "hwpod-case-run-evidence-v1",
|
||||
caseId: run.caseId,
|
||||
runId: run.runId,
|
||||
compileOnly: true,
|
||||
ok: okStatus,
|
||||
status: okStatus ? "succeeded" : "failed",
|
||||
autoEvaluation: false,
|
||||
status: "recorded",
|
||||
observedAt: context.now(),
|
||||
apiUrl: run.apiUrl,
|
||||
subject: run.subject,
|
||||
agentTask: run.agentTask ? agentTaskSummary(run.agentTask) : null,
|
||||
agent: run.agent ? agentSummary(run.agent) : null,
|
||||
agentDiff: run.agentDiff ? diffSummary(run.agentDiff) : null,
|
||||
hwpod: clean({
|
||||
command: commandVisibility(command),
|
||||
exitCode: invoked.exitCode,
|
||||
@@ -175,7 +233,7 @@ export async function buildCaseRun(context: CaseContext, prepared?: PreparedCase
|
||||
artifacts: job?.summary?.artifacts ?? [],
|
||||
decisions: {
|
||||
downloadSkipped: true,
|
||||
reason: "compile-only validation is the current case-run acceptance line"
|
||||
reason: "flow-only run: CaseRun records agent, diff and compile evidence without auto-grading them"
|
||||
}
|
||||
});
|
||||
await writeJson(path.join(run.runDir, "evidence.json"), evidence);
|
||||
@@ -188,7 +246,7 @@ export async function buildCaseRun(context: CaseContext, prepared?: PreparedCase
|
||||
summary: buildSummary(evidence),
|
||||
evidence,
|
||||
run
|
||||
}, evidence.status as string);
|
||||
}, "completed");
|
||||
}
|
||||
|
||||
export async function collectCaseRun(context: CaseContext, prepared?: PreparedCaseRun, knownEvidence?: any) {
|
||||
@@ -206,9 +264,9 @@ export async function collectCaseRun(context: CaseContext, prepared?: PreparedCa
|
||||
caseRepo: run.caseRepo,
|
||||
caseRepoFiles: files,
|
||||
status: evidence.status,
|
||||
ok: evidence.ok === true
|
||||
autoEvaluation: evidence.autoEvaluation === false
|
||||
};
|
||||
return ok("case.collect", { caseId: run.caseId, runId: run.runId, runDir: run.runDir, compileOnly: true, summary, evidence, run }, evidence.status);
|
||||
return ok("case.collect", { caseId: run.caseId, runId: run.runId, runDir: run.runDir, compileOnly: true, summary, evidence, run }, "completed");
|
||||
}
|
||||
|
||||
export function extractKeilJobId(payload: any) {
|
||||
@@ -269,6 +327,326 @@ async function loadCaseDefinition(caseRepo: string, caseId: string) {
|
||||
return { caseDir, caseFile, definition, specPath, specText };
|
||||
}
|
||||
|
||||
function agentTaskFromDefinition(definition: Record<string, unknown>, caseId: string) {
|
||||
const value = definition.agentTask;
|
||||
if (value === undefined || value === null) return defaultAgentTask(definition, caseId);
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw cliError("invalid_agent_task", "case.json agentTask must be an object", { field: "agentTask" });
|
||||
}
|
||||
const source = value as Record<string, unknown>;
|
||||
const prompt = text(source.prompt);
|
||||
if (!prompt) throw cliError("agent_task_prompt_required", "case.json agentTask.prompt is required for Code Agent CaseRun", { field: "agentTask.prompt" });
|
||||
const workspace = text(source.workspace) || "subjectWorktree";
|
||||
if (workspace !== "subjectWorktree") throw cliError("unsupported_agent_task_workspace", "case.json agentTask.workspace must be subjectWorktree", { workspace });
|
||||
return {
|
||||
prompt,
|
||||
workspace,
|
||||
constraints: stringArray(source.constraints),
|
||||
providerProfile: text(source.providerProfile) || "deepseek",
|
||||
projectId: text(source.projectId) || "prj_hwpod_workbench"
|
||||
} as PreparedAgentTask;
|
||||
}
|
||||
|
||||
function defaultAgentTask(definition: Record<string, unknown>, caseId: string): PreparedAgentTask {
|
||||
const title = text(definition.title) || caseId;
|
||||
return {
|
||||
prompt: `请根据 CaseRun ${caseId}(${title})在 isolated subject worktree 中完成任务准备或最小可观察修改;不要修改 case registry 或原 subject repo checkout。`,
|
||||
workspace: "subjectWorktree",
|
||||
constraints: [
|
||||
"不得修改 case registry",
|
||||
"不得修改原 subject repo checkout",
|
||||
"只允许修改 isolated subject worktree",
|
||||
"保持 compile-only 流程可继续执行"
|
||||
],
|
||||
providerProfile: "deepseek",
|
||||
projectId: "prj_hwpod_workbench"
|
||||
};
|
||||
}
|
||||
|
||||
async function runAgentTaskStage(context: CaseContext, run: PreparedCaseRun) {
|
||||
if (!run.agentTask) {
|
||||
run = await updateRun(context, run, { agentTask: defaultAgentTask(run.definition, run.caseId) });
|
||||
}
|
||||
const promptPath = path.join(run.runDir, "agent-prompt.md");
|
||||
const prompt = renderAgentTaskPrompt(run, run.agentTask);
|
||||
await writeFile(promptPath, prompt, "utf8");
|
||||
const promptSha256 = sha256(prompt);
|
||||
const baseUrl = webUrlFrom(context, run.definition);
|
||||
const providerProfile = text(context.parsed.providerProfile) || run.agentTask.providerProfile;
|
||||
const projectId = text(context.parsed.projectId) || run.agentTask.projectId;
|
||||
const conversationId = text(context.parsed.conversationId) || `cnv_case_${slug(run.caseId)}_${slug(run.runId)}`;
|
||||
const session = await tryCreateAgentSession(context, { baseUrl, projectId, providerProfile, conversationId });
|
||||
if (!session.ok) {
|
||||
const agent = agentFailureStage({ stageStatus: "session_failed", baseUrl, projectId, providerProfile, conversationId, promptPath, promptSha256, error: session.error, sessionResponse: session.body });
|
||||
const nextRun = await updateRun(context, run, { agent });
|
||||
return { run: nextRun, agent };
|
||||
}
|
||||
const sessionId = session.sessionId;
|
||||
const threadId = session.threadId;
|
||||
const traceId = text(context.parsed.traceId) || `trc_case_${slug(run.caseId)}_${randomUUID().replace(/-/gu, "")}`;
|
||||
const accepted = await submitAgentTask(context, { baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, prompt });
|
||||
if (!accepted.ok) {
|
||||
const agent = agentFailureStage({ stageStatus: "submit_failed", baseUrl, projectId, providerProfile, conversationId, sessionId, threadId, traceId, promptPath, promptSha256, accepted: accepted.body, error: accepted.error, sessionResponse: session.body });
|
||||
const nextRun = await updateRun(context, run, { agent });
|
||||
return { run: nextRun, agent };
|
||||
}
|
||||
const result = await pollAgentResult(context, { baseUrl, traceId, acceptedBody: accepted.body });
|
||||
const agent: AgentRunStage = clean({
|
||||
stageStatus: result.stageStatus,
|
||||
baseUrl,
|
||||
projectId,
|
||||
providerProfile,
|
||||
conversationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
traceId,
|
||||
promptPath,
|
||||
promptSha256,
|
||||
accepted: compactObject(accepted.body),
|
||||
result: compactObject(result.body),
|
||||
polls: result.polls,
|
||||
timedOut: result.timedOut,
|
||||
sessionResponse: compactObject(session.body),
|
||||
error: result.error
|
||||
});
|
||||
const nextRun = await updateRun(context, run, { agent });
|
||||
return { run: nextRun, agent };
|
||||
}
|
||||
|
||||
async function tryCreateAgentSession(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string }) {
|
||||
try {
|
||||
return await createAgentSession(context, input);
|
||||
} catch (error) {
|
||||
const code = typeof (error as any)?.code === "string" ? (error as any).code : "agent_session_exception";
|
||||
return { ok: false, body: null, error: { code, message: error instanceof Error ? error.message : String(error), details: (error as any)?.details ?? null } };
|
||||
}
|
||||
}
|
||||
|
||||
function agentFailureStage(input: { stageStatus: string; baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId?: string; threadId?: string; traceId?: string; promptPath: string; promptSha256: string; accepted?: Record<string, unknown> | null; sessionResponse?: Record<string, unknown> | null; error?: Record<string, unknown> | null }): AgentRunStage {
|
||||
return clean({
|
||||
stageStatus: input.stageStatus,
|
||||
baseUrl: input.baseUrl,
|
||||
projectId: input.projectId,
|
||||
providerProfile: input.providerProfile,
|
||||
conversationId: input.conversationId,
|
||||
sessionId: input.sessionId ?? "",
|
||||
threadId: input.threadId ?? "",
|
||||
traceId: input.traceId ?? "",
|
||||
promptPath: input.promptPath,
|
||||
promptSha256: input.promptSha256,
|
||||
accepted: input.accepted ?? null,
|
||||
result: null,
|
||||
polls: 0,
|
||||
timedOut: false,
|
||||
sessionResponse: input.sessionResponse ?? null,
|
||||
error: input.error ?? null
|
||||
}) as AgentRunStage;
|
||||
}
|
||||
|
||||
async function createAgentSession(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string }) {
|
||||
const response = await caseFetchJson(context, `${input.baseUrl}/v1/agent/sessions`, {
|
||||
method: "POST",
|
||||
headers: caseAgentHeaders(context),
|
||||
body: JSON.stringify({ projectId: input.projectId, providerProfile: input.providerProfile, conversationId: input.conversationId, updatedByClient: "hwlab-cli.case-run" })
|
||||
}, "agent_session_create");
|
||||
if (!response.ok) return { ok: false, error: response.error, body: response.body };
|
||||
const session = response.body?.session ?? response.body;
|
||||
const sessionId = text(session?.sessionId ?? session?.id);
|
||||
if (!sessionId) return { ok: false, error: { code: "agent_session_id_missing", httpStatus: response.status, body: compactObject(response.body) }, body: response.body };
|
||||
return {
|
||||
ok: true,
|
||||
sessionId,
|
||||
threadId: text(session?.threadId ?? response.body?.threadId),
|
||||
body: response.body
|
||||
};
|
||||
}
|
||||
|
||||
async function submitAgentTask(context: CaseContext, input: { baseUrl: string; projectId: string; providerProfile: string; conversationId: string; sessionId: string; threadId: string; traceId: string; prompt: string }) {
|
||||
return caseFetchJson(context, `${input.baseUrl}/v1/agent/chat`, {
|
||||
method: "POST",
|
||||
headers: { ...caseAgentHeaders(context), "x-trace-id": input.traceId, prefer: "respond-async", "x-hwlab-short-connection": "1" },
|
||||
body: JSON.stringify(clean({
|
||||
message: input.prompt,
|
||||
conversationId: input.conversationId,
|
||||
sessionId: input.sessionId,
|
||||
threadId: input.threadId,
|
||||
traceId: input.traceId,
|
||||
providerProfile: input.providerProfile,
|
||||
shortConnection: true,
|
||||
projectId: input.projectId,
|
||||
updatedByClient: "hwlab-cli.case-run"
|
||||
}))
|
||||
}, "agent_task_submit");
|
||||
}
|
||||
|
||||
async function pollAgentResult(context: CaseContext, input: { baseUrl: string; traceId: string; acceptedBody: any }) {
|
||||
const requestedTimeoutMs = numberOption(context.parsed.agentTimeoutMs ?? context.parsed.timeoutMs) ?? DEFAULT_AGENT_TIMEOUT_MS;
|
||||
const timeoutMs = Math.max(Math.min(requestedTimeoutMs, DEFAULT_AGENT_TIMEOUT_MS), 1000);
|
||||
const pollIntervalMs = Math.max(numberOption(context.parsed.agentPollIntervalMs ?? context.parsed.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS, 250);
|
||||
const startedAt = Date.now();
|
||||
const resultPath = text(input.acceptedBody?.resultUrl) || `/v1/agent/chat/result/${encodeURIComponent(input.traceId)}`;
|
||||
const resultUrl = resultPath.startsWith("http") ? resultPath : `${input.baseUrl}${resultPath}`;
|
||||
let polls = 0;
|
||||
let lastBody: any = null;
|
||||
let lastStatus = 0;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
const response = await caseFetchJson(context, resultUrl, { method: "GET", headers: caseAgentHeaders(context) }, "agent_task_result");
|
||||
lastBody = response.body;
|
||||
lastStatus = response.status;
|
||||
if (!response.ok) return { stageStatus: "result_request_failed", body: lastBody, polls, timedOut: false, error: response.error };
|
||||
const status = text(lastBody?.status);
|
||||
if (status && status !== "running") {
|
||||
return { stageStatus: status, body: lastBody, polls, timedOut: false, error: null };
|
||||
}
|
||||
await context.sleep(pollIntervalMs);
|
||||
}
|
||||
return { stageStatus: "timeout", body: lastBody, polls, timedOut: true, error: { code: "agent_task_timeout", traceId: input.traceId, timeoutMs, lastHttpStatus: lastStatus, lastBody: compactObject(lastBody) } };
|
||||
}
|
||||
|
||||
async function collectAgentDiff(context: CaseContext, run: PreparedCaseRun) {
|
||||
const status = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "status", "--short"]);
|
||||
const diffStat = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "diff", "--stat"]);
|
||||
const diffPatch = await requestSubjectCommand(context, run, ["-C", run.subject.worktreePath, "diff", "--binary"]);
|
||||
const sourceStatus = await requestSubjectCommand(context, run, ["status", "--short"]);
|
||||
const statusShort = text(status.stdout);
|
||||
const patchText = text(diffPatch.stdout);
|
||||
const diffPatchPath = path.join(run.runDir, "agent-diff.patch");
|
||||
await writeFile(diffPatchPath, patchText, "utf8");
|
||||
const diff: AgentDiffStage = {
|
||||
statusShort,
|
||||
diffStat: text(diffStat.stdout),
|
||||
diffPatchPath,
|
||||
diffPatchSha256: sha256(patchText),
|
||||
sourceRootStatusShort: text(sourceStatus.stdout),
|
||||
requests: [
|
||||
requestSummary("status", status),
|
||||
requestSummary("diff-stat", diffStat),
|
||||
requestSummary("diff-patch", diffPatch),
|
||||
requestSummary("source-root-status", sourceStatus)
|
||||
]
|
||||
};
|
||||
const nextRun = await updateRun(context, run, { agentDiff: diff });
|
||||
return { run: nextRun, diff };
|
||||
}
|
||||
|
||||
async function requestSubjectCommand(context: CaseContext, run: PreparedCaseRun, argv: string[]) {
|
||||
const document = await readHwpodSpec(run.specPath);
|
||||
const hwpodId = text(document.metadata.name) || text(document.metadata.uid) || run.caseId;
|
||||
const plan = {
|
||||
contractVersion: "hwpod-node-ops-v1",
|
||||
planId: `case_subject_cmd_${randomUUID()}`,
|
||||
hwpodId,
|
||||
nodeId: document.spec.nodeBinding.nodeId,
|
||||
intent: "cmd.run",
|
||||
source: { compiler: "hwlab-cli.case", specPath: run.specPath, specAuthority: "case-run-state" },
|
||||
ops: [{
|
||||
opId: "op_01_subject_cmd",
|
||||
op: "cmd.run",
|
||||
args: { hwpodId, workspacePath: run.subject.repoLocalPath, command: "git", argv }
|
||||
}]
|
||||
};
|
||||
const response = await context.fetchImpl(`${run.apiUrl}/v1/hwpod-node-ops`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(plan)
|
||||
});
|
||||
const textBody = await response.text();
|
||||
const body = parseJsonMaybe(textBody) ?? { raw: textBody };
|
||||
const result = firstHwpodOutput(body);
|
||||
const exitCode = numberOption(result?.exitCode) ?? (response.status >= 200 && response.status < 300 ? 0 : 1);
|
||||
return { ok: response.status >= 200 && response.status < 300 && exitCode === 0, httpStatus: response.status, exitCode, stdout: text(result?.stdout), stderr: text(result?.stderr), body: compactObject(body) };
|
||||
}
|
||||
|
||||
async function updateRun(context: CaseContext, run: PreparedCaseRun, patch: Partial<PreparedCaseRun> & Record<string, unknown>) {
|
||||
const nextRun = { ...run, ...patch, updatedAt: context.now() } as PreparedCaseRun;
|
||||
await writeJson(path.join(nextRun.runDir, "run.json"), nextRun);
|
||||
return nextRun;
|
||||
}
|
||||
|
||||
function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAgentTask) {
|
||||
const constraints = [
|
||||
...agentTask.constraints,
|
||||
"只能修改 isolated subject worktree,不得修改 case registry repo。",
|
||||
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
|
||||
"完成后不要自行运行确定性 CaseRun 答案执行器;CaseRun 会读取 diff 并执行 compile-only 验收。"
|
||||
].filter(Boolean);
|
||||
return [
|
||||
`# HWPOD CaseRun Code Agent Task`,
|
||||
``,
|
||||
`caseId: ${run.caseId}`,
|
||||
`runId: ${run.runId}`,
|
||||
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
|
||||
`subjectCommitId: ${run.subject.commitId}`,
|
||||
`subjectWorktreePath: ${run.subject.worktreePath}`,
|
||||
`runLocalHwpodSpec: ${run.specPath}`,
|
||||
`compileOnly: true`,
|
||||
``,
|
||||
`## Task`,
|
||||
agentTask.prompt,
|
||||
``,
|
||||
`## Constraints`,
|
||||
...constraints.map((item) => `- ${item}`),
|
||||
``,
|
||||
`## Flow`,
|
||||
`- CaseRun will inspect git diff under subjectWorktreePath after your turn completes.`,
|
||||
`- CaseRun will run HWPOD Keil compile-only smoke with the run-local hwpod-spec.yaml.`,
|
||||
`- CaseRun records trace/session/conversation, workspace diff and Keil build evidence without auto-grading them.`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function caseFetchJson(context: CaseContext, url: string, init: RequestInit, errorCode: string) {
|
||||
try {
|
||||
const response = await context.fetchImpl(url, init);
|
||||
const textBody = await response.text();
|
||||
const body = parseJsonMaybe(textBody) ?? { raw: textBody };
|
||||
const ok = response.status >= 200 && response.status < 300 && body?.ok !== false;
|
||||
return { ok, status: response.status, body, error: ok ? null : { code: errorCode, httpStatus: response.status, body: compactObject(body) } };
|
||||
} catch (error) {
|
||||
return { ok: false, status: 0, body: null, error: { code: errorCode, message: error instanceof Error ? error.message : String(error) } };
|
||||
}
|
||||
}
|
||||
|
||||
function caseAgentHeaders(context: CaseContext) {
|
||||
const apiKey = text(context.env.HWLAB_API_KEY);
|
||||
if (!apiKey.startsWith(HWLAB_API_KEY_PREFIX)) throw cliError("api_key_required", "case run Code Agent stage requires HWLAB_API_KEY for HWLAB v0.2 Cloud Web", { allowed: "HWLAB_API_KEY" });
|
||||
return { "content-type": "application/json", authorization: `Bearer ${apiKey}` };
|
||||
}
|
||||
|
||||
function webUrlFrom(context: CaseContext, definition: any) {
|
||||
return text(context.parsed.baseUrl ?? context.parsed.webUrl ?? definition?.runtime?.webUrl ?? context.env.HWLAB_RUNTIME_WEB_URL ?? context.env.HWLAB_CLOUD_WEB_URL) || DEFAULT_WEB_URL;
|
||||
}
|
||||
|
||||
function firstHwpodOutput(body: any) {
|
||||
const results = Array.isArray(body?.body?.results) ? body.body.results : Array.isArray(body?.results) ? body.results : [];
|
||||
const first = results[0] ?? null;
|
||||
return first?.output ?? first;
|
||||
}
|
||||
|
||||
function requestSummary(label: string, value: any) {
|
||||
return { label, httpStatus: value.httpStatus, exitCode: value.exitCode, stdoutBytes: Buffer.byteLength(text(value.stdout)), stderr: clipText(value.stderr, 1000) };
|
||||
}
|
||||
|
||||
function agentTaskSummary(agentTask: PreparedAgentTask) {
|
||||
return { workspace: agentTask.workspace, providerProfile: agentTask.providerProfile, projectId: agentTask.projectId, promptSha256: sha256(agentTask.prompt), constraints: agentTask.constraints };
|
||||
}
|
||||
|
||||
function agentSummary(agent: AgentRunStage) {
|
||||
return clean({ stageStatus: agent.stageStatus, baseUrl: agent.baseUrl, projectId: agent.projectId, providerProfile: agent.providerProfile, conversationId: agent.conversationId, sessionId: agent.sessionId, threadId: agent.threadId, traceId: agent.traceId, promptPath: agent.promptPath, promptSha256: agent.promptSha256, polls: agent.polls, timedOut: agent.timedOut, error: agent.error });
|
||||
}
|
||||
|
||||
function diffSummary(diff: AgentDiffStage) {
|
||||
return { statusShort: diff.statusShort, diffStat: diff.diffStat, diffPatchPath: diff.diffPatchPath, diffPatchSha256: diff.diffPatchSha256, sourceRootStatusShort: diff.sourceRootStatusShort, requests: diff.requests };
|
||||
}
|
||||
|
||||
function stringArray(value: unknown) {
|
||||
return Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
function sha256(value: string) {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
async function prepareSubjectWorktree(context: CaseContext, input: { caseId: string; runId: string; runDir: string; apiUrl: string; definition: Record<string, unknown>; sourceSpecPath: string }) {
|
||||
const subject = subjectFromDefinition(input.definition);
|
||||
const worktreePath = subjectWorktreePath(subject.repoLocalPath, input.runId);
|
||||
@@ -485,11 +863,11 @@ function prepareSummary(run: PreparedCaseRun) {
|
||||
}
|
||||
|
||||
function buildSummary(evidence: any) {
|
||||
return { status: evidence.status, ok: evidence.ok, jobId: evidence.keilJob?.jobId ?? null, artifacts: evidence.artifacts ?? [] };
|
||||
return { status: evidence.status, autoEvaluation: evidence.autoEvaluation === false, hwpodExitCode: evidence.hwpod?.exitCode ?? null, jobId: evidence.keilJob?.jobId ?? null, keilStatus: evidence.keilJob?.status ?? null, artifacts: evidence.artifacts ?? [] };
|
||||
}
|
||||
|
||||
function renderEvidenceSummary(evidence: any) {
|
||||
return `# HWPOD CaseRun ${evidence.caseId}\n\n- runId: ${evidence.runId}\n- status: ${evidence.status}\n- compileOnly: ${evidence.compileOnly}\n- subjectRepoLocalPath: ${evidence.subject?.repoLocalPath ?? ""}\n- subjectCommitId: ${evidence.subject?.commitId ?? ""}\n- subjectWorktreePath: ${evidence.subject?.worktreePath ?? ""}\n- jobId: ${evidence.keilJob?.jobId ?? ""}\n- downloadSkipped: true\n`;
|
||||
return `# HWPOD CaseRun ${evidence.caseId}\n\n- runId: ${evidence.runId}\n- status: ${evidence.status}\n- autoEvaluation: false\n- compileOnly: ${evidence.compileOnly}\n- subjectRepoLocalPath: ${evidence.subject?.repoLocalPath ?? ""}\n- subjectCommitId: ${evidence.subject?.commitId ?? ""}\n- subjectWorktreePath: ${evidence.subject?.worktreePath ?? ""}\n- agentTraceId: ${evidence.agent?.traceId ?? ""}\n- agentSessionId: ${evidence.agent?.sessionId ?? ""}\n- diffPatchPath: ${evidence.agentDiff?.diffPatchPath ?? ""}\n- hwpodExitCode: ${evidence.hwpod?.exitCode ?? ""}\n- jobId: ${evidence.keilJob?.jobId ?? ""}\n- downloadSkipped: true\n`;
|
||||
}
|
||||
|
||||
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) {
|
||||
@@ -503,11 +881,6 @@ function hwpodResultTexts(payload: any) {
|
||||
return results.flatMap((item: any) => [item?.stdout, item?.stderr, item?.summary, item?.output?.stdout, item?.output?.stderr, item?.output?.summary]).filter((item: unknown) => text(item));
|
||||
}
|
||||
|
||||
function firstHwpodResultOk(payload: any) {
|
||||
const results = Array.isArray(payload?.body?.results) ? payload.body.results : [];
|
||||
return results[0]?.ok;
|
||||
}
|
||||
|
||||
function compactHwpodPayload(payload: any) {
|
||||
if (!payload) return null;
|
||||
return compactObject({ ok: payload.ok, action: payload.action, status: payload.status, traceId: payload.body?.traceId, results: payload.body?.results });
|
||||
|
||||
@@ -30,6 +30,7 @@ type CliOptions = {
|
||||
cwd?: string;
|
||||
now?: () => string;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
runProcess?: (command: string[], cwd: string, env: Record<string, string | undefined>) => Promise<{ command: string[]; stdout: string; stderr: string; exitCode: number }>;
|
||||
};
|
||||
|
||||
export async function main(argv = process.argv.slice(2), options: CliOptions = {}) {
|
||||
@@ -51,7 +52,8 @@ export async function runHwlabCli(argv: string[], options: CliOptions = {}) {
|
||||
stdinText: options.stdinText,
|
||||
cwd: options.cwd ?? process.cwd(),
|
||||
now,
|
||||
sleep: options.sleep ?? wait
|
||||
sleep: options.sleep ?? wait,
|
||||
runProcess: options.runProcess
|
||||
};
|
||||
const payload = target === "client" ? await clientCommand({ ...context, rest: parsed._.slice(1) }) : target === "case" ? await caseCommand({ ...context, rest: parsed._.slice(1) }) : help();
|
||||
return { exitCode: payload.ok === false ? 1 : 0, payload: withMeta(payload, now) };
|
||||
|
||||
Reference in New Issue
Block a user