Files
pikasTech-HWLAB/internal/harnessrl/harnessrl.test.ts
T

325 lines
20 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { createHarnessRLActivities } from "./activities.ts";
import type { ActivityResult, CaseRunEvent, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, HarnessRLTemporalGateway, StartCaseRunInput } from "./contracts.ts";
import { terminalStatus } from "./contracts.ts";
import { createHarnessRLHttpApp } from "./http.ts";
import { createHarnessRLService } from "./service.ts";
import { recoverAgentTaskStage } from "../../tools/src/hwlab-caserun-runtime.ts";
test("real AgentRun recovery helper maps only result 404 to typed not-found", async () => {
const run: any = { caseId: "case-1", runId: "run-1", runDir: "/tmp/run-1", agentTask: { providerProfile: "default", projectId: "project-1", timeoutMs: 1000, pollIntervalMs: 250 } };
const context = (status: number): any => ({
parsed: { _: [], baseUrl: "https://hwlab.example.test", traceId: "trace-authority" },
env: { HWLAB_API_KEY: "hwl_live_test" },
fetchImpl: async () => new Response(JSON.stringify({ ok: false, status }), { status, headers: { "content-type": "application/json" } }),
cwd: "/tmp", now: () => "2026-07-17T03:00:00.000Z", sleep: async () => {}, rest: []
});
await assert.rejects(recoverAgentTaskStage(context(404), run), (error: any) => error?.code === "agentrun_task_not_found" && error?.status === 404);
const failed = await recoverAgentTaskStage(context(503), run);
assert.equal(failed.agent.stageStatus, "result_request_failed");
assert.equal(failed.agent.lastHttpStatus, 503);
});
test("API restart preserves PostgreSQL-shaped registry read model", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const temporal = new FakeTemporal();
const first = createHarnessRLService({ registry, temporal, cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
const app = createHarnessRLHttpApp({ service: first });
const response = await app.fetch(new Request("http://harnessrl.test/v1/caserun/runs", {
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ caseId: fixture.caseId, runId: "restart-run" })
}));
assert.equal(response.status, 202);
assert.equal(temporal.starts, 1);
const restarted = createHarnessRLService({ registry, temporal: new FakeTemporal(), cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
const record = await restarted.getRun("restart-run");
assert.equal(record.status, "queued");
assert.equal(record.workflowRunId, "temporal-run-1");
});
test("run history lists shared registry results newest first with an opaque cursor", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const service = createHarnessRLService({ registry, temporal: new FakeTemporal(), cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
const app = createHarnessRLHttpApp({ service });
await service.submit({ caseId: fixture.caseId, runId: "cli-smoke-older", runtimeApiUrl: "https://public.example.test" });
await service.submit({ caseId: fixture.caseId, runId: "cli-smoke-newer", runtimeApiUrl: "https://public.example.test" });
const older = registry.runs.get("cli-smoke-older")!;
const newer = registry.runs.get("cli-smoke-newer")!;
older.createdAt = "2026-07-17T10:00:00.000Z";
newer.createdAt = "2026-07-18T10:00:00.000Z";
await registry.updateRun("cli-smoke-older", { status: "completed", stage: "completed" });
await registry.updateRun("cli-smoke-newer", {
status: "completed",
stage: "completed",
result: {
artifactManifestSha256: "d".repeat(64),
summary: { artifactCount: 1, artifacts: [{ path: "software-smoke.json" }] },
evidence: { status: "recorded" }
}
});
const firstResponse = await app.fetch(new Request("http://harnessrl.test/v1/caserun/runs?status=completed&limit=1"));
assert.equal(firstResponse.status, 200);
const first = await firstResponse.json() as any;
assert.equal(first.count, 1);
assert.equal(first.runs[0].runId, "cli-smoke-newer");
assert.equal(first.runs[0].artifactCount, 1);
assert.equal(first.runs[0].evidenceStatus, "recorded");
assert.equal(first.runs[0].artifactManifestSha256, "d".repeat(64));
assert.equal("runDir" in first.runs[0], false);
assert.equal(typeof first.nextCursor, "string");
const secondResponse = await app.fetch(new Request(`http://harnessrl.test/v1/caserun/runs?limit=1&cursor=${encodeURIComponent(first.nextCursor)}`));
const second = await secondResponse.json() as any;
assert.equal(second.runs[0].runId, "cli-smoke-older");
assert.equal(second.nextCursor, null);
const invalidResponse = await app.fetch(new Request("http://harnessrl.test/v1/caserun/runs?status=done"));
assert.equal(invalidResponse.status, 400);
assert.equal((await invalidResponse.json() as any).error.code, "invalid_status");
});
test("worker restart reuses idempotent prepare/build/collect activity results", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
await registry.createRun({ runId: "worker-restart", caseId: fixture.caseId, workflowId: "workflow-worker-restart", caseRepo: fixture.root, runDir: path.join(fixture.stateRoot, "runs", "worker-restart"), runtimeApiUrl: "http://127.0.0.1:6667" });
const firstWorker = createHarnessRLActivities({ registry, env: {} });
await firstWorker.markRunning({ runId: "worker-restart" });
const prepared = await firstWorker.prepare({ runId: "worker-restart", identity: "worker-restart:prepare:v1" });
await firstWorker.agent({ runId: "worker-restart", identity: "worker-restart:agent:v1" });
await firstWorker.trace({ runId: "worker-restart", identity: "worker-restart:trace:v1" });
await firstWorker.diff({ runId: "worker-restart", identity: "worker-restart:diff:v1" });
const built = await firstWorker.build({ runId: "worker-restart", identity: "worker-restart:build:v1" });
const restartedWorker = createHarnessRLActivities({ registry, env: {} });
assert.deepEqual(await restartedWorker.prepare({ runId: "worker-restart", identity: "worker-restart:prepare:v1" }), prepared);
await restartedWorker.agent({ runId: "worker-restart", identity: "worker-restart:agent:v1" });
await restartedWorker.trace({ runId: "worker-restart", identity: "worker-restart:trace:v1" });
await restartedWorker.diff({ runId: "worker-restart", identity: "worker-restart:diff:v1" });
const rebuilt = await restartedWorker.build({ runId: "worker-restart", identity: "worker-restart:build:v1" });
assert.equal(rebuilt.mode, built.mode);
await restartedWorker.collect({ runId: "worker-restart", identity: "worker-restart:collect:v1" });
await restartedWorker.markCompleted({ runId: "worker-restart" });
const record = await registry.getRun("worker-restart");
assert.equal(record?.status, "completed");
assert.equal((record?.result as any)?.summary?.mode, "software-smoke");
const artifact = JSON.parse(await readFile(path.join(record!.runDir, "software-smoke.json"), "utf8"));
assert.equal(artifact.checks.temporalWorkerRunning, true);
assert.equal(registry.activityResults.size, 6);
});
test("worker uses configured cloud-api authority and resubmits the same identity after typed not-found", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const runId = "authority-retry";
const identity = `${runId}:agent:v1`;
await registry.createRun({ runId, caseId: fixture.caseId, workflowId: `workflow-${runId}`, caseRepo: fixture.root, runDir: path.join(fixture.stateRoot, "runs", runId), runtimeApiUrl: "https://public.example.test" });
await registry.saveActivityResult({ runId, activityName: "prepare", identity: `${runId}:prepare:v1`, output: { state: "completed", result: { mode: "hardware", prepared: { run: { runId } } } } });
await registry.saveActivityResult({ runId, activityName: "agent", identity, output: { state: "started", authorityIdentity: identity } });
let submitCount = 0;
let recoverCount = 0;
const activities = createHarnessRLActivities({
registry,
env: { HWLAB_CASERUN_INTERNAL_API_URL: "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667" },
stages: {
agent: async (context: any, run: any) => { submitCount += 1; assert.equal(context.parsed.apiUrl, "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667"); assert.equal(context.parsed.operationIdentity, identity); return { run, agent: { traceId: "trace-authority-retry" } }; },
recoverAgent: async () => { recoverCount += 1; throw Object.assign(new Error("not found"), { code: "agentrun_task_not_found" }); }
}
});
const result = await activities.agent({ runId, identity });
assert.equal(result.agent.traceId, "trace-authority-retry");
assert.equal(recoverCount, 1);
assert.equal(submitCount, 1);
const deploy = await readFile(path.join(process.cwd(), "deploy", "deploy.yaml"), "utf8");
assert.match(deploy, /HWLAB_CASERUN_INTERNAL_API_URL: http:\/\/hwlab-cloud-api\.hwlab-v03\.svc\.cluster\.local:6667/u);
});
test("AgentRun and HWPOD crash gaps reuse stable authoritative operation identities", async () => {
const fixture = await hardwareFixture();
const registry = new MemoryRegistry();
await registry.createRun({ runId: "authority-gap", caseId: fixture.caseId, workflowId: "workflow-authority-gap", caseRepo: fixture.root, runDir: path.join(fixture.stateRoot, "runs", "authority-gap"), runtimeApiUrl: "http://127.0.0.1:6667" });
let agentDispatchCount = 0;
let hwpodDispatchCount = 0;
let agentLookupCount = 0;
let hwpodLookupCount = 0;
let agentCrash = true;
let hwpodCrash = true;
const stages = {
async prepare(context: any) { return { run: { runId: "authority-gap", caseId: fixture.caseId, runDir: context.parsed.runDir } }; },
async agent(context: any, run: any) {
agentDispatchCount += 1;
if (agentCrash) { agentCrash = false; throw new Error("worker exited after AgentRun accepted"); }
return { run: { ...run, agent: { traceId: context.parsed.traceId, sessionId: "ses-authoritative", resultUrl: "/v1/agentrun/result" } }, agent: { traceId: context.parsed.traceId, sessionId: "ses-authoritative", resultUrl: "/v1/agentrun/result" } };
},
async recoverAgent(context: any, run: any) {
agentLookupCount += 1;
return { run: { ...run, agent: { traceId: context.parsed.traceId, sessionId: "ses-authoritative", resultUrl: "/v1/agentrun/result" } }, agent: { traceId: context.parsed.traceId, sessionId: "ses-authoritative", resultUrl: "/v1/agentrun/result" } };
},
async trace(_context: any, run: any) { return { run, trace: { traceId: run.agent.traceId, status: "completed" } }; },
async diff(_context: any, run: any) { return { run, diff: { diffPatchSha256: "d".repeat(64) } }; },
async build(context: any, run: any) {
hwpodDispatchCount += 1;
if (hwpodCrash) { hwpodCrash = false; throw new Error("worker exited after HWPOD accepted"); }
return { run, summary: { jobId: "job-authoritative" }, evidence: { operation: { operationId: context.parsed.operationIdentity, jobId: "job-authoritative" } } };
},
async recoverBuild(context: any, run: any) {
hwpodLookupCount += 1;
return { run, summary: { jobId: "job-authoritative" }, evidence: { operation: { operationId: context.parsed.operationIdentity, jobId: "job-authoritative" } } };
},
async collect(_context: any, run: any, evidence: any) {
const manifestPath = path.join(run.runDir, "artifact-manifest.json");
await mkdir(run.runDir, { recursive: true });
await writeFile(manifestPath, `${JSON.stringify({ aggregate: { path: "aggregate.md", sha256: "a".repeat(64) } })}\n`);
return { run, status: "completed", summary: { artifactManifestPath: manifestPath }, evidence };
}
};
const worker = createHarnessRLActivities({ registry, env: {}, stages });
await worker.prepare({ runId: "authority-gap", identity: "authority-gap:prepare:v1" });
await assert.rejects(worker.agent({ runId: "authority-gap", identity: "authority-gap:agent:v1" }), /AgentRun accepted/);
await Promise.all([
worker.agent({ runId: "authority-gap", identity: "authority-gap:agent:v1" }),
worker.agent({ runId: "authority-gap", identity: "authority-gap:agent:v1" })
]);
await worker.trace({ runId: "authority-gap", identity: "authority-gap:trace:v1" });
await worker.diff({ runId: "authority-gap", identity: "authority-gap:diff:v1" });
await assert.rejects(worker.build({ runId: "authority-gap", identity: "authority-gap:build:v1" }), /HWPOD accepted/);
await Promise.all([
worker.build({ runId: "authority-gap", identity: "authority-gap:build:v1" }),
worker.build({ runId: "authority-gap", identity: "authority-gap:build:v1" })
]);
const result = await worker.collect({ runId: "authority-gap", identity: "authority-gap:collect:v1" });
assert.equal(agentDispatchCount, 1);
assert.equal(hwpodDispatchCount, 1);
assert.equal(agentLookupCount, 2);
assert.equal(hwpodLookupCount, 2);
assert.equal((result as any).authority.agentRun.sessionId, "ses-authoritative");
assert.equal((result as any).authority.hwpod.jobId, "job-authoritative");
assert.equal(typeof (result as any).artifactManifestSha256, "string");
assert.equal((result as any).aggregate.sha256, "a".repeat(64));
});
test("Temporal start crash gap reuses the existing workflow identity", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const temporal = new CrashGapTemporal();
const service = createHarnessRLService({ registry, temporal, cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
await assert.rejects(service.submit({ caseId: fixture.caseId, runId: "start-gap", runtimeApiUrl: "http://harnessrl.test" }), /API exited/);
const recovered = await service.submit({ caseId: fixture.caseId, runId: "start-gap", runtimeApiUrl: "http://harnessrl.test" });
assert.equal(recovered?.workflowRunId, "temporal-start-gap");
assert.equal(temporal.accepted.size, 1);
});
test("cancel converges to durable canceled through workflow signal", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const temporal = new FakeTemporal(async (workflowId) => {
const record = [...registry.runs.values()].find((item) => item.workflowId === workflowId)!;
await registry.updateRun(record.runId, { status: "canceled", stage: "canceled" });
await registry.appendEvent(record.runId, "canceled", "canceled", { source: "temporal-signal" });
});
const service = createHarnessRLService({ registry, temporal, cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
await service.submit({ caseId: fixture.caseId, runId: "cancel-run", runtimeApiUrl: "http://harnessrl.test" });
const canceled = await service.cancel("cancel-run");
assert.equal(canceled?.status, "canceled");
assert.equal(canceled?.terminal, true);
assert.deepEqual((await registry.listEvents("cancel-run")).map((event) => event.status), ["queued", "cancel_requested", "canceled"]);
});
test("read model keeps workflow identity separate from result authority", async () => {
const fixture = await softwareSmokeFixture();
const registry = new MemoryRegistry();
const service = createHarnessRLService({ registry, temporal: new FakeTemporal(), cwd: fixture.root, caseRepo: fixture.root, stateRoot: fixture.stateRoot });
await service.submit({ caseId: fixture.caseId, runId: "read-model", runtimeApiUrl: "http://harnessrl.test" });
await registry.updateRun("read-model", { status: "completed", stage: "completed", result: { artifactManifestSha256: "a".repeat(64), sourceAuthority: "hwpod" } });
const record = await service.getRun("read-model");
assert.equal(record.workflowId, "harnessrl-caserun-read-model");
assert.equal(record.status, "completed");
assert.equal(record.result?.sourceAuthority, "hwpod");
});
async function softwareSmokeFixture() {
const root = await mkdtemp(path.join(os.tmpdir(), "harnessrl-native-"));
const caseId = "software-smoke";
await mkdir(path.join(root, "cases", caseId), { recursive: true });
await writeFile(path.join(root, "cases", caseId, "case.json"), `${JSON.stringify({ caseId, title: "Native software smoke", mode: "software-smoke" }, null, 2)}\n`);
return { root, caseId, stateRoot: path.join(root, ".state", "harnessrl") };
}
async function hardwareFixture() {
const root = await mkdtemp(path.join(os.tmpdir(), "harnessrl-authority-"));
const caseId = "hardware-authority";
await mkdir(path.join(root, "cases", caseId), { recursive: true });
await writeFile(path.join(root, "cases", caseId, "case.json"), `${JSON.stringify({ caseId, title: "Authoritative fixture", mode: "compile-only" }, null, 2)}\n`);
return { root, caseId, stateRoot: path.join(root, ".state", "harnessrl") };
}
class FakeTemporal implements HarnessRLTemporalGateway {
starts = 0;
constructor(private readonly onCancel?: (workflowId: string) => Promise<void>) {}
async start() { this.starts += 1; return { workflowRunId: `temporal-run-${this.starts}` }; }
async requestCancel(workflowId: string) { await this.onCancel?.(workflowId); }
async close() {}
}
class CrashGapTemporal implements HarnessRLTemporalGateway {
accepted = new Map<string, string>();
crashed = false;
async start(input: { workflowId: string }) {
const workflowRunId = this.accepted.get(input.workflowId) ?? "temporal-start-gap";
this.accepted.set(input.workflowId, workflowRunId);
if (!this.crashed) { this.crashed = true; throw new Error("API exited after Temporal accepted"); }
return { workflowRunId, reused: true };
}
async requestCancel() {}
async close() {}
}
class MemoryRegistry implements HarnessRLRegistry {
runs = new Map<string, CaseRunRecord>();
events = new Map<string, CaseRunEvent[]>();
activityResults = new Map<string, ActivityResult>();
async ensureSchema() {}
async createRun(input: StartCaseRunInput) {
const existing = this.runs.get(input.runId);
if (existing) return structuredClone(existing);
const now = new Date().toISOString();
const record: CaseRunRecord = { ...input, workflowRunId: null, status: "queued", stage: "queued", terminal: false, result: null, error: null, blocker: null, createdAt: now, updatedAt: now };
this.runs.set(input.runId, record);
return structuredClone(record);
}
async getRun(runId: string) { const value = this.runs.get(runId); return value ? structuredClone(value) : null; }
async listRuns(input: { limit: number; status?: CaseRunStatus; before?: { createdAt: string; runId: string } }) {
return [...this.runs.values()]
.filter((record) => !input.status || record.status === input.status)
.filter((record) => !input.before || record.createdAt < input.before.createdAt || (record.createdAt === input.before.createdAt && record.runId < input.before.runId))
.sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.runId.localeCompare(left.runId))
.slice(0, input.limit)
.map((record) => structuredClone(record));
}
async updateRun(runId: string, patch: any) {
const current = this.runs.get(runId);
if (!current) throw new Error("missing run");
const next = { ...current, ...patch, terminal: terminalStatus((patch.status ?? current.status) as CaseRunStatus), updatedAt: new Date().toISOString() };
this.runs.set(runId, next);
return structuredClone(next);
}
async appendEvent(runId: string, status: CaseRunStatus, stage: string, payload: Record<string, unknown> = {}) {
const events = this.events.get(runId) ?? [];
const event = { id: events.length + 1, runId, status, stage, payload, createdAt: new Date().toISOString() };
events.push(event); this.events.set(runId, events); return structuredClone(event);
}
async listEvents(runId: string) { return structuredClone(this.events.get(runId) ?? []); }
async getActivityResult(runId: string, activityName: string, identity: string) { return structuredClone(this.activityResults.get(`${runId}:${activityName}:${identity}`) ?? null); }
async saveActivityResult(result: ActivityResult) { const key = `${result.runId}:${result.activityName}:${result.identity}`; this.activityResults.set(key, structuredClone(result)); return structuredClone(this.activityResults.get(key)!); }
async health() { return { ok: true as const }; }
async close() {}
}