Files
bknd/app/__test__/flows/workflow-basic.test.ts
dswbx a298b65abf Release 0.16 (#196)
* initial refactor

* fixes

* test secrets extraction

* updated lock

* fix secret schema

* updated schemas, fixed tests, skipping flow tests for now

* added validator for rjsf, hook form via standard schema

* removed @sinclair/typebox

* remove unneeded vite dep

* fix jsonv literal on Field.tsx

* fix schema import path

* fix schema modals

* fix schema modals

* fix json field form, replaced auth form

* initial waku

* finalize waku example

* fix jsonv-ts version

* fix schema updates with falsy values

* fix media api to respect options' init, improve types

* checking media controller test

* checking media controller test

* checking media controller test

* clean up mediacontroller test

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials` (#214)

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials`

* fix server test

* fix data api (updated jsonv-ts)

* enhance cloudflare image optimization plugin with new options and explain endpoint (#215)

* feat: add ability to serve static by using dynamic imports (#197)

* feat: add ability to serve static by using dynamic imports

* serveStaticViaImport: make manifest optional

* serveStaticViaImport: add error log

* refactor/imports (#217)

* refactored core and core/utils imports

* refactored core and core/utils imports

* refactored media imports

* refactored auth imports

* refactored data imports

* updated package json exports, fixed mm config

* fix tests

* feat/deno (#219)

* update bun version

* fix module manager's em reference

* add basic deno example

* finalize

* docs: fumadocs migration (#185)

* feat(docs): initialize documentation structure with Fumadocs

* feat(docs): remove home route and move /docs route to /route

* feat(docs): add redirect to /start page

* feat(docs): migrate Getting Started chapters

* feat(docs): migrate Usage and Extending chapters

* feat(callout): add CalloutCaution, CalloutDanger, CalloutInfo, and CalloutPositive

* feat(layout): add Discord and GitHub links to documentation layout

* feat(docs): add integration chapters draft

* feat(docs): add modules chapters draft

* refactor(mdx-components): remove unused Icon import

* refactor(StackBlitz): enhance type safety by using unknown instead of any

* refactor(layout): update navigation mode to 'top' in layout configuration

* feat(docs): add @iconify/react package

* docs(mdx-components): add Icon component to MDX components list

* feat(docs): update Next.js integration guide

* feat(docs): update React Router integration guide

* feat(docs): update Astro integration guide

* feat(docs): update Vite integration guide

* fix(docs): update package manager initialization commands

* feat(docs): migrate Modules chapters

* chore(docs): update package.json with new devDependencies

* feat(docs): migrate Integration Runtimes chapters

* feat(docs): update Database usage chapter

* feat(docs): restructure documentation paths

* chore(docs): clean up unused imports and files in documentation

* style(layout): revert navigation mode to previous state

* fix(docs): routing for documentation structure

* feat(openapi): add API documentation generation from OpenAPI schema

* feat(docs): add icons to documentation pages

* chore(dependencies): remove unused content-collections packages

* fix(types): fix type error for attachFile in source.ts

* feat(redirects): update root redirect destination to '/start'

* feat(search): add static search functionality

* chore(dependencies): update fumadocs-core and fumadocs-ui to latest versions

* feat(search): add Powered by Orama link

* feat(generate-openapi): add error handling for missing OpenAPI schema

* feat(scripts): add OpenAPI generation to build process

* feat(config): enable dynamic redirects and rewrites in development mode

* feat(layout): add GitHub token support for improved API rate limits

* feat(redirects): add 301 redirects for cloudflare pages

* feat(docs): add Vercel redirects configuration

* feat(config): enable standalone output for development environment

* chore(layout): adjust layout settings

* refactor(package): clean up ajv dependency versions

* feat(docs): add twoslash support

* refactor(layout): update DocsLayout import and navigation configuration

* chore(layout): clean up layout.tsx by commenting out GithubInfo

* fix(Search): add locale to search initialization

* chore(package): update fumadocs and orama to latest versions

* docs: add menu items descriptions

* feat(layout): add GitHub URL to the layout component

* feat(docs): add AutoTypeTable component to MDX components

* feat(app): implement AutoTypeTable rendering for AppEvents type

* docs(layout): switch callouts back to default components

* fix(config): use __filename and __dirname for module paths

* docs: add note about node.js 22 requirement

* feat(styles): add custom color variables for light and dark themes

* docs: add S3 setup instructions for media module

* docs: fix typos and indentation in media module docs

* docs: add local media adapter example for Node.js

* docs(media): add S3/R2 URL format examples and fix typo

* docs: add cross-links to initial config and seeding sections

* indent numbered lists content, clarified media serve locations

* fix mediacontroller tests

* feat(layout): add AnimatedGridPattern component for dynamic background

* style(layout): configure fancy ToC style ('clerk')

* fix(AnimatedGridPattern): correct strokeDasharray type

* docs: actualize docs

* feat: add favicon

* style(cloudflare): format code examples

* feat(layout): add Github and Discord footer icons

* feat(footer): add SVG social media icons for GitHub and Discord

* docs: adjusted auto type table, added llm functions

* added static deployment to cloudflare workers

* docs: change cf redirects to proxy *.mdx instead of redirecting

---------

Co-authored-by: dswbx <dennis.senn@gmx.ch>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>

* build: improve build script

* add missing exports, fix EntityTypescript imports

* media: Dropzone: add programmatic upload, additional events, loading state

* schema object: disable extended defaults to allow empty config values

* Feat/new docs deploy (#224)

* test

* try fixing pm

* try fixing pm

* fix docs on imports, export events correctly

---------

Co-authored-by: Tim Seriakov <59409712+timseriakov@users.noreply.github.com>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>
2025-08-01 15:55:59 +02:00

453 lines
15 KiB
TypeScript

// eslint-disable-next-line import/no-unresolved
import { describe, expect, test } from "bun:test";
import { isEqual } from "lodash-es";
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
import { s } from "core/utils/schema";
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 = s.object({
delay: s.number({ default: 10 }),
});
constructor(
name: string,
params: s.Static<typeof ExecTask.schema>,
private func: () => Promise<any>,
) {
super(name, params);
}
override clone(name: string, params: s.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.skip("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();
withDisabledConsole(async () => {
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");
});
});