126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { TaskDetail, WorkflowIr, WorkflowIrNode } from "@fusion/core";
|
|
|
|
import { WorkflowGraphExecutor } from "../workflow-graph-executor.js";
|
|
import {
|
|
WorkflowNodeRunnerRegistry,
|
|
handlerBackedRunner,
|
|
type WorkflowNodeRunner,
|
|
} from "../workflow-node-runner.js";
|
|
import { createMergeAttemptHandler } from "../workflow-node-runners/merge-runner.js";
|
|
|
|
const task = { id: "FN-7300" } as TaskDetail;
|
|
|
|
function settingsOn() {
|
|
return { experimentalFeatures: { workflowGraphExecutor: true } };
|
|
}
|
|
|
|
function linearIr(node: WorkflowIrNode): WorkflowIr {
|
|
return {
|
|
version: "v1",
|
|
name: "runner-linear",
|
|
nodes: [
|
|
{ id: "start", kind: "start" },
|
|
node,
|
|
{ id: "end", kind: "end" },
|
|
],
|
|
edges: [
|
|
{ from: "start", to: node.id },
|
|
{ from: node.id, to: "end", condition: "success" },
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("WorkflowNodeRunnerRegistry", () => {
|
|
it("adapts a registered runner into graph execution", async () => {
|
|
const run = vi.fn(async () => ({ outcome: "success" as const, value: "sent" }));
|
|
const registry = new WorkflowNodeRunnerRegistry({
|
|
runners: [{
|
|
kind: "notify",
|
|
run,
|
|
} satisfies WorkflowNodeRunner],
|
|
});
|
|
|
|
const notifyNode: WorkflowIrNode = { id: "notify-team", kind: "notify" };
|
|
const executor = new WorkflowGraphExecutor({ runnerRegistry: registry });
|
|
const result = await executor.run(task, settingsOn(), linearIr(notifyNode));
|
|
|
|
expect(result.outcome).toBe("success");
|
|
expect(run).toHaveBeenCalledWith(
|
|
notifyNode,
|
|
expect.objectContaining({
|
|
task,
|
|
context: expect.any(Object),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("lets explicit handler overrides win over registered runners", async () => {
|
|
const runner = vi.fn(async () => ({ outcome: "success" as const, value: "runner" }));
|
|
const handler = vi.fn(async () => ({ outcome: "failure" as const, value: "handler" }));
|
|
const registry = new WorkflowNodeRunnerRegistry({
|
|
runners: [{
|
|
kind: "prompt",
|
|
run: runner,
|
|
} satisfies WorkflowNodeRunner],
|
|
});
|
|
|
|
const executor = new WorkflowGraphExecutor({
|
|
runnerRegistry: registry,
|
|
handlers: { prompt: handler },
|
|
});
|
|
const result = await executor.run(task, settingsOn(), linearIr({ id: "p", kind: "prompt" }));
|
|
|
|
expect(result.outcome).toBe("failure");
|
|
expect(result.visitedNodeIds).toEqual(["start", "p"]);
|
|
expect(handler).toHaveBeenCalledOnce();
|
|
expect(runner).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("preserves default fail-closed behavior when no runner dependency is registered", async () => {
|
|
const registry = new WorkflowNodeRunnerRegistry();
|
|
const executor = new WorkflowGraphExecutor({ runnerRegistry: registry });
|
|
const result = await executor.run(task, settingsOn(), linearIr({ id: "parse", kind: "parse-steps" }));
|
|
|
|
expect(result).toMatchObject({
|
|
outcome: "failure",
|
|
visitedNodeIds: ["start", "parse"],
|
|
});
|
|
expect(result.context["node:parse:value"]).toBe("parse-steps-unwired");
|
|
});
|
|
|
|
it("adapts existing handlers into runners for migration compatibility", async () => {
|
|
const handler = vi.fn(async () => ({ outcome: "success" as const, value: "handler-backed" }));
|
|
const registry = new WorkflowNodeRunnerRegistry({
|
|
runners: [handlerBackedRunner("script", handler)],
|
|
});
|
|
const scriptNode: WorkflowIrNode = { id: "script", kind: "script" };
|
|
|
|
const executor = new WorkflowGraphExecutor({ runnerRegistry: registry });
|
|
const result = await executor.run(task, settingsOn(), linearIr(scriptNode));
|
|
|
|
expect(result.outcome).toBe("success");
|
|
expect(result.context["node:script:value"]).toBe("handler-backed");
|
|
expect(handler).toHaveBeenCalledWith(scriptNode, expect.objectContaining({ task }));
|
|
});
|
|
|
|
it("delegates merge-attempt to the legacy merge seam when primitives are unwired", async () => {
|
|
const merge = vi.fn(async () => ({ outcome: "success" as const, value: "legacy-merged" }));
|
|
const handler = createMergeAttemptHandler({
|
|
seams: { merge },
|
|
buildPrimitiveContext: vi.fn(),
|
|
});
|
|
const node: WorkflowIrNode = { id: "merge-attempt", kind: "merge-attempt" };
|
|
const context = { branch: "main" };
|
|
|
|
const result = await handler(node, {
|
|
task,
|
|
settings: settingsOn(),
|
|
context,
|
|
});
|
|
|
|
expect(result).toEqual({ outcome: "success", value: "legacy-merged" });
|
|
expect(merge).toHaveBeenCalledWith(task, context);
|
|
});
|
|
});
|