@assistkick/create 1.10.0 → 1.12.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/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrate.ts +82 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0000_outgoing_ultron.sql +277 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0000_snapshot.json +972 -22
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +2 -100
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import { describe, it, mock, beforeEach } from 'node:test';
|
|
1
|
+
import { describe, it, mock, beforeEach, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
3
6
|
import {WorkflowEngine} from "./workflow_engine.js";
|
|
4
7
|
|
|
8
|
+
// ── Test Skills Directory ────────────────────────────────────────────
|
|
9
|
+
const TEST_SKILLS_DIR = mkdtempSync(join(tmpdir(), 'wf-engine-test-skills-'));
|
|
10
|
+
|
|
11
|
+
// Create test skill files
|
|
12
|
+
for (const skillId of ['assistkick-developer', 'assistkick-code-reviewer', 'assistkick-debugger']) {
|
|
13
|
+
const dir = join(TEST_SKILLS_DIR, skillId);
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
writeFileSync(join(dir, 'SKILL.md'), `# ${skillId} skill instructions`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
after(() => {
|
|
19
|
+
rmSync(TEST_SKILLS_DIR, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
5
22
|
// ── Default workflow graph (matches seeded data) ──────────────────────
|
|
6
23
|
|
|
7
24
|
const DEFAULT_GRAPH = {
|
|
@@ -103,14 +120,20 @@ const createMockDb = (graphData = DEFAULT_GRAPH) => {
|
|
|
103
120
|
'dev-agent-id': {
|
|
104
121
|
id: 'dev-agent-id',
|
|
105
122
|
name: 'Developer',
|
|
106
|
-
|
|
123
|
+
model: 'claude-opus-4-6',
|
|
124
|
+
skills: '["assistkick-developer"]',
|
|
125
|
+
grounding: 'Implement feature {{featureId}} in {{worktreePath}}',
|
|
126
|
+
defaultGrounding: 'Implement feature {{featureId}} in {{worktreePath}}',
|
|
107
127
|
projectId: null,
|
|
108
128
|
isDefault: 1,
|
|
109
129
|
},
|
|
110
130
|
'rev-agent-id': {
|
|
111
131
|
id: 'rev-agent-id',
|
|
112
132
|
name: 'Reviewer',
|
|
113
|
-
|
|
133
|
+
model: 'claude-opus-4-6',
|
|
134
|
+
skills: '["assistkick-code-reviewer"]',
|
|
135
|
+
grounding: 'Review feature {{featureId}}',
|
|
136
|
+
defaultGrounding: 'Review feature {{featureId}}',
|
|
114
137
|
projectId: null,
|
|
115
138
|
isDefault: 1,
|
|
116
139
|
},
|
|
@@ -243,7 +266,7 @@ const createMockClaudeService = () => ({
|
|
|
243
266
|
});
|
|
244
267
|
|
|
245
268
|
const createMockGitWorkflow = () => ({
|
|
246
|
-
createWorktree: mock.fn(async (featureId: string) => `/tmp/worktrees/${featureId}`),
|
|
269
|
+
createWorktree: mock.fn(async (featureId: string, _projectId: string) => `/tmp/worktrees/${featureId}`),
|
|
247
270
|
removeWorktree: mock.fn(async () => {}),
|
|
248
271
|
deleteBranch: mock.fn(async () => {}),
|
|
249
272
|
commitChanges: mock.fn(async () => {}),
|
|
@@ -253,6 +276,8 @@ const createMockGitWorkflow = () => ({
|
|
|
253
276
|
stash: mock.fn(async () => false),
|
|
254
277
|
unstash: mock.fn(async () => {}),
|
|
255
278
|
getDirtyFiles: mock.fn(async () => []),
|
|
279
|
+
pullDefaultBranch: mock.fn(async () => {}),
|
|
280
|
+
pushToRemote: mock.fn(async () => {}),
|
|
256
281
|
});
|
|
257
282
|
|
|
258
283
|
const createMockLog = () => mock.fn((_tag: string, _msg: string) => {});
|
|
@@ -277,6 +302,7 @@ describe('WorkflowEngine', () => {
|
|
|
277
302
|
kanban: createMockKanban(),
|
|
278
303
|
claudeService: createMockClaudeService(),
|
|
279
304
|
gitWorkflow: createMockGitWorkflow(),
|
|
305
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
280
306
|
log: createMockLog(),
|
|
281
307
|
});
|
|
282
308
|
|
|
@@ -294,6 +320,7 @@ describe('WorkflowEngine', () => {
|
|
|
294
320
|
kanban: createMockKanban(),
|
|
295
321
|
claudeService: createMockClaudeService(),
|
|
296
322
|
gitWorkflow: createMockGitWorkflow(),
|
|
323
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
297
324
|
log: createMockLog(),
|
|
298
325
|
});
|
|
299
326
|
|
|
@@ -311,6 +338,7 @@ describe('WorkflowEngine', () => {
|
|
|
311
338
|
kanban: createMockKanban(),
|
|
312
339
|
claudeService: createMockClaudeService(),
|
|
313
340
|
gitWorkflow: createMockGitWorkflow(),
|
|
341
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
314
342
|
log: createMockLog(),
|
|
315
343
|
});
|
|
316
344
|
|
|
@@ -344,6 +372,7 @@ describe('WorkflowEngine', () => {
|
|
|
344
372
|
kanban: createMockKanban(),
|
|
345
373
|
claudeService: createMockClaudeService(),
|
|
346
374
|
gitWorkflow: createMockGitWorkflow(),
|
|
375
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
347
376
|
log: createMockLog(),
|
|
348
377
|
});
|
|
349
378
|
|
|
@@ -357,6 +386,7 @@ describe('WorkflowEngine', () => {
|
|
|
357
386
|
kanban: createMockKanban(),
|
|
358
387
|
claudeService: createMockClaudeService(),
|
|
359
388
|
gitWorkflow: createMockGitWorkflow(),
|
|
389
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
360
390
|
log: createMockLog(),
|
|
361
391
|
});
|
|
362
392
|
|
|
@@ -370,6 +400,7 @@ describe('WorkflowEngine', () => {
|
|
|
370
400
|
kanban: createMockKanban(),
|
|
371
401
|
claudeService: createMockClaudeService(),
|
|
372
402
|
gitWorkflow: createMockGitWorkflow(),
|
|
403
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
373
404
|
log: createMockLog(),
|
|
374
405
|
});
|
|
375
406
|
|
|
@@ -383,6 +414,7 @@ describe('WorkflowEngine', () => {
|
|
|
383
414
|
kanban: createMockKanban(),
|
|
384
415
|
claudeService: createMockClaudeService(),
|
|
385
416
|
gitWorkflow: createMockGitWorkflow(),
|
|
417
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
386
418
|
log: createMockLog(),
|
|
387
419
|
});
|
|
388
420
|
|
|
@@ -396,6 +428,7 @@ describe('WorkflowEngine', () => {
|
|
|
396
428
|
kanban: createMockKanban(),
|
|
397
429
|
claudeService: createMockClaudeService(),
|
|
398
430
|
gitWorkflow: createMockGitWorkflow(),
|
|
431
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
399
432
|
log: createMockLog(),
|
|
400
433
|
});
|
|
401
434
|
|
|
@@ -409,6 +442,7 @@ describe('WorkflowEngine', () => {
|
|
|
409
442
|
kanban: createMockKanban(),
|
|
410
443
|
claudeService: createMockClaudeService(),
|
|
411
444
|
gitWorkflow: createMockGitWorkflow(),
|
|
445
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
412
446
|
log: createMockLog(),
|
|
413
447
|
});
|
|
414
448
|
|
|
@@ -422,6 +456,7 @@ describe('WorkflowEngine', () => {
|
|
|
422
456
|
kanban: createMockKanban(),
|
|
423
457
|
claudeService: createMockClaudeService(),
|
|
424
458
|
gitWorkflow: createMockGitWorkflow(),
|
|
459
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
425
460
|
log: createMockLog(),
|
|
426
461
|
});
|
|
427
462
|
|
|
@@ -439,6 +474,7 @@ describe('WorkflowEngine', () => {
|
|
|
439
474
|
kanban,
|
|
440
475
|
claudeService: createMockClaudeService(),
|
|
441
476
|
gitWorkflow: createMockGitWorkflow(),
|
|
477
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
442
478
|
log: createMockLog(),
|
|
443
479
|
});
|
|
444
480
|
|
|
@@ -458,6 +494,7 @@ describe('WorkflowEngine', () => {
|
|
|
458
494
|
kanban,
|
|
459
495
|
claudeService: createMockClaudeService(),
|
|
460
496
|
gitWorkflow: createMockGitWorkflow(),
|
|
497
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
461
498
|
log: createMockLog(),
|
|
462
499
|
});
|
|
463
500
|
|
|
@@ -476,6 +513,7 @@ describe('WorkflowEngine', () => {
|
|
|
476
513
|
kanban,
|
|
477
514
|
claudeService: createMockClaudeService(),
|
|
478
515
|
gitWorkflow: createMockGitWorkflow(),
|
|
516
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
479
517
|
log: createMockLog(),
|
|
480
518
|
});
|
|
481
519
|
|
|
@@ -496,6 +534,7 @@ describe('WorkflowEngine', () => {
|
|
|
496
534
|
kanban,
|
|
497
535
|
claudeService: createMockClaudeService(),
|
|
498
536
|
gitWorkflow: createMockGitWorkflow(),
|
|
537
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
499
538
|
log: createMockLog(),
|
|
500
539
|
});
|
|
501
540
|
|
|
@@ -512,6 +551,7 @@ describe('WorkflowEngine', () => {
|
|
|
512
551
|
kanban,
|
|
513
552
|
claudeService: createMockClaudeService(),
|
|
514
553
|
gitWorkflow: createMockGitWorkflow(),
|
|
554
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
515
555
|
log: createMockLog(),
|
|
516
556
|
});
|
|
517
557
|
|
|
@@ -529,6 +569,7 @@ describe('WorkflowEngine', () => {
|
|
|
529
569
|
kanban,
|
|
530
570
|
claudeService: createMockClaudeService(),
|
|
531
571
|
gitWorkflow: createMockGitWorkflow(),
|
|
572
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
532
573
|
log: createMockLog(),
|
|
533
574
|
});
|
|
534
575
|
|
|
@@ -546,6 +587,7 @@ describe('WorkflowEngine', () => {
|
|
|
546
587
|
kanban: createMockKanban(),
|
|
547
588
|
claudeService: createMockClaudeService(),
|
|
548
589
|
gitWorkflow: createMockGitWorkflow(),
|
|
590
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
549
591
|
log: createMockLog(),
|
|
550
592
|
});
|
|
551
593
|
|
|
@@ -563,6 +605,7 @@ describe('WorkflowEngine', () => {
|
|
|
563
605
|
kanban: createMockKanban(),
|
|
564
606
|
claudeService: createMockClaudeService(),
|
|
565
607
|
gitWorkflow: createMockGitWorkflow(),
|
|
608
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
566
609
|
log: createMockLog(),
|
|
567
610
|
});
|
|
568
611
|
|
|
@@ -580,6 +623,7 @@ describe('WorkflowEngine', () => {
|
|
|
580
623
|
kanban: createMockKanban(),
|
|
581
624
|
claudeService: createMockClaudeService(),
|
|
582
625
|
gitWorkflow: createMockGitWorkflow(),
|
|
626
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
583
627
|
log: createMockLog(),
|
|
584
628
|
});
|
|
585
629
|
|
|
@@ -601,6 +645,7 @@ describe('WorkflowEngine', () => {
|
|
|
601
645
|
kanban,
|
|
602
646
|
claudeService: createMockClaudeService(),
|
|
603
647
|
gitWorkflow: createMockGitWorkflow(),
|
|
648
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
604
649
|
log: createMockLog(),
|
|
605
650
|
});
|
|
606
651
|
|
|
@@ -618,6 +663,7 @@ describe('WorkflowEngine', () => {
|
|
|
618
663
|
kanban,
|
|
619
664
|
claudeService: createMockClaudeService(),
|
|
620
665
|
gitWorkflow: createMockGitWorkflow(),
|
|
666
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
621
667
|
log: createMockLog(),
|
|
622
668
|
});
|
|
623
669
|
|
|
@@ -635,6 +681,7 @@ describe('WorkflowEngine', () => {
|
|
|
635
681
|
kanban: createMockKanban(),
|
|
636
682
|
claudeService: createMockClaudeService(),
|
|
637
683
|
gitWorkflow: createMockGitWorkflow(),
|
|
684
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
638
685
|
log: createMockLog(),
|
|
639
686
|
});
|
|
640
687
|
|
|
@@ -659,7 +706,60 @@ describe('WorkflowEngine', () => {
|
|
|
659
706
|
|
|
660
707
|
const engine = new WorkflowEngine({
|
|
661
708
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
662
|
-
gitWorkflow, log: createMockLog(),
|
|
709
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
|
|
713
|
+
const context = defaultContext({ cycle: 1 });
|
|
714
|
+
|
|
715
|
+
await (engine as any).handleEnd(node, context, execId);
|
|
716
|
+
|
|
717
|
+
assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
|
|
718
|
+
assert.equal(gitWorkflow.removeWorktree.mock.calls.length, 1);
|
|
719
|
+
assert.equal(gitWorkflow.deleteBranch.mock.calls.length, 1);
|
|
720
|
+
assert.equal(db._stores.executionsStore[execId].status, 'completed');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('pushes to remote after successful merge', async () => {
|
|
724
|
+
const db = createMockDb();
|
|
725
|
+
const gitWorkflow = createMockGitWorkflow();
|
|
726
|
+
const execId = 'test-exec-push';
|
|
727
|
+
|
|
728
|
+
db._stores.executionsStore[execId] = {
|
|
729
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
730
|
+
status: 'running', currentNodeId: 'end_success',
|
|
731
|
+
context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const engine = new WorkflowEngine({
|
|
735
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
736
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
|
|
740
|
+
const context = defaultContext({ cycle: 1, projectId: 'proj_test' });
|
|
741
|
+
|
|
742
|
+
await (engine as any).handleEnd(node, context, execId);
|
|
743
|
+
|
|
744
|
+
assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 1);
|
|
745
|
+
assert.equal(gitWorkflow.pushToRemote.mock.calls[0].arguments[0], 'proj_test');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('does not fail workflow when push to remote fails', async () => {
|
|
749
|
+
const db = createMockDb();
|
|
750
|
+
const gitWorkflow = createMockGitWorkflow();
|
|
751
|
+
gitWorkflow.pushToRemote = mock.fn(async () => { throw new Error('remote unreachable'); });
|
|
752
|
+
const execId = 'test-exec-push-fail';
|
|
753
|
+
|
|
754
|
+
db._stores.executionsStore[execId] = {
|
|
755
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
756
|
+
status: 'running', currentNodeId: 'end_success',
|
|
757
|
+
context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const engine = new WorkflowEngine({
|
|
761
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
762
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
663
763
|
});
|
|
664
764
|
|
|
665
765
|
const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
|
|
@@ -667,12 +767,37 @@ describe('WorkflowEngine', () => {
|
|
|
667
767
|
|
|
668
768
|
await (engine as any).handleEnd(node, context, execId);
|
|
669
769
|
|
|
770
|
+
// Merge still succeeded and cleanup happened
|
|
670
771
|
assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
|
|
671
772
|
assert.equal(gitWorkflow.removeWorktree.mock.calls.length, 1);
|
|
672
773
|
assert.equal(gitWorkflow.deleteBranch.mock.calls.length, 1);
|
|
673
774
|
assert.equal(db._stores.executionsStore[execId].status, 'completed');
|
|
674
775
|
});
|
|
675
776
|
|
|
777
|
+
it('does not push to remote on blocked outcome', async () => {
|
|
778
|
+
const db = createMockDb();
|
|
779
|
+
const gitWorkflow = createMockGitWorkflow();
|
|
780
|
+
const execId = 'test-exec-no-push';
|
|
781
|
+
|
|
782
|
+
db._stores.executionsStore[execId] = {
|
|
783
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
784
|
+
status: 'running', currentNodeId: 'end_blocked',
|
|
785
|
+
context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const engine = new WorkflowEngine({
|
|
789
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
790
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const node = { id: 'end_blocked', type: 'end', data: { outcome: 'blocked' } };
|
|
794
|
+
const context = defaultContext({ cycle: 3 });
|
|
795
|
+
|
|
796
|
+
await (engine as any).handleEnd(node, context, execId);
|
|
797
|
+
|
|
798
|
+
assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 0);
|
|
799
|
+
});
|
|
800
|
+
|
|
676
801
|
it('cleans up worktree without merge and sets completed on blocked outcome', async () => {
|
|
677
802
|
const db = createMockDb();
|
|
678
803
|
const gitWorkflow = createMockGitWorkflow();
|
|
@@ -686,7 +811,7 @@ describe('WorkflowEngine', () => {
|
|
|
686
811
|
|
|
687
812
|
const engine = new WorkflowEngine({
|
|
688
813
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
689
|
-
gitWorkflow, log: createMockLog(),
|
|
814
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
690
815
|
});
|
|
691
816
|
|
|
692
817
|
const node = { id: 'end_blocked', type: 'end', data: { outcome: 'blocked' } };
|
|
@@ -713,7 +838,7 @@ describe('WorkflowEngine', () => {
|
|
|
713
838
|
|
|
714
839
|
const engine = new WorkflowEngine({
|
|
715
840
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
716
|
-
gitWorkflow, log: createMockLog(),
|
|
841
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
717
842
|
});
|
|
718
843
|
|
|
719
844
|
const node = { id: 'end_fail', type: 'end', data: { outcome: 'failed' } };
|
|
@@ -735,7 +860,7 @@ describe('WorkflowEngine', () => {
|
|
|
735
860
|
|
|
736
861
|
const engine = new WorkflowEngine({
|
|
737
862
|
db: db as any, kanban: createMockKanban(), claudeService,
|
|
738
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
863
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
739
864
|
});
|
|
740
865
|
|
|
741
866
|
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
|
|
@@ -749,10 +874,14 @@ describe('WorkflowEngine', () => {
|
|
|
749
874
|
assert.equal(result.outputData.agentId, 'dev-agent-id');
|
|
750
875
|
assert.equal(result.outputData.agentName, 'Developer');
|
|
751
876
|
|
|
752
|
-
// Verify spawnClaude called with
|
|
877
|
+
// Verify spawnClaude called with composed prompt (skill content + resolved grounding) and worktree path
|
|
753
878
|
const call = claudeService.spawnClaude.mock.calls[0];
|
|
754
|
-
|
|
879
|
+
const prompt = call.arguments[0] as string;
|
|
880
|
+
assert.ok(prompt.includes('# assistkick-developer skill instructions'), 'prompt should include skill file content');
|
|
881
|
+
assert.ok(prompt.includes('Implement feature feat_test in /tmp/wt'), 'prompt should include resolved grounding');
|
|
755
882
|
assert.equal(call.arguments[1], '/tmp/wt');
|
|
883
|
+
// Verify model is passed through
|
|
884
|
+
assert.equal(call.arguments[3].model, 'claude-opus-4-6');
|
|
756
885
|
});
|
|
757
886
|
|
|
758
887
|
it('passes onToolUse and onResult callbacks to spawnClaude', async () => {
|
|
@@ -781,7 +910,7 @@ describe('WorkflowEngine', () => {
|
|
|
781
910
|
|
|
782
911
|
const engine = new WorkflowEngine({
|
|
783
912
|
db: db as any, kanban: createMockKanban(), claudeService,
|
|
784
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
913
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
785
914
|
});
|
|
786
915
|
|
|
787
916
|
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
|
|
@@ -806,7 +935,7 @@ describe('WorkflowEngine', () => {
|
|
|
806
935
|
|
|
807
936
|
const engine = new WorkflowEngine({
|
|
808
937
|
db: db as any, kanban: createMockKanban(), claudeService,
|
|
809
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
938
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
810
939
|
});
|
|
811
940
|
|
|
812
941
|
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
|
|
@@ -831,7 +960,7 @@ describe('WorkflowEngine', () => {
|
|
|
831
960
|
|
|
832
961
|
const engine = new WorkflowEngine({
|
|
833
962
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
834
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
963
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
835
964
|
});
|
|
836
965
|
|
|
837
966
|
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'nonexistent-id' } };
|
|
@@ -848,7 +977,10 @@ describe('WorkflowEngine', () => {
|
|
|
848
977
|
db._stores.agentsStore['custom-agent'] = {
|
|
849
978
|
id: 'custom-agent',
|
|
850
979
|
name: 'Custom',
|
|
851
|
-
|
|
980
|
+
model: 'claude-opus-4-6',
|
|
981
|
+
skills: '[]',
|
|
982
|
+
grounding: 'Feature: {{featureId}}, Branch: {{branchName}}, Cycle: {{cycle}}',
|
|
983
|
+
defaultGrounding: null,
|
|
852
984
|
projectId: null,
|
|
853
985
|
isDefault: 0,
|
|
854
986
|
};
|
|
@@ -856,7 +988,7 @@ describe('WorkflowEngine', () => {
|
|
|
856
988
|
|
|
857
989
|
const engine = new WorkflowEngine({
|
|
858
990
|
db: db as any, kanban: createMockKanban(), claudeService,
|
|
859
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
991
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
860
992
|
});
|
|
861
993
|
|
|
862
994
|
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'custom-agent' } };
|
|
@@ -867,6 +999,123 @@ describe('WorkflowEngine', () => {
|
|
|
867
999
|
const call = claudeService.spawnClaude.mock.calls[0];
|
|
868
1000
|
assert.equal(call.arguments[0], 'Feature: feat_test, Branch: feature/feat_test, Cycle: 2');
|
|
869
1001
|
});
|
|
1002
|
+
|
|
1003
|
+
it('composes prompt from skill files and resolved grounding', async () => {
|
|
1004
|
+
const db = createMockDb();
|
|
1005
|
+
const claudeService = createMockClaudeService();
|
|
1006
|
+
claudeService.spawnClaude = mock.fn(async () => 'output');
|
|
1007
|
+
|
|
1008
|
+
const engine = new WorkflowEngine({
|
|
1009
|
+
db: db as any, kanban: createMockKanban(), claudeService,
|
|
1010
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
|
|
1014
|
+
const context = defaultContext({ cycle: 1 });
|
|
1015
|
+
|
|
1016
|
+
await (engine as any).handleRunAgent(node, context, 'exec-id');
|
|
1017
|
+
|
|
1018
|
+
const call = claudeService.spawnClaude.mock.calls[0];
|
|
1019
|
+
const prompt = call.arguments[0] as string;
|
|
1020
|
+
// Skill content comes first, grounding comes after
|
|
1021
|
+
const skillIdx = prompt.indexOf('# assistkick-developer skill instructions');
|
|
1022
|
+
const groundingIdx = prompt.indexOf('Implement feature feat_test');
|
|
1023
|
+
assert.ok(skillIdx >= 0, 'prompt should contain skill content');
|
|
1024
|
+
assert.ok(groundingIdx > skillIdx, 'grounding should come after skill content');
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('does not resolve placeholders in skill content', async () => {
|
|
1028
|
+
const db = createMockDb();
|
|
1029
|
+
// Create a skill file with a placeholder-like pattern
|
|
1030
|
+
const skillDir = join(TEST_SKILLS_DIR, 'test-no-resolve');
|
|
1031
|
+
mkdirSync(skillDir, { recursive: true });
|
|
1032
|
+
writeFileSync(join(skillDir, 'SKILL.md'), 'Skill with {{featureId}} in it');
|
|
1033
|
+
|
|
1034
|
+
db._stores.agentsStore['skill-no-resolve'] = {
|
|
1035
|
+
id: 'skill-no-resolve',
|
|
1036
|
+
name: 'TestNoResolve',
|
|
1037
|
+
model: 'claude-opus-4-6',
|
|
1038
|
+
skills: '["test-no-resolve"]',
|
|
1039
|
+
grounding: 'Grounding for {{featureId}}',
|
|
1040
|
+
defaultGrounding: null,
|
|
1041
|
+
projectId: null,
|
|
1042
|
+
isDefault: 0,
|
|
1043
|
+
};
|
|
1044
|
+
const claudeService = createMockClaudeService();
|
|
1045
|
+
claudeService.spawnClaude = mock.fn(async () => 'output');
|
|
1046
|
+
|
|
1047
|
+
const engine = new WorkflowEngine({
|
|
1048
|
+
db: db as any, kanban: createMockKanban(), claudeService,
|
|
1049
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'skill-no-resolve' } };
|
|
1053
|
+
const context = defaultContext({ cycle: 1 });
|
|
1054
|
+
|
|
1055
|
+
await (engine as any).handleRunAgent(node, context, 'exec-id');
|
|
1056
|
+
|
|
1057
|
+
const prompt = claudeService.spawnClaude.mock.calls[0].arguments[0] as string;
|
|
1058
|
+
// Skill content should NOT have {{featureId}} resolved
|
|
1059
|
+
assert.ok(prompt.includes('Skill with {{featureId}} in it'), 'skill content should not be interpolated');
|
|
1060
|
+
// Grounding should have {{featureId}} resolved
|
|
1061
|
+
assert.ok(prompt.includes('Grounding for feat_test'), 'grounding should be interpolated');
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it('passes agent model to spawnClaude', async () => {
|
|
1065
|
+
const db = createMockDb();
|
|
1066
|
+
db._stores.agentsStore['custom-model'] = {
|
|
1067
|
+
id: 'custom-model',
|
|
1068
|
+
name: 'CustomModel',
|
|
1069
|
+
model: 'claude-sonnet-4-6',
|
|
1070
|
+
skills: '[]',
|
|
1071
|
+
grounding: 'test grounding',
|
|
1072
|
+
defaultGrounding: null,
|
|
1073
|
+
projectId: null,
|
|
1074
|
+
isDefault: 0,
|
|
1075
|
+
};
|
|
1076
|
+
const claudeService = createMockClaudeService();
|
|
1077
|
+
claudeService.spawnClaude = mock.fn(async () => 'output');
|
|
1078
|
+
|
|
1079
|
+
const engine = new WorkflowEngine({
|
|
1080
|
+
db: db as any, kanban: createMockKanban(), claudeService,
|
|
1081
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'custom-model' } };
|
|
1085
|
+
const context = defaultContext({ cycle: 1 });
|
|
1086
|
+
|
|
1087
|
+
await (engine as any).handleRunAgent(node, context, 'exec-id');
|
|
1088
|
+
|
|
1089
|
+
const opts = claudeService.spawnClaude.mock.calls[0].arguments[3];
|
|
1090
|
+
assert.equal(opts.model, 'claude-sonnet-4-6');
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it('throws error when skill file is not found', async () => {
|
|
1094
|
+
const db = createMockDb();
|
|
1095
|
+
db._stores.agentsStore['missing-skill'] = {
|
|
1096
|
+
id: 'missing-skill',
|
|
1097
|
+
name: 'MissingSkill',
|
|
1098
|
+
model: 'claude-opus-4-6',
|
|
1099
|
+
skills: '["nonexistent-skill"]',
|
|
1100
|
+
grounding: 'test',
|
|
1101
|
+
defaultGrounding: null,
|
|
1102
|
+
projectId: null,
|
|
1103
|
+
isDefault: 0,
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
const engine = new WorkflowEngine({
|
|
1107
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
1108
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const node = { id: 'run1', type: 'runAgent', data: { agentId: 'missing-skill' } };
|
|
1112
|
+
const context = defaultContext({ cycle: 1 });
|
|
1113
|
+
|
|
1114
|
+
await assert.rejects(
|
|
1115
|
+
() => (engine as any).handleRunAgent(node, context, 'exec-id'),
|
|
1116
|
+
/Failed to read skill file for "nonexistent-skill"/,
|
|
1117
|
+
);
|
|
1118
|
+
});
|
|
870
1119
|
});
|
|
871
1120
|
|
|
872
1121
|
describe('getExecutionMetrics', () => {
|
|
@@ -913,7 +1162,7 @@ describe('WorkflowEngine', () => {
|
|
|
913
1162
|
|
|
914
1163
|
const engine = new WorkflowEngine({
|
|
915
1164
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
916
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
1165
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
917
1166
|
});
|
|
918
1167
|
|
|
919
1168
|
const metrics = await engine.getExecutionMetrics(execId);
|
|
@@ -931,7 +1180,7 @@ describe('WorkflowEngine', () => {
|
|
|
931
1180
|
const db = createMockDb();
|
|
932
1181
|
const engine = new WorkflowEngine({
|
|
933
1182
|
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
934
|
-
gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
|
|
1183
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
935
1184
|
});
|
|
936
1185
|
|
|
937
1186
|
const metrics = await engine.getExecutionMetrics('nonexistent');
|
|
@@ -972,7 +1221,7 @@ describe('WorkflowEngine', () => {
|
|
|
972
1221
|
|
|
973
1222
|
const engine = new WorkflowEngine({
|
|
974
1223
|
db: db as any, kanban, claudeService: createMockClaudeService(),
|
|
975
|
-
gitWorkflow, log: createMockLog(),
|
|
1224
|
+
gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
976
1225
|
});
|
|
977
1226
|
|
|
978
1227
|
const { executionId } = await engine.start('feat_test', WORKFLOW_ID, 'proj_test');
|
|
@@ -992,6 +1241,7 @@ describe('WorkflowEngine', () => {
|
|
|
992
1241
|
kanban: createMockKanban(),
|
|
993
1242
|
claudeService: createMockClaudeService(),
|
|
994
1243
|
gitWorkflow: createMockGitWorkflow(),
|
|
1244
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
995
1245
|
log: createMockLog(),
|
|
996
1246
|
});
|
|
997
1247
|
|
|
@@ -1018,6 +1268,7 @@ describe('WorkflowEngine', () => {
|
|
|
1018
1268
|
kanban: createMockKanban(),
|
|
1019
1269
|
claudeService: createMockClaudeService(),
|
|
1020
1270
|
gitWorkflow: createMockGitWorkflow(),
|
|
1271
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1021
1272
|
log: createMockLog(),
|
|
1022
1273
|
});
|
|
1023
1274
|
|
|
@@ -1040,6 +1291,7 @@ describe('WorkflowEngine', () => {
|
|
|
1040
1291
|
kanban: createMockKanban(),
|
|
1041
1292
|
claudeService: createMockClaudeService(),
|
|
1042
1293
|
gitWorkflow: createMockGitWorkflow(),
|
|
1294
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1043
1295
|
log: createMockLog(),
|
|
1044
1296
|
});
|
|
1045
1297
|
|
|
@@ -1070,6 +1322,7 @@ describe('WorkflowEngine', () => {
|
|
|
1070
1322
|
db: db as any, kanban: createMockKanban(),
|
|
1071
1323
|
claudeService: createMockClaudeService(),
|
|
1072
1324
|
gitWorkflow: createMockGitWorkflow(),
|
|
1325
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1073
1326
|
log: createMockLog(),
|
|
1074
1327
|
});
|
|
1075
1328
|
|
|
@@ -1105,6 +1358,7 @@ describe('WorkflowEngine', () => {
|
|
|
1105
1358
|
db: db as any, kanban: createMockKanban(),
|
|
1106
1359
|
claudeService: createMockClaudeService(),
|
|
1107
1360
|
gitWorkflow: createMockGitWorkflow(),
|
|
1361
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1108
1362
|
log: createMockLog(),
|
|
1109
1363
|
});
|
|
1110
1364
|
|
|
@@ -1137,7 +1391,7 @@ describe('WorkflowEngine', () => {
|
|
|
1137
1391
|
const engine = new WorkflowEngine({
|
|
1138
1392
|
db: db as any, kanban: createMockKanban(),
|
|
1139
1393
|
claudeService: createMockClaudeService(), gitWorkflow,
|
|
1140
|
-
log: createMockLog(),
|
|
1394
|
+
skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1141
1395
|
});
|
|
1142
1396
|
|
|
1143
1397
|
const { executionId } = await engine.start('feat_test', WORKFLOW_ID, 'proj_test');
|
|
@@ -1147,11 +1401,16 @@ describe('WorkflowEngine', () => {
|
|
|
1147
1401
|
|
|
1148
1402
|
// Execution row was created
|
|
1149
1403
|
assert.ok(db._stores.executionsStore[executionId]);
|
|
1404
|
+
// Pull was called before worktree creation
|
|
1405
|
+
assert.equal(gitWorkflow.pullDefaultBranch.mock.calls.length, 1);
|
|
1406
|
+
assert.equal(gitWorkflow.pullDefaultBranch.mock.calls[0].arguments[0], 'proj_test');
|
|
1150
1407
|
// Worktree was created
|
|
1151
1408
|
assert.equal(gitWorkflow.createWorktree.mock.calls.length, 1);
|
|
1152
1409
|
assert.equal(gitWorkflow.createWorktree.mock.calls[0].arguments[0], 'feat_test');
|
|
1153
1410
|
// Merge was called (success outcome)
|
|
1154
1411
|
assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
|
|
1412
|
+
// Push was called after merge
|
|
1413
|
+
assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 1);
|
|
1155
1414
|
// Execution completed
|
|
1156
1415
|
assert.equal(db._stores.executionsStore[executionId].status, 'completed');
|
|
1157
1416
|
// Node executions were created for both nodes
|
|
@@ -1177,6 +1436,7 @@ describe('WorkflowEngine', () => {
|
|
|
1177
1436
|
db: db as any, kanban,
|
|
1178
1437
|
claudeService: createMockClaudeService(),
|
|
1179
1438
|
gitWorkflow: createMockGitWorkflow(),
|
|
1439
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1180
1440
|
log: createMockLog(),
|
|
1181
1441
|
});
|
|
1182
1442
|
|
|
@@ -1210,6 +1470,7 @@ describe('WorkflowEngine', () => {
|
|
|
1210
1470
|
db: db as any, kanban: createMockKanban(),
|
|
1211
1471
|
claudeService: createMockClaudeService(),
|
|
1212
1472
|
gitWorkflow: createMockGitWorkflow(),
|
|
1473
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1213
1474
|
log: createMockLog(),
|
|
1214
1475
|
});
|
|
1215
1476
|
|
|
@@ -1239,6 +1500,7 @@ describe('WorkflowEngine', () => {
|
|
|
1239
1500
|
db: db as any, kanban: createMockKanban(),
|
|
1240
1501
|
claudeService: createMockClaudeService(),
|
|
1241
1502
|
gitWorkflow: createMockGitWorkflow(),
|
|
1503
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1242
1504
|
log: createMockLog(),
|
|
1243
1505
|
});
|
|
1244
1506
|
|
|
@@ -1270,6 +1532,7 @@ describe('WorkflowEngine', () => {
|
|
|
1270
1532
|
db: db as any, kanban,
|
|
1271
1533
|
claudeService: createMockClaudeService(),
|
|
1272
1534
|
gitWorkflow: createMockGitWorkflow(),
|
|
1535
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1273
1536
|
log: createMockLog(),
|
|
1274
1537
|
});
|
|
1275
1538
|
|
|
@@ -1301,6 +1564,7 @@ describe('WorkflowEngine', () => {
|
|
|
1301
1564
|
db: db as any, kanban: createMockKanban(),
|
|
1302
1565
|
claudeService: createMockClaudeService(),
|
|
1303
1566
|
gitWorkflow: createMockGitWorkflow(),
|
|
1567
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1304
1568
|
log: createMockLog(),
|
|
1305
1569
|
});
|
|
1306
1570
|
|
|
@@ -1351,7 +1615,7 @@ describe('WorkflowEngine', () => {
|
|
|
1351
1615
|
const engine = new WorkflowEngine({
|
|
1352
1616
|
db: db as any, kanban: createMockKanban(),
|
|
1353
1617
|
claudeService: createMockClaudeService(), gitWorkflow,
|
|
1354
|
-
log: createMockLog(),
|
|
1618
|
+
skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
1355
1619
|
});
|
|
1356
1620
|
|
|
1357
1621
|
await engine.resume(execId);
|
|
@@ -1381,6 +1645,7 @@ describe('WorkflowEngine', () => {
|
|
|
1381
1645
|
db: db as any, kanban: createMockKanban(),
|
|
1382
1646
|
claudeService: createMockClaudeService(),
|
|
1383
1647
|
gitWorkflow: createMockGitWorkflow(),
|
|
1648
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1384
1649
|
log: createMockLog(),
|
|
1385
1650
|
});
|
|
1386
1651
|
|
|
@@ -1406,6 +1671,7 @@ describe('WorkflowEngine', () => {
|
|
|
1406
1671
|
db: db as any, kanban: createMockKanban(),
|
|
1407
1672
|
claudeService: createMockClaudeService(),
|
|
1408
1673
|
gitWorkflow: createMockGitWorkflow(),
|
|
1674
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1409
1675
|
log: createMockLog(),
|
|
1410
1676
|
});
|
|
1411
1677
|
|
|
@@ -1423,6 +1689,7 @@ describe('WorkflowEngine', () => {
|
|
|
1423
1689
|
kanban: createMockKanban(),
|
|
1424
1690
|
claudeService: createMockClaudeService(),
|
|
1425
1691
|
gitWorkflow: createMockGitWorkflow(),
|
|
1692
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1426
1693
|
log: createMockLog(),
|
|
1427
1694
|
});
|
|
1428
1695
|
|
|
@@ -1442,6 +1709,7 @@ describe('WorkflowEngine', () => {
|
|
|
1442
1709
|
kanban: createMockKanban(),
|
|
1443
1710
|
claudeService: createMockClaudeService(),
|
|
1444
1711
|
gitWorkflow: createMockGitWorkflow(),
|
|
1712
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1445
1713
|
log: createMockLog(),
|
|
1446
1714
|
});
|
|
1447
1715
|
});
|
|
@@ -1506,6 +1774,7 @@ describe('WorkflowEngine', () => {
|
|
|
1506
1774
|
kanban: createMockKanban(),
|
|
1507
1775
|
claudeService: createMockClaudeService(),
|
|
1508
1776
|
gitWorkflow: createMockGitWorkflow(),
|
|
1777
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1509
1778
|
log: createMockLog(),
|
|
1510
1779
|
});
|
|
1511
1780
|
});
|
|
@@ -1549,6 +1818,7 @@ describe('WorkflowEngine', () => {
|
|
|
1549
1818
|
kanban: createMockKanban(),
|
|
1550
1819
|
claudeService: createMockClaudeService(),
|
|
1551
1820
|
gitWorkflow: createMockGitWorkflow(),
|
|
1821
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1552
1822
|
log: createMockLog(),
|
|
1553
1823
|
});
|
|
1554
1824
|
});
|
|
@@ -1577,6 +1847,7 @@ describe('WorkflowEngine', () => {
|
|
|
1577
1847
|
kanban: createMockKanban(),
|
|
1578
1848
|
claudeService: createMockClaudeService(),
|
|
1579
1849
|
gitWorkflow: createMockGitWorkflow(),
|
|
1850
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1580
1851
|
log: createMockLog(),
|
|
1581
1852
|
});
|
|
1582
1853
|
|
|
@@ -1612,6 +1883,7 @@ describe('WorkflowEngine', () => {
|
|
|
1612
1883
|
outputData: JSON.stringify({ result: 'ok' }),
|
|
1613
1884
|
error: null,
|
|
1614
1885
|
attempt: 1,
|
|
1886
|
+
cycle: 1,
|
|
1615
1887
|
};
|
|
1616
1888
|
|
|
1617
1889
|
// Seed tool calls
|
|
@@ -1635,6 +1907,7 @@ describe('WorkflowEngine', () => {
|
|
|
1635
1907
|
kanban: createMockKanban(),
|
|
1636
1908
|
claudeService: createMockClaudeService(),
|
|
1637
1909
|
gitWorkflow: createMockGitWorkflow(),
|
|
1910
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1638
1911
|
log: createMockLog(),
|
|
1639
1912
|
});
|
|
1640
1913
|
|
|
@@ -1653,14 +1926,16 @@ describe('WorkflowEngine', () => {
|
|
|
1653
1926
|
assert.ok(graphData.nodes.length > 0);
|
|
1654
1927
|
assert.ok(graphData.edges.length > 0);
|
|
1655
1928
|
|
|
1656
|
-
// Check node executions
|
|
1657
|
-
const nodeExecs = result.nodeExecutions as Record<string, any>;
|
|
1929
|
+
// Check node executions — now an array per node
|
|
1930
|
+
const nodeExecs = result.nodeExecutions as Record<string, any[]>;
|
|
1658
1931
|
assert.ok(nodeExecs['agent_dev']);
|
|
1659
|
-
assert.equal(nodeExecs['agent_dev'].
|
|
1660
|
-
assert.
|
|
1932
|
+
assert.equal(nodeExecs['agent_dev'].length, 1);
|
|
1933
|
+
assert.equal(nodeExecs['agent_dev'][0].status, 'completed');
|
|
1934
|
+
assert.equal(nodeExecs['agent_dev'][0].cycle, 1);
|
|
1935
|
+
assert.deepEqual(nodeExecs['agent_dev'][0].outputData, { result: 'ok' });
|
|
1661
1936
|
|
|
1662
1937
|
// Check tool calls grouped under node execution
|
|
1663
|
-
const toolCalls = nodeExecs['agent_dev'].toolCalls as any[];
|
|
1938
|
+
const toolCalls = nodeExecs['agent_dev'][0].toolCalls as any[];
|
|
1664
1939
|
assert.equal(toolCalls.length, 2);
|
|
1665
1940
|
assert.equal(toolCalls[0].toolName, 'Read');
|
|
1666
1941
|
assert.equal(toolCalls[0].target, '/src/index.ts');
|
|
@@ -1700,6 +1975,7 @@ describe('WorkflowEngine', () => {
|
|
|
1700
1975
|
kanban: createMockKanban(),
|
|
1701
1976
|
claudeService: createMockClaudeService(),
|
|
1702
1977
|
gitWorkflow: createMockGitWorkflow(),
|
|
1978
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1703
1979
|
log: createMockLog(),
|
|
1704
1980
|
});
|
|
1705
1981
|
|
|
@@ -1734,6 +2010,147 @@ describe('WorkflowEngine', () => {
|
|
|
1734
2010
|
outputData: null,
|
|
1735
2011
|
error: null,
|
|
1736
2012
|
attempt: 1,
|
|
2013
|
+
cycle: 1,
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
const engine = new WorkflowEngine({
|
|
2017
|
+
db: db as any,
|
|
2018
|
+
kanban: createMockKanban(),
|
|
2019
|
+
claudeService: createMockClaudeService(),
|
|
2020
|
+
gitWorkflow: createMockGitWorkflow(),
|
|
2021
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
2022
|
+
log: createMockLog(),
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
const result = await engine.getMonitorData('feat_test');
|
|
2026
|
+
assert.ok(result);
|
|
2027
|
+
const nodeExecs = result.nodeExecutions as Record<string, any[]>;
|
|
2028
|
+
assert.deepEqual(nodeExecs['transition_1'][0].toolCalls, []);
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
it('returns multiple cycle executions per node sorted by cycle', async () => {
|
|
2032
|
+
const db = createMockDb();
|
|
2033
|
+
const execId = 'exec-cycles';
|
|
2034
|
+
|
|
2035
|
+
db._stores.executionsStore[execId] = {
|
|
2036
|
+
id: execId,
|
|
2037
|
+
workflowId: WORKFLOW_ID,
|
|
2038
|
+
featureId: 'feat_test',
|
|
2039
|
+
status: 'completed',
|
|
2040
|
+
currentNodeId: 'end_success',
|
|
2041
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
2042
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
2043
|
+
contextData: null,
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// Cycle 1 execution of agent_dev
|
|
2047
|
+
db._stores.nodeExecutionsStore['ne-c1'] = {
|
|
2048
|
+
id: 'ne-c1',
|
|
2049
|
+
executionId: execId,
|
|
2050
|
+
nodeId: 'agent_dev',
|
|
2051
|
+
status: 'completed',
|
|
2052
|
+
startedAt: '2026-01-01T00:01:00.000Z',
|
|
2053
|
+
completedAt: '2026-01-01T00:03:00.000Z',
|
|
2054
|
+
outputData: JSON.stringify({ costUsd: 0.05 }),
|
|
2055
|
+
error: null,
|
|
2056
|
+
attempt: 1,
|
|
2057
|
+
cycle: 1,
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
// Cycle 2 execution of agent_dev
|
|
2061
|
+
db._stores.nodeExecutionsStore['ne-c2'] = {
|
|
2062
|
+
id: 'ne-c2',
|
|
2063
|
+
executionId: execId,
|
|
2064
|
+
nodeId: 'agent_dev',
|
|
2065
|
+
status: 'completed',
|
|
2066
|
+
startedAt: '2026-01-01T00:05:00.000Z',
|
|
2067
|
+
completedAt: '2026-01-01T00:08:00.000Z',
|
|
2068
|
+
outputData: JSON.stringify({ costUsd: 0.08 }),
|
|
2069
|
+
error: null,
|
|
2070
|
+
attempt: 1,
|
|
2071
|
+
cycle: 2,
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
const engine = new WorkflowEngine({
|
|
2075
|
+
db: db as any,
|
|
2076
|
+
kanban: createMockKanban(),
|
|
2077
|
+
claudeService: createMockClaudeService(),
|
|
2078
|
+
gitWorkflow: createMockGitWorkflow(),
|
|
2079
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
2080
|
+
log: createMockLog(),
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
const result = await engine.getMonitorData('feat_test');
|
|
2084
|
+
assert.ok(result);
|
|
2085
|
+
|
|
2086
|
+
const nodeExecs = result.nodeExecutions as Record<string, any[]>;
|
|
2087
|
+
const devExecs = nodeExecs['agent_dev'];
|
|
2088
|
+
assert.equal(devExecs.length, 2);
|
|
2089
|
+
|
|
2090
|
+
// Sorted by cycle ascending
|
|
2091
|
+
assert.equal(devExecs[0].cycle, 1);
|
|
2092
|
+
assert.equal(devExecs[1].cycle, 2);
|
|
2093
|
+
|
|
2094
|
+
// Each has its own data
|
|
2095
|
+
assert.deepEqual(devExecs[0].outputData, { costUsd: 0.05 });
|
|
2096
|
+
assert.deepEqual(devExecs[1].outputData, { costUsd: 0.08 });
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
it('preserves tool calls per cycle execution', async () => {
|
|
2100
|
+
const db = createMockDb();
|
|
2101
|
+
const execId = 'exec-tc-cycles';
|
|
2102
|
+
|
|
2103
|
+
db._stores.executionsStore[execId] = {
|
|
2104
|
+
id: execId,
|
|
2105
|
+
workflowId: WORKFLOW_ID,
|
|
2106
|
+
featureId: 'feat_test',
|
|
2107
|
+
status: 'completed',
|
|
2108
|
+
currentNodeId: 'end_success',
|
|
2109
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
2110
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
2111
|
+
contextData: null,
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
db._stores.nodeExecutionsStore['ne-tc1'] = {
|
|
2115
|
+
id: 'ne-tc1',
|
|
2116
|
+
executionId: execId,
|
|
2117
|
+
nodeId: 'agent_dev',
|
|
2118
|
+
status: 'completed',
|
|
2119
|
+
startedAt: '2026-01-01T00:01:00.000Z',
|
|
2120
|
+
completedAt: '2026-01-01T00:03:00.000Z',
|
|
2121
|
+
outputData: null,
|
|
2122
|
+
error: null,
|
|
2123
|
+
attempt: 1,
|
|
2124
|
+
cycle: 1,
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
db._stores.nodeExecutionsStore['ne-tc2'] = {
|
|
2128
|
+
id: 'ne-tc2',
|
|
2129
|
+
executionId: execId,
|
|
2130
|
+
nodeId: 'agent_dev',
|
|
2131
|
+
status: 'completed',
|
|
2132
|
+
startedAt: '2026-01-01T00:05:00.000Z',
|
|
2133
|
+
completedAt: '2026-01-01T00:08:00.000Z',
|
|
2134
|
+
outputData: null,
|
|
2135
|
+
error: null,
|
|
2136
|
+
attempt: 1,
|
|
2137
|
+
cycle: 2,
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
// Tool calls for cycle 1
|
|
2141
|
+
db._stores.toolCallsStore['tc-c1-1'] = {
|
|
2142
|
+
id: 'tc-c1-1', nodeExecutionId: 'ne-tc1',
|
|
2143
|
+
timestamp: '2026-01-01T00:01:30.000Z', toolName: 'Read', target: '/src/a.ts',
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
// Tool calls for cycle 2
|
|
2147
|
+
db._stores.toolCallsStore['tc-c2-1'] = {
|
|
2148
|
+
id: 'tc-c2-1', nodeExecutionId: 'ne-tc2',
|
|
2149
|
+
timestamp: '2026-01-01T00:06:00.000Z', toolName: 'Write', target: '/src/b.ts',
|
|
2150
|
+
};
|
|
2151
|
+
db._stores.toolCallsStore['tc-c2-2'] = {
|
|
2152
|
+
id: 'tc-c2-2', nodeExecutionId: 'ne-tc2',
|
|
2153
|
+
timestamp: '2026-01-01T00:07:00.000Z', toolName: 'Bash', target: 'npm test',
|
|
1737
2154
|
};
|
|
1738
2155
|
|
|
1739
2156
|
const engine = new WorkflowEngine({
|
|
@@ -1741,13 +2158,20 @@ describe('WorkflowEngine', () => {
|
|
|
1741
2158
|
kanban: createMockKanban(),
|
|
1742
2159
|
claudeService: createMockClaudeService(),
|
|
1743
2160
|
gitWorkflow: createMockGitWorkflow(),
|
|
2161
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1744
2162
|
log: createMockLog(),
|
|
1745
2163
|
});
|
|
1746
2164
|
|
|
1747
2165
|
const result = await engine.getMonitorData('feat_test');
|
|
1748
2166
|
assert.ok(result);
|
|
1749
|
-
|
|
1750
|
-
|
|
2167
|
+
|
|
2168
|
+
const devExecs = (result.nodeExecutions as Record<string, any[]>)['agent_dev'];
|
|
2169
|
+
assert.equal(devExecs[0].toolCalls.length, 1);
|
|
2170
|
+
assert.equal(devExecs[0].toolCalls[0].toolName, 'Read');
|
|
2171
|
+
|
|
2172
|
+
assert.equal(devExecs[1].toolCalls.length, 2);
|
|
2173
|
+
assert.equal(devExecs[1].toolCalls[0].toolName, 'Write');
|
|
2174
|
+
assert.equal(devExecs[1].toolCalls[1].toolName, 'Bash');
|
|
1751
2175
|
});
|
|
1752
2176
|
});
|
|
1753
2177
|
|
|
@@ -1771,6 +2195,7 @@ describe('WorkflowEngine', () => {
|
|
|
1771
2195
|
claudeService: createMockClaudeService(),
|
|
1772
2196
|
gitWorkflow: createMockGitWorkflow(),
|
|
1773
2197
|
bundleService: mockBundleService,
|
|
2198
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1774
2199
|
log: createMockLog(),
|
|
1775
2200
|
});
|
|
1776
2201
|
|
|
@@ -1799,6 +2224,7 @@ describe('WorkflowEngine', () => {
|
|
|
1799
2224
|
claudeService: createMockClaudeService(),
|
|
1800
2225
|
gitWorkflow: createMockGitWorkflow(),
|
|
1801
2226
|
bundleService: mockBundleService,
|
|
2227
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1802
2228
|
log: createMockLog(),
|
|
1803
2229
|
});
|
|
1804
2230
|
|
|
@@ -1815,6 +2241,7 @@ describe('WorkflowEngine', () => {
|
|
|
1815
2241
|
kanban: createMockKanban(),
|
|
1816
2242
|
claudeService: createMockClaudeService(),
|
|
1817
2243
|
gitWorkflow: createMockGitWorkflow(),
|
|
2244
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1818
2245
|
log: createMockLog(),
|
|
1819
2246
|
});
|
|
1820
2247
|
|
|
@@ -1845,6 +2272,7 @@ describe('WorkflowEngine', () => {
|
|
|
1845
2272
|
claudeService: createMockClaudeService(),
|
|
1846
2273
|
gitWorkflow: createMockGitWorkflow(),
|
|
1847
2274
|
ttsService: mockTtsService,
|
|
2275
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1848
2276
|
log: createMockLog(),
|
|
1849
2277
|
});
|
|
1850
2278
|
|
|
@@ -1875,6 +2303,7 @@ describe('WorkflowEngine', () => {
|
|
|
1875
2303
|
kanban: createMockKanban(),
|
|
1876
2304
|
claudeService: createMockClaudeService(),
|
|
1877
2305
|
gitWorkflow: createMockGitWorkflow(),
|
|
2306
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1878
2307
|
log: createMockLog(),
|
|
1879
2308
|
});
|
|
1880
2309
|
|
|
@@ -1905,6 +2334,7 @@ describe('WorkflowEngine', () => {
|
|
|
1905
2334
|
claudeService: createMockClaudeService(),
|
|
1906
2335
|
gitWorkflow: createMockGitWorkflow(),
|
|
1907
2336
|
videoRenderService: mockVideoRenderService,
|
|
2337
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1908
2338
|
log: createMockLog(),
|
|
1909
2339
|
});
|
|
1910
2340
|
|
|
@@ -1946,6 +2376,7 @@ describe('WorkflowEngine', () => {
|
|
|
1946
2376
|
claudeService: createMockClaudeService(),
|
|
1947
2377
|
gitWorkflow: createMockGitWorkflow(),
|
|
1948
2378
|
videoRenderService: mockVideoRenderService,
|
|
2379
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1949
2380
|
log: createMockLog(),
|
|
1950
2381
|
});
|
|
1951
2382
|
|
|
@@ -1964,6 +2395,7 @@ describe('WorkflowEngine', () => {
|
|
|
1964
2395
|
kanban: createMockKanban(),
|
|
1965
2396
|
claudeService: createMockClaudeService(),
|
|
1966
2397
|
gitWorkflow: createMockGitWorkflow(),
|
|
2398
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1967
2399
|
log: createMockLog(),
|
|
1968
2400
|
});
|
|
1969
2401
|
|
|
@@ -1985,6 +2417,7 @@ describe('WorkflowEngine', () => {
|
|
|
1985
2417
|
claudeService: createMockClaudeService(),
|
|
1986
2418
|
gitWorkflow: createMockGitWorkflow(),
|
|
1987
2419
|
videoRenderService: mockVideoRenderService,
|
|
2420
|
+
skillsDir: TEST_SKILLS_DIR,
|
|
1988
2421
|
log: createMockLog(),
|
|
1989
2422
|
});
|
|
1990
2423
|
|
|
@@ -1996,4 +2429,347 @@ describe('WorkflowEngine', () => {
|
|
|
1996
2429
|
);
|
|
1997
2430
|
});
|
|
1998
2431
|
});
|
|
2432
|
+
|
|
2433
|
+
describe('stopNode', () => {
|
|
2434
|
+
it('marks node execution as stopped and workflow execution as failed', async () => {
|
|
2435
|
+
const db = createMockDb();
|
|
2436
|
+
const execId = 'exec-stop-1';
|
|
2437
|
+
const nodeExecId = 'ne-stop-1';
|
|
2438
|
+
|
|
2439
|
+
db._stores.executionsStore[execId] = {
|
|
2440
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2441
|
+
status: 'running', currentNodeId: 'agent_dev',
|
|
2442
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2443
|
+
};
|
|
2444
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2445
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2446
|
+
status: 'running', startedAt: '2026-01-01', completedAt: null,
|
|
2447
|
+
outputData: null, error: null, attempt: 1,
|
|
2448
|
+
};
|
|
2449
|
+
|
|
2450
|
+
const engine = new WorkflowEngine({
|
|
2451
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2452
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
await engine.stopNode('feat_test', 'agent_dev');
|
|
2456
|
+
|
|
2457
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].status, 'stopped');
|
|
2458
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].error, 'Stopped by user');
|
|
2459
|
+
assert.ok(db._stores.nodeExecutionsStore[nodeExecId].completedAt);
|
|
2460
|
+
assert.equal(db._stores.executionsStore[execId].status, 'failed');
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
it('throws when no running execution exists', async () => {
|
|
2464
|
+
const db = createMockDb();
|
|
2465
|
+
const execId = 'exec-stop-2';
|
|
2466
|
+
|
|
2467
|
+
db._stores.executionsStore[execId] = {
|
|
2468
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2469
|
+
status: 'completed', currentNodeId: null,
|
|
2470
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2471
|
+
};
|
|
2472
|
+
|
|
2473
|
+
const engine = new WorkflowEngine({
|
|
2474
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2475
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
await assert.rejects(
|
|
2479
|
+
() => engine.stopNode('feat_test', 'agent_dev'),
|
|
2480
|
+
/No running execution found/,
|
|
2481
|
+
);
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
it('throws when nodeId is not the current node', async () => {
|
|
2485
|
+
const db = createMockDb();
|
|
2486
|
+
const execId = 'exec-stop-3';
|
|
2487
|
+
|
|
2488
|
+
db._stores.executionsStore[execId] = {
|
|
2489
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2490
|
+
status: 'running', currentNodeId: 'agent_rev',
|
|
2491
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
const engine = new WorkflowEngine({
|
|
2495
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2496
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
await assert.rejects(
|
|
2500
|
+
() => engine.stopNode('feat_test', 'agent_dev'),
|
|
2501
|
+
/not the current execution node/,
|
|
2502
|
+
);
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
it('kills active child process with SIGTERM', async () => {
|
|
2506
|
+
const db = createMockDb();
|
|
2507
|
+
const execId = 'exec-stop-4';
|
|
2508
|
+
const nodeExecId = 'ne-stop-4';
|
|
2509
|
+
|
|
2510
|
+
db._stores.executionsStore[execId] = {
|
|
2511
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2512
|
+
status: 'running', currentNodeId: 'agent_dev',
|
|
2513
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2514
|
+
};
|
|
2515
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2516
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2517
|
+
status: 'running', startedAt: '2026-01-01', completedAt: null,
|
|
2518
|
+
outputData: null, error: null, attempt: 1,
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
const engine = new WorkflowEngine({
|
|
2522
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2523
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
// Simulate an active child process in the map
|
|
2527
|
+
const mockChild = { kill: mock.fn(() => true) };
|
|
2528
|
+
(engine as any).activeProcesses.set(execId, mockChild);
|
|
2529
|
+
|
|
2530
|
+
await engine.stopNode('feat_test', 'agent_dev');
|
|
2531
|
+
|
|
2532
|
+
assert.equal(mockChild.kill.mock.calls.length, 1);
|
|
2533
|
+
assert.equal(mockChild.kill.mock.calls[0].arguments[0], 'SIGTERM');
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
it('adds execution to stoppedExecutions set to prevent status overwrite', async () => {
|
|
2537
|
+
const db = createMockDb();
|
|
2538
|
+
const execId = 'exec-stop-5';
|
|
2539
|
+
const nodeExecId = 'ne-stop-5';
|
|
2540
|
+
|
|
2541
|
+
db._stores.executionsStore[execId] = {
|
|
2542
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2543
|
+
status: 'running', currentNodeId: 'agent_dev',
|
|
2544
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2545
|
+
};
|
|
2546
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2547
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2548
|
+
status: 'running', startedAt: '2026-01-01', completedAt: null,
|
|
2549
|
+
outputData: null, error: null, attempt: 1,
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
const engine = new WorkflowEngine({
|
|
2553
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2554
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
await engine.stopNode('feat_test', 'agent_dev');
|
|
2558
|
+
|
|
2559
|
+
assert.ok((engine as any).stoppedExecutions.has(execId));
|
|
2560
|
+
});
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
describe('continueNode', () => {
|
|
2564
|
+
it('resets node execution to running and workflow execution to running', async () => {
|
|
2565
|
+
const db = createMockDb();
|
|
2566
|
+
const execId = 'exec-cont-1';
|
|
2567
|
+
const nodeExecId = 'ne-cont-1';
|
|
2568
|
+
const sessionId = 'session-abc-123';
|
|
2569
|
+
|
|
2570
|
+
db._stores.executionsStore[execId] = {
|
|
2571
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2572
|
+
status: 'failed', currentNodeId: 'agent_dev',
|
|
2573
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2574
|
+
};
|
|
2575
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2576
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2577
|
+
status: 'failed', startedAt: '2026-01-01', completedAt: '2026-01-01T00:05:00Z',
|
|
2578
|
+
outputData: null, error: 'Process died', attempt: 1, claudeSessionId: sessionId,
|
|
2579
|
+
};
|
|
2580
|
+
|
|
2581
|
+
const engine = new WorkflowEngine({
|
|
2582
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2583
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
await engine.continueNode('feat_test', 'agent_dev');
|
|
2587
|
+
|
|
2588
|
+
// Node execution should be reset to running
|
|
2589
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].status, 'running');
|
|
2590
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].error, null);
|
|
2591
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].completedAt, null);
|
|
2592
|
+
// Attempt should NOT be incremented (continuation, not retry)
|
|
2593
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId].attempt, 1);
|
|
2594
|
+
// Workflow execution should be set back to running
|
|
2595
|
+
assert.equal(db._stores.executionsStore[execId].status, 'running');
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
it('throws when no execution exists for the feature', async () => {
|
|
2599
|
+
const db = createMockDb();
|
|
2600
|
+
|
|
2601
|
+
const engine = new WorkflowEngine({
|
|
2602
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2603
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
await assert.rejects(
|
|
2607
|
+
() => engine.continueNode('feat_nonexistent', 'agent_dev'),
|
|
2608
|
+
/No execution found/,
|
|
2609
|
+
);
|
|
2610
|
+
});
|
|
2611
|
+
|
|
2612
|
+
it('throws when nodeId is not the current node', async () => {
|
|
2613
|
+
const db = createMockDb();
|
|
2614
|
+
const execId = 'exec-cont-3';
|
|
2615
|
+
|
|
2616
|
+
db._stores.executionsStore[execId] = {
|
|
2617
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2618
|
+
status: 'failed', currentNodeId: 'agent_rev',
|
|
2619
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2620
|
+
};
|
|
2621
|
+
|
|
2622
|
+
const engine = new WorkflowEngine({
|
|
2623
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2624
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2627
|
+
await assert.rejects(
|
|
2628
|
+
() => engine.continueNode('feat_test', 'agent_dev'),
|
|
2629
|
+
/not the current execution node/,
|
|
2630
|
+
);
|
|
2631
|
+
});
|
|
2632
|
+
|
|
2633
|
+
it('throws when node has no claudeSessionId', async () => {
|
|
2634
|
+
const db = createMockDb();
|
|
2635
|
+
const execId = 'exec-cont-4';
|
|
2636
|
+
const nodeExecId = 'ne-cont-4';
|
|
2637
|
+
|
|
2638
|
+
db._stores.executionsStore[execId] = {
|
|
2639
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2640
|
+
status: 'failed', currentNodeId: 'agent_dev',
|
|
2641
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2642
|
+
};
|
|
2643
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2644
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2645
|
+
status: 'failed', startedAt: '2026-01-01', completedAt: null,
|
|
2646
|
+
outputData: null, error: 'Process died', attempt: 1, claudeSessionId: null,
|
|
2647
|
+
};
|
|
2648
|
+
|
|
2649
|
+
const engine = new WorkflowEngine({
|
|
2650
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2651
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
await assert.rejects(
|
|
2655
|
+
() => engine.continueNode('feat_test', 'agent_dev'),
|
|
2656
|
+
/no Claude session ID/,
|
|
2657
|
+
);
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
it('calls spawnClaude with resume sessionId and "Continue" prompt', async () => {
|
|
2661
|
+
const db = createMockDb();
|
|
2662
|
+
const execId = 'exec-cont-5';
|
|
2663
|
+
const nodeExecId = 'ne-cont-5';
|
|
2664
|
+
const sessionId = 'session-resume-456';
|
|
2665
|
+
|
|
2666
|
+
db._stores.executionsStore[execId] = {
|
|
2667
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2668
|
+
status: 'failed', currentNodeId: 'agent_dev',
|
|
2669
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2670
|
+
};
|
|
2671
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2672
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2673
|
+
status: 'failed', startedAt: '2026-01-01', completedAt: null,
|
|
2674
|
+
outputData: null, error: 'Process died', attempt: 1, claudeSessionId: sessionId,
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
const claudeService = createMockClaudeService();
|
|
2678
|
+
const engine = new WorkflowEngine({
|
|
2679
|
+
db: db as any, kanban: createMockKanban(), claudeService,
|
|
2680
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
await engine.continueNode('feat_test', 'agent_dev');
|
|
2684
|
+
// Allow the fire-and-forget async to run
|
|
2685
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2686
|
+
|
|
2687
|
+
assert.equal(claudeService.spawnClaude.mock.calls.length, 1);
|
|
2688
|
+
const call = claudeService.spawnClaude.mock.calls[0];
|
|
2689
|
+
// First arg is the prompt "Continue"
|
|
2690
|
+
assert.equal(call.arguments[0], 'Continue');
|
|
2691
|
+
// Fourth arg is the options object with resume
|
|
2692
|
+
assert.equal(call.arguments[3].resume, sessionId);
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
it('kills existing alive process before resuming', async () => {
|
|
2696
|
+
const db = createMockDb();
|
|
2697
|
+
const execId = 'exec-cont-6';
|
|
2698
|
+
const nodeExecId = 'ne-cont-6';
|
|
2699
|
+
const sessionId = 'session-kill-789';
|
|
2700
|
+
|
|
2701
|
+
db._stores.executionsStore[execId] = {
|
|
2702
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2703
|
+
status: 'running', currentNodeId: 'agent_dev',
|
|
2704
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2705
|
+
};
|
|
2706
|
+
db._stores.nodeExecutionsStore[nodeExecId] = {
|
|
2707
|
+
id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
|
|
2708
|
+
status: 'running', startedAt: '2026-01-01', completedAt: null,
|
|
2709
|
+
outputData: null, error: null, attempt: 1, claudeSessionId: sessionId,
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
const engine = new WorkflowEngine({
|
|
2713
|
+
db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
|
|
2714
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
// Simulate an active child process with an EventEmitter-like on()
|
|
2718
|
+
const mockChild = {
|
|
2719
|
+
kill: mock.fn(() => true),
|
|
2720
|
+
on: mock.fn((_event: string, cb: () => void) => { cb(); }), // immediately emit 'close'
|
|
2721
|
+
};
|
|
2722
|
+
(engine as any).activeProcesses.set(execId, mockChild);
|
|
2723
|
+
|
|
2724
|
+
await engine.continueNode('feat_test', 'agent_dev');
|
|
2725
|
+
|
|
2726
|
+
// The existing process should have been killed with SIGTERM
|
|
2727
|
+
assert.equal(mockChild.kill.mock.calls.length, 1);
|
|
2728
|
+
assert.equal(mockChild.kill.mock.calls[0].arguments[0], 'SIGTERM');
|
|
2729
|
+
// The process should be removed from activeProcesses (before the resumed one is added)
|
|
2730
|
+
// stoppedExecutions should be cleaned up (no lingering entries)
|
|
2731
|
+
assert.ok(!(engine as any).stoppedExecutions.has(execId));
|
|
2732
|
+
});
|
|
2733
|
+
|
|
2734
|
+
it('picks the highest-attempt node execution record', async () => {
|
|
2735
|
+
const db = createMockDb();
|
|
2736
|
+
const execId = 'exec-cont-7';
|
|
2737
|
+
const nodeExecId1 = 'ne-cont-7a';
|
|
2738
|
+
const nodeExecId2 = 'ne-cont-7b';
|
|
2739
|
+
const sessionId = 'session-latest';
|
|
2740
|
+
|
|
2741
|
+
db._stores.executionsStore[execId] = {
|
|
2742
|
+
id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
|
|
2743
|
+
status: 'failed', currentNodeId: 'agent_dev',
|
|
2744
|
+
context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
|
|
2745
|
+
};
|
|
2746
|
+
// Attempt 1 — completed previously
|
|
2747
|
+
db._stores.nodeExecutionsStore[nodeExecId1] = {
|
|
2748
|
+
id: nodeExecId1, executionId: execId, nodeId: 'agent_dev',
|
|
2749
|
+
status: 'completed', startedAt: '2026-01-01', completedAt: '2026-01-01',
|
|
2750
|
+
outputData: null, error: null, attempt: 1, claudeSessionId: 'session-old',
|
|
2751
|
+
};
|
|
2752
|
+
// Attempt 2 — the one that failed and should be continued
|
|
2753
|
+
db._stores.nodeExecutionsStore[nodeExecId2] = {
|
|
2754
|
+
id: nodeExecId2, executionId: execId, nodeId: 'agent_dev',
|
|
2755
|
+
status: 'failed', startedAt: '2026-01-01', completedAt: null,
|
|
2756
|
+
outputData: null, error: 'OOM', attempt: 2, claudeSessionId: sessionId,
|
|
2757
|
+
};
|
|
2758
|
+
|
|
2759
|
+
const claudeService = createMockClaudeService();
|
|
2760
|
+
const engine = new WorkflowEngine({
|
|
2761
|
+
db: db as any, kanban: createMockKanban(), claudeService,
|
|
2762
|
+
gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
|
|
2763
|
+
});
|
|
2764
|
+
|
|
2765
|
+
await engine.continueNode('feat_test', 'agent_dev');
|
|
2766
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2767
|
+
|
|
2768
|
+
// Should resume the latest session (attempt 2), not attempt 1
|
|
2769
|
+
const call = claudeService.spawnClaude.mock.calls[0];
|
|
2770
|
+
assert.equal(call.arguments[3].resume, sessionId);
|
|
2771
|
+
// Attempt should still be 2 — not incremented (continuation, not retry)
|
|
2772
|
+
assert.equal(db._stores.nodeExecutionsStore[nodeExecId2].attempt, 2);
|
|
2773
|
+
});
|
|
2774
|
+
});
|
|
1999
2775
|
});
|