fix(caserun): 纠正 HWPOD 失败事实投影

This commit is contained in:
root
2026-07-16 21:33:39 +02:00
parent 574125afbb
commit 2d72128223
4 changed files with 58 additions and 8 deletions
+4 -2
View File
@@ -11,17 +11,19 @@ test("CaseRun internal requests reuse the existing bootstrap admin API key only
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") });
calls.push({ url: input instanceof Request ? input.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" });
await internalFetch(new Request("http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", { headers: { authorization: "Bearer caller-token" } }), { 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 }
{ url: "https://lab-dev.hwpod.com/v1/hwpod-node-ops", authorization: null },
{ url: "http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops", authorization: "Bearer caller-token" }
]);
});
+2 -1
View File
@@ -223,7 +223,8 @@ export function caseRunInternalFetch(fetchImpl: typeof fetch, internalApiUrl: st
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);
const headers = new Headers(input instanceof Request ? input.headers : undefined);
new Headers(init.headers).forEach((value, name) => headers.set(name, value));
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${apiKey}`);
return fetchImpl(input, { ...init, headers });
};
+24 -1
View File
@@ -32,11 +32,12 @@ test("subject worktree failures preserve common Windows operation identity witho
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" }
blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "node unavailable token=hwl_live_should_not_leak", details: { dispatchMode: "none" } }
};
const failure = subjectWorktreeFailureDetails(body, plan, { stderr: "" });
assert.equal(failure.partialWrite, false);
assert.equal(failure.dispatched, 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));
@@ -44,6 +45,28 @@ test("subject worktree failures preserve common Windows operation identity witho
}
});
test("subject worktree failure facts remain null unless dispatch or writes are authoritative", () => {
const plan = {
contractVersion: "hwpod-node-ops-v1",
planId: "case_subject_facts",
hwpodId: "d601-f103-v2",
nodeId: "node-d601-f103-v2",
intent: "cmd.run",
ops: [{ opId: "op_02_subject_worktree_add", op: "cmd.run", args: {} }]
};
const unknown = subjectWorktreeFailureDetails({ status: "blocked", results: [{ opId: "op_02_subject_worktree_add", ok: false, status: "blocked" }] }, plan, { stderr: "blocked" });
assert.equal(unknown.dispatched, null);
assert.equal(unknown.partialWrite, null);
const dispatched = subjectWorktreeFailureDetails({ status: "failed", blocker: { details: { dispatchMode: "websocket" } }, results: [{ opId: "op_02_subject_worktree_add", ok: false, status: "failed" }] }, plan, { stderr: "failed" });
assert.equal(dispatched.dispatched, true);
assert.equal(dispatched.partialWrite, null);
const written = subjectWorktreeFailureDetails({ status: "failed", dispatched: true, results: [{ opId: "op_02_subject_worktree_add", ok: true, status: "completed" }] }, plan, { stderr: "later operation failed" });
assert.equal(written.dispatched, true);
assert.equal(written.partialWrite, true);
});
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 {
+28 -4
View File
@@ -120,7 +120,7 @@ export async function requestSubjectWorktree(context: CaseContext, input: { case
const status = subjectWorktreeStatus(response.status, body, { commitId: subject.commitId, worktreePath });
if (!status.ok) {
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 }));
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: failure.dispatched, 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, operation: subjectWorktreeOperationIdentity(body, plan), stdout: clipText(status.stdout), stderr: clipText(status.stderr), keilSidecars: keilSidecarSummary(status.stdout) });
}
@@ -140,7 +140,10 @@ export function subjectWorktreeStatus(httpStatus: number, body: any, expected: {
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 dispatched = subjectWorktreeBooleanFact(body, results, "dispatched") ?? subjectWorktreeDispatchedFromMode(body, results);
const explicitPartialWrite = subjectWorktreeBooleanFact(body, results, "partialWrite");
const successfulMutatingResult = results.some((result: any) => mutatingOpIds.has(text(result?.opId)) && result?.ok === true);
const partialWrite = explicitPartialWrite ?? (dispatched === false ? false : successfulMutatingResult ? 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({
@@ -148,9 +151,30 @@ export function subjectWorktreeFailureDetails(body: any, plan: any, status: { st
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 })
details: clean({ nodeId: plan.nodeId, planId: plan.planId, hwpodId: plan.hwpodId, partialWrite, dispatched })
});
return { partialWrite, errorSummary, blocker, operation: subjectWorktreeOperationIdentity(body, plan) };
return { partialWrite, dispatched, errorSummary, blocker, operation: subjectWorktreeOperationIdentity(body, plan) };
}
function subjectWorktreeBooleanFact(body: any, results: any[], field: "dispatched" | "partialWrite") {
const containers = [body, body?.body];
const values = [
...containers.flatMap((container) => [container?.[field], container?.blocker?.details?.[field], container?.diagnostic?.details?.[field]]),
...results.flatMap((result: any) => [result?.[field], result?.blocker?.details?.[field], result?.blocker?.diagnostic?.details?.[field]])
];
return values.find((value) => typeof value === "boolean") ?? null;
}
function subjectWorktreeDispatchedFromMode(body: any, results: any[]) {
const containers = [body, body?.body];
const modes = [
...containers.flatMap((container) => [container?.dispatchMode, container?.blocker?.details?.dispatchMode, container?.diagnostic?.details?.dispatchMode]),
...results.flatMap((result: any) => [result?.dispatchMode, result?.blocker?.details?.dispatchMode, result?.blocker?.diagnostic?.details?.dispatchMode])
].map(text).filter(Boolean);
const dispatchMode = modes[0];
if (dispatchMode === "none") return false;
if (["websocket", "direct-url", "handler", "handler-exception"].includes(dispatchMode)) return true;
return null;
}
function subjectWorktreeOperationIdentity(body: any, plan: any) {