feat: add CaseRun local and API transports

This commit is contained in:
root
2026-07-17 05:51:52 +02:00
parent 32be75b48d
commit bc5cd8d4cc
8 changed files with 234 additions and 173 deletions
+3 -6
View File
@@ -1,3 +1,5 @@
import { caseRunAccepted } from "./transport.ts";
export function createHarnessRLHttpApp(options: { service: ReturnType<typeof import("./service.ts")["createHarnessRLService"]> }) {
return {
async fetch(request: Request) {
@@ -13,7 +15,7 @@ export function createHarnessRLHttpApp(options: { service: ReturnType<typeof imp
if (url.pathname === "/v1/caserun/runs" && request.method === "POST") {
const body = await bodyObject(request);
const record = await options.service.submit({ caseId: String(body.caseId ?? ""), runId: body.runId ? String(body.runId) : undefined, runtimeApiUrl: runtimeApiUrl(request) });
return json(202, accepted(record));
return json(202, caseRunAccepted(record));
}
const match = /^\/v1\/caserun\/runs\/([^/]+)(?:\/(events|aggregate|cancel))?$/u.exec(url.pathname);
if (match) {
@@ -36,11 +38,6 @@ export function createHarnessRLHttpApp(options: { service: ReturnType<typeof imp
};
}
function accepted(record: any) {
return { ok: true, contractVersion: "hwlab-caserun-web-v1", runId: record.runId, caseId: record.caseId, status: record.status, stage: record.stage,
statusUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}`, eventsUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/events`,
aggregateUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/aggregate`, cancelUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/cancel` };
}
async function bodyObject(request: Request) { const body = await request.json().catch(() => null); if (!body || typeof body !== "object" || Array.isArray(body)) throw Object.assign(new Error("JSON object body is required"), { code: "invalid_json" }); return body as Record<string, unknown>; }
function runtimeApiUrl(request: Request) { const url = new URL(request.url); return `${url.protocol}//${url.host}`; }
function errorBody(code: string, message: string, details?: unknown) { return { ok: false, error: { code, message, details } }; }
+117
View File
@@ -0,0 +1,117 @@
import type { CaseRunRecord } from "./contracts.ts";
export type CaseRunCommand =
| { kind: "start"; caseId: string; runId?: string }
| { kind: "status"; runId: string }
| { kind: "events"; runId: string }
| { kind: "aggregate"; runId: string }
| { kind: "cancel"; runId: string };
export type HarnessRLService = {
submit(input: { caseId: string; runId?: string; runtimeApiUrl: string }): Promise<CaseRunRecord | null>;
getRun(runId: string): Promise<CaseRunRecord>;
events(runId: string): Promise<Record<string, unknown>>;
aggregate(runId: string): Promise<Record<string, unknown>>;
cancel(runId: string): Promise<CaseRunRecord | null>;
};
export type CaseRunTransport = {
mode: "local" | "api";
execute(command: CaseRunCommand): Promise<Record<string, unknown>>;
close(): Promise<void>;
};
export class CaseRunTransportError extends Error {
code: string;
details?: unknown;
constructor(code: string, message: string, details?: unknown) {
super(message);
this.name = "CaseRunTransportError";
this.code = code;
this.details = details;
}
}
export function createLocalCaseRunTransport(options: { service: HarnessRLService; close?: () => Promise<void> }): CaseRunTransport {
return {
mode: "local",
async execute(command) {
try {
if (command.kind === "start") return caseRunAccepted(await options.service.submit({ caseId: command.caseId, runId: command.runId, runtimeApiUrl: "local://harnessrl" }));
if (command.kind === "status") return options.service.getRun(command.runId);
if (command.kind === "events") return options.service.events(command.runId);
if (command.kind === "aggregate") return options.service.aggregate(command.runId);
return acceptedRecord(await options.service.cancel(command.runId));
} catch (error) {
throw typedError(error, "harnessrl_error");
}
},
close: options.close ?? (async () => {})
};
}
export function createApiCaseRunTransport(options: { baseUrl: string; fetchImpl?: typeof fetch; apiKey?: string }): CaseRunTransport {
const baseUrl = apiOrigin(options.baseUrl);
const fetchImpl = options.fetchImpl ?? fetch;
return {
mode: "api",
async execute(command) {
const runPath = command.kind === "start" ? "/v1/caserun/runs" : `/v1/caserun/runs/${encodeURIComponent(command.runId)}${command.kind === "status" ? "" : `/${command.kind}`}`;
const response = await fetchImpl(`${baseUrl}${runPath}`, {
method: command.kind === "start" || command.kind === "cancel" ? "POST" : "GET",
headers: {
...(command.kind === "start" ? { "content-type": "application/json" } : {}),
...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {})
},
body: command.kind === "start" ? JSON.stringify({ caseId: command.caseId, ...(command.runId ? { runId: command.runId } : {}) }) : undefined
}).catch((error) => { throw typedError(error, "caserun_api_unavailable"); });
const body = await response.json().catch(() => null) as any;
if (!response.ok || !body || typeof body !== "object") {
throw new CaseRunTransportError(body?.error?.code ?? "caserun_api_error", body?.error?.message ?? `CaseRun API returned HTTP ${response.status}`, body?.error?.details ?? { status: response.status });
}
return body;
},
close: async () => {}
};
}
export function caseRunAccepted(record: CaseRunRecord | null) {
if (!record) throw new CaseRunTransportError("caserun_start_failed", "CaseRun start did not return a run record");
return {
ok: true,
contractVersion: "hwlab-caserun-web-v1",
runId: record.runId,
caseId: record.caseId,
workflowId: record.workflowId,
workflowRunId: record.workflowRunId,
status: record.status,
stage: record.stage,
statusUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}`,
eventsUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/events`,
aggregateUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/aggregate`,
cancelUrl: `/v1/caserun/runs/${encodeURIComponent(record.runId)}/cancel`
};
}
function acceptedRecord(record: CaseRunRecord | null) {
if (!record) throw new CaseRunTransportError("caserun_cancel_failed", "CaseRun cancel did not return a run record");
return record;
}
function apiOrigin(value: string) {
const text = String(value ?? "").trim();
if (!text) throw new CaseRunTransportError("caserun_base_url_required", "--base-url <origin> is required with --over-api");
try {
const url = new URL(text);
if ((url.protocol !== "http:" && url.protocol !== "https:") || url.pathname !== "/" || url.search || url.hash) throw new Error("origin required");
return url.origin;
} catch {
throw new CaseRunTransportError("invalid_caserun_base_url", "--base-url must be an absolute HTTP(S) origin", { baseUrl: text });
}
}
function typedError(error: any, fallbackCode: string) {
if (error instanceof CaseRunTransportError) return error;
return new CaseRunTransportError(error?.code ?? fallbackCode, error?.message ?? String(error), error?.details);
}