diff --git a/cases/d601-f103-v2-compile/case.json b/cases/d601-f103-v2-compile/case.json index 97c3764e..ad970747 100644 --- a/cases/d601-f103-v2-compile/case.json +++ b/cases/d601-f103-v2-compile/case.json @@ -9,9 +9,6 @@ "commitId": "df7a4e6e551fa90d64bde5537cc000f89d63dd20", "subdir": "projects/01_baseline" }, - "runtime": { - "apiUrl": "http://74.48.78.17:19667" - }, "expected": { "compile": { "success": true, diff --git a/internal/cloud/server-caserun-http.test.ts b/internal/cloud/server-caserun-http.test.ts index b7856ee7..6579dfe0 100644 --- a/internal/cloud/server-caserun-http.test.ts +++ b/internal/cloud/server-caserun-http.test.ts @@ -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); diff --git a/tools/hwlab-caserun.test.ts b/tools/hwlab-caserun.test.ts index db1e09af..1b063ab5 100644 --- a/tools/hwlab-caserun.test.ts +++ b/tools/hwlab-caserun.test.ts @@ -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, diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 99f585d0..1a2f7725 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -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", diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 0122e34b..85dd13ef 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -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) { - const runtime = definition.runtime; - if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) return ""; - return text((runtime as Record).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(),