FN-5768: add dual-observe workflow parity instrumentation
Add default-off workflow interpreter dual-observe parity instrumentation across core and engine. - add workflow parity comparison primitives and exports in @fusion/core - add engine parity observer seam with experimental flag and public exports - add regression coverage for parity contracts and dual-observe reliability behavior - document the dual-observe parity mode, events, and contract in workflow docs - add a patch changeset for @runfusion/fusion release notes Files changed: .changeset/fn-5768-workflow-parity-observer.md | 9 + docs/workflow-steps.md | 18 ++ .../core/src/__tests__/workflow-parity.test.ts | 143 ++++++++++++++ packages/core/src/index.ts | 18 ++ packages/core/src/workflow-parity.ts | 205 +++++++++++++++++++++ .../workflow-interpreter-dual-observe.test.ts | 144 +++++++++++++++ packages/engine/src/index.ts | 7 + packages/engine/src/workflow-parity-observer.ts | 116 ++++++++++++ 8 files changed, 660 insertions(+) Fusion-Task-Id: FN-5768 Fusion-Task-Lineage: fb4f8111-f48d-482c-b1d9-9106c43c829a
This commit is contained in:
143
packages/core/src/__tests__/workflow-parity.test.ts
Normal file
143
packages/core/src/__tests__/workflow-parity.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
compareWorkflowRunAudits,
|
||||
compareWorkflowRunObservations,
|
||||
type RunAuditEvent,
|
||||
type WorkflowRunObservation,
|
||||
} from "../index.js";
|
||||
|
||||
function observation(overrides: Partial<WorkflowRunObservation> = {}): WorkflowRunObservation {
|
||||
return {
|
||||
stageTransitions: ["triage", "execute", "review", "merge"],
|
||||
terminalColumn: "done",
|
||||
terminalStatus: null,
|
||||
reviewVerdict: "APPROVE",
|
||||
mergeOutcome: "merged",
|
||||
invariants: {
|
||||
fileScopeGuardOutcome: "pass",
|
||||
squashMergeContractOutcome: "pass",
|
||||
autoMergeTerminalUntilMergedRespected: true,
|
||||
moveTaskHardCancelRespected: true,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function auditEvent(mutationType: string, target: string, phase: string): RunAuditEvent {
|
||||
return {
|
||||
id: `${mutationType}-${target}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId: "FN-1",
|
||||
agentId: "executor",
|
||||
runId: "run-1",
|
||||
domain: "database",
|
||||
mutationType,
|
||||
target,
|
||||
metadata: { phase },
|
||||
};
|
||||
}
|
||||
|
||||
describe("workflow parity", () => {
|
||||
it("agrees for identical observations", () => {
|
||||
const report = compareWorkflowRunObservations(observation(), observation());
|
||||
expect(report).toEqual({ agree: true, diffs: [] });
|
||||
});
|
||||
|
||||
it("reports lifecycle transition drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({ stageTransitions: ["triage", "execute", "merge"] }),
|
||||
);
|
||||
expect(report.agree).toBe(false);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ field: "stageTransitions", category: "lifecycle" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports terminal status and review verdict drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({ terminalColumn: "in-review", reviewVerdict: "REVISE" }),
|
||||
);
|
||||
expect(report.agree).toBe(false);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "terminalColumn" }),
|
||||
expect.objectContaining({ field: "reviewVerdict" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports file-scope guard invariant drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({ invariants: { ...observation().invariants, fileScopeGuardOutcome: "fail" } }),
|
||||
);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "invariants.fileScopeGuardOutcome", category: "invariant" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports squash merge invariant drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({ invariants: { ...observation().invariants, squashMergeContractOutcome: "blocked" } }),
|
||||
);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "invariants.squashMergeContractOutcome", category: "invariant" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports auto-merge terminal invariant drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({
|
||||
invariants: { ...observation().invariants, autoMergeTerminalUntilMergedRespected: false },
|
||||
}),
|
||||
);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "invariants.autoMergeTerminalUntilMergedRespected", category: "invariant" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports moveTask hard-cancel invariant drift", () => {
|
||||
const report = compareWorkflowRunObservations(
|
||||
observation(),
|
||||
observation({ invariants: { ...observation().invariants, moveTaskHardCancelRespected: false } }),
|
||||
);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "invariants.moveTaskHardCancelRespected", category: "invariant" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("agrees on identical comparable run-audit slices", () => {
|
||||
const events = [
|
||||
auditEvent("task:move", "FN-1", "execute"),
|
||||
auditEvent("task:update", "FN-1", "review"),
|
||||
];
|
||||
const report = compareWorkflowRunAudits(events, events);
|
||||
expect(report).toEqual({ agree: true, diffs: [] });
|
||||
});
|
||||
|
||||
it("reports run-audit drift", () => {
|
||||
const legacy = [auditEvent("task:move", "FN-1", "execute")];
|
||||
const interpreter = [auditEvent("task:update", "FN-2", "review")];
|
||||
const report = compareWorkflowRunAudits(legacy, interpreter);
|
||||
expect(report.agree).toBe(false);
|
||||
expect(report.diffs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ field: "audit[0].mutationType", category: "audit" }),
|
||||
expect.objectContaining({ field: "audit[0].target", category: "audit" }),
|
||||
expect.objectContaining({ field: "audit[0].phase", category: "audit" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user