feat: add caserun prompt preview
This commit is contained in:
@@ -103,6 +103,49 @@ test("case prepare uses the new registry by default and refuses the removed hwpo
|
||||
}
|
||||
});
|
||||
|
||||
test("case prompt previews the real assembled prompt in Chinese without starting a run", 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-prompt-preview");
|
||||
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-prompt-preview",
|
||||
title: "D601-F103-V2 中文 Prompt 预览",
|
||||
hwpodSpec: "hwpod-spec.yaml",
|
||||
subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" },
|
||||
agentTask: { workspace: "subjectWorktree", prompt: "请在隔离主体工作区中读取 main.c,并只回报当前文件中与串口初始化相关的函数名。", constraints: ["不要修改源码。"] }
|
||||
}, null, 2), "utf8");
|
||||
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
|
||||
|
||||
const result = await runHwlabCli(["case", "prompt", "d601-f103-v2-prompt-preview", "--case-repo", caseRepo, "--run-id", "run-prompt-preview"], { cwd, now: () => "2026-06-08T00:00:00.000Z" });
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "case.prompt");
|
||||
assert.equal(result.payload.previewOnly, true);
|
||||
assert.equal(result.payload.serviceRuntime, false);
|
||||
assert.match(result.payload.prompt, /# HWPOD CaseRun 代码代理任务/u);
|
||||
assert.match(result.payload.prompt, /## 运行时装配/u);
|
||||
assert.match(result.payload.prompt, /## HWPOD 运行时/u);
|
||||
assert.match(result.payload.prompt, /## 任务/u);
|
||||
assert.match(result.payload.prompt, /## 约束/u);
|
||||
assert.match(result.payload.prompt, /## 执行流程/u);
|
||||
assert.match(result.payload.prompt, /思维过程和输出消息一律使用中文/u);
|
||||
assert.match(result.payload.prompt, /验证模式: 仅执行编译构建检查/u);
|
||||
assert.match(result.payload.prompt, /请在隔离主体工作区中读取 main\.c/u);
|
||||
assert.match(result.payload.prompt, /hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path/u);
|
||||
assert.doesNotMatch(result.payload.prompt, /Runtime Assembly|Standard smoke sequence|verificationMode: compile-only build check|## Task|## Constraints|## Flow/u);
|
||||
await assert.rejects(stat(path.join(cwd, ".state", "hwlab-cli", "caserun", "run-prompt-preview")));
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("case run extracts Keil async job ids from hwpod-node output", () => {
|
||||
const jobId = extractKeilJobId({ body: { results: [{ stdout: JSON.stringify({ ok: true, job_id: "20260605_203835_798515c0" }) }] } });
|
||||
assert.equal(jobId, "20260605_203835_798515c0");
|
||||
@@ -284,7 +327,7 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
|
||||
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" },
|
||||
agentTask: { workspace: "subjectWorktree", prompt: "Make a tiny source change through HWPOD.", timeoutMs: 234000, pollIntervalMs: 10 },
|
||||
agentTask: { workspace: "subjectWorktree", prompt: "请通过 HWPOD 做一个很小的源码修改。", timeoutMs: 234000, pollIntervalMs: 10 },
|
||||
runtime: { apiUrl: "http://api.test", webUrl: "http://web.test" }
|
||||
}, null, 2), "utf8");
|
||||
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
|
||||
@@ -462,10 +505,13 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
|
||||
assert.doesNotMatch(summaryMarkdown, /downloadSkipped/u);
|
||||
|
||||
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);
|
||||
assert.match(prompt, /Runtime Assembly/u);
|
||||
assert.match(prompt, /不做自动评分或门禁判断/u);
|
||||
assert.match(prompt, /运行时装配/u);
|
||||
assert.match(prompt, /kind=gitbundle/u);
|
||||
assert.match(prompt, /verificationMode: compile-only build check/u);
|
||||
assert.match(prompt, /思维过程和输出消息一律使用中文/u);
|
||||
assert.match(prompt, /验证模式: 仅执行编译构建检查/u);
|
||||
assert.match(prompt, /请通过 HWPOD 做一个很小的源码修改/u);
|
||||
assert.doesNotMatch(prompt, /Runtime Assembly|verificationMode: compile-only build check|Standard smoke sequence|## Task|## Constraints|## Flow|without auto-grading/u);
|
||||
assert.doesNotMatch(prompt, /compile-only 阶段不要修改 subject 源码/u);
|
||||
assert.doesNotMatch(prompt, /Write exactly this YAML/u);
|
||||
assert.doesNotMatch(prompt, /```yaml/u);
|
||||
@@ -477,10 +523,10 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
|
||||
assert.match(prompt, /hwpodWorkspaceArgs: --hwpod-id d601-f103-v2 --workspace-path 'F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-agent-flow'/u);
|
||||
assert.match(prompt, /hwpod-ctl spec validate --hwpod-id d601-f103-v2 --workspace-path/u);
|
||||
assert.match(prompt, /不要创建、复制或修补本地 spec 文件/u);
|
||||
assert.match(prompt, /standard hwpod\/hwpod-ctl commands/u);
|
||||
assert.match(prompt, /标准 hwpod\/hwpod-ctl 命令/u);
|
||||
assert.match(prompt, /hwpod job status <jobId>/u);
|
||||
assert.match(prompt, /不要再用 shell sleep\/&&\/timeout\/watch\/head\/pipe/u);
|
||||
assert.match(prompt, /separate short `hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path/u);
|
||||
assert.match(prompt, /不要用 shell sleep、&&、timeout、watch、head、pipe/u);
|
||||
assert.match(prompt, /独立短命令 `hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path/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);
|
||||
|
||||
@@ -206,6 +206,7 @@ type ArchivedAgentTraceSnapshot = {
|
||||
export async function caseCommand(context: CaseContext) {
|
||||
const subcommand = context.rest[0] || "help";
|
||||
if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp();
|
||||
if (subcommand === "prompt") return promptCaseRun(context);
|
||||
if (subcommand === "aggregate") return aggregateCaseRun(context);
|
||||
if (subcommand === "prepare") return prepareCaseRun(context);
|
||||
if (subcommand === "build") return buildCaseRun(context);
|
||||
@@ -237,6 +238,7 @@ export function caseHelp() {
|
||||
commitId: "fixed subject repo commit id; no GitHub clone/fetch fallback"
|
||||
},
|
||||
commands: [
|
||||
`case prompt CASE_ID --case-repo ${STANDARD_CASE_REPO_PATH} [--run-id RUN_ID]`,
|
||||
`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`,
|
||||
@@ -248,6 +250,7 @@ export function caseHelp() {
|
||||
`case run logs RUN_ID [--tail 8000]`
|
||||
],
|
||||
notes: [
|
||||
"case prompt 是无服务预览入口,只按真实 CaseRun 组装路径渲染输入 prompt,不准备 worktree、不提交 agent、不访问硬件。",
|
||||
"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.",
|
||||
"case aggregate is a service-free post-processing command: it reads an existing case registry run and writes one aggregate markdown entry without pass/fail grading.",
|
||||
@@ -257,6 +260,50 @@ export function caseHelp() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function promptCaseRun(context: CaseContext) {
|
||||
const caseId = requiredText(context.parsed.caseId ?? context.rest[1], "caseId");
|
||||
const caseRepo = await resolveCaseRepo(context);
|
||||
const loaded = await loadCaseDefinition(caseRepo, caseId);
|
||||
const runId = runIdFrom(context, caseId);
|
||||
const runDir = path.resolve(context.cwd, text(context.parsed.runDir) || path.join(stateRoot(context), runId));
|
||||
const subject = subjectFromDefinition(loaded.definition);
|
||||
const worktreePath = subjectWorktreePath(subject.repoLocalPath, runId);
|
||||
const agentTask = agentTaskFromDefinition(loaded.definition, caseId);
|
||||
const now = context.now();
|
||||
const run: PreparedCaseRun = {
|
||||
caseId,
|
||||
runId,
|
||||
runDir,
|
||||
caseRepo,
|
||||
caseDir: loaded.caseDir,
|
||||
caseFile: loaded.caseFile,
|
||||
sourceSpecPath: loaded.specPath,
|
||||
specPath: loaded.specPath,
|
||||
apiUrl: apiUrlFrom(context, loaded.definition),
|
||||
compileOnly: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
definition: loaded.definition,
|
||||
subject: { ...subject, worktreePath, workspacePath: worktreePath, setup: { previewOnly: true } },
|
||||
agentTask
|
||||
};
|
||||
const prompt = await renderAgentTaskPrompt(run, agentTask);
|
||||
return ok("case.prompt", {
|
||||
caseId,
|
||||
runId,
|
||||
runDir,
|
||||
caseRepo,
|
||||
sourceSpecPath: loaded.specPath,
|
||||
compileOnly: true,
|
||||
serviceRuntime: false,
|
||||
previewOnly: true,
|
||||
promptSha256: sha256(prompt),
|
||||
agentTask: agentTaskSummary(agentTask),
|
||||
subject: { repoLocalPath: subject.repoLocalPath, commitId: subject.commitId, worktreePath },
|
||||
prompt
|
||||
});
|
||||
}
|
||||
|
||||
export async function aggregateCaseRun(context: CaseContext) {
|
||||
const caseRepo = await resolveCaseRepo(context);
|
||||
const aggregate = await writeCaseAggregateFromRegistry(context, caseRepo, { sync: true });
|
||||
@@ -1080,6 +1127,7 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
|
||||
const hwpodId = text(hwpodDocument.metadata.name) || text(hwpodDocument.metadata.uid) || run.caseId;
|
||||
const hwpodWorkspaceArgs = `--hwpod-id ${hwpodId} --workspace-path ${promptShellArg(run.subject.worktreePath)}`;
|
||||
const constraints = [
|
||||
"思维过程和输出消息一律使用中文",
|
||||
...agentTask.constraints,
|
||||
"只能修改 isolated subject worktree,不得修改 case registry repo。",
|
||||
"不得修改原 subject repo checkout;所有源码修改必须落在 subjectWorktreePath。",
|
||||
@@ -1092,41 +1140,41 @@ async function renderAgentTaskPrompt(run: PreparedCaseRun, agentTask: PreparedAg
|
||||
`hwpod build/download 是长任务短连接入口;不要再用 shell sleep/&&/timeout/watch/head/pipe 或 shell loop 包住它们。记录返回 JSON 里的 jobId/job_id,再用独立的 hwpod job status <jobId> ${hwpodWorkspaceArgs} 短命令做有限轮询。`
|
||||
].filter(Boolean);
|
||||
return [
|
||||
`# HWPOD CaseRun Code Agent Task`,
|
||||
`# HWPOD CaseRun 代码代理任务`,
|
||||
``,
|
||||
`caseId: ${run.caseId}`,
|
||||
`runId: ${run.runId}`,
|
||||
`subjectRepoLocalPath: ${run.subject.repoLocalPath}`,
|
||||
`subjectCommitId: ${run.subject.commitId}`,
|
||||
`subjectWorktreePath: ${run.subject.worktreePath}`,
|
||||
`案例ID: ${run.caseId}`,
|
||||
`运行ID: ${run.runId}`,
|
||||
`主体仓库本地路径: ${run.subject.repoLocalPath}`,
|
||||
`主体提交ID: ${run.subject.commitId}`,
|
||||
`主体隔离工作区路径: ${run.subject.worktreePath}`,
|
||||
`hwpodId: ${hwpodId}`,
|
||||
`hwpodWorkspaceArgs: ${hwpodWorkspaceArgs}`,
|
||||
`verificationMode: compile-only build check; no download or runtime smoke unless the case explicitly asks for it`,
|
||||
`验证模式: 仅执行编译构建检查;除非案例明确要求,否则不下载、不做运行态冒烟验证。`,
|
||||
``,
|
||||
`## Runtime Assembly`,
|
||||
`AgentRun materializes HWLAB runtime assets from ResourceBundleRef kind=gitbundle: repo subpath \`tools\` is copied to workspace \`tools\`, and repo subpath \`skills\` is copied to workspace \`.agents/skills\`. CaseRun no longer sends ad hoc workspaceFiles or seed-file payloads.`,
|
||||
`## 运行时装配`,
|
||||
`AgentRun 通过 ResourceBundleRef kind=gitbundle 装配 HWLAB 运行时资源:仓库子路径 \`tools\` 会复制到工作区 \`tools\`,仓库子路径 \`skills\` 会复制到工作区 \`.agents/skills\`。CaseRun 不再发送临时 workspaceFiles 或 seed-file 载荷。`,
|
||||
``,
|
||||
`## HWPOD Runtime`,
|
||||
`Use HWPOD by id through the runtime service. Do not require a runner-local \`.hwlab/hwpod-spec.yaml\`.`,
|
||||
`Use these arguments on every hwpod/hwpod-ctl command: \`${hwpodWorkspaceArgs}\`.`,
|
||||
`Standard smoke sequence for this task:`,
|
||||
`## HWPOD 运行时`,
|
||||
`通过运行时服务按 ID 使用 HWPOD,不要求 runner 本地存在 \`.hwlab/hwpod-spec.yaml\`。`,
|
||||
`每个 hwpod/hwpod-ctl 命令都必须携带这些参数:\`${hwpodWorkspaceArgs}\`。`,
|
||||
`本任务的标准冒烟步骤:`,
|
||||
`- \`hwpod-ctl spec validate ${hwpodWorkspaceArgs}\``,
|
||||
`- \`hwpod inspect ${hwpodWorkspaceArgs}\``,
|
||||
`- workspace reads/searches/edits: \`hwpod workspace ... ${hwpodWorkspaceArgs}\``,
|
||||
`- compile check: \`hwpod build ${hwpodWorkspaceArgs}\``,
|
||||
`- 工作区读取、搜索和编辑:\`hwpod workspace ... ${hwpodWorkspaceArgs}\``,
|
||||
`- 编译检查:\`hwpod build ${hwpodWorkspaceArgs}\``,
|
||||
``,
|
||||
`## Task`,
|
||||
`## 任务`,
|
||||
agentTask.prompt,
|
||||
``,
|
||||
`## Constraints`,
|
||||
`## 约束`,
|
||||
...constraints.map((item) => `- ${item}`),
|
||||
``,
|
||||
`## Flow`,
|
||||
`- Confirm standard \`hwpod\`, \`hwpod-ctl\` and \`hwpod-compiler\` commands are available from the gitbundle tools directory.`,
|
||||
`- Follow the case task using standard hwpod/hwpod-ctl commands with \`${hwpodWorkspaceArgs}\`. Run build/download/UART steps only when the case explicitly asks for them, and report returned JSON/job/artifact/serial summaries.`,
|
||||
`- For hwpod build/download, keep the HWPOD command unwrapped so it can return async JSON; then poll the returned job id with separate short \`hwpod job status <jobId> ${hwpodWorkspaceArgs}\` commands a bounded number of times. Do not wrap status polling with shell sleep, &&, timeout, watch, head, pipes, or shell loops.`,
|
||||
`- CaseRun will inspect git diff under subjectWorktreePath after your turn completes and may run a runner post-check compile as separate evidence.`,
|
||||
`- CaseRun records trace/session/conversation, agent commandExecution, workspace diff and Keil build evidence without auto-grading them.`
|
||||
`## 执行流程`,
|
||||
`- 先确认标准 \`hwpod\`、\`hwpod-ctl\` 和 \`hwpod-compiler\` 命令可从 gitbundle 工具目录调用。`,
|
||||
`- 使用标准 hwpod/hwpod-ctl 命令并携带 \`${hwpodWorkspaceArgs}\` 完成案例任务。只有案例明确要求时才运行 build/download/UART 步骤,并回报返回的 JSON、job、artifact 或串口摘要。`,
|
||||
`- 对 hwpod build/download,保持 HWPOD 命令本身不被包装,让它返回异步 JSON;随后用独立短命令 \`hwpod job status <jobId> ${hwpodWorkspaceArgs}\` 对返回的 job id 做有限轮询。不要用 shell sleep、&&、timeout、watch、head、pipe 或 shell loop 包裹状态轮询。`,
|
||||
`- 你的回合结束后,CaseRun 会检查 subjectWorktreePath 下的 git diff,并可能把 runner 后置编译作为单独证据记录。`,
|
||||
`- CaseRun 只记录 trace、session、conversation、agent 命令执行、workspace diff 和 Keil 构建证据,不做自动评分或门禁判断。`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user