fix: authenticate CaseRun HWPOD prepare
This commit is contained in:
@@ -5,6 +5,25 @@ import path from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { createCloudApiServer } from "./server.ts";
|
||||
import { caseRunInternalFetch } from "./server-caserun-http.ts";
|
||||
|
||||
test("CaseRun internal requests reuse the existing bootstrap admin API key only for the internal origin", async () => {
|
||||
const calls: Array<{ url: string; authorization: string | null }> = [];
|
||||
const fetchImpl = async (input: URL | RequestInfo, init: RequestInit = {}) => {
|
||||
const headers = new Headers(init.headers);
|
||||
calls.push({ url: String(input), authorization: headers.get("authorization") });
|
||||
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
|
||||
};
|
||||
const internalFetch = caseRunInternalFetch(fetchImpl as typeof fetch, "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667", { HWLAB_BOOTSTRAP_ADMIN_API_KEY: "hwl_live_test" });
|
||||
|
||||
await internalFetch("http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", { method: "POST" });
|
||||
await internalFetch("https://lab-dev.hwpod.com/v1/hwpod-node-ops", { method: "POST" });
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{ url: "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", authorization: "Bearer hwl_live_test" },
|
||||
{ url: "https://lab-dev.hwpod.com/v1/hwpod-node-ops", authorization: null }
|
||||
]);
|
||||
});
|
||||
|
||||
test("cloud-api exposes web CaseRun cases, run status, events and aggregate", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-caserun-api-"));
|
||||
|
||||
@@ -184,6 +184,7 @@ async function runSoftwareSmokeCase(context: any, emit: (stage: string, payload?
|
||||
|
||||
function caseContextForRecord(record: any, input: any) {
|
||||
const internalApiUrl = internalRuntimeApiUrlForCaseRun(record, input.env ?? {});
|
||||
const fetchImpl = caseRunInternalFetch(input.fetchImpl ?? globalThis.fetch, internalApiUrl, input.env ?? {});
|
||||
const parsed = {
|
||||
_: [],
|
||||
caseId: record.caseId,
|
||||
@@ -204,7 +205,7 @@ function caseContextForRecord(record: any, input: any) {
|
||||
HWLAB_CASERUN_PUBLIC_RUNTIME_API_URL: record.runtimeApiUrl,
|
||||
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1"
|
||||
},
|
||||
fetchImpl: input.fetchImpl ?? globalThis.fetch,
|
||||
fetchImpl,
|
||||
cwd: input.cwd,
|
||||
now: () => new Date().toISOString(),
|
||||
sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
@@ -215,6 +216,19 @@ function caseContextForRecord(record: any, input: any) {
|
||||
};
|
||||
}
|
||||
|
||||
export function caseRunInternalFetch(fetchImpl: typeof fetch, internalApiUrl: string, env: Record<string, string | undefined>) {
|
||||
const apiKey = String(env.HWLAB_BOOTSTRAP_ADMIN_API_KEY ?? "").trim();
|
||||
if (!apiKey) return fetchImpl;
|
||||
const internalOrigin = new URL(internalApiUrl).origin;
|
||||
return async (input: URL | RequestInfo, init: RequestInit = {}) => {
|
||||
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
||||
if (target.origin !== internalOrigin) return fetchImpl(input, init);
|
||||
const headers = new Headers(init.headers);
|
||||
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${apiKey}`);
|
||||
return fetchImpl(input, { ...init, headers });
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCwd(env: Record<string, string | undefined>) {
|
||||
return path.resolve(String(env.HWLAB_CASERUN_REPO_ROOT ?? process.cwd()));
|
||||
}
|
||||
|
||||
@@ -5,11 +5,45 @@ import path from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { prepareCaseRun } from "./src/hwlab-caserun-lib.ts";
|
||||
import { subjectWorktreeFailureDetails } from "./src/hwlab-caserun-subject.ts";
|
||||
|
||||
const NOW = "2026-06-06T00:00:00.000Z";
|
||||
const SUBJECT_COMMIT = "df7a4e6e551fa90d64bde5537cc000f89d63dd20";
|
||||
const TEST_RUNTIME_API_URL = "http://api.test";
|
||||
|
||||
test("subject worktree failures preserve common Windows operation identity without run-specific constants", () => {
|
||||
for (const repoLocalPath of ["F:\\Work\\HWLAB-CASE-F103", "F:\\Work\\ConStart"]) {
|
||||
const plan = {
|
||||
contractVersion: "hwpod-node-ops-v1",
|
||||
planId: "case_subject_test",
|
||||
hwpodId: "d601-f103-v2",
|
||||
nodeId: "node-d601-f103-v2",
|
||||
intent: "cmd.run",
|
||||
ops: [
|
||||
{ opId: "op_01_subject_commit", op: "cmd.run", args: { workspacePath: repoLocalPath } },
|
||||
{ opId: "op_02_subject_worktree_add", op: "cmd.run", args: { workspacePath: repoLocalPath } },
|
||||
{ opId: "op_03_subject_worktree_head", op: "cmd.run", args: { workspacePath: repoLocalPath } }
|
||||
]
|
||||
};
|
||||
const body = {
|
||||
contractVersion: "hwpod-node-ops-v1",
|
||||
planId: plan.planId,
|
||||
hwpodId: plan.hwpodId,
|
||||
nodeId: plan.nodeId,
|
||||
status: "blocked",
|
||||
results: plan.ops.map((op) => ({ opId: op.opId, op: op.op, ok: false, status: "blocked", blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "node unavailable token=hwl_live_should_not_leak" } })),
|
||||
blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "node unavailable token=hwl_live_should_not_leak" }
|
||||
};
|
||||
|
||||
const failure = subjectWorktreeFailureDetails(body, plan, { stderr: "" });
|
||||
assert.equal(failure.partialWrite, false);
|
||||
assert.equal(failure.blocker.details.dispatched, false);
|
||||
assert.equal(failure.operation.nodeId, "node-d601-f103-v2");
|
||||
assert.deepEqual(failure.operation.ops.map((op: any) => op.opId), plan.ops.map((op) => op.opId));
|
||||
assert.doesNotMatch(failure.errorSummary, /hwl_live_should_not_leak/u);
|
||||
}
|
||||
});
|
||||
|
||||
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-"));
|
||||
try {
|
||||
|
||||
@@ -119,9 +119,10 @@ export async function requestSubjectWorktree(context: CaseContext, input: { case
|
||||
const body = parseJsonMaybe(textBody) ?? { raw: textBody };
|
||||
const status = subjectWorktreeStatus(response.status, body, { commitId: subject.commitId, worktreePath });
|
||||
if (!status.ok) {
|
||||
throw cliError("subject_worktree_prepare_failed", "failed to prepare isolated subject worktree on the HWPOD node", clean({ httpStatus: response.status, nodeId: document.spec.nodeBinding.nodeId, repoLocalPath: subject.repoLocalPath, commitId: subject.commitId, worktreePath, exitCode: status.exitCode, stdout: clipText(status.stdout), stderr: clipText(status.stderr), body: compactObject(body) }));
|
||||
const failure = subjectWorktreeFailureDetails(body, plan, status);
|
||||
throw cliError("subject_worktree_prepare_failed", "failed to prepare isolated subject worktree on the HWPOD node", clean({ httpStatus: response.status, nodeId: document.spec.nodeBinding.nodeId, repoLocalPath: subject.repoLocalPath, commitId: subject.commitId, worktreePath, exitCode: status.exitCode, partialWrite: failure.partialWrite, dispatched: false, operation: failure.operation, errorSummary: failure.errorSummary, blocker: failure.blocker }));
|
||||
}
|
||||
return clean({ httpStatus: response.status, planId: plan.planId, nodeId: document.spec.nodeBinding.nodeId, exitCode: status.exitCode, stdout: clipText(status.stdout), stderr: clipText(status.stderr), keilSidecars: keilSidecarSummary(status.stdout) });
|
||||
return clean({ httpStatus: response.status, planId: plan.planId, nodeId: document.spec.nodeBinding.nodeId, exitCode: status.exitCode, operation: subjectWorktreeOperationIdentity(body, plan), stdout: clipText(status.stdout), stderr: clipText(status.stderr), keilSidecars: keilSidecarSummary(status.stdout) });
|
||||
}
|
||||
|
||||
export function subjectWorktreeStatus(httpStatus: number, body: any, expected: { commitId: string; worktreePath: string }) {
|
||||
@@ -136,6 +137,48 @@ export function subjectWorktreeStatus(httpStatus: number, body: any, expected: {
|
||||
return { ok, exitCode: exitCodes.find((exitCode) => exitCode !== 0) ?? exitCodes[exitCodes.length - 1], stdout, stderr, actualHead, actualWorktreePath: expected.worktreePath };
|
||||
}
|
||||
|
||||
export function subjectWorktreeFailureDetails(body: any, plan: any, status: { stderr?: string }) {
|
||||
const results = subjectWorktreeResults(body);
|
||||
const mutatingOpIds = new Set(["op_02_subject_worktree_add", "op_04_keil_sidecars"]);
|
||||
const partialWrite = results.length > 0 ? results.some((result: any) => mutatingOpIds.has(text(result?.opId)) && result?.ok === true) : null;
|
||||
const sourceBlocker = body?.blocker ?? results.find((result: any) => result?.blocker)?.blocker ?? body?.error ?? null;
|
||||
const errorSummary = redactSubjectWorktreeError(text(sourceBlocker?.summary ?? sourceBlocker?.message ?? status.stderr) || "HWPOD subject worktree operation failed without an error summary");
|
||||
const blocker = clean({
|
||||
code: text(sourceBlocker?.code) || "subject_worktree_prepare_failed",
|
||||
layer: text(sourceBlocker?.layer) || "hwpod-node",
|
||||
retryable: typeof sourceBlocker?.retryable === "boolean" ? sourceBlocker.retryable : true,
|
||||
summary: errorSummary,
|
||||
details: clean({ nodeId: plan.nodeId, planId: plan.planId, hwpodId: plan.hwpodId, partialWrite, dispatched: false })
|
||||
});
|
||||
return { partialWrite, errorSummary, blocker, operation: subjectWorktreeOperationIdentity(body, plan) };
|
||||
}
|
||||
|
||||
function subjectWorktreeOperationIdentity(body: any, plan: any) {
|
||||
const results = subjectWorktreeResults(body);
|
||||
const resultById = new Map(results.map((result: any) => [text(result?.opId), result]));
|
||||
return clean({
|
||||
contractVersion: text(body?.contractVersion) || plan.contractVersion,
|
||||
planId: text(body?.planId) || plan.planId,
|
||||
hwpodId: text(body?.hwpodId) || plan.hwpodId,
|
||||
nodeId: text(body?.nodeId) || plan.nodeId,
|
||||
intent: plan.intent,
|
||||
ops: plan.ops.map((op: any) => {
|
||||
const result: any = resultById.get(text(op.opId));
|
||||
return clean({ opId: op.opId, op: op.op, status: text(result?.status) || null, ok: typeof result?.ok === "boolean" ? result.ok : null, blockerCode: text(result?.blocker?.code) || null });
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function subjectWorktreeResults(body: any) {
|
||||
return Array.isArray(body?.body?.results) ? body.body.results : Array.isArray(body?.results) ? body.results : [];
|
||||
}
|
||||
|
||||
function redactSubjectWorktreeError(value: string) {
|
||||
return clipText(value)
|
||||
.replace(/\b(?:hwl_live|sk|ghp)_[A-Za-z0-9_-]+\b/gu, "[REDACTED]")
|
||||
.replace(/\b(?:authorization|api[_-]?key|token|secret|password)\s*[:=]\s*\S+/giu, "$1=[REDACTED]");
|
||||
}
|
||||
|
||||
export function keilSidecarCopyOp(document: any, subject: { repoLocalPath: string }, hwpodId: string, relativeWorktreePath: string) {
|
||||
const project = subjectRelativeKeilProject(document, subject.repoLocalPath);
|
||||
if (!project) return null;
|
||||
|
||||
Reference in New Issue
Block a user