diff --git a/packages/rpa-core/src/engine/runtime.ts b/packages/rpa-core/src/engine/runtime.ts index b3dfd5d..10c3763 100644 --- a/packages/rpa-core/src/engine/runtime.ts +++ b/packages/rpa-core/src/engine/runtime.ts @@ -255,14 +255,14 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map { expect(first.idempotencyKey).toBe("run_cp:first"); // 双端透明调度(ADR-R6 补记):本地执行驻地必须随事件上报,供审计区分 expect(first.executor).toBe("local"); + // attempt 审计字段(无人值守审批对账:actionId=runId:stepId:attempt) + expect(first.attempt).toBe(1); const second = finished.find((e) => e.nodeId === "second")!; expect(second.checkpointId).toBe("second"); @@ -111,4 +113,30 @@ describe("A++ 检查点事件负载", () => { expect(await runOnce("run_a")).toBe("run_a:only"); expect(await runOnce("run_b")).toBe("run_b:only"); }); + + it("attempt 字段全事件覆盖:失败/跳过/取消/成功都带尝试序号,重试节点终态 attempt 反映实际次数", async () => { + let calls = 0; + register("Cp_Flaky", async () => { + calls += 1; + if (calls < 3) throw new Error("transient"); + return "ok"; + }, { idempotent: true }); + const events: EmittedEvent[] = []; + const flaky = { ...node("flaky", "Cp_Flaky"), options: { retryCount: 2, retryIntervalMs: 1 } }; + const flow = makeFlow({ + nodes: [flaky, { ...node("skip", "Cp_Idempotent"), enabled: false }], + edges: [{ id: "e1", source: "flaky", target: "skip" }] + }); + await runFlow(flow, { ...baseOptions((event) => events.push(event)) }); + + const byNode = (id: string) => events.filter((e) => e.type === "node.finished" && e.nodeId === id); + const flakyFinished = byNode("flaky"); + expect(flakyFinished).toHaveLength(1); + expect(flakyFinished[0].status).toBe("succeeded"); + expect(flakyFinished[0].attempt).toBe(3); // 第 3 次尝试成功 + expect(byNode("skip")[0].attempt).toBe(1); // 跳过节点也是 1 + for (const event of events) { + if (event.type === "node.finished") expect(event.attempt).toBeGreaterThanOrEqual(1); + } + }, 10_000); });