@nocobase/flow-engine
Version:
A standalone flow engine for NocoBase, managing workflows, models, and actions.
188 lines (163 loc) • 5.47 kB
text/typescript
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ContextPathProxy } from '../ContextPathProxy';
import { FlowRuntimeContext } from '../flowContext';
import { FlowEngine } from '../flowEngine';
import { FlowModel } from '../models/flowModel';
describe('FlowRuntimeContext', () => {
let engine: FlowEngine;
let model: FlowModel;
beforeEach(() => {
engine = new FlowEngine();
model = engine.createModel({ use: 'FlowModel' });
});
it('should support nested property access in runtime mode', () => {
const ctx = new FlowRuntimeContext(model, 'flow1');
ctx.defineProperty('steps', { value: { step1: { result: 42 } } });
ctx.defineProperty('runId', { value: 'abc123' });
expect(ctx.steps.step1.result).toBe(42);
expect(ctx.runId).toBe('abc123');
expect(ctx.steps.step2).toBeUndefined();
expect(ctx.steps.step1.notExist).toBeUndefined();
expect(ctx.notFound).toBeUndefined();
});
it('should return string template in settings mode', () => {
const ctx = new FlowRuntimeContext(model, 'flow1', 'settings');
ctx.defineProperty('runId', { value: 'mock' });
ctx.defineProperty('steps', { value: {} });
expect(ctx.runId.toString()).toBe('{{ctx.runId}}');
expect(`${ctx.runId}`).toBe('{{ctx.runId}}');
expect(ctx.runId).instanceOf(ContextPathProxy);
expect(ctx.notFound).toBeUndefined();
expect(`${ctx.steps.step1.result}`).toBe('{{ctx.steps.step1.result}}');
});
it('should throw on exit()', () => {
const ctx = new FlowRuntimeContext(model, 'flow1', 'runtime');
expect(() => ctx.exit()).toThrow();
});
});
describe('FlowRuntimeContext.runAction', () => {
let engine: FlowEngine;
let model: FlowModel;
let ctx: FlowRuntimeContext;
beforeEach(() => {
engine = new FlowEngine();
model = engine.createModel({ use: 'FlowModel' });
ctx = new FlowRuntimeContext(model, 'unitFlow');
});
it('executes a registered action by name and returns result', async () => {
engine.registerActions({
hello: {
name: 'hello',
handler: (_ctx, params) => ({ ok: true, params }),
},
});
const res = await ctx.runAction('hello', { x: 1 });
expect(res).toEqual({ ok: true, params: { x: 1 } });
});
it('merges defaultParams and resolves expressions when useRawParams is falsy', async () => {
engine.registerActions({
combine: {
name: 'combine',
defaultParams: {
a: 1,
uidFromDefault: '{{ ctx.model.uid }}',
},
handler: (_ctx, params) => params,
},
});
const res = await ctx.runAction('combine', {
b: 2,
uidFromParam: '{{ ctx.model.uid }}',
});
expect(res).toEqual({
a: 1,
b: 2,
uidFromDefault: model.uid,
uidFromParam: model.uid,
});
});
it('does not resolve expressions when useRawParams=true', async () => {
engine.registerActions({
raw: {
name: 'raw',
defaultParams: {
fromDefault: '{{ ctx.model.uid }}',
},
useRawParams: true,
handler: (_ctx, params) => params,
},
});
const res = await ctx.runAction('raw', {
fromParam: '{{ ctx.model.uid }}',
n: 123,
});
expect(res).toEqual({
fromDefault: '{{ ctx.model.uid }}',
fromParam: '{{ ctx.model.uid }}',
n: 123,
});
});
it('supports useRawParams as function', async () => {
engine.registerActions({
decide: {
name: 'decide',
defaultParams: { d: '{{ ctx.model.uid }}' },
useRawParams: () => Promise.resolve(true),
handler: (_ctx, params) => params,
},
});
const res = await ctx.runAction('decide', { p: '{{ ctx.model.uid }}' });
expect(res).toEqual({ d: '{{ ctx.model.uid }}', p: '{{ ctx.model.uid }}' });
});
it('throws if action not found', async () => {
await expect(ctx.runAction('no-such-action')).rejects.toThrow(/not found/i);
});
});
describe('Flow run with step that triggers another action', () => {
it('step action calls ctx.runAction to invoke another action', async () => {
const engine = new FlowEngine();
const model = engine.createModel({ use: 'FlowModel' });
// second action: merges defaults and resolves expressions
engine.registerActions({
second: {
name: 'second',
defaultParams: { a: 1, uidDefault: '{{ ctx.model.uid }}' },
handler: (_ctx, params) => ({ from: 'second', ...params }),
},
});
// first action: triggers second via ctx.runAction
engine.registerActions({
first: {
name: 'first',
handler: async (ctx) => {
return await ctx.runAction('second', {
b: 2,
uidFromFirst: '{{ ctx.model.uid }}',
});
},
},
});
// Define a flow with a single step using 'first'
model.flowRegistry.addFlow('chainFlow', {
title: 'Chain Flow',
steps: {
s1: { use: 'first' },
},
});
const res = await engine.executor.runFlow(model, 'chainFlow');
expect(res.s1).toEqual({
from: 'second',
a: 1,
b: 2,
uidDefault: model.uid,
uidFromFirst: model.uid,
});
});
});