fix(caserun): 纠正 HWPOD 失败事实投影
This commit is contained in:
@@ -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" }
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user