@ai-setting/roy-agent-core 1.0.0
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/dist/index.js +99145 -0
- package/package.json +114 -0
- package/src/config/config-component.test.ts +627 -0
- package/src/config/config-component.ts +906 -0
- package/src/config/config-parser.test.ts +319 -0
- package/src/config/config-parser.ts +203 -0
- package/src/config/decentralized-config.test.ts +740 -0
- package/src/config/env-key.ts +210 -0
- package/src/config/env-source.test.ts +252 -0
- package/src/config/env-source.ts +301 -0
- package/src/config/file-source.test.ts +357 -0
- package/src/config/file-source.ts +421 -0
- package/src/config/index.ts +24 -0
- package/src/config/protocol-resolver.test.ts +217 -0
- package/src/config/protocol-resolver.ts +228 -0
- package/src/env/agent/agent-component.abort.test.ts +511 -0
- package/src/env/agent/agent-component.record-session.test.ts +349 -0
- package/src/env/agent/agent-component.test.ts +1389 -0
- package/src/env/agent/agent-component.tool-error.test.ts +327 -0
- package/src/env/agent/agent-component.ts +1711 -0
- package/src/env/agent/agent-config-registration.test.ts +226 -0
- package/src/env/agent/agent-config-registration.ts +46 -0
- package/src/env/agent/agent-reminder-plugin.integration.test.ts +243 -0
- package/src/env/agent/index.ts +10 -0
- package/src/env/agent/summary-agent.parse-hint.test.ts +360 -0
- package/src/env/agent/summary-agent.ts +508 -0
- package/src/env/agent/types.ts +536 -0
- package/src/env/commands/commands-component.test.ts +364 -0
- package/src/env/commands/commands-component.ts +604 -0
- package/src/env/commands/commands-config-registration.test.ts +198 -0
- package/src/env/commands/commands-config-registration.ts +38 -0
- package/src/env/commands/index.ts +21 -0
- package/src/env/commands/parser.test.ts +203 -0
- package/src/env/commands/parser.ts +115 -0
- package/src/env/commands/types.ts +184 -0
- package/src/env/commands-prompt-integration.test.ts +243 -0
- package/src/env/component-env.test.ts +119 -0
- package/src/env/component.ts +335 -0
- package/src/env/constants.test.ts +72 -0
- package/src/env/constants.ts +123 -0
- package/src/env/debug/debug-component.test.ts +114 -0
- package/src/env/debug/debug-component.ts +547 -0
- package/src/env/debug/formatters/index.ts +9 -0
- package/src/env/debug/formatters/repl-formatter.test.ts +139 -0
- package/src/env/debug/formatters/repl-formatter.ts +358 -0
- package/src/env/debug/formatters/trace-formatter.test.ts +119 -0
- package/src/env/debug/formatters/trace-formatter.ts +191 -0
- package/src/env/debug/formatters/tree-formatter.test.ts +107 -0
- package/src/env/debug/formatters/tree-formatter.ts +325 -0
- package/src/env/debug/index.ts +38 -0
- package/src/env/debug/parser/regex-parser.test.ts +201 -0
- package/src/env/debug/parser/regex-parser.ts +196 -0
- package/src/env/debug/parser/span-builder.test.ts +241 -0
- package/src/env/debug/parser/span-builder.ts +386 -0
- package/src/env/debug/reader/log-reader.test.ts +170 -0
- package/src/env/debug/reader/log-reader.ts +186 -0
- package/src/env/debug/reader/span-db-reader.test.ts +118 -0
- package/src/env/debug/reader/span-db-reader.ts +201 -0
- package/src/env/debug/types.test.ts +187 -0
- package/src/env/debug/types.ts +171 -0
- package/src/env/environment-init.test.ts +183 -0
- package/src/env/environment-lifecycle.test.ts +516 -0
- package/src/env/environment-service.test.ts +332 -0
- package/src/env/environment.handle-query.test.ts +96 -0
- package/src/env/environment.test.ts +232 -0
- package/src/env/environment.ts +708 -0
- package/src/env/errors.test.ts +165 -0
- package/src/env/errors.ts +157 -0
- package/src/env/event-source/event-source-agent-handler.test.ts +193 -0
- package/src/env/event-source/event-source-agent-handler.ts +111 -0
- package/src/env/event-source/event-source-component.process-cleanup.test.ts +236 -0
- package/src/env/event-source/event-source-component.stop.test.ts +346 -0
- package/src/env/event-source/event-source-component.test.ts +1207 -0
- package/src/env/event-source/event-source-component.ts +1379 -0
- package/src/env/event-source/event-source-config-registration.test.ts +242 -0
- package/src/env/event-source/event-source-config-registration.ts +37 -0
- package/src/env/event-source/event-source-integration.test.ts +320 -0
- package/src/env/event-source/event-source-platform.test.ts +630 -0
- package/src/env/event-source/types.ts +298 -0
- package/src/env/hook/global-hook-manager.ts +162 -0
- package/src/env/hook/hook-manager.test.ts +374 -0
- package/src/env/hook/hook-manager.ts +309 -0
- package/src/env/hook/index.ts +38 -0
- package/src/env/hook/types.ts +138 -0
- package/src/env/index.ts +144 -0
- package/src/env/interface.ts +203 -0
- package/src/env/llm/hooks.test.ts +293 -0
- package/src/env/llm/hooks.ts +316 -0
- package/src/env/llm/index.ts +61 -0
- package/src/env/llm/invoke-threshold-check.test.ts +88 -0
- package/src/env/llm/invoke-timeout.test.ts +54 -0
- package/src/env/llm/invoke.test.ts +71 -0
- package/src/env/llm/invoke.ts +1039 -0
- package/src/env/llm/llm-config.test.ts +523 -0
- package/src/env/llm/llm.test.ts +233 -0
- package/src/env/llm/llm.ts +568 -0
- package/src/env/llm/provider.test.ts +182 -0
- package/src/env/llm/provider.ts +108 -0
- package/src/env/llm/transform.test.ts +251 -0
- package/src/env/llm/transform.ts +286 -0
- package/src/env/llm/types.test.ts +580 -0
- package/src/env/llm/types.ts +424 -0
- package/src/env/log-trace/decorator-otel.test.ts +182 -0
- package/src/env/log-trace/decorator.ts +230 -0
- package/src/env/log-trace/index.ts +79 -0
- package/src/env/log-trace/log-trace-component.test.ts +242 -0
- package/src/env/log-trace/log-trace-component.ts +497 -0
- package/src/env/log-trace/log-trace-config-registration.test.ts +348 -0
- package/src/env/log-trace/log-trace-config-registration.ts +45 -0
- package/src/env/log-trace/logger.test.ts +149 -0
- package/src/env/log-trace/logger.ts +522 -0
- package/src/env/log-trace/opentelemetry/cli-propagation.test.ts +147 -0
- package/src/env/log-trace/opentelemetry/cli-propagation.ts +194 -0
- package/src/env/log-trace/opentelemetry/integration.test.ts +668 -0
- package/src/env/log-trace/opentelemetry/mod.ts +25 -0
- package/src/env/log-trace/opentelemetry/propagation-env.test.ts +181 -0
- package/src/env/log-trace/opentelemetry/propagation-env.ts +136 -0
- package/src/env/log-trace/opentelemetry/propagation.test.ts +259 -0
- package/src/env/log-trace/opentelemetry/propagation.ts +215 -0
- package/src/env/log-trace/opentelemetry/tracer-provider-context.test.ts +166 -0
- package/src/env/log-trace/opentelemetry/tracer-provider.test.ts +379 -0
- package/src/env/log-trace/opentelemetry/tracer-provider.ts +612 -0
- package/src/env/log-trace/span-storage.test.ts +145 -0
- package/src/env/log-trace/span-storage.ts +230 -0
- package/src/env/log-trace/trace-context.test.ts +187 -0
- package/src/env/log-trace/trace-context.ts +162 -0
- package/src/env/log-trace/types.test.ts +63 -0
- package/src/env/log-trace/types.ts +172 -0
- package/src/env/mcp/README.md +244 -0
- package/src/env/mcp/__integration__/mcp-component.integration.test.ts +373 -0
- package/src/env/mcp/config.test.ts +74 -0
- package/src/env/mcp/config.ts +116 -0
- package/src/env/mcp/index.ts +41 -0
- package/src/env/mcp/loader.test.ts +161 -0
- package/src/env/mcp/loader.ts +209 -0
- package/src/env/mcp/mcp-component.test.ts +111 -0
- package/src/env/mcp/mcp-component.ts +358 -0
- package/src/env/mcp/mcp-config-registration.test.ts +304 -0
- package/src/env/mcp/mcp-config-registration.ts +50 -0
- package/src/env/mcp/scanner.test.ts +170 -0
- package/src/env/mcp/scanner.ts +246 -0
- package/src/env/mcp/tool/adapter.test.ts +520 -0
- package/src/env/mcp/tool/adapter.ts +521 -0
- package/src/env/mcp/tool/index.ts +5 -0
- package/src/env/mcp/types.test.ts +171 -0
- package/src/env/mcp/types.ts +79 -0
- package/src/env/memory/README.md +177 -0
- package/src/env/memory/built-in/index.ts +59 -0
- package/src/env/memory/built-in/recall-memory.ts +103 -0
- package/src/env/memory/built-in/record-memory.ts +148 -0
- package/src/env/memory/index.ts +20 -0
- package/src/env/memory/memory-component.test.ts +239 -0
- package/src/env/memory/memory-component.ts +503 -0
- package/src/env/memory/memory-config-registration.test.ts +67 -0
- package/src/env/memory/memory-config-registration.ts +48 -0
- package/src/env/memory/memory-config.ts +45 -0
- package/src/env/memory/memory-file.test.ts +268 -0
- package/src/env/memory/plugin/index.ts +48 -0
- package/src/env/memory/plugin/memory-agent.test.ts +249 -0
- package/src/env/memory/plugin/memory-agent.ts +365 -0
- package/src/env/memory/plugin/memory-manager.ts +198 -0
- package/src/env/memory/plugin/memory-plugin-agent.test.ts +145 -0
- package/src/env/memory/plugin/memory-plugin.ts +210 -0
- package/src/env/memory/plugin/plugin-simplified.test.ts +51 -0
- package/src/env/memory/plugin/recall-memory.test.ts +106 -0
- package/src/env/memory/plugin/recall-memory.ts +53 -0
- package/src/env/memory/plugin/types.ts +101 -0
- package/src/env/memory/tools/memory-agent-tools.ts +228 -0
- package/src/env/memory/types.ts +85 -0
- package/src/env/paths.ts +118 -0
- package/src/env/prompt/index.ts +18 -0
- package/src/env/prompt/memory-prompts.test.ts +91 -0
- package/src/env/prompt/prompt-component.test.ts +491 -0
- package/src/env/prompt/prompt-component.ts +619 -0
- package/src/env/prompt/prompt-config-registration.test.ts +213 -0
- package/src/env/prompt/prompt-config-registration.ts +39 -0
- package/src/env/prompt/prompts-index.ts +504 -0
- package/src/env/prompt/renderer.ts +67 -0
- package/src/env/prompt/types.ts +136 -0
- package/src/env/session/hooks.ts +18 -0
- package/src/env/session/index.ts +37 -0
- package/src/env/session/search-query-parser.test.ts +425 -0
- package/src/env/session/search-query-parser.ts +171 -0
- package/src/env/session/session-checkpoint.test.ts +523 -0
- package/src/env/session/session-component.extract-recent-messages.test.ts +209 -0
- package/src/env/session/session-component.test.ts +132 -0
- package/src/env/session/session-component.ts +1249 -0
- package/src/env/session/session-config-registration.test.ts +138 -0
- package/src/env/session/session-config-registration.ts +52 -0
- package/src/env/session/session-message-converter.test.ts +763 -0
- package/src/env/session/session-message-converter.ts +415 -0
- package/src/env/session/session-message-e2e.test.ts +448 -0
- package/src/env/session/session-search.test.ts +391 -0
- package/src/env/session/session-store.test.ts +362 -0
- package/src/env/session/session-store.ts +141 -0
- package/src/env/session/storage/index.ts +6 -0
- package/src/env/session/storage/memory.ts +502 -0
- package/src/env/session/storage/sqlite.ts +794 -0
- package/src/env/session/types.ts +742 -0
- package/src/env/skill/config.ts +39 -0
- package/src/env/skill/index.ts +6 -0
- package/src/env/skill/parser.test.ts +116 -0
- package/src/env/skill/parser.ts +77 -0
- package/src/env/skill/scanner.test.ts +211 -0
- package/src/env/skill/scanner.ts +119 -0
- package/src/env/skill/skill-component.test.ts +234 -0
- package/src/env/skill/skill-component.ts +352 -0
- package/src/env/skill/skill-config-registration.test.ts +60 -0
- package/src/env/skill/skill-config-registration.ts +43 -0
- package/src/env/skill/tool/index.ts +1 -0
- package/src/env/skill/tool/skill-tool.test.ts +100 -0
- package/src/env/skill/tool/skill-tool.ts +72 -0
- package/src/env/skill/types.ts +64 -0
- package/src/env/task/delegate/delegate-tool.test.ts +498 -0
- package/src/env/task/delegate/delegate-tool.ts +1014 -0
- package/src/env/task/delegate/index.ts +18 -0
- package/src/env/task/delegate/stop-tool.test.ts +140 -0
- package/src/env/task/delegate/stop-tool.ts +119 -0
- package/src/env/task/delegate/task-events.test.ts +178 -0
- package/src/env/task/delegate/task-events.ts +143 -0
- package/src/env/task/hooks/contexts.test.ts +92 -0
- package/src/env/task/hooks/contexts.ts +192 -0
- package/src/env/task/hooks/index.ts +23 -0
- package/src/env/task/hooks/task-hook-points.test.ts +32 -0
- package/src/env/task/hooks/task-hook-points.ts +54 -0
- package/src/env/task/index.ts +7 -0
- package/src/env/task/plugins/index.ts +13 -0
- package/src/env/task/plugins/task-plugin.test.ts +74 -0
- package/src/env/task/plugins/task-plugin.ts +89 -0
- package/src/env/task/plugins/task-tag-plugin.test.ts +377 -0
- package/src/env/task/plugins/task-tag-plugin.ts +319 -0
- package/src/env/task/plugins/task-workflow-extractor.integration.test.ts +226 -0
- package/src/env/task/plugins/workflow-extractor-agent.test.ts +107 -0
- package/src/env/task/plugins/workflow-extractor-agent.ts +225 -0
- package/src/env/task/storage/index.ts +6 -0
- package/src/env/task/storage/sqlite-task-store.test.ts +283 -0
- package/src/env/task/storage/sqlite-task-store.ts +903 -0
- package/src/env/task/storage/task-search.test.ts +291 -0
- package/src/env/task/tag-service.test.ts +198 -0
- package/src/env/task/tag-service.ts +264 -0
- package/src/env/task/task-component.test.ts +193 -0
- package/src/env/task/task-component.ts +658 -0
- package/src/env/task/task-config-registration.test.ts +57 -0
- package/src/env/task/task-config-registration.ts +37 -0
- package/src/env/task/task-types.test.ts +137 -0
- package/src/env/task/tools/complete-tool.ts +44 -0
- package/src/env/task/tools/create-tool.ts +49 -0
- package/src/env/task/tools/delete-tool.ts +43 -0
- package/src/env/task/tools/get-tool.ts +59 -0
- package/src/env/task/tools/index.ts +10 -0
- package/src/env/task/tools/list-tool.ts +40 -0
- package/src/env/task/tools/operation/create-tool.ts +48 -0
- package/src/env/task/tools/operation/delete-tool.ts +43 -0
- package/src/env/task/tools/operation/get-tool.ts +43 -0
- package/src/env/task/tools/operation/index.ts +9 -0
- package/src/env/task/tools/operation/list-tool.ts +40 -0
- package/src/env/task/tools/operation/operation-tools.test.ts +274 -0
- package/src/env/task/tools/operation/operation-types.ts +75 -0
- package/src/env/task/tools/operation/update-tool.ts +47 -0
- package/src/env/task/tools/task-tools.test.ts +203 -0
- package/src/env/task/tools/task-types.test.ts +75 -0
- package/src/env/task/tools/task-types.ts +68 -0
- package/src/env/task/tools/update-tool.ts +70 -0
- package/src/env/task/types.ts +160 -0
- package/src/env/tool/built-in/bash.ts +201 -0
- package/src/env/tool/built-in/echo.ts +29 -0
- package/src/env/tool/built-in/edit-file.test.ts +136 -0
- package/src/env/tool/built-in/edit-file.ts +92 -0
- package/src/env/tool/built-in/glob.test.ts +94 -0
- package/src/env/tool/built-in/glob.ts +65 -0
- package/src/env/tool/built-in/grep.test.ts +122 -0
- package/src/env/tool/built-in/grep.ts +108 -0
- package/src/env/tool/built-in/index.ts +44 -0
- package/src/env/tool/built-in/read-file.test.ts +84 -0
- package/src/env/tool/built-in/read-file.ts +75 -0
- package/src/env/tool/built-in/write-file.test.ts +119 -0
- package/src/env/tool/built-in/write-file.ts +68 -0
- package/src/env/tool/index.ts +24 -0
- package/src/env/tool/registry.test.ts +257 -0
- package/src/env/tool/registry.ts +167 -0
- package/src/env/tool/tool-component.test.ts +559 -0
- package/src/env/tool/tool-component.ts +563 -0
- package/src/env/tool/tool-config-registration.test.ts +249 -0
- package/src/env/tool/tool-config-registration.ts +46 -0
- package/src/env/tool/types.ts +267 -0
- package/src/env/tool/validator.test.ts +143 -0
- package/src/env/tool/validator.ts +44 -0
- package/src/env/types.ts +180 -0
- package/src/env/workflow/ask-user-tool-registration.test.ts +216 -0
- package/src/env/workflow/complex-workflow.integration.test.ts +1900 -0
- package/src/env/workflow/decorators/decorator-node.ts +229 -0
- package/src/env/workflow/decorators/decorator.test.ts +196 -0
- package/src/env/workflow/decorators/edge.ts +82 -0
- package/src/env/workflow/decorators/index.ts +31 -0
- package/src/env/workflow/decorators/node-as.ts +98 -0
- package/src/env/workflow/decorators/workflow.ts +54 -0
- package/src/env/workflow/engine/dag-manager.test.ts +570 -0
- package/src/env/workflow/engine/dag-manager.ts +594 -0
- package/src/env/workflow/engine/engine.ts +1422 -0
- package/src/env/workflow/engine/event-bus.test.ts +359 -0
- package/src/env/workflow/engine/event-bus.ts +156 -0
- package/src/env/workflow/engine/executor-agent-session.test.ts +84 -0
- package/src/env/workflow/engine/executor.test.ts +619 -0
- package/src/env/workflow/engine/executor.ts +593 -0
- package/src/env/workflow/engine/index.ts +24 -0
- package/src/env/workflow/engine/node-registry.test.ts +560 -0
- package/src/env/workflow/engine/node-registry.ts +289 -0
- package/src/env/workflow/engine/resume-removed.test.ts +22 -0
- package/src/env/workflow/engine/scheduler.test.ts +715 -0
- package/src/env/workflow/engine/scheduler.ts +318 -0
- package/src/env/workflow/engine/workflow-engine.test.ts +815 -0
- package/src/env/workflow/extractor/workflow-converter.ts +306 -0
- package/src/env/workflow/fixtures.ts +380 -0
- package/src/env/workflow/index.ts +38 -0
- package/src/env/workflow/integration/run-resume-unified.test.ts +186 -0
- package/src/env/workflow/integration/service-integration.test.ts +267 -0
- package/src/env/workflow/metadata/keys.ts +12 -0
- package/src/env/workflow/nodes/agent-component-adapter.test.ts +318 -0
- package/src/env/workflow/nodes/agent-component-adapter.ts +448 -0
- package/src/env/workflow/nodes/agent-node.test.ts +371 -0
- package/src/env/workflow/nodes/agent-node.ts +598 -0
- package/src/env/workflow/nodes/ask-user-node.ts +113 -0
- package/src/env/workflow/nodes/condition-node.ts +200 -0
- package/src/env/workflow/nodes/index.ts +9 -0
- package/src/env/workflow/nodes/merge-node.ts +141 -0
- package/src/env/workflow/nodes/skill-node.test.ts +253 -0
- package/src/env/workflow/nodes/skill-node.ts +393 -0
- package/src/env/workflow/nodes/tool-node.test.ts +251 -0
- package/src/env/workflow/nodes/tool-node.ts +493 -0
- package/src/env/workflow/nodes/workflow-llm-history.test.ts +455 -0
- package/src/env/workflow/nodes/workflow-node.test.ts +315 -0
- package/src/env/workflow/nodes/workflow-node.ts +311 -0
- package/src/env/workflow/service/index.ts +27 -0
- package/src/env/workflow/service/registry.test.ts +133 -0
- package/src/env/workflow/service/registry.ts +71 -0
- package/src/env/workflow/service/workflow-service.test.ts +310 -0
- package/src/env/workflow/service/workflow-service.ts +393 -0
- package/src/env/workflow/storage/index.ts +28 -0
- package/src/env/workflow/storage/mock-repositories.ts +385 -0
- package/src/env/workflow/storage/sqlite.test.ts +179 -0
- package/src/env/workflow/storage/sqlite.ts +163 -0
- package/src/env/workflow/storage/workflow-repo.test.ts +780 -0
- package/src/env/workflow/storage/workflow-repo.ts +342 -0
- package/src/env/workflow/tools/ask-user-tool.ts +82 -0
- package/src/env/workflow/tools/index.ts +26 -0
- package/src/env/workflow/tools/run-workflow.test.ts +352 -0
- package/src/env/workflow/tools/run-workflow.ts +214 -0
- package/src/env/workflow/types/context.ts +18 -0
- package/src/env/workflow/types/decorators-types.ts +198 -0
- package/src/env/workflow/types/event.test.ts +515 -0
- package/src/env/workflow/types/event.ts +193 -0
- package/src/env/workflow/types/index.ts +49 -0
- package/src/env/workflow/types/run.test.ts +437 -0
- package/src/env/workflow/types/run.ts +173 -0
- package/src/env/workflow/types/workflow-hil.ts +114 -0
- package/src/env/workflow/types/workflow-message.test.ts +138 -0
- package/src/env/workflow/types/workflow-message.ts +196 -0
- package/src/env/workflow/types/workflow-session.test.ts +95 -0
- package/src/env/workflow/types/workflow-session.ts +59 -0
- package/src/env/workflow/types/workflow.test.ts +495 -0
- package/src/env/workflow/types/workflow.ts +195 -0
- package/src/env/workflow/types_compat.ts +51 -0
- package/src/env/workflow/utils/create-workflow.ts +47 -0
- package/src/env/workflow/utils/execution-state.ts +245 -0
- package/src/env/workflow/utils/index.ts +18 -0
- package/src/env/workflow/utils/node-registry-helper.ts +58 -0
- package/src/env/workflow/utils/recovery-validator.test.ts +460 -0
- package/src/env/workflow/utils/recovery-validator.ts +377 -0
- package/src/env/workflow/utils/session-parser.test.ts +111 -0
- package/src/env/workflow/utils/session-parser.ts +94 -0
- package/src/env/workflow/utils/session-recovery.test.ts +334 -0
- package/src/env/workflow/utils/session-recovery.ts +188 -0
- package/src/env/workflow/utils/template-resolver.test.ts +258 -0
- package/src/env/workflow/utils/template-resolver.ts +436 -0
- package/src/env/workflow/utils/validation-rules.ts +149 -0
- package/src/env/workflow/workflow-component.ts +544 -0
- package/src/index.ts +422 -0
- package/src/utils/id.ts +21 -0
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complex Multi-Node Workflow Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for complex workflow patterns including:
|
|
5
|
+
* - Parallel execution branches
|
|
6
|
+
* - Diamond patterns (merge and fork)
|
|
7
|
+
* - Error handling and retry logic
|
|
8
|
+
* - Conditional execution
|
|
9
|
+
* - Template variable passing
|
|
10
|
+
* - Nested workflows
|
|
11
|
+
* - Complex dependency graphs
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { WorkflowDefinition, NodeDefinition } from './types';
|
|
17
|
+
|
|
18
|
+
// ==================== Mock Workflow Executor ====================
|
|
19
|
+
|
|
20
|
+
interface MockNodeExecution {
|
|
21
|
+
nodeId: string;
|
|
22
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
23
|
+
input: any;
|
|
24
|
+
output: any;
|
|
25
|
+
error?: string;
|
|
26
|
+
startTime?: number;
|
|
27
|
+
endTime?: number;
|
|
28
|
+
retryCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ExecutionTrace {
|
|
32
|
+
nodes: Map<string, MockNodeExecution>;
|
|
33
|
+
executionOrder: string[];
|
|
34
|
+
parallelBatches: string[][];
|
|
35
|
+
errors: Array<{ nodeId: string; error: string; attempt: number }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Synchronous Workflow Executor for Testing
|
|
40
|
+
*/
|
|
41
|
+
class SyncWorkflowExecutor {
|
|
42
|
+
private trace: ExecutionTrace;
|
|
43
|
+
private nodeConfigs: Map<string, any>;
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
this.trace = {
|
|
47
|
+
nodes: new Map(),
|
|
48
|
+
executionOrder: [],
|
|
49
|
+
parallelBatches: [],
|
|
50
|
+
errors: [],
|
|
51
|
+
};
|
|
52
|
+
this.nodeConfigs = new Map();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setNodeConfig(nodeId: string, config: any): void {
|
|
56
|
+
this.nodeConfigs.set(nodeId, config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getTrace(): ExecutionTrace {
|
|
60
|
+
return this.trace;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearTrace(): void {
|
|
64
|
+
this.trace = {
|
|
65
|
+
nodes: new Map(),
|
|
66
|
+
executionOrder: [],
|
|
67
|
+
parallelBatches: [],
|
|
68
|
+
errors: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
executeNode(nodeId: string, input: any, dependencies: Map<string, any>, nodeType?: string): any {
|
|
73
|
+
const config = this.nodeConfigs.get(nodeId) || {};
|
|
74
|
+
|
|
75
|
+
// Initialize trace entry
|
|
76
|
+
const traceEntry: MockNodeExecution = {
|
|
77
|
+
nodeId,
|
|
78
|
+
status: 'pending',
|
|
79
|
+
input,
|
|
80
|
+
output: null,
|
|
81
|
+
retryCount: 0,
|
|
82
|
+
};
|
|
83
|
+
this.trace.nodes.set(nodeId, traceEntry);
|
|
84
|
+
|
|
85
|
+
// Execute node
|
|
86
|
+
traceEntry.status = 'running';
|
|
87
|
+
traceEntry.startTime = Date.now();
|
|
88
|
+
|
|
89
|
+
// Use provided nodeType or fall back to config.type
|
|
90
|
+
const effectiveNodeType = nodeType || config.type || 'unknown';
|
|
91
|
+
let output: any;
|
|
92
|
+
|
|
93
|
+
switch (effectiveNodeType) {
|
|
94
|
+
case 'tool':
|
|
95
|
+
output = this.executeToolNode(nodeId, config, input);
|
|
96
|
+
break;
|
|
97
|
+
case 'skill':
|
|
98
|
+
output = this.executeSkillNode(nodeId, config, input, dependencies);
|
|
99
|
+
break;
|
|
100
|
+
case 'agent':
|
|
101
|
+
output = this.executeAgentNode(nodeId, config, input, dependencies);
|
|
102
|
+
break;
|
|
103
|
+
case 'condition':
|
|
104
|
+
output = this.executeConditionNode(nodeId, config, input);
|
|
105
|
+
break;
|
|
106
|
+
case 'merge':
|
|
107
|
+
output = this.executeMergeNode(nodeId, config, dependencies);
|
|
108
|
+
break;
|
|
109
|
+
case 'nested':
|
|
110
|
+
case 'workflow':
|
|
111
|
+
output = this.executeNestedWorkflowNode(nodeId, config, input);
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
output = { result: `${nodeId}_result`, input };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
traceEntry.output = output;
|
|
118
|
+
traceEntry.status = 'completed';
|
|
119
|
+
traceEntry.endTime = Date.now();
|
|
120
|
+
this.trace.executionOrder.push(nodeId);
|
|
121
|
+
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private executeToolNode(nodeId: string, config: any, input: any): any {
|
|
126
|
+
const toolName = config.tool || 'default-tool';
|
|
127
|
+
if (toolName === 'echo') {
|
|
128
|
+
return { echo: input, message: config.message };
|
|
129
|
+
}
|
|
130
|
+
if (toolName === 'calculator') {
|
|
131
|
+
return { result: 42, operation: config.operation };
|
|
132
|
+
}
|
|
133
|
+
if (toolName === 'validator') {
|
|
134
|
+
return { valid: true, validated: input, source: config.source };
|
|
135
|
+
}
|
|
136
|
+
return { tool: toolName, result: `processed_by_${toolName}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private executeSkillNode(
|
|
140
|
+
nodeId: string,
|
|
141
|
+
config: any,
|
|
142
|
+
input: any,
|
|
143
|
+
dependencies: Map<string, any>
|
|
144
|
+
): any {
|
|
145
|
+
const skillName = config.skill || 'default-skill';
|
|
146
|
+
return {
|
|
147
|
+
skill: skillName,
|
|
148
|
+
result: `skill_${skillName}_executed`,
|
|
149
|
+
inputsFromDeps: Object.fromEntries(dependencies),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private executeAgentNode(
|
|
154
|
+
nodeId: string,
|
|
155
|
+
config: any,
|
|
156
|
+
input: any,
|
|
157
|
+
dependencies: Map<string, any>
|
|
158
|
+
): any {
|
|
159
|
+
const agentType = config.agent || 'general';
|
|
160
|
+
return {
|
|
161
|
+
agent: agentType,
|
|
162
|
+
reasoning: `Agent ${agentType} processed input`,
|
|
163
|
+
input,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private executeConditionNode(nodeId: string, config: any, input: any): any {
|
|
168
|
+
const condition = config.condition || 'true';
|
|
169
|
+
const result = this.evaluateCondition(condition, input);
|
|
170
|
+
return { conditionMet: result, evaluatedCondition: condition };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private executeMergeNode(
|
|
174
|
+
nodeId: string,
|
|
175
|
+
config: any,
|
|
176
|
+
dependencies: Map<string, any>
|
|
177
|
+
): any {
|
|
178
|
+
const mergedInputs: any = {};
|
|
179
|
+
for (const [depId, depOutput] of dependencies) {
|
|
180
|
+
mergedInputs[depId] = depOutput;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
merged: true,
|
|
184
|
+
inputs: mergedInputs,
|
|
185
|
+
mergeStrategy: config.strategy || 'collect',
|
|
186
|
+
valid: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private executeNestedWorkflowNode(nodeId: string, config: any, input: any): any {
|
|
191
|
+
const workflowName = config.workflow || config.workflow_name || 'default-nested';
|
|
192
|
+
return {
|
|
193
|
+
nestedWorkflow: workflowName,
|
|
194
|
+
workflowName,
|
|
195
|
+
input,
|
|
196
|
+
result: `nested_${workflowName}_executed`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private evaluateCondition(condition: string, context: any): boolean {
|
|
201
|
+
// Handle template variable conditions
|
|
202
|
+
if (condition === '{{input.skip}}') {
|
|
203
|
+
return context?.skip !== false && !!context?.skip;
|
|
204
|
+
}
|
|
205
|
+
if (condition === '{{input.mode}}') {
|
|
206
|
+
return !!context?.mode;
|
|
207
|
+
}
|
|
208
|
+
if (condition === 'true') return true;
|
|
209
|
+
if (condition.includes('valid')) return true;
|
|
210
|
+
if (condition.includes('{{process.result}}')) return true;
|
|
211
|
+
return condition !== 'false';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==================== Workflow Execution Engine ====================
|
|
216
|
+
|
|
217
|
+
function executeWorkflowSync(
|
|
218
|
+
definition: WorkflowDefinition,
|
|
219
|
+
input?: any,
|
|
220
|
+
executor?: SyncWorkflowExecutor
|
|
221
|
+
): Map<string, any> {
|
|
222
|
+
const exec = executor || new SyncWorkflowExecutor();
|
|
223
|
+
const results = new Map<string, any>();
|
|
224
|
+
const nodeMap = new Map<string, NodeDefinition>();
|
|
225
|
+
|
|
226
|
+
// Build node map and config
|
|
227
|
+
for (const node of definition.nodes) {
|
|
228
|
+
nodeMap.set(node.id, node);
|
|
229
|
+
exec.setNodeConfig(node.id, node.config);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Topological sort for execution order
|
|
233
|
+
const order = topologicalSort(definition);
|
|
234
|
+
|
|
235
|
+
// Execute in order
|
|
236
|
+
const dependencies = new Map<string, any>();
|
|
237
|
+
|
|
238
|
+
for (const nodeId of order) {
|
|
239
|
+
const node = nodeMap.get(nodeId)!;
|
|
240
|
+
|
|
241
|
+
// Get dependency outputs
|
|
242
|
+
const depOutputs = new Map<string, any>();
|
|
243
|
+
for (const depId of node.depends_on || []) {
|
|
244
|
+
depOutputs.set(depId, dependencies.get(depId));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Execute node with explicit node type
|
|
248
|
+
const output = exec.executeNode(nodeId, { ...input }, depOutputs, node.type);
|
|
249
|
+
dependencies.set(nodeId, output);
|
|
250
|
+
results.set(nodeId, output);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function topologicalSort(workflow: WorkflowDefinition): string[] {
|
|
257
|
+
const nodeMap = new Map(workflow.nodes.map(n => [n.id, n]));
|
|
258
|
+
const visited = new Set<string>();
|
|
259
|
+
const temp = new Set<string>();
|
|
260
|
+
const order: string[] = [];
|
|
261
|
+
|
|
262
|
+
function visit(nodeId: string): void {
|
|
263
|
+
if (temp.has(nodeId)) {
|
|
264
|
+
throw new Error(`Circular dependency detected: ${nodeId}`);
|
|
265
|
+
}
|
|
266
|
+
if (visited.has(nodeId)) return;
|
|
267
|
+
|
|
268
|
+
temp.add(nodeId);
|
|
269
|
+
|
|
270
|
+
const node = nodeMap.get(nodeId);
|
|
271
|
+
if (node) {
|
|
272
|
+
for (const depId of node.depends_on || []) {
|
|
273
|
+
visit(depId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
temp.delete(nodeId);
|
|
278
|
+
visited.add(nodeId);
|
|
279
|
+
order.push(nodeId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Visit all nodes
|
|
283
|
+
for (const node of workflow.nodes) {
|
|
284
|
+
if (!visited.has(node.id)) {
|
|
285
|
+
visit(node.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return order;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ==================== Complex Workflow Definitions ====================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Complex Workflow: Data Processing Pipeline
|
|
296
|
+
*
|
|
297
|
+
* This workflow simulates a complex data processing pipeline with:
|
|
298
|
+
* - 12 nodes total
|
|
299
|
+
* - Parallel data fetching branches
|
|
300
|
+
* - Diamond merge pattern
|
|
301
|
+
* - Conditional processing
|
|
302
|
+
* - Error handling with retry
|
|
303
|
+
*/
|
|
304
|
+
const createDataProcessingWorkflow = (): WorkflowDefinition => ({
|
|
305
|
+
name: 'data-processing-pipeline',
|
|
306
|
+
version: '1.0',
|
|
307
|
+
description: 'Complex data processing pipeline with parallel branches',
|
|
308
|
+
config: {
|
|
309
|
+
parallel_limit: 3,
|
|
310
|
+
timeout: 300000,
|
|
311
|
+
},
|
|
312
|
+
nodes: [
|
|
313
|
+
// Entry node - Initialize pipeline
|
|
314
|
+
{
|
|
315
|
+
id: 'init',
|
|
316
|
+
type: 'tool',
|
|
317
|
+
name: 'Initialize Pipeline',
|
|
318
|
+
config: { tool: 'echo', message: 'Starting data pipeline' },
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
// Parallel fetch branch 1
|
|
322
|
+
{
|
|
323
|
+
id: 'fetch-users',
|
|
324
|
+
type: 'tool',
|
|
325
|
+
name: 'Fetch Users',
|
|
326
|
+
config: { tool: 'validator', source: 'users' },
|
|
327
|
+
depends_on: ['init'],
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
// Parallel fetch branch 2
|
|
331
|
+
{
|
|
332
|
+
id: 'fetch-products',
|
|
333
|
+
type: 'tool',
|
|
334
|
+
name: 'Fetch Products',
|
|
335
|
+
config: { tool: 'validator', source: 'products' },
|
|
336
|
+
depends_on: ['init'],
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
// Parallel fetch branch 3
|
|
340
|
+
{
|
|
341
|
+
id: 'fetch-orders',
|
|
342
|
+
type: 'tool',
|
|
343
|
+
name: 'Fetch Orders',
|
|
344
|
+
config: { tool: 'validator', source: 'orders' },
|
|
345
|
+
depends_on: ['init'],
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
// Diamond pattern - merge point
|
|
349
|
+
{
|
|
350
|
+
id: 'merge-data',
|
|
351
|
+
type: 'merge',
|
|
352
|
+
name: 'Merge All Data',
|
|
353
|
+
config: { strategy: 'collect' },
|
|
354
|
+
depends_on: ['fetch-users', 'fetch-products', 'fetch-orders'],
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
// Diamond pattern - fork to processing branches
|
|
358
|
+
{
|
|
359
|
+
id: 'validate-data',
|
|
360
|
+
type: 'condition',
|
|
361
|
+
name: 'Validate Merged Data',
|
|
362
|
+
config: { condition: '{{merge-data.valid}}' },
|
|
363
|
+
depends_on: ['merge-data'],
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
// Processing branch 1 - with retry
|
|
367
|
+
{
|
|
368
|
+
id: 'process-users',
|
|
369
|
+
type: 'tool',
|
|
370
|
+
name: 'Process Users',
|
|
371
|
+
config: { tool: 'calculator', operation: 'user-scoring' },
|
|
372
|
+
depends_on: ['validate-data'],
|
|
373
|
+
retry: { max_attempts: 3, backoff: 'exponential', initial_delay: 1000 },
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
// Processing branch 2
|
|
377
|
+
{
|
|
378
|
+
id: 'process-products',
|
|
379
|
+
type: 'tool',
|
|
380
|
+
name: 'Process Products',
|
|
381
|
+
config: { tool: 'calculator', operation: 'product-rating' },
|
|
382
|
+
depends_on: ['validate-data'],
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
// Diamond merge - final processing
|
|
386
|
+
{
|
|
387
|
+
id: 'finalize-results',
|
|
388
|
+
type: 'tool',
|
|
389
|
+
name: 'Finalize Results',
|
|
390
|
+
config: { tool: 'echo', message: 'Finalizing' },
|
|
391
|
+
depends_on: ['process-users', 'process-products'],
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
// Conditional node
|
|
395
|
+
{
|
|
396
|
+
id: 'generate-report',
|
|
397
|
+
type: 'condition',
|
|
398
|
+
name: 'Check Report Generation',
|
|
399
|
+
config: { condition: '{{input.generateReport}}' },
|
|
400
|
+
depends_on: ['finalize-results'],
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
// Nested workflow call
|
|
404
|
+
{
|
|
405
|
+
id: 'create-report',
|
|
406
|
+
type: 'nested',
|
|
407
|
+
name: 'Create Report',
|
|
408
|
+
config: { workflow: 'report-generation' },
|
|
409
|
+
depends_on: ['generate-report'],
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
// Final output node
|
|
413
|
+
{
|
|
414
|
+
id: 'output',
|
|
415
|
+
type: 'tool',
|
|
416
|
+
name: 'Output Results',
|
|
417
|
+
config: { tool: 'echo', message: 'Pipeline complete' },
|
|
418
|
+
depends_on: ['create-report'],
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
entry: 'init',
|
|
422
|
+
outputs: [
|
|
423
|
+
{ name: 'final_result', source: 'output', path: 'result' },
|
|
424
|
+
{ name: 'user_data', source: 'process-users', path: 'result' },
|
|
425
|
+
{ name: 'product_data', source: 'process-products', path: 'result' },
|
|
426
|
+
],
|
|
427
|
+
metadata: {
|
|
428
|
+
author: 'Roy Agent',
|
|
429
|
+
tags: ['data-processing', 'pipeline', 'complex'],
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Complex Workflow: Parallel Processing with Error Handling
|
|
435
|
+
*
|
|
436
|
+
* Features:
|
|
437
|
+
* - Multiple parallel branches
|
|
438
|
+
* - Retry logic on failure-prone nodes
|
|
439
|
+
* - Error recovery paths
|
|
440
|
+
* - Timeout handling
|
|
441
|
+
*/
|
|
442
|
+
const createParallelProcessingWorkflow = (): WorkflowDefinition => ({
|
|
443
|
+
name: 'parallel-processing',
|
|
444
|
+
version: '1.0',
|
|
445
|
+
description: 'Parallel processing with error handling and retry',
|
|
446
|
+
nodes: [
|
|
447
|
+
{ id: 'entry', type: 'tool', config: { tool: 'echo' } },
|
|
448
|
+
|
|
449
|
+
// Parallel branch A
|
|
450
|
+
{ id: 'task-a1', type: 'tool', config: { tool: 'calculator' }, depends_on: ['entry'] },
|
|
451
|
+
{ id: 'task-a2', type: 'tool', config: { tool: 'calculator' }, depends_on: ['task-a1'] },
|
|
452
|
+
{ id: 'task-a3', type: 'tool', config: { tool: 'calculator' }, depends_on: ['task-a2'] },
|
|
453
|
+
|
|
454
|
+
// Parallel branch B
|
|
455
|
+
{ id: 'task-b1', type: 'tool', config: { tool: 'calculator' }, depends_on: ['entry'] },
|
|
456
|
+
{ id: 'task-b2', type: 'tool', config: { tool: 'calculator' }, depends_on: ['task-b1'] },
|
|
457
|
+
{
|
|
458
|
+
id: 'task-b3',
|
|
459
|
+
type: 'tool',
|
|
460
|
+
config: { tool: 'calculator' },
|
|
461
|
+
depends_on: ['task-b1'],
|
|
462
|
+
retry: { max_attempts: 2, backoff: 'fixed', initial_delay: 500 },
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
// Parallel branch C - with potential failure
|
|
466
|
+
{ id: 'task-c1', type: 'tool', config: { tool: 'calculator' }, depends_on: ['entry'] },
|
|
467
|
+
{
|
|
468
|
+
id: 'task-c2',
|
|
469
|
+
type: 'tool',
|
|
470
|
+
config: { tool: 'calculator' },
|
|
471
|
+
depends_on: ['task-c1'],
|
|
472
|
+
retry: { max_attempts: 3, backoff: 'exponential', initial_delay: 1000 },
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
// Merge all branches
|
|
476
|
+
{
|
|
477
|
+
id: 'merge-ab',
|
|
478
|
+
type: 'merge',
|
|
479
|
+
config: { strategy: 'collect' },
|
|
480
|
+
depends_on: ['task-a3', 'task-b2', 'task-c2'],
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
// Final processing
|
|
484
|
+
{ id: 'final', type: 'tool', config: { tool: 'echo' }, depends_on: ['merge-ab'] },
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Complex Workflow: Nested Workflow Composition
|
|
490
|
+
*
|
|
491
|
+
* Features:
|
|
492
|
+
* - Nested workflow calls
|
|
493
|
+
* - Multiple levels of abstraction
|
|
494
|
+
* - Shared state passing
|
|
495
|
+
* - Complex dependency chains
|
|
496
|
+
*/
|
|
497
|
+
const createNestedWorkflow = (): WorkflowDefinition => ({
|
|
498
|
+
name: 'nested-workflow-composition',
|
|
499
|
+
version: '1.0',
|
|
500
|
+
nodes: [
|
|
501
|
+
{ id: 'start', type: 'tool', config: { tool: 'echo' }, depends_on: [] },
|
|
502
|
+
|
|
503
|
+
// First level nested
|
|
504
|
+
{ id: 'sub-workflow-1', type: 'workflow', config: { workflow_name: 'sub-workflow-a' }, depends_on: ['start'] },
|
|
505
|
+
{ id: 'sub-workflow-2', type: 'workflow', config: { workflow_name: 'sub-workflow-b' }, depends_on: ['start'] },
|
|
506
|
+
|
|
507
|
+
// Second level - parallel execution
|
|
508
|
+
{ id: 'level-2-a', type: 'tool', config: { tool: 'validator' }, depends_on: ['sub-workflow-1'] },
|
|
509
|
+
{ id: 'level-2-b', type: 'tool', config: { tool: 'validator' }, depends_on: ['sub-workflow-1'] },
|
|
510
|
+
{ id: 'level-2-c', type: 'tool', config: { tool: 'validator' }, depends_on: ['sub-workflow-2'] },
|
|
511
|
+
|
|
512
|
+
// Merge and continue
|
|
513
|
+
{ id: 'merge-2', type: 'merge', config: { strategy: 'collect' }, depends_on: ['level-2-a', 'level-2-b', 'level-2-c'] },
|
|
514
|
+
|
|
515
|
+
// Third level
|
|
516
|
+
{ id: 'level-3', type: 'tool', config: { tool: 'calculator' }, depends_on: ['merge-2'] },
|
|
517
|
+
|
|
518
|
+
// Final nested call
|
|
519
|
+
{ id: 'final-nested', type: 'workflow', config: { workflow_name: 'final-workflow' }, depends_on: ['level-3'] },
|
|
520
|
+
],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Complex Workflow: Conditional Execution Graph
|
|
525
|
+
*
|
|
526
|
+
* Features:
|
|
527
|
+
* - Multiple conditional branches
|
|
528
|
+
* - Skip logic
|
|
529
|
+
* - Dynamic paths based on runtime conditions
|
|
530
|
+
*/
|
|
531
|
+
const createConditionalWorkflow = (): WorkflowDefinition => ({
|
|
532
|
+
name: 'conditional-execution',
|
|
533
|
+
version: '1.0',
|
|
534
|
+
nodes: [
|
|
535
|
+
{ id: 'init', type: 'tool', config: { tool: 'echo' } },
|
|
536
|
+
|
|
537
|
+
// First condition
|
|
538
|
+
{
|
|
539
|
+
id: 'check-mode',
|
|
540
|
+
type: 'condition',
|
|
541
|
+
config: { condition: '{{input.mode}}' },
|
|
542
|
+
depends_on: ['init'],
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
// Mode A path
|
|
546
|
+
{ id: 'mode-a-1', type: 'tool', config: { tool: 'calculator' }, depends_on: ['check-mode'] },
|
|
547
|
+
{ id: 'mode-a-2', type: 'tool', config: { tool: 'calculator' }, depends_on: ['mode-a-1'] },
|
|
548
|
+
|
|
549
|
+
// Mode B path
|
|
550
|
+
{ id: 'mode-b-1', type: 'tool', config: { tool: 'validator' }, depends_on: ['check-mode'] },
|
|
551
|
+
{ id: 'mode-b-2', type: 'tool', config: { tool: 'validator' }, depends_on: ['mode-b-1'] },
|
|
552
|
+
|
|
553
|
+
// Merge after conditional paths
|
|
554
|
+
{ id: 'merge-cond', type: 'merge', config: { strategy: 'collect' }, depends_on: ['mode-a-2', 'mode-b-2'] },
|
|
555
|
+
|
|
556
|
+
// Final check
|
|
557
|
+
{
|
|
558
|
+
id: 'final-check',
|
|
559
|
+
type: 'condition',
|
|
560
|
+
config: { condition: '{{merge-cond.valid}}' },
|
|
561
|
+
depends_on: ['merge-cond'],
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
// Output
|
|
565
|
+
{ id: 'output', type: 'tool', config: { tool: 'echo' }, depends_on: ['final-check'] },
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Complex Workflow: Diamond Pattern
|
|
571
|
+
*
|
|
572
|
+
* A -> B1 -> C -> D
|
|
573
|
+
* -> B2 ->
|
|
574
|
+
* -> B3 ->
|
|
575
|
+
*
|
|
576
|
+
* Classic diamond dependency pattern
|
|
577
|
+
*/
|
|
578
|
+
const createDiamondWorkflow = (): WorkflowDefinition => ({
|
|
579
|
+
name: 'diamond-pattern',
|
|
580
|
+
version: '1.0',
|
|
581
|
+
nodes: [
|
|
582
|
+
{ id: 'A', type: 'tool', config: { tool: 'echo' }, depends_on: [] },
|
|
583
|
+
{ id: 'B1', type: 'tool', config: { tool: 'calculator' }, depends_on: ['A'] },
|
|
584
|
+
{ id: 'B2', type: 'tool', config: { tool: 'calculator' }, depends_on: ['A'] },
|
|
585
|
+
{ id: 'B3', type: 'tool', config: { tool: 'calculator' }, depends_on: ['A'] },
|
|
586
|
+
{
|
|
587
|
+
id: 'C',
|
|
588
|
+
type: 'tool',
|
|
589
|
+
config: { tool: 'calculator' },
|
|
590
|
+
depends_on: ['B1', 'B2', 'B3'],
|
|
591
|
+
retry: { max_attempts: 2, backoff: 'exponential', initial_delay: 500 },
|
|
592
|
+
},
|
|
593
|
+
{ id: 'D', type: 'tool', config: { tool: 'echo' }, depends_on: ['C'] },
|
|
594
|
+
],
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Complex Workflow: Multi-Node Type Orchestration
|
|
599
|
+
*
|
|
600
|
+
* Demonstrates chaining different node types:
|
|
601
|
+
* - ToolNode → AgentNode → SkillNode → WorkflowNode → ToolNode
|
|
602
|
+
* - Template variable passing between different node types
|
|
603
|
+
* - Real-world AI pipeline pattern
|
|
604
|
+
*/
|
|
605
|
+
const createMultiNodeTypeWorkflow = (): WorkflowDefinition => ({
|
|
606
|
+
name: 'multi-node-type-orchestration',
|
|
607
|
+
version: '1.0',
|
|
608
|
+
description: 'Orchestration across Tool, Agent, Skill, and Workflow nodes',
|
|
609
|
+
nodes: [
|
|
610
|
+
// ========== Step 1: Tool - Data Preparation ==========
|
|
611
|
+
{
|
|
612
|
+
id: 'prepare-data',
|
|
613
|
+
type: 'tool',
|
|
614
|
+
name: 'Prepare Input Data',
|
|
615
|
+
config: { tool: 'echo', message: 'Preparing initial data' },
|
|
616
|
+
depends_on: [],
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
// ========== Step 2: Agent - Analysis ==========
|
|
620
|
+
{
|
|
621
|
+
id: 'analyze-data',
|
|
622
|
+
type: 'agent',
|
|
623
|
+
name: 'AI Data Analyst',
|
|
624
|
+
config: {
|
|
625
|
+
agent_type: 'general',
|
|
626
|
+
prompt: 'Analyze the input data: {{prepare-data.output}}. Provide insights.',
|
|
627
|
+
},
|
|
628
|
+
depends_on: ['prepare-data'],
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
// ========== Step 3: Tool - Validate Analysis ==========
|
|
632
|
+
{
|
|
633
|
+
id: 'validate-analysis',
|
|
634
|
+
type: 'tool',
|
|
635
|
+
name: 'Validate Analysis Results',
|
|
636
|
+
config: { tool: 'validator', source: 'analysis' },
|
|
637
|
+
depends_on: ['analyze-data'],
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
// ========== Step 4: Agent - Decision Making ==========
|
|
641
|
+
{
|
|
642
|
+
id: 'make-decision',
|
|
643
|
+
type: 'agent',
|
|
644
|
+
name: 'Decision Agent',
|
|
645
|
+
config: {
|
|
646
|
+
agent_type: 'general',
|
|
647
|
+
prompt: 'Based on analysis: {{analyze-data.output}}, decide next steps.',
|
|
648
|
+
},
|
|
649
|
+
depends_on: ['validate-analysis'],
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
// ========== Step 5: Skill - Transform Data ==========
|
|
653
|
+
{
|
|
654
|
+
id: 'transform-data',
|
|
655
|
+
type: 'skill',
|
|
656
|
+
name: 'Data Transformation Skill',
|
|
657
|
+
config: {
|
|
658
|
+
skill: 'data-transform',
|
|
659
|
+
input: {
|
|
660
|
+
data: '{{analyze-data.output}}',
|
|
661
|
+
format: 'json',
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
depends_on: ['make-decision'],
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
// ========== Step 6: Nested Workflow ==========
|
|
668
|
+
{
|
|
669
|
+
id: 'process-workflow',
|
|
670
|
+
type: 'workflow',
|
|
671
|
+
name: 'Sub-Process Workflow',
|
|
672
|
+
config: {
|
|
673
|
+
workflow_name: 'data-processing-subflow',
|
|
674
|
+
input: {
|
|
675
|
+
source: '{{transform-data.output}}',
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
depends_on: ['transform-data'],
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
// ========== Step 7: Tool - Final Output ==========
|
|
682
|
+
{
|
|
683
|
+
id: 'format-output',
|
|
684
|
+
type: 'tool',
|
|
685
|
+
name: 'Format Final Output',
|
|
686
|
+
config: { tool: 'calculator', operation: 'format' },
|
|
687
|
+
depends_on: ['process-workflow'],
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
outputs: [
|
|
691
|
+
{ name: 'analysis_result', source: 'analyze-data', path: 'output' },
|
|
692
|
+
{ name: 'decision', source: 'make-decision', path: 'output' },
|
|
693
|
+
{ name: 'transformed_data', source: 'transform-data', path: 'output' },
|
|
694
|
+
{ name: 'final_result', source: 'format-output', path: 'result' },
|
|
695
|
+
],
|
|
696
|
+
metadata: {
|
|
697
|
+
author: 'Roy Agent',
|
|
698
|
+
tags: ['multi-node', 'orchestration', 'ai-pipeline'],
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Complex Workflow: Tool-Skill-Agent Chain
|
|
704
|
+
*
|
|
705
|
+
* A realistic AI pipeline demonstrating:
|
|
706
|
+
* - Tool (data fetch)
|
|
707
|
+
* - Agent (reasoning)
|
|
708
|
+
* - Skill (processing)
|
|
709
|
+
* - Another Agent (verification)
|
|
710
|
+
* - Tool (output)
|
|
711
|
+
*/
|
|
712
|
+
const createToolSkillAgentChain = (): WorkflowDefinition => ({
|
|
713
|
+
name: 'tool-skill-agent-chain',
|
|
714
|
+
version: '1.0',
|
|
715
|
+
nodes: [
|
|
716
|
+
// Tool: Fetch raw data
|
|
717
|
+
{
|
|
718
|
+
id: 'fetch-raw-data',
|
|
719
|
+
type: 'tool',
|
|
720
|
+
name: 'Fetch Raw Data',
|
|
721
|
+
config: { tool: 'echo', message: 'Fetching data from source' },
|
|
722
|
+
depends_on: [],
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
// Agent: Initial understanding
|
|
726
|
+
{
|
|
727
|
+
id: 'understand-context',
|
|
728
|
+
type: 'agent',
|
|
729
|
+
name: 'Context Understanding Agent',
|
|
730
|
+
config: {
|
|
731
|
+
agent_type: 'general',
|
|
732
|
+
prompt: 'Understand the context of: {{fetch-raw-data.output}}',
|
|
733
|
+
},
|
|
734
|
+
depends_on: ['fetch-raw-data'],
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
// Skill: Process with skill
|
|
738
|
+
{
|
|
739
|
+
id: 'process-with-skill',
|
|
740
|
+
type: 'skill',
|
|
741
|
+
name: 'Processing Skill',
|
|
742
|
+
config: {
|
|
743
|
+
skill: 'text-processor',
|
|
744
|
+
input: {
|
|
745
|
+
text: '{{understand-context.output}}',
|
|
746
|
+
options: { lowercase: true },
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
depends_on: ['understand-context'],
|
|
750
|
+
},
|
|
751
|
+
|
|
752
|
+
// Agent: Deep reasoning
|
|
753
|
+
{
|
|
754
|
+
id: 'deep-reasoning',
|
|
755
|
+
type: 'agent',
|
|
756
|
+
name: 'Deep Reasoning Agent',
|
|
757
|
+
config: {
|
|
758
|
+
agent_type: 'general',
|
|
759
|
+
prompt: 'Perform deep reasoning on: {{process-with-skill.output}}',
|
|
760
|
+
},
|
|
761
|
+
depends_on: ['process-with-skill'],
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
// Skill: Another skill processing
|
|
765
|
+
{
|
|
766
|
+
id: 'enhance-result',
|
|
767
|
+
type: 'skill',
|
|
768
|
+
name: 'Enhancement Skill',
|
|
769
|
+
config: {
|
|
770
|
+
skill: 'enhancer',
|
|
771
|
+
input: {
|
|
772
|
+
data: '{{deep-reasoning.output}}',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
depends_on: ['deep-reasoning'],
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
// Tool: Final validation
|
|
779
|
+
{
|
|
780
|
+
id: 'final-validate',
|
|
781
|
+
type: 'tool',
|
|
782
|
+
name: 'Final Validation',
|
|
783
|
+
config: { tool: 'validator', source: 'final' },
|
|
784
|
+
depends_on: ['enhance-result'],
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Complex Workflow: Parallel Tool-Agent Branches
|
|
791
|
+
*
|
|
792
|
+
* Multiple parallel branches, each with Tool → Agent pattern
|
|
793
|
+
*/
|
|
794
|
+
const createParallelToolAgentWorkflow = (): WorkflowDefinition => ({
|
|
795
|
+
name: 'parallel-tool-agent-branches',
|
|
796
|
+
version: '1.0',
|
|
797
|
+
nodes: [
|
|
798
|
+
// Entry
|
|
799
|
+
{ id: 'start', type: 'tool', config: { tool: 'echo' }, depends_on: [] },
|
|
800
|
+
|
|
801
|
+
// Branch A: Tool → Agent
|
|
802
|
+
{ id: 'tool-a', type: 'tool', config: { tool: 'calculator' }, depends_on: ['start'] },
|
|
803
|
+
{
|
|
804
|
+
id: 'agent-a',
|
|
805
|
+
type: 'agent',
|
|
806
|
+
config: { agent_type: 'general', prompt: 'Process: {{tool-a.output}}' },
|
|
807
|
+
depends_on: ['tool-a'],
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
// Branch B: Tool → Agent
|
|
811
|
+
{ id: 'tool-b', type: 'tool', config: { tool: 'validator' }, depends_on: ['start'] },
|
|
812
|
+
{
|
|
813
|
+
id: 'agent-b',
|
|
814
|
+
type: 'agent',
|
|
815
|
+
config: { agent_type: 'general', prompt: 'Process: {{tool-b.output}}' },
|
|
816
|
+
depends_on: ['tool-b'],
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
// Branch C: Tool → Agent
|
|
820
|
+
{ id: 'tool-c', type: 'tool', config: { tool: 'echo' }, depends_on: ['start'] },
|
|
821
|
+
{
|
|
822
|
+
id: 'agent-c',
|
|
823
|
+
type: 'agent',
|
|
824
|
+
config: { agent_type: 'general', prompt: 'Process: {{tool-c.output}}' },
|
|
825
|
+
depends_on: ['tool-c'],
|
|
826
|
+
},
|
|
827
|
+
|
|
828
|
+
// Merge all agent outputs
|
|
829
|
+
{
|
|
830
|
+
id: 'merge-agents',
|
|
831
|
+
type: 'merge',
|
|
832
|
+
config: { strategy: 'collect' },
|
|
833
|
+
depends_on: ['agent-a', 'agent-b', 'agent-c'],
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
// Final aggregation agent
|
|
837
|
+
{
|
|
838
|
+
id: 'aggregate-results',
|
|
839
|
+
type: 'agent',
|
|
840
|
+
name: 'Aggregation Agent',
|
|
841
|
+
config: {
|
|
842
|
+
agent_type: 'general',
|
|
843
|
+
prompt: 'Aggregate and summarize results from all branches.',
|
|
844
|
+
},
|
|
845
|
+
depends_on: ['merge-agents'],
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
// Output tool
|
|
849
|
+
{ id: 'output', type: 'tool', config: { tool: 'echo' }, depends_on: ['aggregate-results'] },
|
|
850
|
+
],
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Complex Workflow: Template Variable Passing
|
|
855
|
+
*
|
|
856
|
+
* Tests HIL (Human Input Language) template variable passing
|
|
857
|
+
*/
|
|
858
|
+
const createTemplateVariableWorkflow = (): WorkflowDefinition => ({
|
|
859
|
+
name: 'template-variables',
|
|
860
|
+
version: '1.0',
|
|
861
|
+
nodes: [
|
|
862
|
+
{
|
|
863
|
+
id: 'init',
|
|
864
|
+
type: 'tool',
|
|
865
|
+
config: { tool: 'echo', message: '{{input.greeting}}' },
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
id: 'process',
|
|
869
|
+
type: 'tool',
|
|
870
|
+
config: { tool: 'calculator', expression: '{{init.result}} + {{input.value}}' },
|
|
871
|
+
depends_on: ['init'],
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: 'validate',
|
|
875
|
+
type: 'condition',
|
|
876
|
+
config: { condition: '{{process.result}} > {{input.threshold}}' },
|
|
877
|
+
depends_on: ['process'],
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: 'output',
|
|
881
|
+
type: 'tool',
|
|
882
|
+
config: { tool: 'echo', message: 'Result: {{process.result}}, Valid: {{validate.conditionMet}}' },
|
|
883
|
+
depends_on: ['validate'],
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Complex Workflow: Complex Dependency Graph
|
|
890
|
+
*
|
|
891
|
+
* Features:
|
|
892
|
+
* - Multiple entry points
|
|
893
|
+
* - Complex merge patterns
|
|
894
|
+
* - Cross-branch dependencies
|
|
895
|
+
*/
|
|
896
|
+
const createComplexDependencyWorkflow = (): WorkflowDefinition => ({
|
|
897
|
+
name: 'complex-dependency-graph',
|
|
898
|
+
version: '1.0',
|
|
899
|
+
nodes: [
|
|
900
|
+
// Two entry points
|
|
901
|
+
{ id: 'entry-1', type: 'tool', config: { tool: 'echo' }, depends_on: [] },
|
|
902
|
+
{ id: 'entry-2', type: 'tool', config: { tool: 'echo' }, depends_on: [] },
|
|
903
|
+
|
|
904
|
+
// Parallel branches from entry-1
|
|
905
|
+
{ id: 'branch-1a', type: 'tool', config: { tool: 'calculator' }, depends_on: ['entry-1'] },
|
|
906
|
+
{ id: 'branch-1b', type: 'tool', config: { tool: 'calculator' }, depends_on: ['entry-1'] },
|
|
907
|
+
|
|
908
|
+
// Parallel branches from entry-2
|
|
909
|
+
{ id: 'branch-2a', type: 'tool', config: { tool: 'validator' }, depends_on: ['entry-2'] },
|
|
910
|
+
{ id: 'branch-2b', type: 'tool', config: { tool: 'validator' }, depends_on: ['entry-2'] },
|
|
911
|
+
|
|
912
|
+
// Cross-branch dependency: branch-1a depends on branch-2a
|
|
913
|
+
{ id: 'cross-dep', type: 'tool', config: { tool: 'calculator' }, depends_on: ['branch-1a', 'branch-2a'] },
|
|
914
|
+
|
|
915
|
+
// Merge all
|
|
916
|
+
{ id: 'merge-1', type: 'merge', config: { strategy: 'collect' }, depends_on: ['branch-1b', 'branch-2b', 'cross-dep'] },
|
|
917
|
+
|
|
918
|
+
// Final processing
|
|
919
|
+
{ id: 'final', type: 'tool', config: { tool: 'echo' }, depends_on: ['merge-1'] },
|
|
920
|
+
],
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// ==================== Test Suites ====================
|
|
924
|
+
|
|
925
|
+
describe('Complex Multi-Node Workflow Integration Tests', () => {
|
|
926
|
+
let executor: SyncWorkflowExecutor;
|
|
927
|
+
|
|
928
|
+
beforeEach(() => {
|
|
929
|
+
executor = new SyncWorkflowExecutor();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
afterEach(() => {
|
|
933
|
+
executor.clearTrace();
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// ==================== Workflow Structure Tests ====================
|
|
937
|
+
|
|
938
|
+
describe('Data Processing Pipeline Workflow', () => {
|
|
939
|
+
const workflow = createDataProcessingWorkflow();
|
|
940
|
+
|
|
941
|
+
it('should have 12 nodes', () => {
|
|
942
|
+
expect(workflow.nodes).toHaveLength(12);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('should have correct node IDs', () => {
|
|
946
|
+
const nodeIds = workflow.nodes.map(n => n.id);
|
|
947
|
+
expect(nodeIds).toContain('init');
|
|
948
|
+
expect(nodeIds).toContain('merge-data');
|
|
949
|
+
expect(nodeIds).toContain('finalize-results');
|
|
950
|
+
expect(nodeIds).toContain('output');
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it('should have parallel branches from init', () => {
|
|
954
|
+
const fetchNodes = workflow.nodes.filter(n => n.depends_on && n.depends_on.includes('init'));
|
|
955
|
+
expect(fetchNodes.length).toBe(3); // fetch-users, fetch-products, fetch-orders
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('should have diamond pattern with merge-data', () => {
|
|
959
|
+
const mergeNode = workflow.nodes.find(n => n.id === 'merge-data')!;
|
|
960
|
+
expect(mergeNode.depends_on).toBeDefined();
|
|
961
|
+
expect(mergeNode.depends_on).toContain('fetch-users');
|
|
962
|
+
expect(mergeNode.depends_on).toContain('fetch-products');
|
|
963
|
+
expect(mergeNode.depends_on).toContain('fetch-orders');
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it('should have nodes with retry configuration', () => {
|
|
967
|
+
const retryNode = workflow.nodes.find(n => n.id === 'process-users')!;
|
|
968
|
+
expect(retryNode.retry).toBeDefined();
|
|
969
|
+
expect(retryNode.retry?.max_attempts).toBe(3);
|
|
970
|
+
expect(retryNode.retry?.backoff).toBe('exponential');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('should have nested workflow node', () => {
|
|
974
|
+
const nestedNode = workflow.nodes.find(n => n.id === 'create-report')!;
|
|
975
|
+
expect(nestedNode.type).toBe('nested');
|
|
976
|
+
expect(nestedNode.config.workflow).toBe('report-generation');
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('should have correct output definitions', () => {
|
|
980
|
+
expect(workflow.outputs).toHaveLength(3);
|
|
981
|
+
expect(workflow.outputs?.find(o => o.name === 'final_result')?.source).toBe('output');
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('should have metadata with tags', () => {
|
|
985
|
+
expect(workflow.metadata?.tags).toContain('data-processing');
|
|
986
|
+
expect(workflow.metadata?.author).toBe('Roy Agent');
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('should execute in correct topological order', () => {
|
|
990
|
+
const order = topologicalSort(workflow);
|
|
991
|
+
|
|
992
|
+
// init should come before fetch-*
|
|
993
|
+
expect(order.indexOf('init')).toBeLessThan(order.indexOf('fetch-users'));
|
|
994
|
+
expect(order.indexOf('init')).toBeLessThan(order.indexOf('fetch-products'));
|
|
995
|
+
expect(order.indexOf('init')).toBeLessThan(order.indexOf('fetch-orders'));
|
|
996
|
+
|
|
997
|
+
// merge-data should come after all fetches
|
|
998
|
+
expect(order.indexOf('merge-data')).toBeGreaterThan(order.indexOf('fetch-users'));
|
|
999
|
+
expect(order.indexOf('merge-data')).toBeGreaterThan(order.indexOf('fetch-products'));
|
|
1000
|
+
expect(order.indexOf('merge-data')).toBeGreaterThan(order.indexOf('fetch-orders'));
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('should execute all nodes successfully', () => {
|
|
1004
|
+
const results = executeWorkflowSync(workflow, { generateReport: true }, executor);
|
|
1005
|
+
|
|
1006
|
+
expect(results.size).toBe(12);
|
|
1007
|
+
expect(results.has('init')).toBe(true);
|
|
1008
|
+
expect(results.has('merge-data')).toBe(true);
|
|
1009
|
+
expect(results.has('output')).toBe(true);
|
|
1010
|
+
|
|
1011
|
+
// Verify execution order in trace
|
|
1012
|
+
const trace = executor.getTrace();
|
|
1013
|
+
expect(trace.executionOrder.length).toBe(12);
|
|
1014
|
+
expect(trace.executionOrder[0]).toBe('init');
|
|
1015
|
+
expect(trace.executionOrder[11]).toBe('output');
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
describe('Parallel Processing Workflow', () => {
|
|
1020
|
+
const workflow = createParallelProcessingWorkflow();
|
|
1021
|
+
|
|
1022
|
+
it('should have parallel branches from entry', () => {
|
|
1023
|
+
const parallelNodes = workflow.nodes.filter(n => n.depends_on?.includes('entry'));
|
|
1024
|
+
expect(parallelNodes.length).toBe(3); // task-a1, task-b1, task-c1
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('should have correct branch structures', () => {
|
|
1028
|
+
// Branch A: entry -> a1 -> a2 -> a3
|
|
1029
|
+
expect(workflow.nodes.find(n => n.id === 'task-a1')?.depends_on).toContain('entry');
|
|
1030
|
+
expect(workflow.nodes.find(n => n.id === 'task-a2')?.depends_on).toContain('task-a1');
|
|
1031
|
+
expect(workflow.nodes.find(n => n.id === 'task-a3')?.depends_on).toContain('task-a2');
|
|
1032
|
+
|
|
1033
|
+
// Branch B: entry -> b1 -> b2/b3
|
|
1034
|
+
expect(workflow.nodes.find(n => n.id === 'task-b1')?.depends_on).toContain('entry');
|
|
1035
|
+
expect(workflow.nodes.find(n => n.id === 'task-b2')?.depends_on).toContain('task-b1');
|
|
1036
|
+
expect(workflow.nodes.find(n => n.id === 'task-b3')?.depends_on).toContain('task-b1');
|
|
1037
|
+
|
|
1038
|
+
// Branch C: entry -> c1 -> c2
|
|
1039
|
+
expect(workflow.nodes.find(n => n.id === 'task-c1')?.depends_on).toContain('entry');
|
|
1040
|
+
expect(workflow.nodes.find(n => n.id === 'task-c2')?.depends_on).toContain('task-c1');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should have retry configured on specific nodes', () => {
|
|
1044
|
+
const retryNode = workflow.nodes.find(n => n.id === 'task-b3')!;
|
|
1045
|
+
expect(retryNode.retry?.max_attempts).toBe(2);
|
|
1046
|
+
expect(retryNode.retry?.backoff).toBe('fixed');
|
|
1047
|
+
|
|
1048
|
+
const retryNode2 = workflow.nodes.find(n => n.id === 'task-c2')!;
|
|
1049
|
+
expect(retryNode2.retry?.max_attempts).toBe(3);
|
|
1050
|
+
expect(retryNode2.retry?.backoff).toBe('exponential');
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('should merge at merge-ab node', () => {
|
|
1054
|
+
const mergeNode = workflow.nodes.find(n => n.id === 'merge-ab')!;
|
|
1055
|
+
expect(mergeNode.depends_on).toContain('task-a3');
|
|
1056
|
+
expect(mergeNode.depends_on).toContain('task-b2');
|
|
1057
|
+
expect(mergeNode.depends_on).toContain('task-c2');
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('should execute all nodes', () => {
|
|
1061
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1062
|
+
// entry + 3 branches * 3 nodes each (minus b3 since b2 and b3 both depend on b1) + merge + final
|
|
1063
|
+
// 1 + 3 + 3 + 3 + 1 + 1 = 12... wait, let me count again
|
|
1064
|
+
// entry, task-a1, task-a2, task-a3, task-b1, task-b2, task-b3, task-c1, task-c2, merge-ab, final = 11 nodes
|
|
1065
|
+
expect(results.size).toBe(11);
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
describe('Nested Workflow Composition', () => {
|
|
1070
|
+
const workflow = createNestedWorkflow();
|
|
1071
|
+
|
|
1072
|
+
it('should have multiple workflow nodes', () => {
|
|
1073
|
+
const workflowNodes = workflow.nodes.filter(n => n.type === 'workflow');
|
|
1074
|
+
expect(workflowNodes.length).toBe(3);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('should have hierarchical structure', () => {
|
|
1078
|
+
const startNode = workflow.nodes.find(n => n.id === 'start')!;
|
|
1079
|
+
expect(startNode.depends_on).toBeDefined();
|
|
1080
|
+
expect(startNode.depends_on!.length).toBe(0);
|
|
1081
|
+
|
|
1082
|
+
const subWorkflow1 = workflow.nodes.find(n => n.id === 'sub-workflow-1')!;
|
|
1083
|
+
expect(subWorkflow1.depends_on).toBeDefined();
|
|
1084
|
+
expect(subWorkflow1.depends_on).toContain('start');
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('should execute workflow nodes', () => {
|
|
1088
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1089
|
+
expect(results.has('sub-workflow-1')).toBe(true);
|
|
1090
|
+
expect(results.has('final-nested')).toBe(true);
|
|
1091
|
+
|
|
1092
|
+
const nestedResult = results.get('sub-workflow-1');
|
|
1093
|
+
expect(nestedResult).toBeDefined();
|
|
1094
|
+
expect(nestedResult.nestedWorkflow).toBe('sub-workflow-a');
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
describe('Multi-Node Type Orchestration', () => {
|
|
1099
|
+
const workflow = createMultiNodeTypeWorkflow();
|
|
1100
|
+
|
|
1101
|
+
it('should have various node types', () => {
|
|
1102
|
+
const toolNodes = workflow.nodes.filter(n => n.type === 'tool');
|
|
1103
|
+
const agentNodes = workflow.nodes.filter(n => n.type === 'agent');
|
|
1104
|
+
const skillNodes = workflow.nodes.filter(n => n.type === 'skill');
|
|
1105
|
+
const workflowNodes = workflow.nodes.filter(n => n.type === 'workflow');
|
|
1106
|
+
|
|
1107
|
+
expect(toolNodes.length).toBeGreaterThan(0);
|
|
1108
|
+
expect(agentNodes.length).toBeGreaterThan(0);
|
|
1109
|
+
expect(skillNodes.length).toBeGreaterThan(0);
|
|
1110
|
+
expect(workflowNodes.length).toBeGreaterThan(0);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
it('should have correct dependency chain', () => {
|
|
1114
|
+
const prepareData = workflow.nodes.find(n => n.id === 'prepare-data')!;
|
|
1115
|
+
expect(prepareData.depends_on?.length).toBe(0);
|
|
1116
|
+
|
|
1117
|
+
const analyzeData = workflow.nodes.find(n => n.id === 'analyze-data')!;
|
|
1118
|
+
expect(analyzeData.depends_on).toContain('prepare-data');
|
|
1119
|
+
|
|
1120
|
+
const makeDecision = workflow.nodes.find(n => n.id === 'make-decision')!;
|
|
1121
|
+
expect(makeDecision.depends_on).toContain('validate-analysis');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('should have agent nodes with prompts', () => {
|
|
1125
|
+
const agentNodes = workflow.nodes.filter(n => n.type === 'agent');
|
|
1126
|
+
|
|
1127
|
+
for (const agentNode of agentNodes) {
|
|
1128
|
+
expect(agentNode.config).toBeDefined();
|
|
1129
|
+
expect((agentNode.config as any).prompt).toBeDefined();
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
it('should have skill nodes with skill names', () => {
|
|
1134
|
+
const skillNodes = workflow.nodes.filter(n => n.type === 'skill');
|
|
1135
|
+
|
|
1136
|
+
for (const skillNode of skillNodes) {
|
|
1137
|
+
expect(skillNode.config).toBeDefined();
|
|
1138
|
+
expect((skillNode.config as any).skill).toBeDefined();
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('should execute all node types', () => {
|
|
1143
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1144
|
+
|
|
1145
|
+
// All 7 nodes should be executed
|
|
1146
|
+
// prepare-data, analyze-data, validate-analysis, make-decision, transform-data, process-workflow, format-output
|
|
1147
|
+
expect(results.size).toBe(7);
|
|
1148
|
+
|
|
1149
|
+
// Verify tool nodes executed
|
|
1150
|
+
expect(results.has('prepare-data')).toBe(true);
|
|
1151
|
+
expect(results.has('format-output')).toBe(true);
|
|
1152
|
+
|
|
1153
|
+
// Verify agent nodes executed
|
|
1154
|
+
expect(results.has('analyze-data')).toBe(true);
|
|
1155
|
+
expect(results.has('make-decision')).toBe(true);
|
|
1156
|
+
|
|
1157
|
+
// Verify skill nodes executed
|
|
1158
|
+
expect(results.has('transform-data')).toBe(true);
|
|
1159
|
+
|
|
1160
|
+
// Verify workflow nodes executed
|
|
1161
|
+
expect(results.has('process-workflow')).toBe(true);
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
describe('Tool-Skill-Agent Chain', () => {
|
|
1166
|
+
const workflow = createToolSkillAgentChain();
|
|
1167
|
+
|
|
1168
|
+
it('should alternate between tool, agent, and skill nodes', () => {
|
|
1169
|
+
const toolNodes = workflow.nodes.filter(n => n.type === 'tool');
|
|
1170
|
+
const agentNodes = workflow.nodes.filter(n => n.type === 'agent');
|
|
1171
|
+
const skillNodes = workflow.nodes.filter(n => n.type === 'skill');
|
|
1172
|
+
|
|
1173
|
+
expect(toolNodes.length).toBe(2); // fetch and validate
|
|
1174
|
+
expect(agentNodes.length).toBe(2); // understand and reasoning
|
|
1175
|
+
expect(skillNodes.length).toBe(2); // process and enhance
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it('should maintain execution order', () => {
|
|
1179
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1180
|
+
expect(results.size).toBe(6);
|
|
1181
|
+
|
|
1182
|
+
// Verify tool → agent → skill chain
|
|
1183
|
+
const toolResult = results.get('fetch-raw-data');
|
|
1184
|
+
const agentResult = results.get('understand-context');
|
|
1185
|
+
const skillResult = results.get('process-with-skill');
|
|
1186
|
+
|
|
1187
|
+
expect(toolResult).toBeDefined();
|
|
1188
|
+
expect(agentResult).toBeDefined();
|
|
1189
|
+
expect(skillResult).toBeDefined();
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
describe('Parallel Tool-Agent Branches', () => {
|
|
1194
|
+
const workflow = createParallelToolAgentWorkflow();
|
|
1195
|
+
|
|
1196
|
+
it('should have parallel tool-agent pairs', () => {
|
|
1197
|
+
const toolNodes = workflow.nodes.filter(n => n.type === 'tool');
|
|
1198
|
+
const agentNodes = workflow.nodes.filter(n => n.type === 'agent');
|
|
1199
|
+
|
|
1200
|
+
expect(toolNodes.length).toBeGreaterThan(3); // start + 3 branch tools + output
|
|
1201
|
+
expect(agentNodes.length).toBe(4); // 3 branch agents + 1 aggregator
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('should have merge node after parallel branches', () => {
|
|
1205
|
+
const mergeNode = workflow.nodes.find(n => n.id === 'merge-agents')!;
|
|
1206
|
+
expect(mergeNode.type).toBe('merge');
|
|
1207
|
+
expect(mergeNode.depends_on).toContain('agent-a');
|
|
1208
|
+
expect(mergeNode.depends_on).toContain('agent-b');
|
|
1209
|
+
expect(mergeNode.depends_on).toContain('agent-c');
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it('should execute all parallel branches', () => {
|
|
1213
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1214
|
+
expect(results.has('tool-a')).toBe(true);
|
|
1215
|
+
expect(results.has('agent-a')).toBe(true);
|
|
1216
|
+
expect(results.has('tool-b')).toBe(true);
|
|
1217
|
+
expect(results.has('agent-b')).toBe(true);
|
|
1218
|
+
expect(results.has('merge-agents')).toBe(true);
|
|
1219
|
+
expect(results.has('aggregate-results')).toBe(true);
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
describe('Diamond Pattern Workflow', () => {
|
|
1224
|
+
const workflow = createDiamondWorkflow();
|
|
1225
|
+
|
|
1226
|
+
it('should have classic diamond structure', () => {
|
|
1227
|
+
// A is the starting point
|
|
1228
|
+
const aNode = workflow.nodes.find(n => n.id === 'A')!;
|
|
1229
|
+
expect(aNode.depends_on).toBeDefined();
|
|
1230
|
+
expect(aNode.depends_on!.length).toBe(0);
|
|
1231
|
+
|
|
1232
|
+
// B1, B2, B3 all depend on A
|
|
1233
|
+
const b1Node = workflow.nodes.find(n => n.id === 'B1')!;
|
|
1234
|
+
const b2Node = workflow.nodes.find(n => n.id === 'B2')!;
|
|
1235
|
+
const b3Node = workflow.nodes.find(n => n.id === 'B3')!;
|
|
1236
|
+
expect(b1Node.depends_on).toContain('A');
|
|
1237
|
+
expect(b2Node.depends_on).toContain('A');
|
|
1238
|
+
expect(b3Node.depends_on).toContain('A');
|
|
1239
|
+
|
|
1240
|
+
// C depends on all B nodes
|
|
1241
|
+
const cNode = workflow.nodes.find(n => n.id === 'C')!;
|
|
1242
|
+
expect(cNode.depends_on).toBeDefined();
|
|
1243
|
+
expect(cNode.depends_on).toContain('B1');
|
|
1244
|
+
expect(cNode.depends_on).toContain('B2');
|
|
1245
|
+
expect(cNode.depends_on).toContain('B3');
|
|
1246
|
+
|
|
1247
|
+
// D depends on C
|
|
1248
|
+
const dNode = workflow.nodes.find(n => n.id === 'D')!;
|
|
1249
|
+
expect(dNode.depends_on).toContain('C');
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it('should have 6 nodes total', () => {
|
|
1253
|
+
expect(workflow.nodes).toHaveLength(6);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
it('should execute diamond pattern correctly', () => {
|
|
1257
|
+
const results = executeWorkflowSync(workflow, { test: 'data' }, executor);
|
|
1258
|
+
|
|
1259
|
+
// All nodes should be executed
|
|
1260
|
+
expect(results.has('A')).toBe(true);
|
|
1261
|
+
expect(results.has('B1')).toBe(true);
|
|
1262
|
+
expect(results.has('B2')).toBe(true);
|
|
1263
|
+
expect(results.has('B3')).toBe(true);
|
|
1264
|
+
expect(results.has('C')).toBe(true);
|
|
1265
|
+
expect(results.has('D')).toBe(true);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('should merge results at C node', () => {
|
|
1269
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1270
|
+
const cResult = results.get('C');
|
|
1271
|
+
expect(cResult).toBeDefined();
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
describe('Conditional Execution Workflow', () => {
|
|
1276
|
+
const workflow = createConditionalWorkflow();
|
|
1277
|
+
|
|
1278
|
+
it('should have conditional branching', () => {
|
|
1279
|
+
const checkNode = workflow.nodes.find(n => n.id === 'check-mode')!;
|
|
1280
|
+
expect(checkNode.type).toBe('condition');
|
|
1281
|
+
|
|
1282
|
+
const modeA1 = workflow.nodes.find(n => n.id === 'mode-a-1')!;
|
|
1283
|
+
const modeB1 = workflow.nodes.find(n => n.id === 'mode-b-1')!;
|
|
1284
|
+
expect(modeA1.depends_on).toContain('check-mode');
|
|
1285
|
+
expect(modeB1.depends_on).toContain('check-mode');
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
it('should merge conditional paths', () => {
|
|
1289
|
+
const mergeNode = workflow.nodes.find(n => n.id === 'merge-cond')!;
|
|
1290
|
+
expect(mergeNode.depends_on).toBeDefined();
|
|
1291
|
+
expect(mergeNode.depends_on).toContain('mode-a-2');
|
|
1292
|
+
expect(mergeNode.depends_on).toContain('mode-b-2');
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('should execute conditional nodes', () => {
|
|
1296
|
+
const results = executeWorkflowSync(workflow, { mode: 'A' }, executor);
|
|
1297
|
+
|
|
1298
|
+
expect(results.has('check-mode')).toBe(true);
|
|
1299
|
+
const checkResult = results.get('check-mode');
|
|
1300
|
+
expect(checkResult).toBeDefined();
|
|
1301
|
+
expect(checkResult.conditionMet).toBe(true);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe('Template Variable Workflow', () => {
|
|
1306
|
+
const workflow = createTemplateVariableWorkflow();
|
|
1307
|
+
|
|
1308
|
+
it('should have template variables in node configs', () => {
|
|
1309
|
+
const initNode = workflow.nodes.find(n => n.id === 'init')!;
|
|
1310
|
+
expect(initNode.config.message).toContain('{{input.greeting}}');
|
|
1311
|
+
|
|
1312
|
+
const processNode = workflow.nodes.find(n => n.id === 'process')!;
|
|
1313
|
+
expect(processNode.config.expression).toContain('{{init.result}}');
|
|
1314
|
+
expect(processNode.config.expression).toContain('{{input.value}}');
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
it('should have condition with template variables', () => {
|
|
1318
|
+
const validateNode = workflow.nodes.find(n => n.id === 'validate')!;
|
|
1319
|
+
expect(validateNode.config.condition).toContain('{{process.result}}');
|
|
1320
|
+
expect(validateNode.config.condition).toContain('{{input.threshold}}');
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
describe('Complex Dependency Graph Workflow', () => {
|
|
1325
|
+
const workflow = createComplexDependencyWorkflow();
|
|
1326
|
+
|
|
1327
|
+
it('should have multiple entry points', () => {
|
|
1328
|
+
const entry1 = workflow.nodes.find(n => n.id === 'entry-1')!;
|
|
1329
|
+
const entry2 = workflow.nodes.find(n => n.id === 'entry-2')!;
|
|
1330
|
+
expect(entry1.depends_on).toBeDefined();
|
|
1331
|
+
expect(entry1.depends_on!.length).toBe(0);
|
|
1332
|
+
expect(entry2.depends_on).toBeDefined();
|
|
1333
|
+
expect(entry2.depends_on!.length).toBe(0);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('should have cross-branch dependencies', () => {
|
|
1337
|
+
const crossDep = workflow.nodes.find(n => n.id === 'cross-dep')!;
|
|
1338
|
+
expect(crossDep.depends_on).toBeDefined();
|
|
1339
|
+
expect(crossDep.depends_on).toContain('branch-1a');
|
|
1340
|
+
expect(crossDep.depends_on).toContain('branch-2a');
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it('should handle complex merge patterns', () => {
|
|
1344
|
+
const mergeNode = workflow.nodes.find(n => n.id === 'merge-1')!;
|
|
1345
|
+
expect(mergeNode.depends_on).toBeDefined();
|
|
1346
|
+
expect(mergeNode.depends_on).toHaveLength(3);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('should execute complex workflow correctly', () => {
|
|
1350
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1351
|
+
// entry-1, entry-2, branch-1a, branch-1b, branch-2a, branch-2b, cross-dep, merge-1, final = 9 nodes
|
|
1352
|
+
expect(results.size).toBe(9);
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// ==================== Retry Logic Tests ====================
|
|
1357
|
+
|
|
1358
|
+
describe('Retry Configuration', () => {
|
|
1359
|
+
it('should configure retry with exponential backoff', () => {
|
|
1360
|
+
const workflow = createDataProcessingWorkflow();
|
|
1361
|
+
const retryNode = workflow.nodes.find(n => n.id === 'process-users')!;
|
|
1362
|
+
|
|
1363
|
+
expect(retryNode.retry).toBeDefined();
|
|
1364
|
+
expect(retryNode.retry?.max_attempts).toBe(3);
|
|
1365
|
+
expect(retryNode.retry?.backoff).toBe('exponential');
|
|
1366
|
+
expect(retryNode.retry?.initial_delay).toBe(1000);
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('should configure retry with fixed backoff', () => {
|
|
1370
|
+
const workflow = createParallelProcessingWorkflow();
|
|
1371
|
+
const retryNode = workflow.nodes.find(n => n.id === 'task-b3')!;
|
|
1372
|
+
|
|
1373
|
+
expect(retryNode.retry).toBeDefined();
|
|
1374
|
+
expect(retryNode.retry?.backoff).toBe('fixed');
|
|
1375
|
+
expect(retryNode.retry?.initial_delay).toBe(500);
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
it('should have retry node in diamond workflow', () => {
|
|
1379
|
+
const workflow = createDiamondWorkflow();
|
|
1380
|
+
const retryNode = workflow.nodes.find(n => n.id === 'C')!;
|
|
1381
|
+
|
|
1382
|
+
expect(retryNode.retry).toBeDefined();
|
|
1383
|
+
expect(retryNode.retry?.max_attempts).toBe(2);
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
// ==================== Workflow Validation Tests ====================
|
|
1388
|
+
|
|
1389
|
+
describe('Workflow Validation', () => {
|
|
1390
|
+
it('should validate workflow with all required fields', () => {
|
|
1391
|
+
const workflow = createDataProcessingWorkflow();
|
|
1392
|
+
|
|
1393
|
+
// Should have name
|
|
1394
|
+
expect(workflow.name).toBeTruthy();
|
|
1395
|
+
|
|
1396
|
+
// Should have at least one node
|
|
1397
|
+
expect(workflow.nodes.length).toBeGreaterThan(0);
|
|
1398
|
+
|
|
1399
|
+
// All nodes should have id and type
|
|
1400
|
+
for (const node of workflow.nodes) {
|
|
1401
|
+
expect(node.id).toBeTruthy();
|
|
1402
|
+
expect(node.type).toBeTruthy();
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it('should have valid dependency references', () => {
|
|
1407
|
+
const workflow = createDataProcessingWorkflow();
|
|
1408
|
+
const nodeIds = new Set(workflow.nodes.map(n => n.id));
|
|
1409
|
+
|
|
1410
|
+
for (const node of workflow.nodes) {
|
|
1411
|
+
if (node.depends_on) {
|
|
1412
|
+
for (const depId of node.depends_on) {
|
|
1413
|
+
expect(nodeIds.has(depId)).toBe(true);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
it('should detect circular dependencies', () => {
|
|
1420
|
+
const circularWorkflow: WorkflowDefinition = {
|
|
1421
|
+
name: 'circular',
|
|
1422
|
+
nodes: [
|
|
1423
|
+
{ id: 'a', type: 'tool', config: {}, depends_on: ['c'] },
|
|
1424
|
+
{ id: 'b', type: 'tool', config: {}, depends_on: ['a'] },
|
|
1425
|
+
{ id: 'c', type: 'tool', config: {}, depends_on: ['b'] },
|
|
1426
|
+
],
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
expect(() => topologicalSort(circularWorkflow)).toThrow();
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// ==================== Schema Validation Tests ====================
|
|
1434
|
+
|
|
1435
|
+
describe('Schema Validation', () => {
|
|
1436
|
+
it('should validate NodeDefinition schema', () => {
|
|
1437
|
+
const NodeDefinitionSchema = z.object({
|
|
1438
|
+
id: z.string().min(1),
|
|
1439
|
+
type: z.string().min(1),
|
|
1440
|
+
name: z.string().optional(),
|
|
1441
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
1442
|
+
depends_on: z.array(z.string()).optional(),
|
|
1443
|
+
condition: z.string().optional(),
|
|
1444
|
+
retry: z.object({
|
|
1445
|
+
max_attempts: z.number().min(1),
|
|
1446
|
+
backoff: z.enum(['fixed', 'exponential']),
|
|
1447
|
+
initial_delay: z.number().min(0),
|
|
1448
|
+
}).optional(),
|
|
1449
|
+
timeout: z.number().optional(),
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const validNode = {
|
|
1453
|
+
id: 'test-node',
|
|
1454
|
+
type: 'tool',
|
|
1455
|
+
name: 'Test Node',
|
|
1456
|
+
config: { tool: 'test' },
|
|
1457
|
+
depends_on: ['dep1'],
|
|
1458
|
+
retry: { max_attempts: 3, backoff: 'exponential', initial_delay: 1000 },
|
|
1459
|
+
};
|
|
1460
|
+
|
|
1461
|
+
const result = NodeDefinitionSchema.safeParse(validNode);
|
|
1462
|
+
expect(result.success).toBe(true);
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
it('should reject invalid retry config', () => {
|
|
1466
|
+
const RetryConfigSchema = z.object({
|
|
1467
|
+
max_attempts: z.number().min(1),
|
|
1468
|
+
backoff: z.enum(['fixed', 'exponential']),
|
|
1469
|
+
initial_delay: z.number().min(0),
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
const invalidConfig = { max_attempts: 0, backoff: 'invalid', initial_delay: -100 };
|
|
1473
|
+
const result = RetryConfigSchema.safeParse(invalidConfig);
|
|
1474
|
+
expect(result.success).toBe(false);
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
it('should validate WorkflowDefinition schema', () => {
|
|
1478
|
+
const WorkflowDefinitionSchema = z.object({
|
|
1479
|
+
name: z.string().min(1),
|
|
1480
|
+
version: z.string().default('1.0'),
|
|
1481
|
+
description: z.string().optional(),
|
|
1482
|
+
nodes: z.array(z.object({
|
|
1483
|
+
id: z.string().min(1),
|
|
1484
|
+
type: z.string().min(1),
|
|
1485
|
+
})).min(1),
|
|
1486
|
+
entry: z.union([z.string(), z.array(z.string())]).default('__default_entry__'),
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
const validWorkflow = {
|
|
1490
|
+
name: 'test-workflow',
|
|
1491
|
+
nodes: [{ id: 'n1', type: 'tool' }],
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const result = WorkflowDefinitionSchema.safeParse(validWorkflow);
|
|
1495
|
+
expect(result.success).toBe(true);
|
|
1496
|
+
expect(result.data?.version).toBe('1.0');
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
// ==================== Topological Sort Tests ====================
|
|
1502
|
+
|
|
1503
|
+
describe('Topological Sort', () => {
|
|
1504
|
+
it('should sort nodes in dependency order', () => {
|
|
1505
|
+
const workflow = createDiamondWorkflow();
|
|
1506
|
+
const order = topologicalSort(workflow);
|
|
1507
|
+
|
|
1508
|
+
// A should come before B1, B2, B3
|
|
1509
|
+
expect(order.indexOf('A')).toBeLessThan(order.indexOf('B1'));
|
|
1510
|
+
expect(order.indexOf('A')).toBeLessThan(order.indexOf('B2'));
|
|
1511
|
+
expect(order.indexOf('A')).toBeLessThan(order.indexOf('B3'));
|
|
1512
|
+
|
|
1513
|
+
// B* should come before C
|
|
1514
|
+
expect(order.indexOf('B1')).toBeLessThan(order.indexOf('C'));
|
|
1515
|
+
expect(order.indexOf('B2')).toBeLessThan(order.indexOf('C'));
|
|
1516
|
+
expect(order.indexOf('B3')).toBeLessThan(order.indexOf('C'));
|
|
1517
|
+
|
|
1518
|
+
// C should come before D
|
|
1519
|
+
expect(order.indexOf('C')).toBeLessThan(order.indexOf('D'));
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
it('should handle parallel branches', () => {
|
|
1523
|
+
const workflow = createParallelProcessingWorkflow();
|
|
1524
|
+
const order = topologicalSort(workflow);
|
|
1525
|
+
|
|
1526
|
+
// Entry should be first
|
|
1527
|
+
expect(order[0]).toBe('entry');
|
|
1528
|
+
|
|
1529
|
+
// All task-* should come after entry
|
|
1530
|
+
const taskNodes = order.filter(id => id.startsWith('task-'));
|
|
1531
|
+
expect(taskNodes.length).toBeGreaterThan(0);
|
|
1532
|
+
|
|
1533
|
+
// Verify dependencies are maintained
|
|
1534
|
+
for (const node of workflow.nodes) {
|
|
1535
|
+
const nodeIndex = order.indexOf(node.id);
|
|
1536
|
+
for (const depId of node.depends_on || []) {
|
|
1537
|
+
expect(order.indexOf(depId)).toBeLessThan(nodeIndex);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
it('should throw on circular dependency', () => {
|
|
1543
|
+
const circularWorkflow: WorkflowDefinition = {
|
|
1544
|
+
name: 'circular',
|
|
1545
|
+
nodes: [
|
|
1546
|
+
{ id: 'a', type: 'tool', config: {}, depends_on: ['b'] },
|
|
1547
|
+
{ id: 'b', type: 'tool', config: {}, depends_on: ['a'] },
|
|
1548
|
+
],
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
expect(() => topologicalSort(circularWorkflow)).toThrow('Circular dependency detected');
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
// ==================== Performance Tests ====================
|
|
1556
|
+
|
|
1557
|
+
describe('Workflow Performance', () => {
|
|
1558
|
+
it('should handle large parallel execution', () => {
|
|
1559
|
+
const nodes: NodeDefinition[] = [
|
|
1560
|
+
{ id: 'start', type: 'tool', config: { tool: 'echo' } },
|
|
1561
|
+
];
|
|
1562
|
+
|
|
1563
|
+
// Create 20 parallel nodes
|
|
1564
|
+
for (let i = 0; i < 20; i++) {
|
|
1565
|
+
nodes.push({
|
|
1566
|
+
id: `parallel-${i}`,
|
|
1567
|
+
type: 'tool',
|
|
1568
|
+
config: { tool: 'calculator' },
|
|
1569
|
+
depends_on: ['start'],
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Merge all parallel results
|
|
1574
|
+
nodes.push({
|
|
1575
|
+
id: 'merge',
|
|
1576
|
+
type: 'merge',
|
|
1577
|
+
config: { strategy: 'collect' },
|
|
1578
|
+
depends_on: nodes.slice(1).map(n => n.id),
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
const workflow: WorkflowDefinition = {
|
|
1582
|
+
name: 'large-parallel',
|
|
1583
|
+
nodes,
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const order = topologicalSort(workflow);
|
|
1587
|
+
expect(order.length).toBe(22); // 1 start + 20 parallel + 1 merge
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it('should handle deep dependency chains', () => {
|
|
1591
|
+
const nodes: NodeDefinition[] = [];
|
|
1592
|
+
|
|
1593
|
+
// Create 50-node deep chain
|
|
1594
|
+
for (let i = 0; i < 50; i++) {
|
|
1595
|
+
nodes.push({
|
|
1596
|
+
id: `node-${i}`,
|
|
1597
|
+
type: 'tool',
|
|
1598
|
+
config: { tool: 'echo' },
|
|
1599
|
+
depends_on: i > 0 ? [`node-${i - 1}`] : [],
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const workflow: WorkflowDefinition = {
|
|
1604
|
+
name: 'deep-chain',
|
|
1605
|
+
nodes,
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
const order = topologicalSort(workflow);
|
|
1609
|
+
|
|
1610
|
+
// Should maintain order
|
|
1611
|
+
for (let i = 1; i < order.length; i++) {
|
|
1612
|
+
expect(order.indexOf(`node-${i - 1}`)).toBeLessThan(order.indexOf(`node-${i}`));
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
it('should execute large workflow efficiently', () => {
|
|
1617
|
+
const nodes: NodeDefinition[] = [
|
|
1618
|
+
{ id: 'start', type: 'tool', config: { tool: 'echo' } },
|
|
1619
|
+
];
|
|
1620
|
+
|
|
1621
|
+
for (let i = 0; i < 10; i++) {
|
|
1622
|
+
nodes.push({
|
|
1623
|
+
id: `node-${i}`,
|
|
1624
|
+
type: 'tool',
|
|
1625
|
+
config: { tool: 'calculator' },
|
|
1626
|
+
depends_on: i === 0 ? ['start'] : [`node-${i - 1}`],
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const workflow: WorkflowDefinition = {
|
|
1631
|
+
name: 'large-chain',
|
|
1632
|
+
nodes,
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
const executor = new SyncWorkflowExecutor();
|
|
1636
|
+
const start = Date.now();
|
|
1637
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1638
|
+
const duration = Date.now() - start;
|
|
1639
|
+
|
|
1640
|
+
expect(results.size).toBe(11);
|
|
1641
|
+
expect(duration).toBeLessThan(100); // Should complete in under 100ms
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// ==================== Edge Cases ====================
|
|
1646
|
+
|
|
1647
|
+
describe('Workflow Edge Cases', () => {
|
|
1648
|
+
it('should handle single node workflow', () => {
|
|
1649
|
+
const workflow: WorkflowDefinition = {
|
|
1650
|
+
name: 'single-node',
|
|
1651
|
+
nodes: [{ id: 'only', type: 'tool', config: {} }],
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
const order = topologicalSort(workflow);
|
|
1655
|
+
expect(order).toEqual(['only']);
|
|
1656
|
+
|
|
1657
|
+
const executor = new SyncWorkflowExecutor();
|
|
1658
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1659
|
+
expect(results.size).toBe(1);
|
|
1660
|
+
expect(results.get('only')).toBeDefined();
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
it('should handle nodes with multiple dependencies', () => {
|
|
1664
|
+
const workflow: WorkflowDefinition = {
|
|
1665
|
+
name: 'multi-dep',
|
|
1666
|
+
nodes: [
|
|
1667
|
+
{ id: 'a', type: 'tool', config: {} },
|
|
1668
|
+
{ id: 'b', type: 'tool', config: {} },
|
|
1669
|
+
{ id: 'c', type: 'tool', config: {} },
|
|
1670
|
+
{ id: 'd', type: 'tool', config: {}, depends_on: ['a', 'b', 'c'] },
|
|
1671
|
+
],
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
const order = topologicalSort(workflow);
|
|
1675
|
+
const dIndex = order.indexOf('d');
|
|
1676
|
+
expect(order.indexOf('a')).toBeLessThan(dIndex);
|
|
1677
|
+
expect(order.indexOf('b')).toBeLessThan(dIndex);
|
|
1678
|
+
expect(order.indexOf('c')).toBeLessThan(dIndex);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
it('should handle multiple entry points', () => {
|
|
1682
|
+
const workflow: WorkflowDefinition = {
|
|
1683
|
+
name: 'multi-entry',
|
|
1684
|
+
entry: ['a', 'b'],
|
|
1685
|
+
nodes: [
|
|
1686
|
+
{ id: 'a', type: 'tool', config: {} },
|
|
1687
|
+
{ id: 'b', type: 'tool', config: {} },
|
|
1688
|
+
{ id: 'c', type: 'tool', config: {}, depends_on: ['a', 'b'] },
|
|
1689
|
+
],
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
const order = topologicalSort(workflow);
|
|
1693
|
+
expect(order.includes('a')).toBe(true);
|
|
1694
|
+
expect(order.includes('b')).toBe(true);
|
|
1695
|
+
expect(order.indexOf('c')).toBeGreaterThan(order.indexOf('a'));
|
|
1696
|
+
expect(order.indexOf('c')).toBeGreaterThan(order.indexOf('b'));
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
it('should handle optional fields in nodes', () => {
|
|
1700
|
+
const minimalNode = {
|
|
1701
|
+
id: 'minimal',
|
|
1702
|
+
type: 'tool',
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
const NodeDefinitionSchema = z.object({
|
|
1706
|
+
id: z.string().min(1),
|
|
1707
|
+
type: z.string().min(1),
|
|
1708
|
+
name: z.string().optional(),
|
|
1709
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
1710
|
+
depends_on: z.array(z.string()).optional(),
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
const result = NodeDefinitionSchema.safeParse(minimalNode);
|
|
1714
|
+
expect(result.success).toBe(true);
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
it('should handle empty depends_on array', () => {
|
|
1718
|
+
const workflow: WorkflowDefinition = {
|
|
1719
|
+
name: 'empty-deps',
|
|
1720
|
+
nodes: [
|
|
1721
|
+
{ id: 'a', type: 'tool', config: {}, depends_on: [] },
|
|
1722
|
+
{ id: 'b', type: 'tool', config: {}, depends_on: ['a'] },
|
|
1723
|
+
],
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const order = topologicalSort(workflow);
|
|
1727
|
+
expect(order.indexOf('a')).toBeLessThan(order.indexOf('b'));
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
it('should handle nodes without config', () => {
|
|
1731
|
+
const workflow: WorkflowDefinition = {
|
|
1732
|
+
name: 'no-config',
|
|
1733
|
+
nodes: [
|
|
1734
|
+
{ id: 'a', type: 'tool' },
|
|
1735
|
+
{ id: 'b', type: 'tool', depends_on: ['a'] },
|
|
1736
|
+
],
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
const executor = new SyncWorkflowExecutor();
|
|
1740
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1741
|
+
expect(results.get('a')).toBeDefined();
|
|
1742
|
+
expect(results.get('b')).toBeDefined();
|
|
1743
|
+
});
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
// ==================== Execution Trace Tests ====================
|
|
1747
|
+
|
|
1748
|
+
describe('Execution Trace', () => {
|
|
1749
|
+
it('should track execution order', () => {
|
|
1750
|
+
const workflow = createDiamondWorkflow();
|
|
1751
|
+
const executor = new SyncWorkflowExecutor();
|
|
1752
|
+
|
|
1753
|
+
executeWorkflowSync(workflow, {}, executor);
|
|
1754
|
+
|
|
1755
|
+
const trace = executor.getTrace();
|
|
1756
|
+
expect(trace.executionOrder).toEqual(['A', 'B1', 'B2', 'B3', 'C', 'D']);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
it('should track node execution status', () => {
|
|
1760
|
+
const workflow = createDiamondWorkflow();
|
|
1761
|
+
const executor = new SyncWorkflowExecutor();
|
|
1762
|
+
|
|
1763
|
+
executeWorkflowSync(workflow, {}, executor);
|
|
1764
|
+
|
|
1765
|
+
const trace = executor.getTrace();
|
|
1766
|
+
|
|
1767
|
+
for (const entry of trace.nodes.values()) {
|
|
1768
|
+
expect(entry.status).toBe('completed');
|
|
1769
|
+
expect(entry.startTime).toBeDefined();
|
|
1770
|
+
expect(entry.endTime).toBeDefined();
|
|
1771
|
+
expect(entry.endTime).toBeGreaterThanOrEqual(entry.startTime);
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
it('should record input for each node', () => {
|
|
1776
|
+
const workflow = createDataProcessingWorkflow();
|
|
1777
|
+
const executor = new SyncWorkflowExecutor();
|
|
1778
|
+
const input = { generateReport: true, test: 'value' };
|
|
1779
|
+
|
|
1780
|
+
executeWorkflowSync(workflow, input, executor);
|
|
1781
|
+
|
|
1782
|
+
const trace = executor.getTrace();
|
|
1783
|
+
const initEntry = trace.nodes.get('init');
|
|
1784
|
+
expect(initEntry?.input).toEqual(input);
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
it('should record output for each node', () => {
|
|
1788
|
+
const workflow = createDiamondWorkflow();
|
|
1789
|
+
const executor = new SyncWorkflowExecutor();
|
|
1790
|
+
|
|
1791
|
+
executeWorkflowSync(workflow, {}, executor);
|
|
1792
|
+
|
|
1793
|
+
const trace = executor.getTrace();
|
|
1794
|
+
|
|
1795
|
+
for (const [nodeId, entry] of trace.nodes) {
|
|
1796
|
+
expect(entry.output).toBeDefined();
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
// ==================== Merge Node Tests ====================
|
|
1802
|
+
|
|
1803
|
+
describe('Merge Node Behavior', () => {
|
|
1804
|
+
it('should collect all dependency outputs', () => {
|
|
1805
|
+
const workflow: WorkflowDefinition = {
|
|
1806
|
+
name: 'merge-test',
|
|
1807
|
+
nodes: [
|
|
1808
|
+
{ id: 'a', type: 'tool', config: { tool: 'calculator' } },
|
|
1809
|
+
{ id: 'b', type: 'tool', config: { tool: 'calculator' } },
|
|
1810
|
+
{ id: 'c', type: 'tool', config: { tool: 'calculator' } },
|
|
1811
|
+
{
|
|
1812
|
+
id: 'merge',
|
|
1813
|
+
type: 'merge',
|
|
1814
|
+
config: { strategy: 'collect' },
|
|
1815
|
+
depends_on: ['a', 'b', 'c'],
|
|
1816
|
+
},
|
|
1817
|
+
],
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
const executor = new SyncWorkflowExecutor();
|
|
1821
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1822
|
+
|
|
1823
|
+
const mergeResult = results.get('merge');
|
|
1824
|
+
expect(mergeResult).toBeDefined();
|
|
1825
|
+
expect(mergeResult.merged).toBe(true);
|
|
1826
|
+
expect(mergeResult.inputs).toBeDefined();
|
|
1827
|
+
expect(mergeResult.inputs.a).toBeDefined();
|
|
1828
|
+
expect(mergeResult.inputs.b).toBeDefined();
|
|
1829
|
+
expect(mergeResult.inputs.c).toBeDefined();
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
it('should handle merge with different strategies', () => {
|
|
1833
|
+
const mergeStrategies = ['collect', 'merge', 'concat', 'zip'];
|
|
1834
|
+
|
|
1835
|
+
for (const strategy of mergeStrategies) {
|
|
1836
|
+
const workflow: WorkflowDefinition = {
|
|
1837
|
+
name: `merge-${strategy}`,
|
|
1838
|
+
nodes: [
|
|
1839
|
+
{ id: 'a', type: 'tool', config: { tool: 'calculator' } },
|
|
1840
|
+
{ id: 'b', type: 'tool', config: { tool: 'calculator' } },
|
|
1841
|
+
{
|
|
1842
|
+
id: 'merge',
|
|
1843
|
+
type: 'merge',
|
|
1844
|
+
config: { strategy },
|
|
1845
|
+
depends_on: ['a', 'b'],
|
|
1846
|
+
},
|
|
1847
|
+
],
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
const executor = new SyncWorkflowExecutor();
|
|
1851
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1852
|
+
const mergeResult = results.get('merge');
|
|
1853
|
+
expect(mergeResult).toBeDefined();
|
|
1854
|
+
expect(mergeResult.mergeStrategy).toBe(strategy);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// ==================== Condition Node Tests ====================
|
|
1860
|
+
|
|
1861
|
+
describe('Condition Node Behavior', () => {
|
|
1862
|
+
it('should evaluate true condition', () => {
|
|
1863
|
+
const workflow: WorkflowDefinition = {
|
|
1864
|
+
name: 'condition-test',
|
|
1865
|
+
nodes: [
|
|
1866
|
+
{ id: 'check', type: 'condition', config: { condition: 'true' } },
|
|
1867
|
+
{ id: 'output', type: 'tool', config: { tool: 'echo' }, depends_on: ['check'] },
|
|
1868
|
+
],
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
const executor = new SyncWorkflowExecutor();
|
|
1872
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1873
|
+
|
|
1874
|
+
const checkResult = results.get('check');
|
|
1875
|
+
expect(checkResult).toBeDefined();
|
|
1876
|
+
expect(checkResult.conditionMet).toBe(true);
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
it('should evaluate template condition', () => {
|
|
1880
|
+
const workflow: WorkflowDefinition = {
|
|
1881
|
+
name: 'condition-template',
|
|
1882
|
+
nodes: [
|
|
1883
|
+
{
|
|
1884
|
+
id: 'check',
|
|
1885
|
+
type: 'condition',
|
|
1886
|
+
config: { condition: '{{merge-data.valid}}' },
|
|
1887
|
+
},
|
|
1888
|
+
{ id: 'output', type: 'tool', config: { tool: 'echo' }, depends_on: ['check'] },
|
|
1889
|
+
],
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
const executor = new SyncWorkflowExecutor();
|
|
1893
|
+
const results = executeWorkflowSync(workflow, {}, executor);
|
|
1894
|
+
|
|
1895
|
+
const checkResult = results.get('check');
|
|
1896
|
+
expect(checkResult).toBeDefined();
|
|
1897
|
+
expect(checkResult.conditionMet).toBe(true); // Template conditions default to true
|
|
1898
|
+
expect(checkResult.evaluatedCondition).toBe('{{merge-data.valid}}');
|
|
1899
|
+
});
|
|
1900
|
+
});
|