fix: remove v0.2 caserun runtime fallback

This commit is contained in:
lyon
2026-06-26 16:26:59 +08:00
parent 25c57c13ef
commit 65f3cc2f8a
5 changed files with 92 additions and 27 deletions
-3
View File
@@ -9,9 +9,6 @@
"commitId": "df7a4e6e551fa90d64bde5537cc000f89d63dd20",
"subdir": "projects/01_baseline"
},
"runtime": {
"apiUrl": "http://74.48.78.17:19667"
},
"expected": {
"compile": {
"success": true,
@@ -21,7 +21,12 @@ test("cloud-api exposes web CaseRun cases, run status, events and aggregate", as
}), "utf8");
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), sampleSpecYaml(), "utf8");
const seen: any[] = [];
const accessController = {
async ensureBootstrap() {},
async requireNavAccess() { return true; }
};
const server = createCloudApiServer({
accessController,
env: { PATH: process.env.PATH, HWLAB_CASERUN_CASE_REPO: root, HWLAB_CASERUN_STATE_DIR: stateRoot },
caseRunExecutor: async (context: any, emit: any) => {
seen.push(context.parsed);
+2 -2
View File
@@ -8,6 +8,7 @@ import { prepareCaseRun } from "./src/hwlab-caserun-lib.ts";
const NOW = "2026-06-06T00:00:00.000Z";
const SUBJECT_COMMIT = "df7a4e6e551fa90d64bde5537cc000f89d63dd20";
const TEST_RUNTIME_API_URL = "http://api.test";
test("CaseRun records source root baseline so pre-existing dirty files are not attributed to the run", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-source-root-"));
@@ -26,7 +27,6 @@ test("CaseRun records source root baseline so pre-existing dirty files are not a
commitId: SUBJECT_COMMIT,
subdir: "projects/01_baseline"
},
runtime: { apiUrl: "http://hwlab.test" },
agentTask: { workspace: "subjectWorktree", prompt: "record source root baseline", constraints: [] }
}, null, 2), "utf8");
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), [
@@ -64,7 +64,7 @@ test("CaseRun records source root baseline so pre-existing dirty files are not a
const result = await prepareCaseRun({
parsed: { _: [], caseRepo, runId: "source-root-baseline" },
env: {},
env: { HWLAB_RUNTIME_API_URL: TEST_RUNTIME_API_URL },
fetchImpl: fetchImpl as typeof fetch,
cwd: root,
now: () => NOW,
+50 -8
View File
@@ -9,6 +9,9 @@ import { artifactsFromKeilStatusForTest, extractKeilJobId, jobStatusCommandForTe
const SUBJECT_COMMIT_ID = "df7a4e6e551fa90d64bde5537cc000f89d63dd20";
const SUBJECT_REPO_LOCAL_PATH = "F:\\Work\\HWLAB-CASE-F103";
const TEST_RUNTIME_API_URL = "http://api.test";
const TEST_RUNTIME_WEB_URL = "http://web.test";
const TEST_RUNTIME_ENV = { HWLAB_RUNTIME_API_URL: TEST_RUNTIME_API_URL, HWLAB_RUNTIME_WEB_URL: TEST_RUNTIME_WEB_URL };
test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state", async () => {
const root = await mkdtempCaseRoot();
@@ -24,7 +27,7 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state",
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
const requests: any[] = [];
const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo, "--run-id", "run-test"], { cwd, now: () => "2026-06-05T00:00:00.000Z", fetchImpl: subjectWorktreeFetch(requests) });
const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo, "--run-id", "run-test"], { cwd, now: () => "2026-06-05T00:00:00.000Z", env: TEST_RUNTIME_ENV, fetchImpl: subjectWorktreeFetch(requests) });
assert.equal(result.exitCode, 0);
assert.equal(result.payload.action, "case.prepare.prepare");
@@ -57,6 +60,40 @@ test("hwlab-cli case prepare copies a case hwpod-spec into isolated run state",
}
});
test("case prepare requires an explicit v0.3 runtime API URL", async () => {
const root = await mkdtempCaseRoot();
const caseRepo = path.join(root, "hwlab-case-registry");
const cwd = path.join(root, "hwlab");
try {
await createCaseRepo(caseRepo, "d601-f103-v2-compile");
await mkdir(cwd, { recursive: true });
const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z" });
assert.equal(result.exitCode, 1);
assert.equal(result.payload.error.code, "caserun_runtime_api_url_required");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("case prepare rejects the removed v0.2 runtime API URL", async () => {
const root = await mkdtempCaseRoot();
const caseRepo = path.join(root, "hwlab-case-registry");
const cwd = path.join(root, "hwlab");
try {
await createCaseRepo(caseRepo, "d601-f103-v2-compile");
await mkdir(cwd, { recursive: true });
const result = await runHwlabCli(["case", "prepare", "d601-f103-v2-compile", "--case-repo", caseRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z", env: { HWLAB_RUNTIME_API_URL: "http://74.48.78.17:19667" } });
assert.equal(result.exitCode, 1);
assert.equal(result.payload.error.code, "caserun_removed_v02_runtime_url");
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("case prepare rejects legacy subject repo/ref without a local repo path and commit id", async () => {
const root = await mkdtempCaseRoot();
const caseRepo = path.join(root, "hwlab-case-registry");
@@ -70,7 +107,7 @@ test("case prepare rejects legacy subject repo/ref without a local repo path and
await writeFile(path.join(caseDir, "case.json"), JSON.stringify({ contractVersion: "hwpod-case-v1", caseId: "legacy-case", hwpodSpec: "hwpod-spec.yaml", subject: { repo: "git@github.com:pikasTech/D601-HWLAB.git", ref: "main" } }, null, 2), "utf8");
await writeFile(path.join(caseDir, "hwpod-spec.yaml"), hwpodSpecText(), "utf8");
const result = await runHwlabCli(["case", "prepare", "legacy-case", "--case-repo", caseRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z", fetchImpl: subjectWorktreeFetch([]) });
const result = await runHwlabCli(["case", "prepare", "legacy-case", "--case-repo", caseRepo], { cwd, now: () => "2026-06-05T00:00:00.000Z", env: TEST_RUNTIME_ENV, fetchImpl: subjectWorktreeFetch([]) });
assert.equal(result.exitCode, 1);
assert.equal(result.payload.error.code, "subject_repo_local_path_required");
@@ -90,7 +127,7 @@ test("case prepare uses the new registry by default and refuses the removed hwpo
await createCaseRepo(removedRepo, caseId);
await mkdir(cwd, { recursive: true });
const defaultResult = await runHwlabCli(["case", "prepare", caseId, "--run-id", "run-default"], { cwd, now: () => "2026-06-05T00:00:00.000Z", fetchImpl: subjectWorktreeFetch([]) });
const defaultResult = await runHwlabCli(["case", "prepare", caseId, "--run-id", "run-default"], { cwd, now: () => "2026-06-05T00:00:00.000Z", env: TEST_RUNTIME_ENV, fetchImpl: subjectWorktreeFetch([]) });
assert.equal(defaultResult.exitCode, 0);
assert.equal(defaultResult.payload.caseRepo, newRepo);
@@ -123,7 +160,7 @@ test("case prompt previews the real assembled prompt in Chinese without starting
}, 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" });
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", env: TEST_RUNTIME_ENV });
assert.equal(result.exitCode, 0);
assert.equal(result.payload.action, "case.prompt");
@@ -190,6 +227,7 @@ test("case run start returns immediately and status/result/logs are short pollin
], {
cwd,
now: () => "2026-06-05T00:00:00.000Z",
env: TEST_RUNTIME_ENV,
startProcess: async (input) => {
workerArgs = input.args;
await writeFile(input.stdoutPath, "worker accepted\n", "utf8");
@@ -328,7 +366,6 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
hwpodSpec: "hwpod-spec.yaml",
subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline" },
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");
@@ -367,7 +404,7 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit
await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8");
sawAgentRunningState = true;
},
env: { HWLAB_API_KEY: "hwl_live_test_case_agent_key" },
env: { ...TEST_RUNTIME_ENV, HWLAB_API_KEY: "hwl_live_test_case_agent_key" },
runProcess: async (command) => {
runProcessCalls.push(command);
const gitArgs = command.slice(1);
@@ -594,6 +631,11 @@ test("case run archives rendered trace transcript when finalResponse is null", a
{ traceId: "trc_final_null", seq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'", stdoutSummary: "build completed", exitCode: 0, createdAt: "2026-06-06T00:00:02.000Z" },
{ traceId: "trc_final_null", seq: 3, label: "agentrun:terminal:failed", type: "result", terminal: true, status: "failed", message: "Error running remote compact task: unexpected status 404 Not Found", createdAt: "2026-06-06T00:00:03.000Z" }
],
rows: [
{ rowId: "trace-assistant:1", seq: 1, tone: "source", header: "00:00:01 助手消息", body: "我先检查当前 HWPOD。" },
{ rowId: "tool:build", seq: 2, tone: "ok", header: "00:00:02 ok commandExecution", bodyFormat: "text", body: "hwpod build --spec .hwlab/hwpod-spec.yaml\nstdout:\nbuild completed\nexitCode=0" },
{ rowId: "trace-terminal:3", seq: 3, tone: "blocked", header: "00:00:03 终止失败", terminal: true, body: "Error running remote compact task: unexpected status 404 Not Found" }
],
error: { message: "Error running remote compact task: unexpected status 404 Not Found" }
};
const evidence = {
@@ -604,7 +646,7 @@ test("case run archives rendered trace transcript when finalResponse is null", a
autoEvaluation: false,
status: "recorded",
subject: run.subject,
agent: { traceId: "trc_final_null", sessionId: "ses_final_null", conversationId: "cnv_final_null", threadId: "thread-final-null", providerProfile: "deepseek", projectId: "prj_hwpod_workbench", promptSha256: "prompt-sha" },
agent: { traceId: "trc_final_null", baseUrl: TEST_RUNTIME_WEB_URL, sessionId: "ses_final_null", conversationId: "cnv_final_null", threadId: "thread-final-null", providerProfile: "deepseek", projectId: "prj_hwpod_workbench", promptSha256: "prompt-sha" },
agentTrace: run.agentTrace,
agentDiff: run.agentDiff,
hwpod: { source: "case-run-runner-post-agent-compile-check", exitCode: 0 },
@@ -705,7 +747,7 @@ test("case aggregate writes one registry markdown entry without grading or broad
runId,
runDir: "/root/hwlab-v02/.state/hwlab-cli/caserun/case04-completed-run",
caseRepo,
apiUrl: "http://74.48.78.17:19667",
apiUrl: TEST_RUNTIME_API_URL,
compileOnly: true,
createdAt: "2026-06-08T00:00:00.000Z",
completedAt: "2026-06-08T00:10:00.000Z",
+35 -14
View File
@@ -1,3 +1,6 @@
// SPEC: PJ2026-01010305 71FREQ preinstall draft-2026-06-26-71freq-v03-hwpod-preinstall.
// Responsibility: v0.3 CaseRun runtime-origin resolution and compile-only HWPOD smoke orchestration.
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { closeSync, openSync } from "node:fs";
@@ -7,8 +10,8 @@ import path from "node:path";
import { renderTraceRowsMarkdown, traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
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 REMOVED_V02_API_URL = "http://74.48.78.17:19667";
const REMOVED_V02_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 = 300000;
@@ -378,7 +381,7 @@ export async function startCaseRun(context: CaseContext) {
caseFile: "",
sourceSpecPath: "",
specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"),
apiUrl: text(context.parsed.apiUrl ?? context.parsed.apiBaseUrl ?? context.parsed.runtimeApiUrl ?? context.env.HWLAB_RUNTIME_API_URL) || DEFAULT_API_URL,
apiUrl: apiUrlFrom(context, {}),
compileOnly: true,
createdAt: context.now(),
updatedAt: context.now(),
@@ -1367,12 +1370,19 @@ async function caseFetchJson(context: CaseContext, url: string, init: RequestIni
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" });
if (!apiKey.startsWith(HWLAB_API_KEY_PREFIX)) throw cliError("api_key_required", "case run Code Agent stage requires HWLAB_API_KEY for HWLAB 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;
const value = text(context.parsed.baseUrl ?? context.parsed.webUrl ?? context.env.HWLAB_RUNTIME_WEB_URL ?? context.env.HWLAB_CLOUD_WEB_URL);
if (!value) {
throw cliError("caserun_runtime_web_url_required", "CaseRun v0.3 requires an explicit runtime Web URL from --base-url, --web-url, HWLAB_RUNTIME_WEB_URL, or HWLAB_CLOUD_WEB_URL; case runtime.webUrl fallback is removed.", {
allowed: ["--base-url", "--web-url", "HWLAB_RUNTIME_WEB_URL", "HWLAB_CLOUD_WEB_URL"],
removedFallbacks: ["case.runtime.webUrl", REMOVED_V02_WEB_URL]
});
}
return rejectRemovedV02RuntimeUrl(value, "web");
}
function firstHwpodOutput(body: any) {
@@ -1755,7 +1765,23 @@ function keilJobStatus(body: any) {
}
function apiUrlFrom(context: CaseContext, definition: any) {
return text(context.parsed.apiUrl ?? context.parsed.apiBaseUrl ?? context.parsed.runtimeApiUrl ?? definition?.runtime?.apiUrl ?? context.env.HWLAB_RUNTIME_API_URL) || DEFAULT_API_URL;
const value = text(context.parsed.apiUrl ?? context.parsed.apiBaseUrl ?? context.parsed.runtimeApiUrl ?? context.env.HWLAB_RUNTIME_API_URL ?? context.env.HWLAB_CASERUN_RUNTIME_API_URL);
if (!value) {
throw cliError("caserun_runtime_api_url_required", "CaseRun v0.3 requires an explicit runtime API URL from --api-url, --api-base-url, --runtime-api-url, HWLAB_RUNTIME_API_URL, or HWLAB_CASERUN_RUNTIME_API_URL; case runtime.apiUrl fallback is removed.", {
allowed: ["--api-url", "--api-base-url", "--runtime-api-url", "HWLAB_RUNTIME_API_URL", "HWLAB_CASERUN_RUNTIME_API_URL"],
removedFallbacks: ["case.runtime.apiUrl", REMOVED_V02_API_URL]
});
}
return rejectRemovedV02RuntimeUrl(value, "api");
}
function rejectRemovedV02RuntimeUrl(value: string, kind: "api" | "web") {
const normalized = value.replace(/\/+$/u, "");
const removed = kind === "web" ? REMOVED_V02_WEB_URL : REMOVED_V02_API_URL;
if (normalized === removed) {
throw cliError("caserun_removed_v02_runtime_url", `CaseRun v0.3 does not support the removed v0.2 ${kind} runtime URL`, { kind, url: normalized });
}
return normalized;
}
function stateRoot(context: CaseContext) {
@@ -1780,16 +1806,11 @@ function agentTraceLookupForRun(run: PreparedCaseRun, evidence?: any) {
const agent = run.agent ?? evidence?.agent;
const traceId = text(agent?.traceId ?? evidence?.agentTrace?.traceId ?? run.agentTrace?.traceId);
if (!traceId) return null;
const baseUrl = text(agent?.baseUrl ?? run.agent?.baseUrl ?? evidence?.agent?.baseUrl) || runtimeWebUrl(run.definition) || DEFAULT_WEB_URL;
const baseUrl = text(agent?.baseUrl ?? run.agent?.baseUrl ?? evidence?.agent?.baseUrl);
if (!baseUrl) return null;
return agentTraceLookup({ baseUrl, traceId, sessionId: text(agent?.sessionId), conversationId: text(agent?.conversationId), threadId: text(agent?.threadId) });
}
function runtimeWebUrl(definition: Record<string, unknown>) {
const runtime = definition.runtime;
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) return "";
return text((runtime as Record<string, unknown>).webUrl);
}
function renderEvidenceSummary(evidence: any) {
const agentStage = agentStageSummary(evidence);
const traceLookup = evidence.traceLookup ?? evidence.agent?.traceLookup;
@@ -2644,7 +2665,7 @@ async function loadControlRunSkeleton(context: CaseContext, caseId: string, runI
caseFile: "",
sourceSpecPath: "",
specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"),
apiUrl: text(context.parsed.apiUrl ?? context.parsed.apiBaseUrl ?? context.parsed.runtimeApiUrl ?? context.env.HWLAB_RUNTIME_API_URL) || DEFAULT_API_URL,
apiUrl: apiUrlFrom(context, {}),
compileOnly: true,
createdAt: context.now(),
updatedAt: context.now(),