Merge remote-tracking branch 'origin/v0.3' into feat/2625-workbench-temporal
# Conflicts: # web/hwlab-cloud-web/vite.config.ts
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import type { CaseRunRecord } from "../../internal/harnessrl/contracts.ts";
|
||||
import { CaseRunTransportError, createApiCaseRunTransport, createLocalCaseRunTransport } from "../../internal/harnessrl/transport.ts";
|
||||
import { runHwlabCli } from "../src/hwlab-cli-lib.ts";
|
||||
|
||||
const record: CaseRunRecord = {
|
||||
runId: "run-2617", caseId: "case-2617", workflowId: "harnessrl-caserun-run-2617", workflowRunId: "workflow-run-2617",
|
||||
status: "queued", stage: "queued", terminal: false, caseRepo: "/cases", runDir: "/state/run-2617", runtimeApiUrl: "local://harnessrl",
|
||||
result: null, error: null, blocker: null, createdAt: "2026-07-17T00:00:00.000Z", updatedAt: "2026-07-17T00:00:00.000Z"
|
||||
};
|
||||
|
||||
test("L0 local adapter executes the shared CaseRun command contract without HTTP", async () => {
|
||||
const calls: string[] = [];
|
||||
const transport = createLocalCaseRunTransport({ service: {
|
||||
async submit() { calls.push("start"); return record; },
|
||||
async getRun() { calls.push("status"); return record; },
|
||||
async events() { calls.push("events"); return { ...record, events: [] }; },
|
||||
async aggregate() { calls.push("aggregate"); return { ok: true, runId: record.runId, status: record.status }; },
|
||||
async cancel() { calls.push("cancel"); return { ...record, status: "cancel_requested", stage: "cancel_requested" }; }
|
||||
}});
|
||||
|
||||
const started = await transport.execute({ kind: "start", caseId: record.caseId, runId: record.runId });
|
||||
assert.equal(started.runId, record.runId);
|
||||
assert.equal(started.workflowId, record.workflowId);
|
||||
await transport.execute({ kind: "status", runId: record.runId });
|
||||
await transport.execute({ kind: "events", runId: record.runId });
|
||||
await transport.execute({ kind: "aggregate", runId: record.runId });
|
||||
await transport.execute({ kind: "cancel", runId: record.runId });
|
||||
assert.deepEqual(calls, ["start", "status", "events", "aggregate", "cancel"]);
|
||||
});
|
||||
|
||||
test("CLI defaults to local and requires explicit over-api for base-url", async () => {
|
||||
const local = createLocalCaseRunTransport({ service: {
|
||||
async submit() { return record; }, async getRun() { return record; }, async events() { return { ...record, events: [] }; },
|
||||
async aggregate() { return { ok: true, runId: record.runId }; }, async cancel() { return record; }
|
||||
}});
|
||||
const result = await runHwlabCli(["case", "run", "start", record.caseId, "--run-id", record.runId], { caseRunTransportFactory: () => local });
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.transport, "local");
|
||||
assert.equal(result.payload.service, "native-harnessrl-application-service");
|
||||
assert.equal(result.payload.baseUrl, null);
|
||||
assert.equal(result.payload.route, null);
|
||||
assert.equal(result.payload.method, null);
|
||||
assert.equal(result.payload.workflowRunId, record.workflowRunId);
|
||||
|
||||
const rejected = await runHwlabCli(["case", "run", "status", record.runId, "--base-url", "http://api.test"]);
|
||||
assert.equal(rejected.exitCode, 1);
|
||||
assert.equal(rejected.payload.error.code, "caserun_over_api_required");
|
||||
});
|
||||
|
||||
test("API adapter uses only the explicit CaseRun origin and preserves typed errors", async () => {
|
||||
const requests: Array<{ url: string; method: string; authorization: string | null }> = [];
|
||||
const transport = createApiCaseRunTransport({ baseUrl: "http://api.test", apiKey: "hwl_live_test", fetchImpl: async (input, init) => {
|
||||
requests.push({ url: String(input), method: String(init?.method), authorization: new Headers(init?.headers).get("authorization") });
|
||||
return Response.json({ ok: true, ...record });
|
||||
}});
|
||||
const response = await transport.execute({ kind: "events", runId: record.runId });
|
||||
assert.equal(response.workflowId, record.workflowId);
|
||||
assert.deepEqual(requests, [{ url: `http://api.test/v1/caserun/runs/${record.runId}/events`, method: "GET", authorization: "Bearer hwl_live_test" }]);
|
||||
|
||||
const failed = createApiCaseRunTransport({ baseUrl: "http://api.test", fetchImpl: async () => Response.json({ ok: false, error: { code: "caserun_run_not_found", message: "missing", details: { runId: record.runId } } }, { status: 404 }) });
|
||||
await assert.rejects(failed.execute({ kind: "status", runId: record.runId }), (error: any) => error instanceof CaseRunTransportError && error.code === "caserun_run_not_found");
|
||||
});
|
||||
|
||||
test("API adapter times out through its real AbortSignal path with typed route details", async () => {
|
||||
const transport = createApiCaseRunTransport({ baseUrl: "http://api.test", timeoutMs: 5, fetchImpl: async (_input, init) => {
|
||||
await new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true }));
|
||||
throw new Error("unreachable");
|
||||
}});
|
||||
await assert.rejects(transport.execute({ kind: "status", runId: record.runId }), (error: any) => {
|
||||
assert.equal(error instanceof CaseRunTransportError, true);
|
||||
assert.equal(error.code, "caserun_api_timeout");
|
||||
assert.deepEqual(error.details, { timeoutMs: 5, baseUrl: "http://api.test", route: `/v1/caserun/runs/${record.runId}`, method: "GET" });
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
test("over-api CLI discloses bounded origin route and method without credentials", async () => {
|
||||
const result = await runHwlabCli(["case", "run", "events", record.runId, "--over-api", "--base-url", "http://api.test", "--timeout-ms", "25"], {
|
||||
env: { HWLAB_API_KEY: "hwl_live_secret" },
|
||||
fetchImpl: async () => Response.json({ ok: true, ...record, events: [] })
|
||||
});
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.transport, "api");
|
||||
assert.equal(result.payload.baseUrl, "http://api.test");
|
||||
assert.equal(result.payload.route, `/v1/caserun/runs/${record.runId}/events`);
|
||||
assert.equal(result.payload.method, "GET");
|
||||
assert.equal(JSON.stringify(result.payload).includes("hwl_live_secret"), false);
|
||||
});
|
||||
@@ -224,79 +224,6 @@ test("case run normalizes v0.3 HWPOD build operation topology and typed wait blo
|
||||
assert.equal(yamlEmpty.topology.ok, true);
|
||||
});
|
||||
|
||||
test("case run start returns immediately and status/result/logs are short polling commands", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const caseRepo = path.join(root, "hwlab-case-registry");
|
||||
const cwd = path.join(root, "hwlab");
|
||||
const caseId = "d601-f103-v2-compile";
|
||||
try {
|
||||
await createCaseRepo(caseRepo, caseId);
|
||||
await mkdir(cwd, { recursive: true });
|
||||
let workerArgs: string[] = [];
|
||||
const started = await runHwlabCli([
|
||||
"case",
|
||||
"run",
|
||||
"start",
|
||||
caseId,
|
||||
"--case-repo",
|
||||
caseRepo,
|
||||
"--run-id",
|
||||
"run-async",
|
||||
"--agent-timeout-ms",
|
||||
"180000",
|
||||
"--job-timeout-ms",
|
||||
"70000"
|
||||
], {
|
||||
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");
|
||||
await writeFile(input.stderrPath, "", "utf8");
|
||||
return { pid: 4242 };
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(started.exitCode, 0);
|
||||
assert.equal(started.payload.action, "case.run.start");
|
||||
assert.equal(started.payload.status, "submitted");
|
||||
assert.equal(started.payload.startReturned, true);
|
||||
assert.equal(started.payload.pid, 4242);
|
||||
assert.match(started.payload.statusCommand, /case run status run-async/u);
|
||||
assert.match(started.payload.resultCommand, /case run result run-async/u);
|
||||
assert.match(started.payload.logsCommand, /case run logs run-async/u);
|
||||
assert.equal(workerArgs.includes("--agent-timeout-ms"), true);
|
||||
assert.equal(workerArgs[workerArgs.indexOf("--agent-timeout-ms") + 1], "180000");
|
||||
assert.equal(workerArgs.includes("--job-timeout-ms"), true);
|
||||
assert.equal(workerArgs[workerArgs.indexOf("--job-timeout-ms") + 1], "70000");
|
||||
|
||||
const status = await runHwlabCli(["case", "run", "status", "run-async"], { cwd, now: () => "2026-06-05T00:00:01.000Z" });
|
||||
assert.equal(status.exitCode, 0);
|
||||
assert.equal(status.payload.action, "case.run.status");
|
||||
assert.equal(status.payload.status, "running");
|
||||
assert.equal(status.payload.stage, "started");
|
||||
assert.equal(status.payload.pid, 4242);
|
||||
assert.equal(status.payload.stdoutBytes, Buffer.byteLength("worker accepted\n"));
|
||||
assert.equal(status.payload.stderrBytes, 0);
|
||||
assert.match(status.payload.nextPollCommand, /case run status run-async/u);
|
||||
|
||||
const result = await runHwlabCli(["case", "run", "result", "run-async"], { cwd, now: () => "2026-06-05T00:00:02.000Z" });
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "case.run.result");
|
||||
assert.equal(result.payload.ready, false);
|
||||
assert.equal(result.payload.status, "running");
|
||||
assert.match(result.payload.nextPollCommand, /case run status run-async/u);
|
||||
|
||||
const logs = await runHwlabCli(["case", "run", "logs", "run-async", "--tail", "200"], { cwd, now: () => "2026-06-05T00:00:03.000Z" });
|
||||
assert.equal(logs.exitCode, 0);
|
||||
assert.equal(logs.payload.action, "case.run.logs");
|
||||
assert.equal(logs.payload.stdout, "worker accepted\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("case run result finalizes legacy build-completed state and refreshes registry artifacts", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const caseRepo = path.join(root, "hwlab-case-registry");
|
||||
@@ -738,56 +665,6 @@ test("case run archives rendered trace transcript when finalResponse is null", a
|
||||
}
|
||||
});
|
||||
|
||||
test("case run status reports latest run stage when async control stage is stale", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const cwd = path.join(root, "hwlab");
|
||||
const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-stale-stage");
|
||||
try {
|
||||
await mkdir(runDir, { recursive: true });
|
||||
await writeFile(path.join(runDir, "worker.stdout.log"), "agent finished\n", "utf8");
|
||||
await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8");
|
||||
await writeFile(path.join(runDir, "run.json"), JSON.stringify({
|
||||
caseId: "d601-f103-v2-compile",
|
||||
runId: "run-stale-stage",
|
||||
runDir,
|
||||
caseRepo: "",
|
||||
caseDir: "",
|
||||
caseFile: "",
|
||||
sourceSpecPath: "",
|
||||
specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"),
|
||||
apiUrl: "http://api.test",
|
||||
compileOnly: true,
|
||||
createdAt: "2026-06-05T00:00:00.000Z",
|
||||
updatedAt: "2026-06-05T00:00:10.000Z",
|
||||
status: "running",
|
||||
stage: "agent-running",
|
||||
definition: {},
|
||||
subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-stale-stage", workspacePath: "", setup: {} },
|
||||
agent: { stageStatus: "completed", baseUrl: "http://web.test", projectId: "prj_hwpod_workbench", providerProfile: "deepseek", conversationId: "cnv_case", sessionId: "ses_case", threadId: "thread-case", traceId: "trc_case", promptPath: path.join(runDir, "agent-prompt.md"), promptSha256: "sha", polls: 3, lastHttpStatus: 200 },
|
||||
agentDiff: { statusShort: " M projects/01_baseline/main.c\n", diffStat: " projects/01_baseline/main.c | 1 +\n", diffPatchPath: path.join(runDir, "agent-diff.patch"), diffPatchSha256: "sha" },
|
||||
_control: { status: "running", stage: "prepared", pid: 1234, stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), startedAt: "2026-06-05T00:00:00.000Z" }
|
||||
}, null, 2), "utf8");
|
||||
|
||||
const status = await runHwlabCli(["case", "run", "status", "run-stale-stage"], { cwd, now: () => "2026-06-05T00:00:12.000Z" });
|
||||
assert.equal(status.exitCode, 0);
|
||||
assert.equal(status.payload.stage, "agent-diff-collected");
|
||||
assert.equal(status.payload.staleControlWarning.code, "stale_control_stage");
|
||||
assert.equal(status.payload.staleControlWarning.controlStage, "prepared");
|
||||
assert.equal(status.payload.staleControlWarning.selectedStage, "agent-diff-collected");
|
||||
assert.equal(status.payload.agent.stageStatus, "completed");
|
||||
assert.match(status.payload.agentDiff.statusShort, /projects\/01_baseline\/main\.c/u);
|
||||
|
||||
const result = await runHwlabCli(["case", "run", "result", "run-stale-stage"], { cwd, now: () => "2026-06-05T00:00:13.000Z" });
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.ready, false);
|
||||
assert.equal(result.payload.stage, "agent-diff-collected");
|
||||
assert.equal(result.payload.traceLookup.commands.trace, "hwlab-cli client agent trace trc_case --render web");
|
||||
assert.equal(result.payload.staleControlWarning.code, "stale_control_stage");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("case aggregate writes one registry markdown entry without grading or broad registry commits", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const caseRepo = path.join(root, "hwlab-case-registry");
|
||||
@@ -909,43 +786,6 @@ test("case aggregate writes one registry markdown entry without grading or broad
|
||||
}
|
||||
});
|
||||
|
||||
test("case run status keeps worker control stage ahead of prepared worktree evidence", async () => {
|
||||
const root = await mkdtempCaseRoot();
|
||||
const cwd = path.join(root, "hwlab");
|
||||
const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", "run-worker-started");
|
||||
try {
|
||||
await mkdir(runDir, { recursive: true });
|
||||
await writeFile(path.join(runDir, "worker.stdout.log"), "", "utf8");
|
||||
await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8");
|
||||
await writeFile(path.join(runDir, "run.json"), JSON.stringify({
|
||||
caseId: "d601-f103-v2-compile",
|
||||
runId: "run-worker-started",
|
||||
runDir,
|
||||
caseRepo: "",
|
||||
caseDir: "",
|
||||
caseFile: "",
|
||||
sourceSpecPath: "",
|
||||
specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"),
|
||||
apiUrl: "http://api.test",
|
||||
compileOnly: true,
|
||||
createdAt: "2026-06-05T00:00:00.000Z",
|
||||
updatedAt: "2026-06-05T00:00:10.000Z",
|
||||
status: "running",
|
||||
stage: "queued",
|
||||
definition: {},
|
||||
subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-worker-started", workspacePath: "", setup: {} },
|
||||
_control: { status: "running", stage: "started", pid: 1234, stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), startedAt: "2026-06-05T00:00:00.000Z" }
|
||||
}, null, 2), "utf8");
|
||||
|
||||
const status = await runHwlabCli(["case", "run", "status", "run-worker-started"], { cwd, now: () => "2026-06-05T00:00:12.000Z" });
|
||||
assert.equal(status.exitCode, 0);
|
||||
assert.equal(status.payload.stage, "started");
|
||||
assert.equal(status.payload.staleControlWarning, undefined);
|
||||
} 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-")));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user