opc-agent 4.0.44 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +2 -2
- package/dist/core/runtime.js +18 -0
- package/dist/deploy/index.js +56 -56
- package/dist/providers/index.js +39 -13
- package/dist/studio/server.js +211 -20
- package/dist/studio-ui/index.html +279 -24
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +2 -2
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +18 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +156 -156
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -608
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +209 -22
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +279 -24
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
GraphWorkflowEngine,
|
|
4
|
-
WorkflowBuilder,
|
|
5
|
-
parseOADWorkflow,
|
|
6
|
-
type WorkflowContext,
|
|
7
|
-
type GraphWorkflow,
|
|
8
|
-
type GraphWorkflowStep,
|
|
9
|
-
} from '../src/core/workflow-graph';
|
|
10
|
-
|
|
11
|
-
function buildSimpleWorkflow(steps: Map<string, GraphWorkflowStep>, entry: string): GraphWorkflow {
|
|
12
|
-
return { name: 'test', entryPoint: entry, steps };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('GraphWorkflowEngine', () => {
|
|
16
|
-
const engine = new GraphWorkflowEngine();
|
|
17
|
-
|
|
18
|
-
it('executes a simple linear workflow', async () => {
|
|
19
|
-
const log: string[] = [];
|
|
20
|
-
const wf = new WorkflowBuilder()
|
|
21
|
-
.name('linear')
|
|
22
|
-
.start('a')
|
|
23
|
-
.addAction('a', async (ctx) => { log.push('a'); ctx.variables.set('x', 1); return 'done-a'; }, { next: 'b' })
|
|
24
|
-
.addAction('b', async (ctx) => { log.push('b'); return 'done-b'; }, { next: 'c' })
|
|
25
|
-
.addAction('c', async () => { log.push('c'); })
|
|
26
|
-
.build();
|
|
27
|
-
|
|
28
|
-
const result = await engine.execute(wf);
|
|
29
|
-
expect(result.status).toBe('completed');
|
|
30
|
-
expect(log).toEqual(['a', 'b', 'c']);
|
|
31
|
-
expect(result.context.results.get('a')).toBe('done-a');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('condition branches to onTrue', async () => {
|
|
35
|
-
const log: string[] = [];
|
|
36
|
-
const wf = new WorkflowBuilder()
|
|
37
|
-
.name('cond-true')
|
|
38
|
-
.start('check')
|
|
39
|
-
.addCondition('check', () => true, 'yes', 'no')
|
|
40
|
-
.addAction('yes', async () => { log.push('yes'); })
|
|
41
|
-
.addAction('no', async () => { log.push('no'); })
|
|
42
|
-
.build();
|
|
43
|
-
|
|
44
|
-
await engine.execute(wf);
|
|
45
|
-
expect(log).toEqual(['yes']);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('condition branches to onFalse', async () => {
|
|
49
|
-
const log: string[] = [];
|
|
50
|
-
const wf = new WorkflowBuilder()
|
|
51
|
-
.name('cond-false')
|
|
52
|
-
.start('check')
|
|
53
|
-
.addCondition('check', () => false, 'yes', 'no')
|
|
54
|
-
.addAction('yes', async () => { log.push('yes'); })
|
|
55
|
-
.addAction('no', async () => { log.push('no'); })
|
|
56
|
-
.build();
|
|
57
|
-
|
|
58
|
-
await engine.execute(wf);
|
|
59
|
-
expect(log).toEqual(['no']);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('condition uses context variables', async () => {
|
|
63
|
-
const wf = new WorkflowBuilder()
|
|
64
|
-
.name('cond-ctx')
|
|
65
|
-
.start('init')
|
|
66
|
-
.addAction('init', async (ctx) => { ctx.variables.set('flag', true); }, { next: 'check' })
|
|
67
|
-
.addCondition('check', (ctx) => ctx.variables.get('flag') === true, 'pass', 'fail')
|
|
68
|
-
.addAction('pass', async () => 'passed')
|
|
69
|
-
.addAction('fail', async () => 'failed')
|
|
70
|
-
.build();
|
|
71
|
-
|
|
72
|
-
const result = await engine.execute(wf);
|
|
73
|
-
expect(result.context.results.get('pass')).toBe('passed');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('parallel executes all steps', async () => {
|
|
77
|
-
const log: string[] = [];
|
|
78
|
-
const wf = new WorkflowBuilder()
|
|
79
|
-
.name('par')
|
|
80
|
-
.start('p')
|
|
81
|
-
.addAction('a', async () => { log.push('a'); })
|
|
82
|
-
.addAction('b', async () => { log.push('b'); })
|
|
83
|
-
.addParallel('p', ['a', 'b'], 'done')
|
|
84
|
-
.addAction('done', async () => { log.push('done'); })
|
|
85
|
-
.build();
|
|
86
|
-
|
|
87
|
-
const result = await engine.execute(wf);
|
|
88
|
-
expect(result.status).toBe('completed');
|
|
89
|
-
expect(log).toContain('a');
|
|
90
|
-
expect(log).toContain('b');
|
|
91
|
-
expect(log).toContain('done');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('loop iterates correct number of times', async () => {
|
|
95
|
-
let count = 0;
|
|
96
|
-
const wf = new WorkflowBuilder()
|
|
97
|
-
.name('loop')
|
|
98
|
-
.start('init')
|
|
99
|
-
.addAction('init', async (ctx) => { ctx.variables.set('i', 0); }, { next: 'loop' })
|
|
100
|
-
.addAction('body', async (ctx) => {
|
|
101
|
-
const i = ctx.variables.get('i');
|
|
102
|
-
ctx.variables.set('i', i + 1);
|
|
103
|
-
count++;
|
|
104
|
-
})
|
|
105
|
-
.addLoop('loop', (ctx) => (ctx.variables.get('i') ?? 0) < 5, 'body')
|
|
106
|
-
.build();
|
|
107
|
-
|
|
108
|
-
await engine.execute(wf);
|
|
109
|
-
expect(count).toBe(5);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('loop respects maxIterations', async () => {
|
|
113
|
-
let count = 0;
|
|
114
|
-
const wf = new WorkflowBuilder()
|
|
115
|
-
.name('loop-max')
|
|
116
|
-
.start('loop')
|
|
117
|
-
.addAction('body', async () => { count++; })
|
|
118
|
-
.addLoop('loop', () => true, 'body', { maxIterations: 3 })
|
|
119
|
-
.build();
|
|
120
|
-
|
|
121
|
-
await engine.execute(wf);
|
|
122
|
-
expect(count).toBe(3);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('retries on failure then succeeds', async () => {
|
|
126
|
-
let attempts = 0;
|
|
127
|
-
const wf = new WorkflowBuilder()
|
|
128
|
-
.name('retry-ok')
|
|
129
|
-
.start('flaky')
|
|
130
|
-
.addAction('flaky', async () => {
|
|
131
|
-
attempts++;
|
|
132
|
-
if (attempts < 3) throw new Error('fail');
|
|
133
|
-
return 'ok';
|
|
134
|
-
}, { retryCount: 3, retryDelay: 1 })
|
|
135
|
-
.build();
|
|
136
|
-
|
|
137
|
-
const result = await engine.execute(wf);
|
|
138
|
-
expect(result.status).toBe('completed');
|
|
139
|
-
expect(attempts).toBe(3);
|
|
140
|
-
expect(result.context.results.get('flaky')).toBe('ok');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('retry exhaustion records error', async () => {
|
|
144
|
-
const wf = new WorkflowBuilder()
|
|
145
|
-
.name('retry-fail')
|
|
146
|
-
.start('bad')
|
|
147
|
-
.addAction('bad', async () => { throw new Error('always-fail'); }, { retryCount: 2, retryDelay: 1, onError: 'stop' })
|
|
148
|
-
.build();
|
|
149
|
-
|
|
150
|
-
const result = await engine.execute(wf);
|
|
151
|
-
expect(result.status).toBe('failed');
|
|
152
|
-
expect(result.context.errors).toHaveLength(1);
|
|
153
|
-
expect(result.context.errors[0].step).toBe('bad');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('timeout kills slow step', async () => {
|
|
157
|
-
const wf = new WorkflowBuilder()
|
|
158
|
-
.name('timeout')
|
|
159
|
-
.start('slow')
|
|
160
|
-
.addAction('slow', async () => {
|
|
161
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
162
|
-
}, { timeout: 50, onError: 'stop' })
|
|
163
|
-
.build();
|
|
164
|
-
|
|
165
|
-
const result = await engine.execute(wf);
|
|
166
|
-
expect(result.status).toBe('failed');
|
|
167
|
-
expect(result.context.errors[0].error.message).toContain('Timeout');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('onError skip continues to next step', async () => {
|
|
171
|
-
const log: string[] = [];
|
|
172
|
-
const wf = new WorkflowBuilder()
|
|
173
|
-
.name('skip')
|
|
174
|
-
.start('bad')
|
|
175
|
-
.addAction('bad', async () => { throw new Error('oops'); }, { onError: 'skip', next: 'good' })
|
|
176
|
-
.addAction('good', async () => { log.push('good'); })
|
|
177
|
-
.build();
|
|
178
|
-
|
|
179
|
-
const result = await engine.execute(wf);
|
|
180
|
-
// skip doesn't add to errors, doesn't throw
|
|
181
|
-
expect(log).toEqual(['good']);
|
|
182
|
-
expect(result.status).toBe('completed');
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('onError stop halts execution', async () => {
|
|
186
|
-
const log: string[] = [];
|
|
187
|
-
const wf = new WorkflowBuilder()
|
|
188
|
-
.name('stop')
|
|
189
|
-
.start('bad')
|
|
190
|
-
.addAction('bad', async () => { throw new Error('halt'); }, { onError: 'stop', next: 'after' })
|
|
191
|
-
.addAction('after', async () => { log.push('after'); })
|
|
192
|
-
.build();
|
|
193
|
-
|
|
194
|
-
const result = await engine.execute(wf);
|
|
195
|
-
expect(result.status).toBe('failed');
|
|
196
|
-
expect(log).toEqual([]);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('WorkflowBuilder creates valid workflow', () => {
|
|
200
|
-
const wf = new WorkflowBuilder()
|
|
201
|
-
.name('builder-test')
|
|
202
|
-
.start('s1')
|
|
203
|
-
.addAction('s1', async () => 'ok', { next: 's2' })
|
|
204
|
-
.addAction('s2', async () => 'done')
|
|
205
|
-
.build();
|
|
206
|
-
|
|
207
|
-
expect(wf.name).toBe('builder-test');
|
|
208
|
-
expect(wf.entryPoint).toBe('s1');
|
|
209
|
-
expect(wf.steps.size).toBe(2);
|
|
210
|
-
expect(wf.steps.get('s1')?.next).toBe('s2');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('WorkflowBuilder throws without start', () => {
|
|
214
|
-
expect(() => new WorkflowBuilder().addAction('a', async () => {}).build())
|
|
215
|
-
.toThrow('entry point');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('context variables persist across steps', async () => {
|
|
219
|
-
const wf = new WorkflowBuilder()
|
|
220
|
-
.name('persist')
|
|
221
|
-
.start('a')
|
|
222
|
-
.addAction('a', async (ctx) => { ctx.variables.set('msg', 'hello'); }, { next: 'b' })
|
|
223
|
-
.addAction('b', async (ctx) => {
|
|
224
|
-
return ctx.variables.get('msg') + ' world';
|
|
225
|
-
})
|
|
226
|
-
.build();
|
|
227
|
-
|
|
228
|
-
const result = await engine.execute(wf);
|
|
229
|
-
expect(result.context.results.get('b')).toBe('hello world');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('parseOADWorkflow creates valid graph workflow', () => {
|
|
233
|
-
const def = {
|
|
234
|
-
name: 'onboarding',
|
|
235
|
-
steps: [
|
|
236
|
-
{ id: 'greet', type: 'action' as const, next: 'check' },
|
|
237
|
-
{ id: 'check', type: 'condition' as const, onTrue: 'existing', onFalse: 'new' },
|
|
238
|
-
{ id: 'existing', type: 'action' as const, next: 'done' },
|
|
239
|
-
{ id: 'new', type: 'action' as const, next: 'done' },
|
|
240
|
-
{ id: 'done', type: 'action' as const },
|
|
241
|
-
],
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const actionMap = new Map<string, (ctx: WorkflowContext) => Promise<any>>();
|
|
245
|
-
actionMap.set('greet', async () => 'hi');
|
|
246
|
-
actionMap.set('existing', async () => 'welcome back');
|
|
247
|
-
actionMap.set('new', async () => 'welcome');
|
|
248
|
-
actionMap.set('done', async () => 'bye');
|
|
249
|
-
|
|
250
|
-
const condMap = new Map<string, (ctx: WorkflowContext) => boolean>();
|
|
251
|
-
condMap.set('check', () => true);
|
|
252
|
-
|
|
253
|
-
const wf = parseOADWorkflow(def, actionMap, condMap);
|
|
254
|
-
expect(wf.name).toBe('onboarding');
|
|
255
|
-
expect(wf.entryPoint).toBe('greet');
|
|
256
|
-
expect(wf.steps.size).toBe(5);
|
|
257
|
-
expect(wf.steps.get('check')?.type).toBe('condition');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('parseOADWorkflow executes correctly', async () => {
|
|
261
|
-
const def = {
|
|
262
|
-
name: 'flow',
|
|
263
|
-
steps: [
|
|
264
|
-
{ id: 's1', type: 'action' as const, next: 's2' },
|
|
265
|
-
{ id: 's2', type: 'action' as const },
|
|
266
|
-
],
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const log: string[] = [];
|
|
270
|
-
const actionMap = new Map<string, (ctx: WorkflowContext) => Promise<any>>();
|
|
271
|
-
actionMap.set('s1', async () => { log.push('s1'); });
|
|
272
|
-
actionMap.set('s2', async () => { log.push('s2'); });
|
|
273
|
-
|
|
274
|
-
const wf = parseOADWorkflow(def, actionMap);
|
|
275
|
-
const result = await engine.execute(wf);
|
|
276
|
-
expect(result.status).toBe('completed');
|
|
277
|
-
expect(log).toEqual(['s1', 's2']);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
GraphWorkflowEngine,
|
|
4
|
+
WorkflowBuilder,
|
|
5
|
+
parseOADWorkflow,
|
|
6
|
+
type WorkflowContext,
|
|
7
|
+
type GraphWorkflow,
|
|
8
|
+
type GraphWorkflowStep,
|
|
9
|
+
} from '../src/core/workflow-graph';
|
|
10
|
+
|
|
11
|
+
function buildSimpleWorkflow(steps: Map<string, GraphWorkflowStep>, entry: string): GraphWorkflow {
|
|
12
|
+
return { name: 'test', entryPoint: entry, steps };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('GraphWorkflowEngine', () => {
|
|
16
|
+
const engine = new GraphWorkflowEngine();
|
|
17
|
+
|
|
18
|
+
it('executes a simple linear workflow', async () => {
|
|
19
|
+
const log: string[] = [];
|
|
20
|
+
const wf = new WorkflowBuilder()
|
|
21
|
+
.name('linear')
|
|
22
|
+
.start('a')
|
|
23
|
+
.addAction('a', async (ctx) => { log.push('a'); ctx.variables.set('x', 1); return 'done-a'; }, { next: 'b' })
|
|
24
|
+
.addAction('b', async (ctx) => { log.push('b'); return 'done-b'; }, { next: 'c' })
|
|
25
|
+
.addAction('c', async () => { log.push('c'); })
|
|
26
|
+
.build();
|
|
27
|
+
|
|
28
|
+
const result = await engine.execute(wf);
|
|
29
|
+
expect(result.status).toBe('completed');
|
|
30
|
+
expect(log).toEqual(['a', 'b', 'c']);
|
|
31
|
+
expect(result.context.results.get('a')).toBe('done-a');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('condition branches to onTrue', async () => {
|
|
35
|
+
const log: string[] = [];
|
|
36
|
+
const wf = new WorkflowBuilder()
|
|
37
|
+
.name('cond-true')
|
|
38
|
+
.start('check')
|
|
39
|
+
.addCondition('check', () => true, 'yes', 'no')
|
|
40
|
+
.addAction('yes', async () => { log.push('yes'); })
|
|
41
|
+
.addAction('no', async () => { log.push('no'); })
|
|
42
|
+
.build();
|
|
43
|
+
|
|
44
|
+
await engine.execute(wf);
|
|
45
|
+
expect(log).toEqual(['yes']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('condition branches to onFalse', async () => {
|
|
49
|
+
const log: string[] = [];
|
|
50
|
+
const wf = new WorkflowBuilder()
|
|
51
|
+
.name('cond-false')
|
|
52
|
+
.start('check')
|
|
53
|
+
.addCondition('check', () => false, 'yes', 'no')
|
|
54
|
+
.addAction('yes', async () => { log.push('yes'); })
|
|
55
|
+
.addAction('no', async () => { log.push('no'); })
|
|
56
|
+
.build();
|
|
57
|
+
|
|
58
|
+
await engine.execute(wf);
|
|
59
|
+
expect(log).toEqual(['no']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('condition uses context variables', async () => {
|
|
63
|
+
const wf = new WorkflowBuilder()
|
|
64
|
+
.name('cond-ctx')
|
|
65
|
+
.start('init')
|
|
66
|
+
.addAction('init', async (ctx) => { ctx.variables.set('flag', true); }, { next: 'check' })
|
|
67
|
+
.addCondition('check', (ctx) => ctx.variables.get('flag') === true, 'pass', 'fail')
|
|
68
|
+
.addAction('pass', async () => 'passed')
|
|
69
|
+
.addAction('fail', async () => 'failed')
|
|
70
|
+
.build();
|
|
71
|
+
|
|
72
|
+
const result = await engine.execute(wf);
|
|
73
|
+
expect(result.context.results.get('pass')).toBe('passed');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('parallel executes all steps', async () => {
|
|
77
|
+
const log: string[] = [];
|
|
78
|
+
const wf = new WorkflowBuilder()
|
|
79
|
+
.name('par')
|
|
80
|
+
.start('p')
|
|
81
|
+
.addAction('a', async () => { log.push('a'); })
|
|
82
|
+
.addAction('b', async () => { log.push('b'); })
|
|
83
|
+
.addParallel('p', ['a', 'b'], 'done')
|
|
84
|
+
.addAction('done', async () => { log.push('done'); })
|
|
85
|
+
.build();
|
|
86
|
+
|
|
87
|
+
const result = await engine.execute(wf);
|
|
88
|
+
expect(result.status).toBe('completed');
|
|
89
|
+
expect(log).toContain('a');
|
|
90
|
+
expect(log).toContain('b');
|
|
91
|
+
expect(log).toContain('done');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('loop iterates correct number of times', async () => {
|
|
95
|
+
let count = 0;
|
|
96
|
+
const wf = new WorkflowBuilder()
|
|
97
|
+
.name('loop')
|
|
98
|
+
.start('init')
|
|
99
|
+
.addAction('init', async (ctx) => { ctx.variables.set('i', 0); }, { next: 'loop' })
|
|
100
|
+
.addAction('body', async (ctx) => {
|
|
101
|
+
const i = ctx.variables.get('i');
|
|
102
|
+
ctx.variables.set('i', i + 1);
|
|
103
|
+
count++;
|
|
104
|
+
})
|
|
105
|
+
.addLoop('loop', (ctx) => (ctx.variables.get('i') ?? 0) < 5, 'body')
|
|
106
|
+
.build();
|
|
107
|
+
|
|
108
|
+
await engine.execute(wf);
|
|
109
|
+
expect(count).toBe(5);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('loop respects maxIterations', async () => {
|
|
113
|
+
let count = 0;
|
|
114
|
+
const wf = new WorkflowBuilder()
|
|
115
|
+
.name('loop-max')
|
|
116
|
+
.start('loop')
|
|
117
|
+
.addAction('body', async () => { count++; })
|
|
118
|
+
.addLoop('loop', () => true, 'body', { maxIterations: 3 })
|
|
119
|
+
.build();
|
|
120
|
+
|
|
121
|
+
await engine.execute(wf);
|
|
122
|
+
expect(count).toBe(3);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('retries on failure then succeeds', async () => {
|
|
126
|
+
let attempts = 0;
|
|
127
|
+
const wf = new WorkflowBuilder()
|
|
128
|
+
.name('retry-ok')
|
|
129
|
+
.start('flaky')
|
|
130
|
+
.addAction('flaky', async () => {
|
|
131
|
+
attempts++;
|
|
132
|
+
if (attempts < 3) throw new Error('fail');
|
|
133
|
+
return 'ok';
|
|
134
|
+
}, { retryCount: 3, retryDelay: 1 })
|
|
135
|
+
.build();
|
|
136
|
+
|
|
137
|
+
const result = await engine.execute(wf);
|
|
138
|
+
expect(result.status).toBe('completed');
|
|
139
|
+
expect(attempts).toBe(3);
|
|
140
|
+
expect(result.context.results.get('flaky')).toBe('ok');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('retry exhaustion records error', async () => {
|
|
144
|
+
const wf = new WorkflowBuilder()
|
|
145
|
+
.name('retry-fail')
|
|
146
|
+
.start('bad')
|
|
147
|
+
.addAction('bad', async () => { throw new Error('always-fail'); }, { retryCount: 2, retryDelay: 1, onError: 'stop' })
|
|
148
|
+
.build();
|
|
149
|
+
|
|
150
|
+
const result = await engine.execute(wf);
|
|
151
|
+
expect(result.status).toBe('failed');
|
|
152
|
+
expect(result.context.errors).toHaveLength(1);
|
|
153
|
+
expect(result.context.errors[0].step).toBe('bad');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('timeout kills slow step', async () => {
|
|
157
|
+
const wf = new WorkflowBuilder()
|
|
158
|
+
.name('timeout')
|
|
159
|
+
.start('slow')
|
|
160
|
+
.addAction('slow', async () => {
|
|
161
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
162
|
+
}, { timeout: 50, onError: 'stop' })
|
|
163
|
+
.build();
|
|
164
|
+
|
|
165
|
+
const result = await engine.execute(wf);
|
|
166
|
+
expect(result.status).toBe('failed');
|
|
167
|
+
expect(result.context.errors[0].error.message).toContain('Timeout');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('onError skip continues to next step', async () => {
|
|
171
|
+
const log: string[] = [];
|
|
172
|
+
const wf = new WorkflowBuilder()
|
|
173
|
+
.name('skip')
|
|
174
|
+
.start('bad')
|
|
175
|
+
.addAction('bad', async () => { throw new Error('oops'); }, { onError: 'skip', next: 'good' })
|
|
176
|
+
.addAction('good', async () => { log.push('good'); })
|
|
177
|
+
.build();
|
|
178
|
+
|
|
179
|
+
const result = await engine.execute(wf);
|
|
180
|
+
// skip doesn't add to errors, doesn't throw
|
|
181
|
+
expect(log).toEqual(['good']);
|
|
182
|
+
expect(result.status).toBe('completed');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('onError stop halts execution', async () => {
|
|
186
|
+
const log: string[] = [];
|
|
187
|
+
const wf = new WorkflowBuilder()
|
|
188
|
+
.name('stop')
|
|
189
|
+
.start('bad')
|
|
190
|
+
.addAction('bad', async () => { throw new Error('halt'); }, { onError: 'stop', next: 'after' })
|
|
191
|
+
.addAction('after', async () => { log.push('after'); })
|
|
192
|
+
.build();
|
|
193
|
+
|
|
194
|
+
const result = await engine.execute(wf);
|
|
195
|
+
expect(result.status).toBe('failed');
|
|
196
|
+
expect(log).toEqual([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('WorkflowBuilder creates valid workflow', () => {
|
|
200
|
+
const wf = new WorkflowBuilder()
|
|
201
|
+
.name('builder-test')
|
|
202
|
+
.start('s1')
|
|
203
|
+
.addAction('s1', async () => 'ok', { next: 's2' })
|
|
204
|
+
.addAction('s2', async () => 'done')
|
|
205
|
+
.build();
|
|
206
|
+
|
|
207
|
+
expect(wf.name).toBe('builder-test');
|
|
208
|
+
expect(wf.entryPoint).toBe('s1');
|
|
209
|
+
expect(wf.steps.size).toBe(2);
|
|
210
|
+
expect(wf.steps.get('s1')?.next).toBe('s2');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('WorkflowBuilder throws without start', () => {
|
|
214
|
+
expect(() => new WorkflowBuilder().addAction('a', async () => {}).build())
|
|
215
|
+
.toThrow('entry point');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('context variables persist across steps', async () => {
|
|
219
|
+
const wf = new WorkflowBuilder()
|
|
220
|
+
.name('persist')
|
|
221
|
+
.start('a')
|
|
222
|
+
.addAction('a', async (ctx) => { ctx.variables.set('msg', 'hello'); }, { next: 'b' })
|
|
223
|
+
.addAction('b', async (ctx) => {
|
|
224
|
+
return ctx.variables.get('msg') + ' world';
|
|
225
|
+
})
|
|
226
|
+
.build();
|
|
227
|
+
|
|
228
|
+
const result = await engine.execute(wf);
|
|
229
|
+
expect(result.context.results.get('b')).toBe('hello world');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('parseOADWorkflow creates valid graph workflow', () => {
|
|
233
|
+
const def = {
|
|
234
|
+
name: 'onboarding',
|
|
235
|
+
steps: [
|
|
236
|
+
{ id: 'greet', type: 'action' as const, next: 'check' },
|
|
237
|
+
{ id: 'check', type: 'condition' as const, onTrue: 'existing', onFalse: 'new' },
|
|
238
|
+
{ id: 'existing', type: 'action' as const, next: 'done' },
|
|
239
|
+
{ id: 'new', type: 'action' as const, next: 'done' },
|
|
240
|
+
{ id: 'done', type: 'action' as const },
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const actionMap = new Map<string, (ctx: WorkflowContext) => Promise<any>>();
|
|
245
|
+
actionMap.set('greet', async () => 'hi');
|
|
246
|
+
actionMap.set('existing', async () => 'welcome back');
|
|
247
|
+
actionMap.set('new', async () => 'welcome');
|
|
248
|
+
actionMap.set('done', async () => 'bye');
|
|
249
|
+
|
|
250
|
+
const condMap = new Map<string, (ctx: WorkflowContext) => boolean>();
|
|
251
|
+
condMap.set('check', () => true);
|
|
252
|
+
|
|
253
|
+
const wf = parseOADWorkflow(def, actionMap, condMap);
|
|
254
|
+
expect(wf.name).toBe('onboarding');
|
|
255
|
+
expect(wf.entryPoint).toBe('greet');
|
|
256
|
+
expect(wf.steps.size).toBe(5);
|
|
257
|
+
expect(wf.steps.get('check')?.type).toBe('condition');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('parseOADWorkflow executes correctly', async () => {
|
|
261
|
+
const def = {
|
|
262
|
+
name: 'flow',
|
|
263
|
+
steps: [
|
|
264
|
+
{ id: 's1', type: 'action' as const, next: 's2' },
|
|
265
|
+
{ id: 's2', type: 'action' as const },
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const log: string[] = [];
|
|
270
|
+
const actionMap = new Map<string, (ctx: WorkflowContext) => Promise<any>>();
|
|
271
|
+
actionMap.set('s1', async () => { log.push('s1'); });
|
|
272
|
+
actionMap.set('s2', async () => { log.push('s2'); });
|
|
273
|
+
|
|
274
|
+
const wf = parseOADWorkflow(def, actionMap);
|
|
275
|
+
const result = await engine.execute(wf);
|
|
276
|
+
expect(result.status).toBe('completed');
|
|
277
|
+
expect(log).toEqual(['s1', 's2']);
|
|
278
|
+
});
|
|
279
|
+
});
|