@desplega.ai/agent-swarm 1.20.0 → 1.51.2
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/README.md +271 -169
- package/openapi.json +5015 -0
- package/package.json +40 -7
- package/plugin/commands/close-issue.md +7 -3
- package/plugin/commands/create-pr.md +18 -12
- package/plugin/commands/implement-issue.md +7 -3
- package/plugin/commands/respond-github.md +8 -4
- package/plugin/commands/review-pr.md +44 -10
- package/plugin/commands/start-leader.md +1 -3
- package/plugin/commands/start-worker.md +1 -3
- package/plugin/commands/work-on-task.md +22 -3
- package/plugin/pi-skills/close-issue/SKILL.md +90 -0
- package/plugin/pi-skills/create-pr/SKILL.md +99 -0
- package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
- package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
- package/plugin/pi-skills/respond-github/SKILL.md +98 -0
- package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
- package/plugin/pi-skills/review-pr/SKILL.md +261 -0
- package/plugin/pi-skills/start-leader/SKILL.md +121 -0
- package/plugin/pi-skills/start-worker/SKILL.md +60 -0
- package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
- package/plugin/pi-skills/todos/SKILL.md +66 -0
- package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
- package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
- package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
- package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
- package/plugin/skills/artifacts/examples/static-report.sh +17 -0
- package/plugin/skills/artifacts/skill.md +71 -0
- package/src/agentmail/app.ts +65 -0
- package/src/agentmail/handlers.ts +262 -0
- package/src/agentmail/index.ts +9 -0
- package/src/agentmail/templates.ts +111 -0
- package/src/agentmail/types.ts +51 -0
- package/src/artifact-sdk/browser-sdk.ts +30 -0
- package/src/artifact-sdk/index.ts +2 -0
- package/src/artifact-sdk/localtunnel.d.ts +20 -0
- package/src/artifact-sdk/port.ts +12 -0
- package/src/artifact-sdk/server.ts +156 -0
- package/src/artifact-sdk/tunnel.ts +19 -0
- package/src/be/chunking.ts +193 -0
- package/src/be/db-queries/oauth.ts +90 -0
- package/src/be/db-queries/tracker.ts +182 -0
- package/src/be/db.ts +3327 -784
- package/src/be/embedding.ts +80 -0
- package/src/be/migrations/001_initial.sql +409 -0
- package/src/be/migrations/002_one_time_schedules.sql +59 -0
- package/src/be/migrations/003_workflows.sql +51 -0
- package/src/be/migrations/004_workflow_source.sql +81 -0
- package/src/be/migrations/005_epic_next_steps.sql +2 -0
- package/src/be/migrations/006_vcs_provider.sql +94 -0
- package/src/be/migrations/007_task_dir.sql +2 -0
- package/src/be/migrations/008_workflow_redesign.sql +85 -0
- package/src/be/migrations/009_tracker_integration.sql +144 -0
- package/src/be/migrations/010_step_diagnostics.sql +1 -0
- package/src/be/migrations/011_step_next_port.sql +1 -0
- package/src/be/migrations/012_trigger_schema.sql +1 -0
- package/src/be/migrations/013_task_output_schema.sql +2 -0
- package/src/be/migrations/014_prompt_templates.sql +33 -0
- package/src/be/migrations/015_workflow_workspace.sql +3 -0
- package/src/be/migrations/016_active_session_runner_session.sql +4 -0
- package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
- package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
- package/src/be/migrations/runner.ts +188 -0
- package/src/be/seed.ts +62 -0
- package/src/cli.tsx +231 -299
- package/src/commands/artifact.ts +241 -0
- package/src/commands/onboard/compose-generator.ts +169 -0
- package/src/commands/onboard/env-generator.ts +79 -0
- package/src/commands/onboard/manifest.ts +37 -0
- package/src/commands/onboard/presets.ts +85 -0
- package/src/commands/onboard/service-names.ts +47 -0
- package/src/commands/onboard/steps/core-credentials.tsx +111 -0
- package/src/commands/onboard/steps/custom-templates.tsx +168 -0
- package/src/commands/onboard/steps/generate.tsx +154 -0
- package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
- package/src/commands/onboard/steps/harness.tsx +21 -0
- package/src/commands/onboard/steps/health-check.tsx +171 -0
- package/src/commands/onboard/steps/integration-github.tsx +105 -0
- package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
- package/src/commands/onboard/steps/integration-menu.tsx +58 -0
- package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
- package/src/commands/onboard/steps/integration-slack.tsx +165 -0
- package/src/commands/onboard/steps/post-connect.tsx +145 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
- package/src/commands/onboard/steps/post-task.tsx +103 -0
- package/src/commands/onboard/steps/prereq-check.tsx +178 -0
- package/src/commands/onboard/steps/review.tsx +82 -0
- package/src/commands/onboard/steps/start.tsx +97 -0
- package/src/commands/onboard/templates.ts +34 -0
- package/src/commands/onboard/types.ts +259 -0
- package/src/commands/onboard.tsx +425 -0
- package/src/commands/runner.ts +1540 -630
- package/src/commands/setup.tsx +23 -38
- package/src/commands/shared/client-config.ts +41 -0
- package/src/commands/templates.ts +172 -0
- package/src/github/app.ts +8 -0
- package/src/github/handlers.ts +384 -151
- package/src/github/index.ts +1 -0
- package/src/github/mentions-aliases.test.ts +73 -0
- package/src/github/mentions.test.ts +3 -3
- package/src/github/mentions.ts +32 -6
- package/src/github/templates.ts +398 -0
- package/src/github/types.ts +1 -0
- package/src/gitlab/auth.ts +63 -0
- package/src/gitlab/handlers.ts +368 -0
- package/src/gitlab/index.ts +19 -0
- package/src/gitlab/reactions.ts +104 -0
- package/src/gitlab/templates.ts +140 -0
- package/src/gitlab/types.ts +130 -0
- package/src/heartbeat/heartbeat.ts +434 -0
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/templates.ts +30 -0
- package/src/hooks/hook.ts +555 -4
- package/src/hooks/tool-loop-detection.test.ts +158 -0
- package/src/hooks/tool-loop-detection.ts +167 -0
- package/src/http/active-sessions.ts +199 -0
- package/src/http/agents.ts +328 -0
- package/src/http/config.ts +191 -0
- package/src/http/core.ts +309 -0
- package/src/http/db-query.ts +91 -0
- package/src/http/ecosystem.ts +63 -0
- package/src/http/epics.ts +460 -0
- package/src/http/index.ts +216 -0
- package/src/http/mcp.ts +77 -0
- package/src/http/memory.ts +168 -0
- package/src/http/openapi.ts +109 -0
- package/src/http/poll.ts +299 -0
- package/src/http/prompt-templates.ts +412 -0
- package/src/http/repos.ts +195 -0
- package/src/http/route-def.ts +123 -0
- package/src/http/schedules.ts +426 -0
- package/src/http/session-data.ts +241 -0
- package/src/http/stats.ts +174 -0
- package/src/http/tasks.ts +468 -0
- package/src/http/trackers/index.ts +10 -0
- package/src/http/trackers/linear.ts +187 -0
- package/src/http/types.ts +12 -0
- package/src/http/utils.ts +87 -0
- package/src/http/webhooks.ts +432 -0
- package/src/http/workflows.ts +530 -0
- package/src/http.ts +1 -1890
- package/src/linear/README.md +65 -0
- package/src/linear/app.ts +48 -0
- package/src/linear/client.ts +18 -0
- package/src/linear/index.ts +1 -0
- package/src/linear/oauth.ts +35 -0
- package/src/linear/outbound.ts +212 -0
- package/src/linear/sync.ts +567 -0
- package/src/linear/templates.ts +47 -0
- package/src/linear/types.ts +7 -0
- package/src/linear/webhook.ts +104 -0
- package/src/oauth/README.md +66 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/wrapper.ts +204 -0
- package/src/prompts/base-prompt.ts +150 -265
- package/src/prompts/defaults.ts +196 -0
- package/src/prompts/registry.ts +57 -0
- package/src/prompts/resolver.ts +296 -0
- package/src/prompts/session-templates.ts +604 -0
- package/src/providers/claude-adapter.ts +442 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/pi-mono-adapter.ts +442 -0
- package/src/providers/pi-mono-extension.ts +624 -0
- package/src/providers/pi-mono-mcp-client.ts +124 -0
- package/src/providers/types.ts +75 -0
- package/src/scheduler/scheduler.test.ts +2 -0
- package/src/scheduler/scheduler.ts +231 -40
- package/src/server.ts +97 -6
- package/src/slack/HEURISTICS.md +105 -0
- package/src/slack/actions.ts +133 -0
- package/src/slack/app.ts +7 -0
- package/src/slack/assistant.ts +118 -0
- package/src/slack/blocks.ts +233 -0
- package/src/slack/channel-activity.ts +177 -0
- package/src/slack/commands.ts +31 -17
- package/src/slack/files.ts +1 -1
- package/src/slack/handlers.test.ts +114 -1
- package/src/slack/handlers.ts +230 -55
- package/src/slack/responses.ts +120 -67
- package/src/slack/router.ts +17 -99
- package/src/slack/templates.ts +55 -0
- package/src/slack/thread-buffer.ts +213 -0
- package/src/slack/watcher.ts +119 -4
- package/src/tests/agent-activity.test.ts +247 -0
- package/src/tests/agentmail-filters.test.ts +97 -0
- package/src/tests/artifact-sdk.test.ts +800 -0
- package/src/tests/base-prompt.test.ts +264 -0
- package/src/tests/build-pi-skills.test.ts +127 -0
- package/src/tests/channel-activity.test.ts +363 -0
- package/src/tests/claude-adapter.test.ts +126 -0
- package/src/tests/context-versioning.test.ts +425 -0
- package/src/tests/db-queries-oauth.test.ts +197 -0
- package/src/tests/db-queries-tracker.test.ts +230 -0
- package/src/tests/epics.test.ts +3 -3
- package/src/tests/error-tracker.test.ts +368 -0
- package/src/tests/fetch-resolved-env.test.ts +167 -0
- package/src/tests/generate-default-claude-md.test.ts +9 -1
- package/src/tests/generate-identity-templates.test.ts +124 -0
- package/src/tests/gitlab-auth.test.ts +109 -0
- package/src/tests/gitlab-handlers.test.ts +691 -0
- package/src/tests/gitlab-vcs-db.test.ts +177 -0
- package/src/tests/heartbeat.test.ts +364 -0
- package/src/tests/http-api-integration.test.ts +1698 -0
- package/src/tests/linear-outbound-sync.test.ts +200 -0
- package/src/tests/linear-webhook.test.ts +406 -0
- package/src/tests/match-route.test.ts +187 -0
- package/src/tests/memory.test.ts +737 -0
- package/src/tests/migration-runner-regressions.test.ts +86 -0
- package/src/tests/model-control.test.ts +338 -0
- package/src/tests/oauth-wrapper.test.ts +147 -0
- package/src/tests/onboard-compose.test.ts +138 -0
- package/src/tests/onboard-env.test.ts +174 -0
- package/src/tests/onboard-manifest.test.ts +137 -0
- package/src/tests/pi-mono-adapter.test.ts +234 -0
- package/src/tests/pool-session-logs.test.ts +199 -0
- package/src/tests/progress-dedup.test.ts +98 -0
- package/src/tests/prompt-template-github.test.ts +682 -0
- package/src/tests/prompt-template-remaining.test.ts +504 -0
- package/src/tests/prompt-template-resolver.test.ts +621 -0
- package/src/tests/prompt-template-session.test.ts +363 -0
- package/src/tests/prompt-templates-db.test.ts +616 -0
- package/src/tests/provider-adapter.test.ts +122 -0
- package/src/tests/provider-command-format.test.ts +98 -0
- package/src/tests/reload-config.test.ts +170 -0
- package/src/tests/runner-polling-api.test.ts +25 -20
- package/src/tests/scheduled-tasks.test.ts +104 -0
- package/src/tests/scheduler-backoff.test.ts +166 -0
- package/src/tests/self-improvement.test.ts +541 -0
- package/src/tests/session-attach.test.ts +536 -0
- package/src/tests/session-costs.test.ts +267 -1
- package/src/tests/slack-actions.test.ts +133 -0
- package/src/tests/slack-assistant.test.ts +136 -0
- package/src/tests/slack-blocks.test.ts +246 -0
- package/src/tests/slack-metadata-inheritance.test.ts +243 -0
- package/src/tests/slack-queue-offline.test.ts +174 -0
- package/src/tests/slack-router.test.ts +181 -0
- package/src/tests/slack-thread-buffer.test.ts +305 -0
- package/src/tests/slack-thread-followups.test.ts +298 -0
- package/src/tests/slack-watcher.test.ts +101 -0
- package/src/tests/structured-output.test.ts +307 -0
- package/src/tests/swarm-repos.test.ts +198 -0
- package/src/tests/task-cancellation.test.ts +6 -4
- package/src/tests/task-working-dir.test.ts +176 -0
- package/src/tests/template-fetch.test.ts +490 -0
- package/src/tests/tool-annotations.test.ts +371 -0
- package/src/tests/tracker-tools.test.ts +184 -0
- package/src/tests/update-profile-agentid.test.ts +248 -0
- package/src/tests/update-profile-api.test.ts +143 -3
- package/src/tests/update-profile-auth.test.ts +195 -0
- package/src/tests/validation-adapters.test.ts +86 -0
- package/src/tests/vcs-provider.test.ts +27 -0
- package/src/tests/workflow-agent-task.test.ts +196 -0
- package/src/tests/workflow-async-v2.test.ts +508 -0
- package/src/tests/workflow-convergence.test.ts +541 -0
- package/src/tests/workflow-definition-validation.test.ts +366 -0
- package/src/tests/workflow-engine-v2.test.ts +691 -0
- package/src/tests/workflow-executors.test.ts +736 -0
- package/src/tests/workflow-http-v2.test.ts +599 -0
- package/src/tests/workflow-integration-io.test.ts +902 -0
- package/src/tests/workflow-io-schemas.test.ts +624 -0
- package/src/tests/workflow-registry.test.ts +592 -0
- package/src/tests/workflow-retry-v2.test.ts +401 -0
- package/src/tests/workflow-retry-validation.test.ts +282 -0
- package/src/tests/workflow-schedule-trigger.test.ts +104 -0
- package/src/tests/workflow-template.test.ts +288 -0
- package/src/tests/workflow-trigger-schema.test.ts +359 -0
- package/src/tests/workflow-triggers-v2.test.ts +264 -0
- package/src/tests/workflow-versions.test.ts +208 -0
- package/src/tests/workflow-workspace.test.ts +272 -0
- package/src/tests/x402-client.test.ts +117 -0
- package/src/tests/x402-config.test.ts +182 -0
- package/src/tests/x402-spending-tracker.test.ts +185 -0
- package/src/tools/cancel-task.ts +2 -0
- package/src/tools/context-diff.ts +171 -0
- package/src/tools/context-history.ts +138 -0
- package/src/tools/create-channel.ts +1 -0
- package/src/tools/db-query.ts +78 -0
- package/src/tools/delete-channel.ts +132 -0
- package/src/tools/epics/assign-task-to-epic.ts +1 -0
- package/src/tools/epics/create-epic.ts +3 -2
- package/src/tools/epics/delete-epic.ts +2 -0
- package/src/tools/epics/get-epic-details.ts +2 -0
- package/src/tools/epics/list-epics.ts +2 -0
- package/src/tools/epics/unassign-task-from-epic.ts +1 -0
- package/src/tools/epics/update-epic.ts +7 -4
- package/src/tools/get-swarm.ts +2 -0
- package/src/tools/get-task-details.ts +2 -0
- package/src/tools/get-tasks.ts +27 -1
- package/src/tools/inject-learning.ts +106 -0
- package/src/tools/join-swarm.ts +17 -7
- package/src/tools/list-channels.ts +2 -0
- package/src/tools/list-services.ts +2 -0
- package/src/tools/memory-get.ts +56 -0
- package/src/tools/memory-search.ts +131 -0
- package/src/tools/my-agent-info.ts +2 -0
- package/src/tools/poll-task.ts +2 -20
- package/src/tools/post-message.ts +1 -0
- package/src/tools/prompt-templates/delete.ts +86 -0
- package/src/tools/prompt-templates/get.ts +89 -0
- package/src/tools/prompt-templates/index.ts +5 -0
- package/src/tools/prompt-templates/list.ts +95 -0
- package/src/tools/prompt-templates/preview.ts +84 -0
- package/src/tools/prompt-templates/set.ts +117 -0
- package/src/tools/read-messages.ts +2 -0
- package/src/tools/register-agentmail-inbox.ts +166 -0
- package/src/tools/register-service.ts +2 -0
- package/src/tools/schedules/create-schedule.ts +134 -24
- package/src/tools/schedules/delete-schedule.ts +2 -0
- package/src/tools/schedules/list-schedules.ts +20 -4
- package/src/tools/schedules/run-schedule-now.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +49 -17
- package/src/tools/send-task.ts +132 -10
- package/src/tools/slack-download-file.ts +4 -2
- package/src/tools/slack-list-channels.ts +2 -0
- package/src/tools/slack-post.ts +2 -0
- package/src/tools/slack-read.ts +2 -0
- package/src/tools/slack-reply.ts +2 -0
- package/src/tools/slack-upload-file.ts +2 -0
- package/src/tools/store-progress.ts +205 -4
- package/src/tools/swarm-config/delete-config.ts +87 -0
- package/src/tools/swarm-config/get-config.ts +108 -0
- package/src/tools/swarm-config/index.ts +4 -0
- package/src/tools/swarm-config/list-config.ts +99 -0
- package/src/tools/swarm-config/set-config.ts +118 -0
- package/src/tools/task-action.ts +50 -5
- package/src/tools/task-dedup.ts +97 -0
- package/src/tools/templates.ts +53 -0
- package/src/tools/tool-config.ts +124 -0
- package/src/tools/tracker/index.ts +6 -0
- package/src/tools/tracker/tracker-link-epic.ts +64 -0
- package/src/tools/tracker/tracker-link-task.ts +64 -0
- package/src/tools/tracker/tracker-map-agent.ts +57 -0
- package/src/tools/tracker/tracker-status.ts +56 -0
- package/src/tools/tracker/tracker-sync-status.ts +42 -0
- package/src/tools/tracker/tracker-unlink.ts +41 -0
- package/src/tools/unregister-service.ts +2 -0
- package/src/tools/update-profile.ts +172 -17
- package/src/tools/update-service-status.ts +2 -0
- package/src/tools/utils.ts +10 -1
- package/src/tools/workflows/create-workflow.ts +129 -0
- package/src/tools/workflows/delete-workflow.ts +42 -0
- package/src/tools/workflows/get-workflow-run.ts +59 -0
- package/src/tools/workflows/get-workflow.ts +53 -0
- package/src/tools/workflows/index.ts +9 -0
- package/src/tools/workflows/list-workflow-runs.ts +48 -0
- package/src/tools/workflows/list-workflows.ts +42 -0
- package/src/tools/workflows/retry-workflow-run.ts +40 -0
- package/src/tools/workflows/trigger-workflow.ts +96 -0
- package/src/tools/workflows/update-workflow.ts +133 -0
- package/src/tracker/types.ts +51 -0
- package/src/types.ts +530 -14
- package/src/utils/credentials.test.ts +156 -0
- package/src/utils/credentials.ts +50 -0
- package/src/utils/error-tracker.ts +190 -0
- package/src/vcs/index.ts +15 -0
- package/src/vcs/types.ts +5 -0
- package/src/workflows/checkpoint.ts +121 -0
- package/src/workflows/cooldown.ts +28 -0
- package/src/workflows/definition.ts +235 -0
- package/src/workflows/engine.ts +580 -0
- package/src/workflows/event-bus.ts +29 -0
- package/src/workflows/executors/agent-task.ts +103 -0
- package/src/workflows/executors/base.ts +86 -0
- package/src/workflows/executors/code-match.ts +88 -0
- package/src/workflows/executors/index.ts +16 -0
- package/src/workflows/executors/notify.ts +93 -0
- package/src/workflows/executors/property-match.ts +104 -0
- package/src/workflows/executors/raw-llm.ts +83 -0
- package/src/workflows/executors/registry.ts +76 -0
- package/src/workflows/executors/script.ts +103 -0
- package/src/workflows/executors/validate.ts +215 -0
- package/src/workflows/executors/vcs.ts +58 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/input.ts +46 -0
- package/src/workflows/json-schema-validator.ts +118 -0
- package/src/workflows/recovery.ts +139 -0
- package/src/workflows/resume.ts +229 -0
- package/src/workflows/retry-poller.ts +216 -0
- package/src/workflows/template.ts +74 -0
- package/src/workflows/templates.ts +86 -0
- package/src/workflows/triggers.ts +124 -0
- package/src/workflows/validation.ts +104 -0
- package/src/workflows/version.ts +44 -0
- package/src/x402/cli.ts +140 -0
- package/src/x402/client.ts +192 -0
- package/src/x402/config.ts +131 -0
- package/src/x402/index.ts +37 -0
- package/src/x402/openfort-signer.ts +83 -0
- package/src/x402/spending-tracker.ts +109 -0
- package/templates/official/coder/CLAUDE.md +49 -0
- package/templates/official/coder/IDENTITY.md +28 -0
- package/templates/official/coder/SOUL.md +43 -0
- package/templates/official/coder/TOOLS.md +40 -0
- package/templates/official/coder/config.json +23 -0
- package/templates/official/coder/start-up.sh +23 -0
- package/templates/official/content-reviewer/CLAUDE.md +68 -0
- package/templates/official/content-reviewer/IDENTITY.md +28 -0
- package/templates/official/content-reviewer/SOUL.md +44 -0
- package/templates/official/content-reviewer/TOOLS.md +37 -0
- package/templates/official/content-reviewer/config.json +23 -0
- package/templates/official/content-reviewer/start-up.sh +23 -0
- package/templates/official/content-strategist/CLAUDE.md +63 -0
- package/templates/official/content-strategist/IDENTITY.md +33 -0
- package/templates/official/content-strategist/SOUL.md +48 -0
- package/templates/official/content-strategist/TOOLS.md +47 -0
- package/templates/official/content-strategist/config.json +23 -0
- package/templates/official/content-strategist/start-up.sh +23 -0
- package/templates/official/content-writer/CLAUDE.md +72 -0
- package/templates/official/content-writer/IDENTITY.md +30 -0
- package/templates/official/content-writer/SOUL.md +46 -0
- package/templates/official/content-writer/TOOLS.md +44 -0
- package/templates/official/content-writer/config.json +23 -0
- package/templates/official/content-writer/start-up.sh +23 -0
- package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
- package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
- package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
- package/templates/official/forward-deployed-engineer/config.json +21 -0
- package/templates/official/lead/CLAUDE.md +33 -0
- package/templates/official/lead/IDENTITY.md +36 -0
- package/templates/official/lead/SOUL.md +51 -0
- package/templates/official/lead/config.json +22 -0
- package/templates/official/researcher/CLAUDE.md +46 -0
- package/templates/official/researcher/IDENTITY.md +28 -0
- package/templates/official/researcher/SOUL.md +43 -0
- package/templates/official/researcher/config.json +21 -0
- package/templates/official/reviewer/CLAUDE.md +63 -0
- package/templates/official/reviewer/IDENTITY.md +28 -0
- package/templates/official/reviewer/SOUL.md +45 -0
- package/templates/official/reviewer/config.json +21 -0
- package/templates/official/tester/CLAUDE.md +53 -0
- package/templates/official/tester/IDENTITY.md +28 -0
- package/templates/official/tester/SOUL.md +55 -0
- package/templates/official/tester/config.json +21 -0
- package/templates/schema.ts +35 -0
- package/.claude/settings.local.json +0 -115
- package/.dockerignore +0 -61
- package/.editorconfig +0 -15
- package/.env.docker.example +0 -39
- package/.env.example +0 -40
- package/.github/workflows/ci.yml +0 -76
- package/.github/workflows/docker-and-deploy.yml +0 -117
- package/.wts-config.json +0 -4
- package/.wts-setup.ts +0 -102
- package/CLAUDE.md +0 -104
- package/CONTRIBUTING.md +0 -270
- package/DEPLOYMENT.md +0 -605
- package/Dockerfile +0 -57
- package/Dockerfile.worker +0 -157
- package/FAQ.md +0 -19
- package/MCP.md +0 -406
- package/UI.md +0 -40
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/assets/agent-swarm.mp4 +0 -0
- package/assets/agent-swarm.png +0 -0
- package/biome.json +0 -39
- package/deploy/DEPLOY.md +0 -60
- package/deploy/agent-swarm.service +0 -17
- package/deploy/docker-push.ts +0 -30
- package/deploy/install.ts +0 -85
- package/deploy/prod-db.ts +0 -42
- package/deploy/uninstall.ts +0 -12
- package/deploy/update.ts +0 -21
- package/docker-compose.example.yml +0 -159
- package/docker-entrypoint.sh +0 -352
- package/ecosystem.config.cjs +0 -66
- package/plugin/README.md +0 -1
- package/plugin/hooks/hooks.json +0 -71
- package/pyproject.toml +0 -9
- package/scripts/generate-mcp-docs.ts +0 -415
- package/slack-manifest.json +0 -71
- package/src/tests/get-inbox-message.test.ts +0 -145
- package/src/tools/get-inbox-message.ts +0 -89
- package/src/tools/inbox-delegate.ts +0 -113
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
- package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
- package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
- package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
- package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
- package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
- package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
- package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
- package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
- package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
- package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
- package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
- package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
- package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
- package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
- package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
- package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
- package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
- package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
- package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
- package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
- package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
- package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
- package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
- package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
- package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
- package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
- package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
- package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
- package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
- package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
- package/tsconfig.json +0 -37
- package/ui/CLAUDE.md +0 -49
- package/ui/bun.lock +0 -771
- package/ui/index.html +0 -22
- package/ui/package-lock.json +0 -5290
- package/ui/package.json +0 -33
- package/ui/pnpm-lock.yaml +0 -3341
- package/ui/postcss.config.js +0 -6
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +0 -63
- package/ui/src/components/ActivityFeed.tsx +0 -440
- package/ui/src/components/AgentDetailPanel.tsx +0 -733
- package/ui/src/components/AgentsPanel.tsx +0 -815
- package/ui/src/components/ChatPanel.tsx +0 -1920
- package/ui/src/components/ConfigModal.tsx +0 -253
- package/ui/src/components/Dashboard.tsx +0 -832
- package/ui/src/components/EditAgentProfileModal.tsx +0 -433
- package/ui/src/components/EpicDetailPage.tsx +0 -741
- package/ui/src/components/EpicsPanel.tsx +0 -566
- package/ui/src/components/Header.tsx +0 -160
- package/ui/src/components/JsonViewer.tsx +0 -171
- package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
- package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
- package/ui/src/components/ServicesPanel.tsx +0 -622
- package/ui/src/components/SessionLogPanel.tsx +0 -1219
- package/ui/src/components/StatsBar.tsx +0 -321
- package/ui/src/components/StatusBadge.tsx +0 -168
- package/ui/src/components/TaskDetailPanel.tsx +0 -903
- package/ui/src/components/TasksPanel.tsx +0 -614
- package/ui/src/components/UsageCharts.tsx +0 -216
- package/ui/src/components/UsageTab.tsx +0 -394
- package/ui/src/hooks/queries.ts +0 -353
- package/ui/src/hooks/useAutoScroll.ts +0 -83
- package/ui/src/index.css +0 -257
- package/ui/src/lib/api.ts +0 -268
- package/ui/src/lib/config.ts +0 -35
- package/ui/src/lib/contentPreview.ts +0 -208
- package/ui/src/lib/theme.ts +0 -214
- package/ui/src/lib/utils.ts +0 -88
- package/ui/src/main.tsx +0 -28
- package/ui/src/types/api.ts +0 -323
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tailwind.config.js +0 -37
- package/ui/tsconfig.json +0 -31
- package/ui/vite.config.ts +0 -35
- /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createWorkflow,
|
|
7
|
+
deleteWorkflow,
|
|
8
|
+
getWorkflowRun,
|
|
9
|
+
getWorkflowRunStepsByRunId,
|
|
10
|
+
initDb,
|
|
11
|
+
} from "../be/db";
|
|
12
|
+
import type { Workflow, WorkflowDefinition } from "../types";
|
|
13
|
+
import { validateDefinition } from "../workflows/definition";
|
|
14
|
+
import { startWorkflowExecution, TriggerSchemaError } from "../workflows/engine";
|
|
15
|
+
import {
|
|
16
|
+
BaseExecutor,
|
|
17
|
+
type ExecutorDependencies,
|
|
18
|
+
type ExecutorResult,
|
|
19
|
+
} from "../workflows/executors/base";
|
|
20
|
+
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
21
|
+
|
|
22
|
+
const TEST_DB_PATH = "./test-workflow-integration-io.sqlite";
|
|
23
|
+
|
|
24
|
+
// ─── Test Executors ──────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Simple executor that echoes its config message. */
|
|
27
|
+
class EchoExecutor extends BaseExecutor<typeof EchoExecutor.schema, typeof EchoExecutor.outSchema> {
|
|
28
|
+
static readonly schema = z.object({ message: z.string() });
|
|
29
|
+
static readonly outSchema = z.object({ echo: z.string() });
|
|
30
|
+
|
|
31
|
+
readonly type = "echo";
|
|
32
|
+
readonly mode = "instant" as const;
|
|
33
|
+
readonly configSchema = EchoExecutor.schema;
|
|
34
|
+
readonly outputSchema = EchoExecutor.outSchema;
|
|
35
|
+
|
|
36
|
+
protected async execute(
|
|
37
|
+
config: z.infer<typeof EchoExecutor.schema>,
|
|
38
|
+
): Promise<ExecutorResult<z.infer<typeof EchoExecutor.outSchema>>> {
|
|
39
|
+
return { status: "success", output: { echo: config.message } };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Branch executor that evaluates a condition on the global context. */
|
|
44
|
+
class BranchExecutor extends BaseExecutor<
|
|
45
|
+
typeof BranchExecutor.schema,
|
|
46
|
+
typeof BranchExecutor.outSchema
|
|
47
|
+
> {
|
|
48
|
+
static readonly schema = z.object({
|
|
49
|
+
conditions: z.array(z.object({ field: z.string(), op: z.string(), value: z.unknown() })),
|
|
50
|
+
});
|
|
51
|
+
static readonly outSchema = z.object({ passed: z.boolean() });
|
|
52
|
+
|
|
53
|
+
readonly type = "property-match";
|
|
54
|
+
readonly mode = "instant" as const;
|
|
55
|
+
readonly configSchema = BranchExecutor.schema;
|
|
56
|
+
readonly outputSchema = BranchExecutor.outSchema;
|
|
57
|
+
|
|
58
|
+
protected async execute(
|
|
59
|
+
config: z.infer<typeof BranchExecutor.schema>,
|
|
60
|
+
context: Readonly<Record<string, unknown>>,
|
|
61
|
+
): Promise<ExecutorResult<z.infer<typeof BranchExecutor.outSchema>>> {
|
|
62
|
+
const cond = config.conditions[0];
|
|
63
|
+
if (!cond) return { status: "success", output: { passed: true }, nextPort: "true" };
|
|
64
|
+
|
|
65
|
+
const fieldPath = cond.field.split(".");
|
|
66
|
+
let value: unknown = context;
|
|
67
|
+
for (const key of fieldPath) {
|
|
68
|
+
if (value == null || typeof value !== "object") {
|
|
69
|
+
value = undefined;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
value = (value as Record<string, unknown>)[key];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let passed = false;
|
|
76
|
+
if (cond.op === "eq") passed = value === cond.value;
|
|
77
|
+
if (cond.op === "neq") passed = value !== cond.value;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
status: "success",
|
|
81
|
+
output: { passed },
|
|
82
|
+
nextPort: passed ? "true" : "false",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Passthrough executor — returns whatever data is passed in config. */
|
|
88
|
+
class PassthroughExecutor extends BaseExecutor<
|
|
89
|
+
typeof PassthroughExecutor.schema,
|
|
90
|
+
typeof PassthroughExecutor.outSchema
|
|
91
|
+
> {
|
|
92
|
+
static readonly schema = z.object({ data: z.unknown() });
|
|
93
|
+
static readonly outSchema = z.record(z.string(), z.unknown());
|
|
94
|
+
|
|
95
|
+
readonly type = "passthrough";
|
|
96
|
+
readonly mode = "instant" as const;
|
|
97
|
+
readonly configSchema = PassthroughExecutor.schema;
|
|
98
|
+
readonly outputSchema = PassthroughExecutor.outSchema;
|
|
99
|
+
|
|
100
|
+
protected async execute(
|
|
101
|
+
config: z.infer<typeof PassthroughExecutor.schema>,
|
|
102
|
+
): Promise<ExecutorResult<z.infer<typeof PassthroughExecutor.outSchema>>> {
|
|
103
|
+
return {
|
|
104
|
+
status: "success",
|
|
105
|
+
output: (config.data ?? {}) as Record<string, unknown>,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Mock Dependencies ───────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const mockDeps: ExecutorDependencies = {
|
|
113
|
+
db: {} as typeof import("../be/db"),
|
|
114
|
+
eventBus: { emit: () => {}, on: () => {}, off: () => {} },
|
|
115
|
+
interpolate: (t: string) => t,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function createTestRegistry(): ExecutorRegistry {
|
|
119
|
+
const registry = new ExecutorRegistry();
|
|
120
|
+
registry.register(new EchoExecutor(mockDeps));
|
|
121
|
+
registry.register(new BranchExecutor(mockDeps));
|
|
122
|
+
registry.register(new PassthroughExecutor(mockDeps));
|
|
123
|
+
return registry;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let workflowCounter = 0;
|
|
127
|
+
const createdWorkflowIds: string[] = [];
|
|
128
|
+
|
|
129
|
+
function makeWorkflow(def: WorkflowDefinition, overrides?: Partial<Workflow>): Workflow {
|
|
130
|
+
workflowCounter++;
|
|
131
|
+
const workflow = createWorkflow({
|
|
132
|
+
name: overrides?.name || `test-integration-io-${workflowCounter}-${Date.now()}`,
|
|
133
|
+
definition: def,
|
|
134
|
+
triggers: overrides?.triggers,
|
|
135
|
+
cooldown: overrides?.cooldown,
|
|
136
|
+
input: overrides?.input,
|
|
137
|
+
triggerSchema: overrides?.triggerSchema,
|
|
138
|
+
});
|
|
139
|
+
createdWorkflowIds.push(workflow.id);
|
|
140
|
+
return { ...workflow, ...overrides };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Tests ───────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe("Workflow Integration — I/O Schemas, Convergence, TriggerSchema (Phase 6)", () => {
|
|
146
|
+
beforeAll(async () => {
|
|
147
|
+
try {
|
|
148
|
+
await unlink(TEST_DB_PATH);
|
|
149
|
+
} catch {
|
|
150
|
+
// File doesn't exist
|
|
151
|
+
}
|
|
152
|
+
initDb(TEST_DB_PATH);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
afterAll(async () => {
|
|
156
|
+
for (const id of createdWorkflowIds) {
|
|
157
|
+
try {
|
|
158
|
+
deleteWorkflow(id);
|
|
159
|
+
} catch {
|
|
160
|
+
// Already deleted
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
closeDb();
|
|
164
|
+
try {
|
|
165
|
+
await unlink(TEST_DB_PATH);
|
|
166
|
+
await unlink(`${TEST_DB_PATH}-wal`);
|
|
167
|
+
await unlink(`${TEST_DB_PATH}-shm`);
|
|
168
|
+
} catch {
|
|
169
|
+
// Files may not exist
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ─── Comprehensive Multi-Feature Workflow ──────────────────
|
|
174
|
+
|
|
175
|
+
describe("Full pipeline: triggerSchema + inputs + inputSchema + convergence + chained data flow", () => {
|
|
176
|
+
// Workflow topology:
|
|
177
|
+
//
|
|
178
|
+
// [A: echo] ──> [B: property-match] ──true──> [C: echo]
|
|
179
|
+
// │ │ │
|
|
180
|
+
// │ false │
|
|
181
|
+
// │ │ │
|
|
182
|
+
// │ v │
|
|
183
|
+
// │ [B_alt: echo] │
|
|
184
|
+
// │ │ │
|
|
185
|
+
// │ └───────────> [D: echo] <
|
|
186
|
+
// └──────────────────────────────────────────┘
|
|
187
|
+
//
|
|
188
|
+
// - triggerSchema enforces { repo: string, action: string }
|
|
189
|
+
// - A: inputs from trigger.repo, inputSchema validates repo is string
|
|
190
|
+
// - B: branches based on A.echo containing "main"
|
|
191
|
+
// - C: reached when B takes "true" port (B_alt skipped)
|
|
192
|
+
// - B_alt: reached when B takes "false" port
|
|
193
|
+
// - D: converges from C (or B_alt), chains data from A and C (or B_alt)
|
|
194
|
+
|
|
195
|
+
const triggerSchema = {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
repo: { type: "string" },
|
|
199
|
+
action: { type: "string" },
|
|
200
|
+
},
|
|
201
|
+
required: ["repo", "action"],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
function buildDef(): WorkflowDefinition {
|
|
205
|
+
return {
|
|
206
|
+
nodes: [
|
|
207
|
+
{
|
|
208
|
+
id: "A",
|
|
209
|
+
type: "echo",
|
|
210
|
+
inputs: { repo: "trigger.repo" },
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: { repo: { type: "string" } },
|
|
214
|
+
required: ["repo"],
|
|
215
|
+
},
|
|
216
|
+
config: { message: "repo={{repo}}" },
|
|
217
|
+
next: "B",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "B",
|
|
221
|
+
type: "property-match",
|
|
222
|
+
config: {
|
|
223
|
+
conditions: [{ field: "A.echo", op: "eq", value: "repo=main-app" }],
|
|
224
|
+
},
|
|
225
|
+
next: { true: "C", false: "B_alt" },
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "C",
|
|
229
|
+
type: "echo",
|
|
230
|
+
inputs: { fromA: "A.echo" },
|
|
231
|
+
config: { message: "C: A said {{fromA}}" },
|
|
232
|
+
next: "D",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "B_alt",
|
|
236
|
+
type: "echo",
|
|
237
|
+
inputs: { fromA: "A.echo" },
|
|
238
|
+
config: { message: "B_alt: A said {{fromA}}" },
|
|
239
|
+
next: "D",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "D",
|
|
243
|
+
type: "echo",
|
|
244
|
+
inputs: { fromA: "A.echo", action: "trigger.action" },
|
|
245
|
+
config: { message: "D: fromA={{fromA}} action={{action}}" },
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
test("triggerSchema rejects invalid payload (missing required field)", async () => {
|
|
252
|
+
const registry = createTestRegistry();
|
|
253
|
+
const def = buildDef();
|
|
254
|
+
const workflow = makeWorkflow(def, { triggerSchema });
|
|
255
|
+
|
|
256
|
+
// Missing "action" field
|
|
257
|
+
await expect(
|
|
258
|
+
startWorkflowExecution(workflow, { repo: "main-app" }, registry),
|
|
259
|
+
).rejects.toThrow(TriggerSchemaError);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await startWorkflowExecution(workflow, { repo: "main-app" }, registry);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
expect(err).toBeInstanceOf(TriggerSchemaError);
|
|
265
|
+
expect((err as TriggerSchemaError).validationErrors.length).toBeGreaterThan(0);
|
|
266
|
+
expect((err as TriggerSchemaError).message).toContain("action");
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("triggerSchema rejects wrong type", async () => {
|
|
271
|
+
const registry = createTestRegistry();
|
|
272
|
+
const def = buildDef();
|
|
273
|
+
const workflow = makeWorkflow(def, { triggerSchema });
|
|
274
|
+
|
|
275
|
+
// repo should be string but passing number
|
|
276
|
+
await expect(
|
|
277
|
+
startWorkflowExecution(workflow, { repo: 123, action: "push" }, registry),
|
|
278
|
+
).rejects.toThrow(TriggerSchemaError);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("triggerSchema accepts valid payload — true branch executes", async () => {
|
|
282
|
+
const registry = createTestRegistry();
|
|
283
|
+
const def = buildDef();
|
|
284
|
+
const workflow = makeWorkflow(def, { triggerSchema });
|
|
285
|
+
|
|
286
|
+
const runId = await startWorkflowExecution(
|
|
287
|
+
workflow,
|
|
288
|
+
{ repo: "main-app", action: "push" },
|
|
289
|
+
registry,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const run = getWorkflowRun(runId);
|
|
293
|
+
expect(run).not.toBeNull();
|
|
294
|
+
expect(run!.status).toBe("completed");
|
|
295
|
+
|
|
296
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
297
|
+
|
|
298
|
+
// A should have echoed the repo from trigger
|
|
299
|
+
expect(ctx.A).toEqual({ echo: "repo=main-app" });
|
|
300
|
+
|
|
301
|
+
// B should have matched (A.echo == "repo=main-app") and taken true port
|
|
302
|
+
expect((ctx.B as Record<string, unknown>).passed).toBe(true);
|
|
303
|
+
|
|
304
|
+
// C should have run (true branch)
|
|
305
|
+
expect(ctx.C).toEqual({ echo: "C: A said repo=main-app" });
|
|
306
|
+
|
|
307
|
+
// D should have combined data from A and trigger
|
|
308
|
+
expect(ctx.D).toEqual({ echo: "D: fromA=repo=main-app action=push" });
|
|
309
|
+
|
|
310
|
+
// B_alt should NOT have run
|
|
311
|
+
expect(ctx.B_alt).toBeUndefined();
|
|
312
|
+
|
|
313
|
+
// Verify step records
|
|
314
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
315
|
+
const nodeIds = steps.map((s) => s.nodeId);
|
|
316
|
+
expect(nodeIds).toContain("A");
|
|
317
|
+
expect(nodeIds).toContain("B");
|
|
318
|
+
expect(nodeIds).toContain("C");
|
|
319
|
+
expect(nodeIds).toContain("D");
|
|
320
|
+
expect(nodeIds).not.toContain("B_alt");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("false branch executes when condition fails — convergence works", async () => {
|
|
324
|
+
const registry = createTestRegistry();
|
|
325
|
+
const def = buildDef();
|
|
326
|
+
const workflow = makeWorkflow(def, { triggerSchema });
|
|
327
|
+
|
|
328
|
+
const runId = await startWorkflowExecution(
|
|
329
|
+
workflow,
|
|
330
|
+
{ repo: "other-repo", action: "deploy" },
|
|
331
|
+
registry,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const run = getWorkflowRun(runId);
|
|
335
|
+
expect(run!.status).toBe("completed");
|
|
336
|
+
|
|
337
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
338
|
+
|
|
339
|
+
// A echoes the different repo
|
|
340
|
+
expect(ctx.A).toEqual({ echo: "repo=other-repo" });
|
|
341
|
+
|
|
342
|
+
// B takes false (A.echo != "repo=main-app")
|
|
343
|
+
expect((ctx.B as Record<string, unknown>).passed).toBe(false);
|
|
344
|
+
|
|
345
|
+
// B_alt should have run (false branch)
|
|
346
|
+
expect(ctx.B_alt).toEqual({ echo: "B_alt: A said repo=other-repo" });
|
|
347
|
+
|
|
348
|
+
// C should NOT have run
|
|
349
|
+
expect(ctx.C).toBeUndefined();
|
|
350
|
+
|
|
351
|
+
// D should still have run (convergence from B_alt)
|
|
352
|
+
expect(ctx.D).toEqual({ echo: "D: fromA=repo=other-repo action=deploy" });
|
|
353
|
+
|
|
354
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
355
|
+
const nodeIds = steps.map((s) => s.nodeId);
|
|
356
|
+
expect(nodeIds).toContain("A");
|
|
357
|
+
expect(nodeIds).toContain("B");
|
|
358
|
+
expect(nodeIds).not.toContain("C");
|
|
359
|
+
expect(nodeIds).toContain("B_alt");
|
|
360
|
+
expect(nodeIds).toContain("D");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ─── InputSchema Validation in Chained Pipeline ─────────────
|
|
365
|
+
|
|
366
|
+
describe("inputSchema validation failure halts pipeline", () => {
|
|
367
|
+
test("node with inputSchema fails when resolved input has wrong type", async () => {
|
|
368
|
+
const registry = createTestRegistry();
|
|
369
|
+
const def: WorkflowDefinition = {
|
|
370
|
+
nodes: [
|
|
371
|
+
{
|
|
372
|
+
id: "source",
|
|
373
|
+
type: "passthrough",
|
|
374
|
+
config: { data: { count: "not-a-number" } },
|
|
375
|
+
next: "consumer",
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id: "consumer",
|
|
379
|
+
type: "echo",
|
|
380
|
+
inputs: { count: "source.count" },
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: { count: { type: "number" } },
|
|
384
|
+
required: ["count"],
|
|
385
|
+
},
|
|
386
|
+
config: { message: "count={{count}}" },
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const workflow = makeWorkflow(def);
|
|
392
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
393
|
+
|
|
394
|
+
const run = getWorkflowRun(runId);
|
|
395
|
+
expect(run!.status).toBe("failed");
|
|
396
|
+
expect(run!.error).toContain("Input schema validation failed");
|
|
397
|
+
expect(run!.error).toContain("count");
|
|
398
|
+
|
|
399
|
+
// consumer step should not have completed
|
|
400
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
401
|
+
const consumerStep = steps.find((s) => s.nodeId === "consumer");
|
|
402
|
+
expect(consumerStep).toBeDefined();
|
|
403
|
+
expect(consumerStep!.status).toBe("failed");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ─── OutputSchema Validation in Pipeline ─────────────────────
|
|
408
|
+
|
|
409
|
+
describe("outputSchema validation in pipeline", () => {
|
|
410
|
+
test("output schema failure prevents downstream execution", async () => {
|
|
411
|
+
const registry = createTestRegistry();
|
|
412
|
+
const def: WorkflowDefinition = {
|
|
413
|
+
nodes: [
|
|
414
|
+
{
|
|
415
|
+
id: "producer",
|
|
416
|
+
type: "passthrough",
|
|
417
|
+
config: { data: { value: "string-not-number" } },
|
|
418
|
+
outputSchema: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: { value: { type: "number" } },
|
|
421
|
+
required: ["value"],
|
|
422
|
+
},
|
|
423
|
+
next: "consumer",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
id: "consumer",
|
|
427
|
+
type: "echo",
|
|
428
|
+
config: { message: "should not run" },
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const workflow = makeWorkflow(def);
|
|
434
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
435
|
+
|
|
436
|
+
const run = getWorkflowRun(runId);
|
|
437
|
+
expect(run!.status).toBe("failed");
|
|
438
|
+
expect(run!.error).toContain("Output schema validation failed");
|
|
439
|
+
|
|
440
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
441
|
+
const nodeIds = steps.map((s) => s.nodeId);
|
|
442
|
+
expect(nodeIds).not.toContain("consumer");
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ─── Unresolved Token Diagnostics ──────────────────────────
|
|
447
|
+
|
|
448
|
+
describe("Unresolved token diagnostics", () => {
|
|
449
|
+
test("unresolved tokens stored in step diagnostics", async () => {
|
|
450
|
+
const registry = createTestRegistry();
|
|
451
|
+
const def: WorkflowDefinition = {
|
|
452
|
+
nodes: [
|
|
453
|
+
{
|
|
454
|
+
id: "step1",
|
|
455
|
+
type: "echo",
|
|
456
|
+
inputs: { typo: "trigger.nonexistent_field" },
|
|
457
|
+
config: { message: "val={{typo}} other={{missing_var}}" },
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const workflow = makeWorkflow(def);
|
|
463
|
+
const runId = await startWorkflowExecution(workflow, { actualField: "data" }, registry);
|
|
464
|
+
|
|
465
|
+
const run = getWorkflowRun(runId);
|
|
466
|
+
expect(run!.status).toBe("completed");
|
|
467
|
+
|
|
468
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
469
|
+
expect(steps).toHaveLength(1);
|
|
470
|
+
|
|
471
|
+
// Check that diagnostics contain unresolved tokens
|
|
472
|
+
const step = steps[0]!;
|
|
473
|
+
if (step.diagnostics) {
|
|
474
|
+
const diag = JSON.parse(step.diagnostics as string);
|
|
475
|
+
expect(diag.unresolvedTokens).toBeDefined();
|
|
476
|
+
expect(diag.unresolvedTokens.length).toBeGreaterThan(0);
|
|
477
|
+
// "missing_var" should be unresolved since it's not in local context
|
|
478
|
+
expect(diag.unresolvedTokens).toContain("missing_var");
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ─── Local Context Isolation ───────────────────────────────
|
|
484
|
+
|
|
485
|
+
describe("Local context isolation — nodes only see declared inputs", () => {
|
|
486
|
+
test("node without inputs mapping cannot access upstream node outputs", async () => {
|
|
487
|
+
const registry = createTestRegistry();
|
|
488
|
+
const def: WorkflowDefinition = {
|
|
489
|
+
nodes: [
|
|
490
|
+
{
|
|
491
|
+
id: "A",
|
|
492
|
+
type: "echo",
|
|
493
|
+
config: { message: "secret" },
|
|
494
|
+
next: "B",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "B",
|
|
498
|
+
type: "echo",
|
|
499
|
+
// No inputs — should NOT be able to see A's output
|
|
500
|
+
config: { message: "A.echo={{A.echo}}" },
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const workflow = makeWorkflow(def);
|
|
506
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
507
|
+
|
|
508
|
+
const run = getWorkflowRun(runId);
|
|
509
|
+
expect(run!.status).toBe("completed");
|
|
510
|
+
|
|
511
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
512
|
+
// B should have an unresolved token — A.echo not in local context
|
|
513
|
+
expect(ctx.B).toEqual({ echo: "A.echo=" });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("node with explicit inputs gets only declared values", async () => {
|
|
517
|
+
const registry = createTestRegistry();
|
|
518
|
+
const def: WorkflowDefinition = {
|
|
519
|
+
nodes: [
|
|
520
|
+
{
|
|
521
|
+
id: "A",
|
|
522
|
+
type: "passthrough",
|
|
523
|
+
config: { data: { x: 1, y: 2 } },
|
|
524
|
+
next: "B",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: "B",
|
|
528
|
+
type: "echo",
|
|
529
|
+
inputs: { xVal: "A.x" },
|
|
530
|
+
// Can access xVal but not A.y directly
|
|
531
|
+
config: { message: "x={{xVal}} y={{A.y}}" },
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const workflow = makeWorkflow(def);
|
|
537
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
538
|
+
|
|
539
|
+
const run = getWorkflowRun(runId);
|
|
540
|
+
expect(run!.status).toBe("completed");
|
|
541
|
+
|
|
542
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
543
|
+
// xVal resolves to 1, A.y is unresolved (not in local context)
|
|
544
|
+
expect(ctx.B).toEqual({ echo: "x=1 y=" });
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// ─── Trigger and Input Built-in Sources ────────────────────
|
|
549
|
+
|
|
550
|
+
describe("Built-in sources (trigger, input) always available", () => {
|
|
551
|
+
test("trigger data accessible even with inputs declared", async () => {
|
|
552
|
+
const registry = createTestRegistry();
|
|
553
|
+
const def: WorkflowDefinition = {
|
|
554
|
+
nodes: [
|
|
555
|
+
{
|
|
556
|
+
id: "step1",
|
|
557
|
+
type: "echo",
|
|
558
|
+
inputs: { custom: "trigger.name" },
|
|
559
|
+
config: { message: "custom={{custom}} direct={{trigger.name}}" },
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const workflow = makeWorkflow(def);
|
|
565
|
+
const runId = await startWorkflowExecution(workflow, { name: "test" }, registry);
|
|
566
|
+
|
|
567
|
+
const run = getWorkflowRun(runId);
|
|
568
|
+
expect(run!.status).toBe("completed");
|
|
569
|
+
|
|
570
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
571
|
+
// Both custom and direct trigger access should resolve
|
|
572
|
+
expect(ctx.step1).toEqual({ echo: "custom=test direct=test" });
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("workflow-level input accessible in nodes", async () => {
|
|
576
|
+
const registry = createTestRegistry();
|
|
577
|
+
const def: WorkflowDefinition = {
|
|
578
|
+
nodes: [
|
|
579
|
+
{
|
|
580
|
+
id: "step1",
|
|
581
|
+
type: "echo",
|
|
582
|
+
config: { message: "env={{input.API_KEY}}" },
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Set env var for input resolution
|
|
588
|
+
process.env.TEST_INTEG_API_KEY = "secret123";
|
|
589
|
+
const workflow = makeWorkflow(def, {
|
|
590
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: testing env var syntax
|
|
591
|
+
input: { API_KEY: "${TEST_INTEG_API_KEY}" },
|
|
592
|
+
});
|
|
593
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
594
|
+
delete process.env.TEST_INTEG_API_KEY;
|
|
595
|
+
|
|
596
|
+
const run = getWorkflowRun(runId);
|
|
597
|
+
expect(run!.status).toBe("completed");
|
|
598
|
+
|
|
599
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
600
|
+
expect(ctx.step1).toEqual({ echo: "env=secret123" });
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// ─── Static Data Flow Validation (validateDefinition) ──────
|
|
605
|
+
|
|
606
|
+
describe("Static data flow validation — validateDefinition", () => {
|
|
607
|
+
test("valid pipeline with upstream inputs passes", () => {
|
|
608
|
+
const registry = createTestRegistry();
|
|
609
|
+
const def: WorkflowDefinition = {
|
|
610
|
+
nodes: [
|
|
611
|
+
{ id: "A", type: "echo", config: { message: "hi" }, next: "B" },
|
|
612
|
+
{
|
|
613
|
+
id: "B",
|
|
614
|
+
type: "echo",
|
|
615
|
+
inputs: { fromA: "A.echo" },
|
|
616
|
+
config: { message: "{{fromA}}" },
|
|
617
|
+
next: "C",
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: "C",
|
|
621
|
+
type: "echo",
|
|
622
|
+
inputs: { fromA: "A.echo", fromB: "B.echo" },
|
|
623
|
+
config: { message: "{{fromA}} {{fromB}}" },
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const result = validateDefinition(def, registry);
|
|
629
|
+
expect(result.valid).toBe(true);
|
|
630
|
+
expect(result.errors).toHaveLength(0);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("input referencing non-existent node fails validation", () => {
|
|
634
|
+
const def: WorkflowDefinition = {
|
|
635
|
+
nodes: [
|
|
636
|
+
{
|
|
637
|
+
id: "A",
|
|
638
|
+
type: "echo",
|
|
639
|
+
inputs: { data: "ghost.output" },
|
|
640
|
+
config: { message: "{{data}}" },
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const result = validateDefinition(def);
|
|
646
|
+
expect(result.valid).toBe(false);
|
|
647
|
+
expect(result.errors.some((e) => e.includes("ghost") && e.includes("non-existent"))).toBe(
|
|
648
|
+
true,
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("input referencing downstream node fails validation", () => {
|
|
653
|
+
const def: WorkflowDefinition = {
|
|
654
|
+
nodes: [
|
|
655
|
+
{
|
|
656
|
+
id: "A",
|
|
657
|
+
type: "echo",
|
|
658
|
+
inputs: { data: "B.echo" },
|
|
659
|
+
config: { message: "{{data}}" },
|
|
660
|
+
next: "B",
|
|
661
|
+
},
|
|
662
|
+
{ id: "B", type: "echo", config: { message: "hi" } },
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const result = validateDefinition(def);
|
|
667
|
+
expect(result.valid).toBe(false);
|
|
668
|
+
expect(result.errors.some((e) => e.includes("B") && e.includes("not upstream"))).toBe(true);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("input referencing trigger/input (built-in) passes validation", () => {
|
|
672
|
+
const def: WorkflowDefinition = {
|
|
673
|
+
nodes: [
|
|
674
|
+
{
|
|
675
|
+
id: "A",
|
|
676
|
+
type: "echo",
|
|
677
|
+
inputs: { repo: "trigger.repo", key: "input.API_KEY" },
|
|
678
|
+
config: { message: "{{repo}} {{key}}" },
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const result = validateDefinition(def);
|
|
684
|
+
expect(result.valid).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("self-referencing node input fails validation", () => {
|
|
688
|
+
const def: WorkflowDefinition = {
|
|
689
|
+
nodes: [
|
|
690
|
+
{
|
|
691
|
+
id: "A",
|
|
692
|
+
type: "echo",
|
|
693
|
+
inputs: { self: "A.echo" },
|
|
694
|
+
config: { message: "{{self}}" },
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const result = validateDefinition(def);
|
|
700
|
+
expect(result.valid).toBe(false);
|
|
701
|
+
expect(result.errors.some((e) => e.includes("A") && e.includes("not upstream"))).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// ─── No triggerSchema — backward compat ────────────────────
|
|
706
|
+
|
|
707
|
+
describe("No triggerSchema — any payload accepted (backward compat)", () => {
|
|
708
|
+
test("workflow without triggerSchema accepts any trigger data", async () => {
|
|
709
|
+
const registry = createTestRegistry();
|
|
710
|
+
const def: WorkflowDefinition = {
|
|
711
|
+
nodes: [
|
|
712
|
+
{
|
|
713
|
+
id: "step1",
|
|
714
|
+
type: "echo",
|
|
715
|
+
config: { message: "hello" },
|
|
716
|
+
},
|
|
717
|
+
],
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const workflow = makeWorkflow(def);
|
|
721
|
+
// No triggerSchema — arbitrary data should work
|
|
722
|
+
const runId = await startWorkflowExecution(
|
|
723
|
+
workflow,
|
|
724
|
+
{ anything: "goes", nested: { deep: true } },
|
|
725
|
+
registry,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const run = getWorkflowRun(runId);
|
|
729
|
+
expect(run!.status).toBe("completed");
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// ─── Deep Interpolation in Nested Config ───────────────────
|
|
734
|
+
|
|
735
|
+
describe("Deep interpolation — arrays and nested objects in config", () => {
|
|
736
|
+
test("interpolation works inside arrays and nested objects", async () => {
|
|
737
|
+
const registry = createTestRegistry();
|
|
738
|
+
const def: WorkflowDefinition = {
|
|
739
|
+
nodes: [
|
|
740
|
+
{
|
|
741
|
+
id: "step1",
|
|
742
|
+
type: "passthrough",
|
|
743
|
+
inputs: { repo: "trigger.repo" },
|
|
744
|
+
config: {
|
|
745
|
+
data: {
|
|
746
|
+
tags: ["{{repo}}", "fixed-tag"],
|
|
747
|
+
metadata: {
|
|
748
|
+
source: "{{trigger.source}}",
|
|
749
|
+
nested: {
|
|
750
|
+
level: "deep-{{repo}}",
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const workflow = makeWorkflow(def);
|
|
760
|
+
const runId = await startWorkflowExecution(
|
|
761
|
+
workflow,
|
|
762
|
+
{ repo: "my-repo", source: "webhook" },
|
|
763
|
+
registry,
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const run = getWorkflowRun(runId);
|
|
767
|
+
expect(run!.status).toBe("completed");
|
|
768
|
+
|
|
769
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
770
|
+
const output = ctx.step1 as Record<string, unknown>;
|
|
771
|
+
expect(output.tags).toEqual(["my-repo", "fixed-tag"]);
|
|
772
|
+
expect((output.metadata as Record<string, unknown>).source).toBe("webhook");
|
|
773
|
+
expect(
|
|
774
|
+
((output.metadata as Record<string, unknown>).nested as Record<string, unknown>).level,
|
|
775
|
+
).toBe("deep-my-repo");
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// ─── Complex Diamond with Data Flow ────────────────────────
|
|
780
|
+
|
|
781
|
+
describe("Diamond convergence with chained data flow", () => {
|
|
782
|
+
test("A fans out to B and C, both converge to D which reads from both", async () => {
|
|
783
|
+
const registry = createTestRegistry();
|
|
784
|
+
const def: WorkflowDefinition = {
|
|
785
|
+
nodes: [
|
|
786
|
+
{
|
|
787
|
+
id: "A",
|
|
788
|
+
type: "echo",
|
|
789
|
+
config: { message: "start" },
|
|
790
|
+
next: { x: "B", y: "C" },
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
id: "B",
|
|
794
|
+
type: "echo",
|
|
795
|
+
inputs: { fromA: "A.echo" },
|
|
796
|
+
config: { message: "B:{{fromA}}" },
|
|
797
|
+
next: "D",
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
id: "C",
|
|
801
|
+
type: "echo",
|
|
802
|
+
inputs: { fromA: "A.echo" },
|
|
803
|
+
config: { message: "C:{{fromA}}" },
|
|
804
|
+
next: "D",
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
id: "D",
|
|
808
|
+
type: "echo",
|
|
809
|
+
inputs: { fromB: "B.echo", fromC: "C.echo" },
|
|
810
|
+
config: { message: "D got {{fromB}} and {{fromC}}" },
|
|
811
|
+
},
|
|
812
|
+
],
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const workflow = makeWorkflow(def);
|
|
816
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
817
|
+
|
|
818
|
+
const run = getWorkflowRun(runId);
|
|
819
|
+
expect(run!.status).toBe("completed");
|
|
820
|
+
|
|
821
|
+
const ctx = run!.context as Record<string, unknown>;
|
|
822
|
+
expect(ctx.A).toEqual({ echo: "start" });
|
|
823
|
+
expect(ctx.B).toEqual({ echo: "B:start" });
|
|
824
|
+
expect(ctx.C).toEqual({ echo: "C:start" });
|
|
825
|
+
expect(ctx.D).toEqual({ echo: "D got B:start and C:start" });
|
|
826
|
+
|
|
827
|
+
// All 4 nodes should have steps
|
|
828
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
829
|
+
expect(steps).toHaveLength(4);
|
|
830
|
+
expect(steps.every((s) => s.status === "completed")).toBe(true);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// ─── MAX_ITERATIONS Guard ─────────────────────────────────
|
|
835
|
+
|
|
836
|
+
describe("MAX_ITERATIONS counts individual node executions", () => {
|
|
837
|
+
test("parallel fan-out counts all nodes, not just batches", async () => {
|
|
838
|
+
const registry = createTestRegistry();
|
|
839
|
+
const def: WorkflowDefinition = {
|
|
840
|
+
nodes: [
|
|
841
|
+
{
|
|
842
|
+
id: "root",
|
|
843
|
+
type: "echo",
|
|
844
|
+
config: { message: "go" },
|
|
845
|
+
next: { a: "p1", b: "p2", c: "p3" },
|
|
846
|
+
},
|
|
847
|
+
{ id: "p1", type: "echo", config: { message: "1" } },
|
|
848
|
+
{ id: "p2", type: "echo", config: { message: "2" } },
|
|
849
|
+
{ id: "p3", type: "echo", config: { message: "3" } },
|
|
850
|
+
],
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const workflow = makeWorkflow(def);
|
|
854
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
855
|
+
|
|
856
|
+
const run = getWorkflowRun(runId);
|
|
857
|
+
expect(run!.status).toBe("completed");
|
|
858
|
+
|
|
859
|
+
// Should have 4 steps (root + 3 parallel)
|
|
860
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
861
|
+
expect(steps).toHaveLength(4);
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// ─── nextPort Persistence for Recovery ─────────────────────
|
|
866
|
+
|
|
867
|
+
describe("nextPort stored in step records", () => {
|
|
868
|
+
test("branch step persists nextPort for recovery reconstruction", async () => {
|
|
869
|
+
const registry = createTestRegistry();
|
|
870
|
+
const def: WorkflowDefinition = {
|
|
871
|
+
nodes: [
|
|
872
|
+
{
|
|
873
|
+
id: "branch",
|
|
874
|
+
type: "property-match",
|
|
875
|
+
config: {
|
|
876
|
+
conditions: [{ field: "trigger.mode", op: "eq", value: "fast" }],
|
|
877
|
+
},
|
|
878
|
+
next: { true: "fast_path", false: "slow_path" },
|
|
879
|
+
},
|
|
880
|
+
{ id: "fast_path", type: "echo", config: { message: "fast" } },
|
|
881
|
+
{ id: "slow_path", type: "echo", config: { message: "slow" } },
|
|
882
|
+
],
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const workflow = makeWorkflow(def);
|
|
886
|
+
const runId = await startWorkflowExecution(workflow, { mode: "fast" }, registry);
|
|
887
|
+
|
|
888
|
+
const run = getWorkflowRun(runId);
|
|
889
|
+
expect(run!.status).toBe("completed");
|
|
890
|
+
|
|
891
|
+
const steps = getWorkflowRunStepsByRunId(runId);
|
|
892
|
+
const branchStep = steps.find((s) => s.nodeId === "branch");
|
|
893
|
+
expect(branchStep).toBeDefined();
|
|
894
|
+
expect(branchStep!.nextPort).toBe("true");
|
|
895
|
+
|
|
896
|
+
// Only fast_path should have executed
|
|
897
|
+
const nodeIds = steps.map((s) => s.nodeId);
|
|
898
|
+
expect(nodeIds).toContain("fast_path");
|
|
899
|
+
expect(nodeIds).not.toContain("slow_path");
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
});
|