mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
public commit
This commit is contained in:
114
app/__test__/flows/FetchTask.spec.ts
Normal file
114
app/__test__/flows/FetchTask.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { afterAll, beforeAll, describe, expect, jest, test } from "bun:test";
|
||||
import { FetchTask, Flow } from "../../src/flows";
|
||||
|
||||
let _oldFetch: typeof fetch;
|
||||
function mockFetch(responseMethods: Partial<Response>) {
|
||||
_oldFetch = global.fetch;
|
||||
// @ts-ignore
|
||||
global.fetch = jest.fn(() => Promise.resolve(responseMethods));
|
||||
}
|
||||
|
||||
function mockFetch2(newFetch: (input: RequestInfo, init: RequestInit) => Promise<Response>) {
|
||||
_oldFetch = global.fetch;
|
||||
// @ts-ignore
|
||||
global.fetch = jest.fn(newFetch);
|
||||
}
|
||||
|
||||
function unmockFetch() {
|
||||
global.fetch = _oldFetch;
|
||||
}
|
||||
|
||||
beforeAll(() =>
|
||||
/*mockFetch({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve({ todos: [1, 2] })
|
||||
})*/
|
||||
mockFetch2(async (input, init) => {
|
||||
const request = {
|
||||
url: String(input),
|
||||
method: init?.method ?? "GET",
|
||||
// @ts-ignore
|
||||
headers: Object.fromEntries(init?.headers?.entries() ?? []),
|
||||
body: init?.body
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ todos: [1, 2], request }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
})
|
||||
);
|
||||
afterAll(unmockFetch);
|
||||
|
||||
describe("FetchTask", async () => {
|
||||
test("Simple fetch", async () => {
|
||||
const task = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1",
|
||||
method: "GET",
|
||||
headers: [{ key: "Content-Type", value: "application/json" }]
|
||||
});
|
||||
|
||||
const result = await task.run();
|
||||
//console.log("result", result);
|
||||
expect(result.output!.todos).toEqual([1, 2]);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test("verify config", async () => {
|
||||
// // @ts-expect-error
|
||||
expect(() => new FetchTask("", {})).toThrow();
|
||||
|
||||
expect(
|
||||
// // @ts-expect-error
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
|
||||
).toThrow();
|
||||
|
||||
expect(
|
||||
new FetchTask("", {
|
||||
url: "https://jsonplaceholder.typicode.com",
|
||||
method: "invalid"
|
||||
}).execute()
|
||||
).rejects.toThrow(/^Invalid method/);
|
||||
|
||||
expect(
|
||||
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
|
||||
).toBeDefined();
|
||||
|
||||
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
|
||||
});
|
||||
|
||||
test("template", async () => {
|
||||
const task = new FetchTask("fetch", {
|
||||
url: "https://example.com/?email={{ flow.output.email }}",
|
||||
method: "{{ flow.output.method }}",
|
||||
headers: [
|
||||
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
|
||||
],
|
||||
body: JSON.stringify({
|
||||
email: "{{ flow.output.email }}"
|
||||
})
|
||||
});
|
||||
const inputs = {
|
||||
headerKey: "Type",
|
||||
apiKey: 123,
|
||||
email: "what@else.com",
|
||||
method: "PATCH"
|
||||
};
|
||||
|
||||
const flow = new Flow("", [task]);
|
||||
const exec = await flow.start(inputs);
|
||||
console.log("errors", exec.getErrors());
|
||||
expect(exec.hasErrors()).toBe(false);
|
||||
|
||||
const { request } = exec.getResponse();
|
||||
|
||||
expect(request.url).toBe(`https://example.com/?email=${inputs.email}`);
|
||||
expect(request.method).toBe(inputs.method);
|
||||
expect(request.headers["content-type"]).toBe("application/json");
|
||||
expect(request.headers.authorization).toBe(`Bearer ${inputs.apiKey}`);
|
||||
expect(request.body).toBe(JSON.stringify({ email: inputs.email }));
|
||||
});
|
||||
});
|
||||
91
app/__test__/flows/SubWorkflowTask.spec.ts
Normal file
91
app/__test__/flows/SubWorkflowTask.spec.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
|
||||
|
||||
describe("SubFlowTask", async () => {
|
||||
test("Simple Subflow", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "subflow"
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: "Subflow output: {{ sub.output }}"
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.task(task2).asInputFor(task3);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: subflow");
|
||||
});
|
||||
|
||||
test("Simple loop", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
});
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: [1, 2, 3]
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.task(task2).asInputFor(task3);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start();
|
||||
|
||||
console.log("errors", execution.getErrors());
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3");
|
||||
});
|
||||
|
||||
test("Simple loop from flow input", async () => {
|
||||
const subTask = new RenderTask("render", {
|
||||
render: "run {{ flow.output }}"
|
||||
});
|
||||
|
||||
const subflow = new Flow("subflow", [subTask]);
|
||||
|
||||
const task = new LogTask("log");
|
||||
const task2 = new SubFlowTask("sub", {
|
||||
flow: subflow,
|
||||
loop: true,
|
||||
input: "{{ flow.output | json }}"
|
||||
});
|
||||
const task3 = new RenderTask("render2", {
|
||||
render: `Subflow output: {{ sub.output | join: ", " }}`
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2, task3], []);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.task(task2).asInputFor(task3);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
await execution.start([4, 5, 6]);
|
||||
|
||||
/*console.log(execution.logs);
|
||||
console.log(execution.getResponse());*/
|
||||
|
||||
expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6");
|
||||
});
|
||||
});
|
||||
112
app/__test__/flows/Task.spec.ts
Normal file
112
app/__test__/flows/Task.spec.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Type } from "../../src/core/utils";
|
||||
import { Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
describe("Task", async () => {
|
||||
test("resolveParams: template with parse", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Number()) }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toBe(1);
|
||||
});
|
||||
|
||||
test("resolveParams: with string", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: Type.String() }),
|
||||
{
|
||||
test: "{{ some.path }}"
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toBe("1/1");
|
||||
});
|
||||
|
||||
test("resolveParams: with object", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
|
||||
{
|
||||
test: { key: "path", value: "{{ some.path }}" }
|
||||
},
|
||||
{
|
||||
some: {
|
||||
path: "1/1"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
});
|
||||
|
||||
test("resolveParams: with json", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
|
||||
}),
|
||||
{
|
||||
test: "{{ some | json }}"
|
||||
},
|
||||
{
|
||||
some: {
|
||||
key: "path",
|
||||
value: "1/1"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual({ key: "path", value: "1/1" });
|
||||
});
|
||||
|
||||
test("resolveParams: with array", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Array(Type.String()))
|
||||
}),
|
||||
{
|
||||
test: '{{ "1,2,3" | split: "," | json }}'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
test("resolveParams: boolean", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Boolean())
|
||||
}),
|
||||
{
|
||||
test: "{{ true }}"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(true);
|
||||
});
|
||||
|
||||
test("resolveParams: float", async () => {
|
||||
const result = await Task.resolveParams(
|
||||
Type.Object({
|
||||
test: dynamic(Type.Number(), Number.parseFloat)
|
||||
}),
|
||||
{
|
||||
test: "{{ 3.14 }}"
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.test).toEqual(3.14);
|
||||
});
|
||||
});
|
||||
24
app/__test__/flows/inc/back.ts
Normal file
24
app/__test__/flows/inc/back.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Condition, Flow } from "../../../src/flows";
|
||||
import { getNamedTask } from "./helper";
|
||||
|
||||
const first = getNamedTask("first");
|
||||
const second = getNamedTask("second");
|
||||
const fourth = getNamedTask("fourth");
|
||||
|
||||
let thirdRuns = 0;
|
||||
const third = getNamedTask("third", async () => {
|
||||
thirdRuns++;
|
||||
if (thirdRuns === 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Third failed");
|
||||
});
|
||||
|
||||
const back = new Flow("back", [first, second, third, fourth]);
|
||||
back.task(first).asInputFor(second);
|
||||
back.task(second).asInputFor(third);
|
||||
back.task(third).asInputFor(second, Condition.error(), 2);
|
||||
back.task(third).asInputFor(fourth, Condition.success());
|
||||
|
||||
export { back };
|
||||
23
app/__test__/flows/inc/fanout-condition.ts
Normal file
23
app/__test__/flows/inc/fanout-condition.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Condition, Flow } from "../../../src/flows";
|
||||
import { getNamedTask } from "./helper";
|
||||
|
||||
const first = getNamedTask(
|
||||
"first",
|
||||
async () => {
|
||||
//throw new Error("Error");
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
};
|
||||
},
|
||||
1000
|
||||
);
|
||||
const second = getNamedTask("second (if match)");
|
||||
const third = getNamedTask("third (if error)");
|
||||
|
||||
const fanout = new Flow("fanout", [first, second, third]);
|
||||
fanout.task(first).asInputFor(third, Condition.error(), 2);
|
||||
fanout.task(first).asInputFor(second, Condition.matches("inner.result", 2));
|
||||
|
||||
export { fanout };
|
||||
61
app/__test__/flows/inc/helper.tsx
Normal file
61
app/__test__/flows/inc/helper.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Task } from "../../../src/flows";
|
||||
|
||||
// @todo: polyfill
|
||||
const Handle = (props: any) => null;
|
||||
type NodeProps<T> = any;
|
||||
const Position = { Top: "top", Bottom: "bottom" };
|
||||
|
||||
class ExecTask extends Task {
|
||||
type = "exec";
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
override clone(name: string, params: any) {
|
||||
return new ExecTask(name, params, this.fn);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
//console.log("executing", this.name);
|
||||
return await this.fn();
|
||||
}
|
||||
}
|
||||
|
||||
/*const ExecNode = ({
|
||||
data,
|
||||
isConnectable,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition = Position.Bottom,
|
||||
selected,
|
||||
}: NodeProps<ExecTask>) => {
|
||||
//console.log("data", data, data.hasDelay());
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data?.name} ({selected ? "selected" : "exec"})
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
};*/
|
||||
|
||||
export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number) {
|
||||
const func =
|
||||
_func ??
|
||||
(async () => {
|
||||
//console.log(`[DONE] Task: ${name}`);
|
||||
return true;
|
||||
});
|
||||
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay
|
||||
},
|
||||
func
|
||||
);
|
||||
}
|
||||
15
app/__test__/flows/inc/parallel.ts
Normal file
15
app/__test__/flows/inc/parallel.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Flow } from "../../../src/flows";
|
||||
import { getNamedTask } from "./helper";
|
||||
|
||||
const first = getNamedTask("first");
|
||||
const second = getNamedTask("second", undefined, 1000);
|
||||
const third = getNamedTask("third");
|
||||
const fourth = getNamedTask("fourth");
|
||||
const fifth = getNamedTask("fifth"); // without connection
|
||||
|
||||
const parallel = new Flow("Parallel", [first, second, third, fourth, fifth]);
|
||||
parallel.task(first).asInputFor(second);
|
||||
parallel.task(first).asInputFor(third);
|
||||
parallel.task(third).asInputFor(fourth);
|
||||
|
||||
export { parallel };
|
||||
18
app/__test__/flows/inc/simple-fetch.ts
Normal file
18
app/__test__/flows/inc/simple-fetch.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FetchTask, Flow, LogTask } from "../../../src/flows";
|
||||
|
||||
const first = new LogTask("First", { delay: 1000 });
|
||||
const second = new LogTask("Second", { delay: 1000 });
|
||||
const third = new LogTask("Long Third", { delay: 2500 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
});
|
||||
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
|
||||
|
||||
const simpleFetch = new Flow("simpleFetch", [first, second, third, fourth, fifth]);
|
||||
simpleFetch.task(first).asInputFor(second);
|
||||
simpleFetch.task(first).asInputFor(third);
|
||||
simpleFetch.task(fourth).asOutputFor(third);
|
||||
|
||||
simpleFetch.setRespondingTask(fourth);
|
||||
|
||||
export { simpleFetch };
|
||||
175
app/__test__/flows/inputs.test.ts
Normal file
175
app/__test__/flows/inputs.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { type Static, type StaticDecode, Type, parse } from "../../src/core/utils";
|
||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||
import { dynamic } from "../../src/flows/tasks/Task";
|
||||
|
||||
class Passthrough extends Task {
|
||||
type = "passthrough";
|
||||
|
||||
async execute(inputs: Map<string, any>) {
|
||||
//console.log("executing passthrough", this.name, inputs);
|
||||
return Array.from(inputs.values()).pop().output + "/" + this.name;
|
||||
}
|
||||
}
|
||||
|
||||
type OutputIn = Static<typeof OutputParamTask.schema>;
|
||||
type OutputOut = StaticDecode<typeof OutputParamTask.schema>;
|
||||
|
||||
class OutputParamTask extends Task<typeof OutputParamTask.schema> {
|
||||
type = "output-param";
|
||||
|
||||
static override schema = Type.Object({
|
||||
number: dynamic(
|
||||
Type.Number({
|
||||
title: "Output number"
|
||||
}),
|
||||
Number.parseInt
|
||||
)
|
||||
});
|
||||
|
||||
async execute(inputs: InputsMap) {
|
||||
//console.log("--***--- executing output", this.params);
|
||||
return this.params.number;
|
||||
}
|
||||
}
|
||||
|
||||
class PassthroughFlowInput extends Task {
|
||||
type = "passthrough-flow-input";
|
||||
|
||||
async execute(inputs: InputsMap) {
|
||||
return inputs.get("flow")?.output;
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow task inputs", async () => {
|
||||
test("types", async () => {
|
||||
const schema = OutputParamTask.schema;
|
||||
|
||||
expect(parse(schema, { number: 123 })).toBeDefined();
|
||||
expect(parse(schema, { number: "{{ some.path }}" })).toBeDefined();
|
||||
|
||||
const task = new OutputParamTask("", { number: 123 });
|
||||
expect(task.params.number).toBe(123);
|
||||
});
|
||||
|
||||
test("passthrough", async () => {
|
||||
const task = new Passthrough("log");
|
||||
const task2 = new Passthrough("log_2");
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.setRespondingTask(task2);
|
||||
|
||||
const exec = await flow.start("pass-through");
|
||||
|
||||
/*console.log(
|
||||
"---- log",
|
||||
exec.logs.map(({ task, ...l }) => ({ ...l, ...task.toJSON() })),
|
||||
);
|
||||
console.log("---- result", exec.getResponse());*/
|
||||
expect(exec.getResponse()).toBe("pass-through/log/log_2");
|
||||
});
|
||||
|
||||
test("output/input", async () => {
|
||||
const task = new OutputParamTask("task1", { number: 111 });
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{ task1.output }}"
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.setRespondingTask(task2);
|
||||
|
||||
const exec = await flow.start();
|
||||
|
||||
/*console.log(
|
||||
"---- log",
|
||||
exec.logs.map(({ task, ...l }) => ({ ...l, ...task.toJSON() })),
|
||||
);
|
||||
console.log("---- result", exec.getResponse());*/
|
||||
expect(exec.getResponse()).toBe(111);
|
||||
});
|
||||
|
||||
test("input from flow", async () => {
|
||||
const task = new OutputParamTask("task1", {
|
||||
number: "{{flow.output.someFancyParam}}"
|
||||
});
|
||||
const task2 = new OutputParamTask("task2", {
|
||||
number: "{{task1.output}}"
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task, task2]);
|
||||
flow.task(task).asInputFor(task2);
|
||||
flow.setRespondingTask(task2);
|
||||
|
||||
// expect to throw because of missing input
|
||||
//expect(flow.start()).rejects.toThrow();
|
||||
|
||||
const exec = await flow.start({ someFancyParam: 123 });
|
||||
|
||||
/*console.log(
|
||||
"---- log",
|
||||
exec.logs.map(({ task, ...l }) => ({ ...l, ...task.toJSON() })),
|
||||
);
|
||||
console.log("---- result", exec.getResponse());*/
|
||||
|
||||
expect(exec.getResponse()).toBe(123);
|
||||
});
|
||||
|
||||
test("manual event trigger with inputs", async () => {
|
||||
class EventTriggerClass extends Event<{ number: number }> {
|
||||
static override slug = "test-event";
|
||||
}
|
||||
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
|
||||
const task = new OutputParamTask("event", {
|
||||
number: "{{flow.output.number}}"
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger({
|
||||
event: "test-event",
|
||||
mode: "sync"
|
||||
})
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
flow.trigger.register(flow, emgr);
|
||||
|
||||
await emgr.emit(new EventTriggerClass({ number: 120 }));
|
||||
const execs = flow.trigger.executions;
|
||||
expect(execs.length).toBe(1);
|
||||
expect(execs[0]!.getResponse()).toBe(120);
|
||||
});
|
||||
|
||||
test("http trigger with response", async () => {
|
||||
const task = new PassthroughFlowInput("");
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
const hono = new Hono();
|
||||
|
||||
flow.trigger.register(flow, hono);
|
||||
|
||||
const res = await hono.request("/test?input=123");
|
||||
const data = await res.json();
|
||||
//console.log("response", data);
|
||||
const execs = flow.trigger.executions;
|
||||
expect(execs.length).toBe(1);
|
||||
expect(execs[0]!.getResponse()).toBeInstanceOf(Request);
|
||||
expect(execs[0]!.getResponse()?.url).toBe("http://localhost/test?input=123");
|
||||
});
|
||||
});
|
||||
186
app/__test__/flows/render.tsx
Normal file
186
app/__test__/flows/render.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Box, Text, render, useApp, useInput } from "ink";
|
||||
import React, { useEffect } from "react";
|
||||
import { ExecutionEvent, type Flow, type Task } from "../../src/flows";
|
||||
import { back } from "./inc/back";
|
||||
import { fanout } from "./inc/fanout-condition";
|
||||
import { parallel } from "./inc/parallel";
|
||||
import { simpleFetch } from "./inc/simple-fetch";
|
||||
|
||||
const flows = {
|
||||
back,
|
||||
fanout,
|
||||
parallel,
|
||||
simpleFetch
|
||||
};
|
||||
|
||||
const arg = process.argv[2];
|
||||
if (!arg) {
|
||||
console.log("Please provide a flow name:", Object.keys(flows).join(", "));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!flows[arg]) {
|
||||
console.log("Flow not found:", arg, Object.keys(flows).join(", "));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(flows[arg].toJSON(), null, 2));
|
||||
process.exit();
|
||||
|
||||
const colors = [
|
||||
"#B5E61D", // Lime Green
|
||||
"#4A90E2", // Bright Blue
|
||||
"#F78F1E", // Saffron
|
||||
"#BD10E0", // Vivid Purple
|
||||
"#50E3C2", // Turquoise
|
||||
"#9013FE" // Grape
|
||||
];
|
||||
|
||||
const colorsCache: Record<string, string> = {};
|
||||
|
||||
type Sequence = { source: string; target: string }[];
|
||||
type Layout = Task[][];
|
||||
type State = { layout: Layout; connections: Sequence };
|
||||
type TaskWithStatus = { task: Task; status: string };
|
||||
|
||||
function TerminalFlow({ flow }: { flow: Flow }) {
|
||||
const [tasks, setTasks] = React.useState<TaskWithStatus[]>([]);
|
||||
const sequence = flow.getSequence();
|
||||
const connections = flow.connections;
|
||||
|
||||
const { exit } = useApp();
|
||||
useInput((input, key) => {
|
||||
if (input === "q") {
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
// Left arrow key pressed
|
||||
console.log("Enter pressed");
|
||||
} else {
|
||||
console.log(input);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTasks(flow.tasks.map((t) => ({ task: t, status: "pending" })));
|
||||
|
||||
const execution = flow.createExecution();
|
||||
execution.subscribe((event) => {
|
||||
if (event instanceof ExecutionEvent) {
|
||||
setTasks((prev) =>
|
||||
prev.map((t) => {
|
||||
if (t.task.name === event.task().name) {
|
||||
let newStatus = "pending";
|
||||
if (event.isStart()) {
|
||||
newStatus = "running";
|
||||
} else {
|
||||
newStatus = event.succeeded() ? "success" : "failure";
|
||||
}
|
||||
|
||||
return { task: t.task, status: newStatus };
|
||||
}
|
||||
|
||||
return t;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
execution.start().then(() => {
|
||||
const response = execution.getResponse();
|
||||
console.log("done", response ? response : "(no response)");
|
||||
console.log(
|
||||
"Executed tasks:",
|
||||
execution.logs.map((l) => l.task.name)
|
||||
);
|
||||
console.log("Executed count:", execution.logs.length);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function getColor(key: string) {
|
||||
if (!colorsCache[key]) {
|
||||
colorsCache[key] = colors[Object.keys(colorsCache).length];
|
||||
}
|
||||
return colorsCache[key];
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return <Text>Loading...</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{sequence.map((step, stepIndex) => {
|
||||
return (
|
||||
<Box key={stepIndex} flexDirection="row">
|
||||
{step.map((_task, index) => {
|
||||
const find = tasks.find((t) => t.task.name === _task.name)!;
|
||||
|
||||
if (!find) {
|
||||
//console.log("couldnt find", _task.name);
|
||||
return null;
|
||||
}
|
||||
const { task, status } = find;
|
||||
|
||||
const inTasks = flow.task(task).getInTasks();
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
borderStyle="single"
|
||||
marginX={1}
|
||||
paddingX={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{inTasks.length > 0 && (
|
||||
<Box>
|
||||
<Text dimColor>In: </Text>
|
||||
<Box>
|
||||
{inTasks.map((inTask, i) => (
|
||||
<Text key={i} color={getColor(inTask.name)}>
|
||||
{i > 0 ? ", " : ""}
|
||||
{inTask.name}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={getColor(task.name)}>
|
||||
{task.name}
|
||||
</Text>
|
||||
<Status status={status} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const Status = ({ status }: { status: string }) => {
|
||||
let color: string | undefined;
|
||||
switch (status) {
|
||||
case "running":
|
||||
color = "orange";
|
||||
break;
|
||||
case "success":
|
||||
color = "green";
|
||||
break;
|
||||
case "failure":
|
||||
color = "red";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text color={color} dimColor={!color}>
|
||||
{status}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render(<TerminalFlow flow={flows[arg]} />);
|
||||
175
app/__test__/flows/trigger.test.ts
Normal file
175
app/__test__/flows/trigger.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { Event, EventManager } from "../../src/core/events";
|
||||
import { EventTrigger, Flow, HttpTrigger, Task } from "../../src/flows";
|
||||
|
||||
const ALL_TESTS = !!process.env.ALL_TESTS;
|
||||
|
||||
class ExecTask extends Task {
|
||||
type = "exec";
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: any,
|
||||
private fn: () => any
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
static create(name: string, fn: () => any) {
|
||||
return new ExecTask(name, undefined, fn);
|
||||
}
|
||||
|
||||
override clone(name: string, params: any) {
|
||||
return new ExecTask(name, params, this.fn);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
//console.log("executing", this.name);
|
||||
return await this.fn();
|
||||
}
|
||||
}
|
||||
|
||||
describe("Flow trigger", async () => {
|
||||
test("manual trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
const task = ExecTask.create("manual", () => {
|
||||
called = true;
|
||||
});
|
||||
const flow = new Flow("", [task]);
|
||||
|
||||
expect(flow.trigger.type).toBe("manual");
|
||||
|
||||
await flow.trigger.register(flow);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
test("event trigger", async () => {
|
||||
class EventTriggerClass extends Event {
|
||||
static override slug = "test-event";
|
||||
}
|
||||
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
let called = false;
|
||||
|
||||
const task = ExecTask.create("event", () => {
|
||||
called = true;
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger({ event: "test-event", mode: "sync" })
|
||||
);
|
||||
|
||||
flow.trigger.register(flow, emgr);
|
||||
|
||||
await emgr.emit(new EventTriggerClass({ test: 1 }));
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
/*test("event trigger with match", async () => {
|
||||
class EventTriggerClass extends Event<{ number: number }> {
|
||||
static slug = "test-event";
|
||||
}
|
||||
|
||||
const emgr = new EventManager({ EventTriggerClass });
|
||||
let called: number = 0;
|
||||
|
||||
const task = ExecTask.create("event", () => {
|
||||
called++;
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new EventTrigger(EventTriggerClass, "sync", (e) => e.params.number === 2)
|
||||
);
|
||||
|
||||
flow.trigger.register(flow, emgr);
|
||||
|
||||
await emgr.emit(new EventTriggerClass({ number: 1 }));
|
||||
await emgr.emit(new EventTriggerClass({ number: 2 }));
|
||||
expect(called).toBe(1);
|
||||
});*/
|
||||
|
||||
test("http trigger", async () => {
|
||||
let called = false;
|
||||
|
||||
const task = ExecTask.create("http", () => {
|
||||
called = true;
|
||||
});
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
);
|
||||
|
||||
const hono = new Hono();
|
||||
|
||||
flow.trigger.register(flow, hono);
|
||||
|
||||
const res = await hono.request("/test");
|
||||
//const data = await res.json();
|
||||
//console.log("response", data);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
test("http trigger with response", async () => {
|
||||
const task = ExecTask.create("http", () => ({
|
||||
called: true
|
||||
}));
|
||||
const flow = new Flow(
|
||||
"test",
|
||||
[task],
|
||||
[],
|
||||
new HttpTrigger({
|
||||
path: "/test",
|
||||
method: "GET",
|
||||
mode: "sync"
|
||||
})
|
||||
);
|
||||
flow.setRespondingTask(task);
|
||||
|
||||
const hono = new Hono();
|
||||
|
||||
flow.trigger.register(flow, hono);
|
||||
|
||||
const res = await hono.request("/test");
|
||||
const data = await res.json();
|
||||
//console.log("response", data);
|
||||
expect(data).toEqual({ called: true });
|
||||
});
|
||||
|
||||
/*test.skipIf(ALL_TESTS)("template with email", async () => {
|
||||
console.log("apikey", process.env.RESEND_API_KEY);
|
||||
const task = new FetchTask("fetch", {
|
||||
url: "https://api.resend.com/emails",
|
||||
method: "POST",
|
||||
headers: [
|
||||
{ key: "Content-Type", value: "application/json" },
|
||||
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" }
|
||||
],
|
||||
body: JSON.stringify({
|
||||
from: "onboarding@resend.dev",
|
||||
to: "dennis.senn@gmail.com",
|
||||
subject:
|
||||
"test from {% if flow.output.someFancyParam > 100 %}flow{% else %}task{% endif %}!",
|
||||
html: "Hello"
|
||||
})
|
||||
});
|
||||
|
||||
const flow = new Flow("test", [task]);
|
||||
|
||||
const exec = await flow.start({ someFancyParam: 80, apiKey: process.env.RESEND_API_KEY });
|
||||
//console.log("exec", exec.logs, exec.finished());
|
||||
expect(exec.finished()).toBe(true);
|
||||
expect(exec.hasErrors()).toBe(false);
|
||||
});*/
|
||||
});
|
||||
449
app/__test__/flows/workflow-basic.test.ts
Normal file
449
app/__test__/flows/workflow-basic.test.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { type Static, Type, _jsonp } from "../../src/core/utils";
|
||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||
|
||||
/*beforeAll(disableConsoleLog);
|
||||
afterAll(enableConsoleLog);*/
|
||||
|
||||
class ExecTask extends Task<typeof ExecTask.schema> {
|
||||
type = "exec";
|
||||
|
||||
static override schema = Type.Object({
|
||||
delay: Type.Number({ default: 10 })
|
||||
});
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
params: Static<typeof ExecTask.schema>,
|
||||
private func: () => Promise<any>
|
||||
) {
|
||||
super(name, params);
|
||||
}
|
||||
|
||||
override clone(name: string, params: Static<typeof ExecTask.schema>) {
|
||||
return new ExecTask(name, params, this.func);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.params.delay ?? 0));
|
||||
return await this.func();
|
||||
}
|
||||
}
|
||||
|
||||
function getTask(num: number = 0, delay: number = 5) {
|
||||
return new ExecTask(
|
||||
`Task ${num}`,
|
||||
{
|
||||
delay
|
||||
},
|
||||
async () => {
|
||||
//console.log(`[DONE] Task: ${num}`);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
//return new LogTask(`Log ${num}`, { delay });
|
||||
}
|
||||
|
||||
function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number) {
|
||||
const func =
|
||||
_func ??
|
||||
(async () => {
|
||||
//console.log(`[DONE] Task: ${name}`);
|
||||
return true;
|
||||
});
|
||||
return new ExecTask(
|
||||
name,
|
||||
{
|
||||
delay: delay ?? 0
|
||||
},
|
||||
func
|
||||
);
|
||||
}
|
||||
|
||||
function getObjectDiff(obj1, obj2) {
|
||||
const diff = Object.keys(obj1).reduce((result, key) => {
|
||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
|
||||
if (!obj2.hasOwnProperty(key)) {
|
||||
result.push(key);
|
||||
} else if (isEqual(obj1[key], obj2[key])) {
|
||||
const resultKeyIndex = result.indexOf(key);
|
||||
result.splice(resultKeyIndex, 1);
|
||||
}
|
||||
return result;
|
||||
}, Object.keys(obj2));
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
describe("Flow tests", async () => {
|
||||
test("Simple single task", async () => {
|
||||
const simple = getTask(0);
|
||||
|
||||
const result = await simple.run();
|
||||
expect(result.success).toBe(true);
|
||||
// @todo: add more
|
||||
});
|
||||
|
||||
function getNamedQueue(flow: Flow) {
|
||||
const namedSequence = flow.getSequence().map((step) => step.map((t) => t.name));
|
||||
//console.log(namedSequence);
|
||||
return namedSequence;
|
||||
}
|
||||
|
||||
test("Simple flow", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
|
||||
// simple
|
||||
const simple = new Flow("simple", [first, second]);
|
||||
simple.task(first).asInputFor(second);
|
||||
expect(getNamedQueue(simple)).toEqual([["Task 0"], ["Task 1"]]);
|
||||
expect(simple.task(first).getDepth()).toBe(0);
|
||||
expect(simple.task(second).getDepth()).toBe(1);
|
||||
|
||||
const execution = simple.createExecution();
|
||||
await execution.start();
|
||||
|
||||
//console.log("execution", execution.logs);
|
||||
//process.exit(0);
|
||||
expect(execution.logs.length).toBe(2);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);
|
||||
});
|
||||
|
||||
test("Test connection uniqueness", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2, 5);
|
||||
const fourth = getTask(3);
|
||||
|
||||
// should be fine
|
||||
expect(() => {
|
||||
const condition = new Flow("", [first, second, third]);
|
||||
condition.task(first).asInputFor(second);
|
||||
condition.task(first).asInputFor(third);
|
||||
}).toBeDefined();
|
||||
|
||||
// should throw
|
||||
expect(() => {
|
||||
const condition = new Flow("", [first, second, third]);
|
||||
condition.task(first).asInputFor(second);
|
||||
condition.task(first).asInputFor(second);
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
const condition = new Flow("", [first, second, third]);
|
||||
condition.task(first).asInputFor(second);
|
||||
condition.task(second).asInputFor(third);
|
||||
condition.task(third).asInputFor(second);
|
||||
condition.task(third).asInputFor(fourth); // this should fail
|
||||
}).toThrow();
|
||||
|
||||
expect(() => {
|
||||
const condition = new Flow("", [first, second, third]);
|
||||
condition.task(first).asInputFor(second);
|
||||
condition.task(second).asInputFor(third);
|
||||
condition.task(third).asInputFor(second);
|
||||
condition.task(third).asInputFor(fourth, Condition.error());
|
||||
}).toBeDefined();
|
||||
});
|
||||
|
||||
test("Flow with 3 steps", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2);
|
||||
|
||||
const three = new Flow("", [first, second, third]);
|
||||
three.task(first).asInputFor(second);
|
||||
three.task(second).asInputFor(third);
|
||||
expect(getNamedQueue(three)).toEqual([["Task 0"], ["Task 1"], ["Task 2"]]);
|
||||
expect(three.task(first).getDepth()).toBe(0);
|
||||
expect(three.task(second).getDepth()).toBe(1);
|
||||
expect(three.task(third).getDepth()).toBe(2);
|
||||
|
||||
const execution = three.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(3);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);
|
||||
});
|
||||
|
||||
test("Flow with parallel tasks", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2);
|
||||
const fourth = getTask(3);
|
||||
const fifth = getTask(4); // without connection
|
||||
|
||||
const parallel = new Flow("", [first, second, third, fourth, fifth]);
|
||||
parallel.task(first).asInputFor(second);
|
||||
parallel.task(first).asInputFor(third);
|
||||
parallel.task(third).asInputFor(fourth);
|
||||
expect(getNamedQueue(parallel)).toEqual([["Task 0"], ["Task 1", "Task 2"], ["Task 3"]]);
|
||||
expect(parallel.task(first).getDepth()).toBe(0);
|
||||
expect(parallel.task(second).getDepth()).toBe(1);
|
||||
expect(parallel.task(third).getDepth()).toBe(1);
|
||||
expect(parallel.task(fourth).getDepth()).toBe(2);
|
||||
|
||||
const execution = parallel.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(4);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);
|
||||
});
|
||||
|
||||
test("Flow with condition", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2);
|
||||
|
||||
const condition = new Flow("", [first, second, third]);
|
||||
condition.task(first).asInputFor(second);
|
||||
condition.task(first).asInputFor(third);
|
||||
});
|
||||
|
||||
test("Flow with back step", async () => {
|
||||
const first = getNamedTask("first");
|
||||
const second = getNamedTask("second");
|
||||
const fourth = getNamedTask("fourth");
|
||||
|
||||
let thirdRuns: number = 0;
|
||||
const third = getNamedTask("third", async () => {
|
||||
thirdRuns++;
|
||||
if (thirdRuns === 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Third failed");
|
||||
});
|
||||
|
||||
const back = new Flow("", [first, second, third, fourth]);
|
||||
back.task(first).asInputFor(second);
|
||||
back.task(second).asInputFor(third);
|
||||
back.task(third).asInputFor(second, Condition.error(), 2);
|
||||
back.task(third).asInputFor(fourth, Condition.success());
|
||||
expect(getNamedQueue(back)).toEqual([["first"], ["second"], ["third"], ["fourth"]]);
|
||||
expect(
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
expect(execution.start()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("Flow with back step: enough retries", async () => {
|
||||
const first = getNamedTask("first");
|
||||
const second = getNamedTask("second");
|
||||
const fourth = getNamedTask("fourth");
|
||||
|
||||
let thirdRuns: number = 0;
|
||||
const third = getNamedTask("third", async () => {
|
||||
thirdRuns++;
|
||||
//console.log("--- third runs", thirdRuns);
|
||||
if (thirdRuns === 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Third failed");
|
||||
});
|
||||
|
||||
const back = new Flow("", [first, second, third, fourth]);
|
||||
back.task(first).asInputFor(second);
|
||||
back.task(second).asInputFor(third);
|
||||
back.task(third).asInputFor(second, Condition.error(), 1);
|
||||
back.task(third).asInputFor(fourth, Condition.success());
|
||||
expect(getNamedQueue(back)).toEqual([["first"], ["second"], ["third"], ["fourth"]]);
|
||||
expect(
|
||||
back
|
||||
.task(third)
|
||||
.getOutTasks()
|
||||
.map((t) => t.name)
|
||||
).toEqual(["second", "fourth"]);
|
||||
|
||||
const execution = back.createExecution();
|
||||
await execution.start();
|
||||
});
|
||||
|
||||
test("flow fanout", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2, 20);
|
||||
|
||||
const fanout = new Flow("", [first, second, third]);
|
||||
fanout.task(first).asInputFor(second);
|
||||
fanout.task(first).asInputFor(third);
|
||||
|
||||
const execution = fanout.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(3);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);
|
||||
});
|
||||
|
||||
test("flow fanout with condition", async () => {
|
||||
const first = getTask(0);
|
||||
const second = getTask(1);
|
||||
const third = getTask(2);
|
||||
|
||||
const fanout = new Flow("", [first, second, third]);
|
||||
fanout.task(first).asInputFor(second, Condition.success());
|
||||
fanout.task(first).asInputFor(third, Condition.error());
|
||||
|
||||
const execution = fanout.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(2);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);
|
||||
});
|
||||
|
||||
test("flow fanout with condition error", async () => {
|
||||
const first = getNamedTask("first", async () => {
|
||||
throw new Error("Error");
|
||||
});
|
||||
const second = getNamedTask("second");
|
||||
const third = getNamedTask("third");
|
||||
|
||||
const fanout = new Flow("", [first, second, third]);
|
||||
fanout.task(first).asInputFor(third, Condition.error());
|
||||
fanout.task(first).asInputFor(second, Condition.success());
|
||||
|
||||
const execution = fanout.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(2);
|
||||
expect(execution.logs.map((l) => l.task.name)).toEqual(["first", "third"]);
|
||||
});
|
||||
|
||||
test("flow fanout with condition matches", async () => {
|
||||
const first = getNamedTask("first", async () => {
|
||||
return {
|
||||
inner: {
|
||||
result: 2
|
||||
}
|
||||
};
|
||||
});
|
||||
const second = getNamedTask("second");
|
||||
const third = getNamedTask("third");
|
||||
|
||||
const fanout = new Flow("", [first, second, third]);
|
||||
fanout.task(first).asInputFor(third, Condition.error());
|
||||
fanout.task(first).asInputFor(second, Condition.matches("inner.result", 2));
|
||||
|
||||
const execution = fanout.createExecution();
|
||||
await execution.start();
|
||||
|
||||
expect(execution.logs.length).toBe(2);
|
||||
expect(execution.logs.map((l) => l.task.name)).toEqual(["first", "second"]);
|
||||
});
|
||||
|
||||
test("flow: responding task", async () => {
|
||||
const first = getNamedTask("first");
|
||||
const second = getNamedTask("second", async () => ({ result: 2 }));
|
||||
const third = getNamedTask("third");
|
||||
|
||||
const flow = new Flow("", [first, second, third]);
|
||||
flow.task(first).asInputFor(second);
|
||||
flow.task(second).asInputFor(third);
|
||||
|
||||
flow.setRespondingTask(second);
|
||||
|
||||
const execution = flow.createExecution();
|
||||
|
||||
execution.subscribe(async (event) => {
|
||||
if (event instanceof ExecutionEvent) {
|
||||
console.log(
|
||||
"[event]",
|
||||
event.isStart() ? "start" : "end",
|
||||
event.task().name,
|
||||
event.isStart() ? undefined : event.succeeded()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await execution.start();
|
||||
|
||||
const response = execution.getResponse();
|
||||
|
||||
expect(response).toEqual({ result: 2 });
|
||||
expect(execution.logs.length).toBe(2);
|
||||
expect(execution.logs.map((l) => l.task.name)).toEqual(["first", "second"]);
|
||||
|
||||
/*console.log("response", response);
|
||||
console.log("execution.logs.length", execution.logs.length);
|
||||
console.log(
|
||||
"executed",
|
||||
execution.logs.map((l) => l.task.name),
|
||||
);*/
|
||||
/*expect(execution.logs.length).toBe(3);
|
||||
expect(execution.logs.every((log) => log.success)).toBe(true);*/
|
||||
});
|
||||
|
||||
test("serialize/deserialize", async () => {
|
||||
const first = new LogTask("Task 0");
|
||||
const second = new LogTask("Task 1");
|
||||
const third = new LogTask("Task 2", { delay: 50 });
|
||||
const fourth = new FetchTask("Fetch Something", {
|
||||
url: "https://jsonplaceholder.typicode.com/todos/1"
|
||||
});
|
||||
const fifth = new LogTask("Task 4"); // without connection
|
||||
|
||||
const flow = new Flow("", [first, second, third, fourth, fifth]);
|
||||
flow.task(first).asInputFor(second);
|
||||
flow.task(first).asInputFor(third);
|
||||
flow.task(fourth).asOutputFor(third, Condition.matches("some", 1));
|
||||
|
||||
flow.setRespondingTask(fourth);
|
||||
|
||||
const original = flow.toJSON();
|
||||
//console.log("flow", original);
|
||||
// @todo: fix
|
||||
const deserialized = Flow.fromObject("", original, {
|
||||
fetch: { cls: FetchTask },
|
||||
log: { cls: LogTask }
|
||||
} as any);
|
||||
|
||||
const diffdeep = getObjectDiff(original, deserialized.toJSON());
|
||||
expect(diffdeep).toEqual([]);
|
||||
|
||||
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
|
||||
expect(flow.respondingTask?.name).toEqual(
|
||||
// @ts-ignore
|
||||
deserialized.respondingTask?.name
|
||||
);
|
||||
|
||||
//console.log("--- creating original sequence");
|
||||
const originalSequence = flow.getSequence();
|
||||
//console.log("--- creating deserialized sequence");
|
||||
const deserializedSequence = deserialized.getSequence();
|
||||
//console.log("--- ");
|
||||
|
||||
expect(originalSequence).toEqual(deserializedSequence);
|
||||
});
|
||||
|
||||
test("error end", async () => {
|
||||
const first = getNamedTask("first", async () => "first");
|
||||
const second = getNamedTask("error", async () => {
|
||||
throw new Error("error");
|
||||
});
|
||||
const third = getNamedTask("third", async () => "third");
|
||||
const errorhandlertask = getNamedTask("errorhandler", async () => "errorhandler");
|
||||
|
||||
const flow = new Flow("", [first, second, third, errorhandlertask]);
|
||||
flow.task(first).asInputFor(second);
|
||||
flow.task(second).asInputFor(third);
|
||||
flow.task(second).asInputFor(errorhandlertask, Condition.error());
|
||||
|
||||
const exec = await flow.start();
|
||||
|
||||
//console.log("logs", JSON.stringify(exec.logs, null, 2));
|
||||
//console.log("errors", exec.hasErrors(), exec.errorCount());
|
||||
|
||||
expect(exec.hasErrors()).toBe(true);
|
||||
expect(exec.errorCount()).toBe(1);
|
||||
expect(exec.getResponse()).toBe("errorhandler");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user