@assistkick/create 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for lib/pipeline.js with mocked dependencies.
|
|
3
|
+
* Uses node:test built-in runner + assert (matching existing test patterns).
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { Pipeline } from '../packages/shared/lib/pipeline.js';
|
|
10
|
+
import { PromptBuilder } from '../packages/shared/lib/prompt_builder.js';
|
|
11
|
+
import { GitWorkflow } from '../packages/shared/lib/git_workflow.js';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const SKILL_DIR = join(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
// --- Mock factories ---
|
|
17
|
+
|
|
18
|
+
const createMockKanban = (initialEntries = {}) => {
|
|
19
|
+
const store = new Map(Object.entries(initialEntries));
|
|
20
|
+
return {
|
|
21
|
+
getKanbanEntry: async (id) => {
|
|
22
|
+
const entry = store.get(id);
|
|
23
|
+
// Return a deep copy so mutations don't affect the store directly
|
|
24
|
+
return entry ? JSON.parse(JSON.stringify(entry)) : null;
|
|
25
|
+
},
|
|
26
|
+
saveKanbanEntry: async (id, entry) => {
|
|
27
|
+
store.set(id, JSON.parse(JSON.stringify(entry)));
|
|
28
|
+
},
|
|
29
|
+
// Helper to read current state in assertions
|
|
30
|
+
_get: (id) => store.get(id),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createMockClaudeService = (overrides = {}) => {
|
|
35
|
+
const calls = { spawnClaude: [], spawnCommand: [] };
|
|
36
|
+
return {
|
|
37
|
+
spawnClaude: overrides.spawnClaude || (async (prompt, cwd, label) => {
|
|
38
|
+
calls.spawnClaude.push({ prompt, cwd, label });
|
|
39
|
+
return 'mock output';
|
|
40
|
+
}),
|
|
41
|
+
spawnCommand: overrides.spawnCommand || (async (cmd, args, cwd) => {
|
|
42
|
+
calls.spawnCommand.push({ cmd, args, cwd });
|
|
43
|
+
return '';
|
|
44
|
+
}),
|
|
45
|
+
_calls: calls,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const mockPaths = {
|
|
50
|
+
projectRoot: '/mock/project',
|
|
51
|
+
worktreesDir: '/mock/worktrees',
|
|
52
|
+
skillsDir: join(SKILL_DIR, '..', '.claude', 'skills'),
|
|
53
|
+
toolsDir: join(SKILL_DIR, 'tools'),
|
|
54
|
+
dataDir: SKILL_DIR,
|
|
55
|
+
developerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-developer', 'SKILL.md'),
|
|
56
|
+
reviewerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-code-reviewer', 'SKILL.md'),
|
|
57
|
+
debuggerSkillPath: join(SKILL_DIR, '..', '.claude', 'skills', 'product-debugger', 'SKILL.md'),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const noop = () => {};
|
|
61
|
+
|
|
62
|
+
/** Create a Pipeline instance with mock dependencies. */
|
|
63
|
+
const createTestPipeline = (mockClaude, mockKanban) => {
|
|
64
|
+
const promptBuilder = new PromptBuilder({ paths: mockPaths, log: noop });
|
|
65
|
+
const gitWorkflow = new GitWorkflow({ claudeService: mockClaude, projectRoot: mockPaths.projectRoot, worktreesDir: mockPaths.worktreesDir, log: noop });
|
|
66
|
+
return new Pipeline({
|
|
67
|
+
promptBuilder,
|
|
68
|
+
gitWorkflow,
|
|
69
|
+
claudeService: mockClaude,
|
|
70
|
+
kanban: mockKanban,
|
|
71
|
+
paths: mockPaths,
|
|
72
|
+
log: noop,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// --- Tests ---
|
|
77
|
+
|
|
78
|
+
describe('pipeline.start() validation', () => {
|
|
79
|
+
it('rejects if feature not found in kanban', async () => {
|
|
80
|
+
const mockKanban = createMockKanban({});
|
|
81
|
+
const mockClaude = createMockClaudeService();
|
|
82
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
83
|
+
|
|
84
|
+
const result = await pipeline.start('feat_999');
|
|
85
|
+
assert.equal(result.status, 404);
|
|
86
|
+
assert.ok(result.error.includes('feat_999'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects if feature not in todo column', async () => {
|
|
90
|
+
const mockKanban = createMockKanban({
|
|
91
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
92
|
+
});
|
|
93
|
+
const mockClaude = createMockClaudeService();
|
|
94
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
95
|
+
|
|
96
|
+
const result = await pipeline.start('feat_010');
|
|
97
|
+
assert.equal(result.status, 400);
|
|
98
|
+
assert.ok(result.error.includes('todo'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('rejects if feature is dev_blocked', async () => {
|
|
102
|
+
const mockKanban = createMockKanban({
|
|
103
|
+
feat_010: { column: 'todo', rejection_count: 0, notes: [], dev_blocked: true },
|
|
104
|
+
});
|
|
105
|
+
const mockClaude = createMockClaudeService();
|
|
106
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
107
|
+
|
|
108
|
+
const result = await pipeline.start('feat_010');
|
|
109
|
+
assert.equal(result.status, 400);
|
|
110
|
+
assert.ok(result.error.includes('dev_blocked'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects if pipeline already running', async () => {
|
|
114
|
+
const mockKanban = createMockKanban({
|
|
115
|
+
feat_010: { column: 'todo', rejection_count: 0, notes: [] },
|
|
116
|
+
});
|
|
117
|
+
// spawnCommand succeeds for worktree creation, spawnClaude never resolves (simulates running)
|
|
118
|
+
const mockClaude = createMockClaudeService({
|
|
119
|
+
spawnClaude: () => new Promise(() => {}), // never resolves
|
|
120
|
+
spawnCommand: async () => '',
|
|
121
|
+
});
|
|
122
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
123
|
+
|
|
124
|
+
// First start succeeds
|
|
125
|
+
const result1 = await pipeline.start('feat_010');
|
|
126
|
+
assert.equal(result1.status, 200);
|
|
127
|
+
|
|
128
|
+
// Reset kanban to todo so it's not blocked by column check
|
|
129
|
+
await mockKanban.saveKanbanEntry('feat_010', { column: 'todo', rejection_count: 0, notes: [] });
|
|
130
|
+
|
|
131
|
+
// Second start should be rejected (pipeline still running)
|
|
132
|
+
const result2 = await pipeline.start('feat_010');
|
|
133
|
+
assert.equal(result2.status, 409);
|
|
134
|
+
assert.ok(result2.error.includes('already running'));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('pipeline.getStatus()', () => {
|
|
139
|
+
it('returns idle when no pipeline exists', async () => {
|
|
140
|
+
const mockKanban = createMockKanban({});
|
|
141
|
+
const mockClaude = createMockClaudeService();
|
|
142
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
143
|
+
|
|
144
|
+
const result = await pipeline.getStatus('feat_999');
|
|
145
|
+
assert.equal(result.status, 'idle');
|
|
146
|
+
assert.equal(result.feature_id, 'feat_999');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('pipeline.unblock()', () => {
|
|
151
|
+
it('returns 404 for unknown feature', async () => {
|
|
152
|
+
const mockKanban = createMockKanban({});
|
|
153
|
+
const mockClaude = createMockClaudeService();
|
|
154
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
155
|
+
|
|
156
|
+
const result = await pipeline.unblock('feat_999');
|
|
157
|
+
assert.equal(result.status, 404);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('clears dev_blocked and pipeline state', async () => {
|
|
161
|
+
const mockKanban = createMockKanban({
|
|
162
|
+
feat_010: { column: 'todo', rejection_count: 3, notes: [], dev_blocked: true },
|
|
163
|
+
});
|
|
164
|
+
const mockClaude = createMockClaudeService();
|
|
165
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
166
|
+
|
|
167
|
+
// Simulate a blocked pipeline state
|
|
168
|
+
pipeline.pipelines.set('feat_010', { status: 'blocked', cycle: 3 });
|
|
169
|
+
|
|
170
|
+
const result = await pipeline.unblock('feat_010');
|
|
171
|
+
assert.equal(result.status, 200);
|
|
172
|
+
assert.equal(result.unblocked, true);
|
|
173
|
+
|
|
174
|
+
// Verify dev_blocked is removed from kanban
|
|
175
|
+
const entry = mockKanban._get('feat_010');
|
|
176
|
+
assert.equal(entry.dev_blocked, undefined);
|
|
177
|
+
|
|
178
|
+
// Verify pipeline state is cleared
|
|
179
|
+
assert.equal(pipeline.pipelines.has('feat_010'), false);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('pipeline.run() — full cycle with reviewer approving', () => {
|
|
184
|
+
it('completes when reviewer moves card to qa', async () => {
|
|
185
|
+
let claudeCallCount = 0;
|
|
186
|
+
const mockKanban = createMockKanban({
|
|
187
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const mockClaude = createMockClaudeService({
|
|
191
|
+
spawnClaude: async (prompt, cwd, label) => {
|
|
192
|
+
claudeCallCount++;
|
|
193
|
+
// When reviewer runs, move the card to qa
|
|
194
|
+
if (label.startsWith('review:')) {
|
|
195
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
196
|
+
entry.column = 'qa';
|
|
197
|
+
entry.moved_at = new Date().toISOString();
|
|
198
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
199
|
+
}
|
|
200
|
+
return 'mock output';
|
|
201
|
+
},
|
|
202
|
+
spawnCommand: async () => '',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
206
|
+
|
|
207
|
+
// Set up pipeline state as if start() ran the setup
|
|
208
|
+
pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
|
|
209
|
+
|
|
210
|
+
await pipeline.run('feat_010');
|
|
211
|
+
|
|
212
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
213
|
+
assert.equal(state.status, 'completed');
|
|
214
|
+
// Developer + Reviewer = 2 Claude calls
|
|
215
|
+
assert.equal(claudeCallCount, 2);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('pipeline.run() — reviewer rejecting 3x causes block', () => {
|
|
220
|
+
it('blocks card after 3 rejected cycles', async () => {
|
|
221
|
+
let claudeCallCount = 0;
|
|
222
|
+
const mockKanban = createMockKanban({
|
|
223
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const mockClaude = createMockClaudeService({
|
|
227
|
+
spawnClaude: async (prompt, cwd, label) => {
|
|
228
|
+
claudeCallCount++;
|
|
229
|
+
// Reviewer always rejects — moves card to todo
|
|
230
|
+
if (label.startsWith('review:')) {
|
|
231
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
232
|
+
entry.column = 'todo';
|
|
233
|
+
entry.moved_at = new Date().toISOString();
|
|
234
|
+
entry.notes = [{ id: 'n1', text: 'Needs fixes' }];
|
|
235
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
236
|
+
}
|
|
237
|
+
return 'mock output';
|
|
238
|
+
},
|
|
239
|
+
spawnCommand: async () => '',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
243
|
+
|
|
244
|
+
pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
|
|
245
|
+
|
|
246
|
+
await pipeline.run('feat_010');
|
|
247
|
+
|
|
248
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
249
|
+
assert.equal(state.status, 'blocked');
|
|
250
|
+
|
|
251
|
+
// 3 cycles × (dev + reviewer) = 6 Claude calls
|
|
252
|
+
assert.equal(claudeCallCount, 6);
|
|
253
|
+
|
|
254
|
+
// Kanban entry should be dev_blocked
|
|
255
|
+
const entry = mockKanban._get('feat_010');
|
|
256
|
+
assert.equal(entry.dev_blocked, true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('pipeline.run() — debug-first step', () => {
|
|
261
|
+
it('runs debugger when unaddressed notes exist', async () => {
|
|
262
|
+
const claudeCalls = [];
|
|
263
|
+
const mockKanban = createMockKanban({
|
|
264
|
+
feat_010: {
|
|
265
|
+
column: 'in_progress',
|
|
266
|
+
rejection_count: 1,
|
|
267
|
+
notes: [
|
|
268
|
+
{ id: 'n1', text: 'Bug: missing validation', addressed: false },
|
|
269
|
+
{ id: 'n2', text: 'Bug: wrong color', addressed: true },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const mockClaude = createMockClaudeService({
|
|
275
|
+
spawnClaude: async (prompt, cwd, label) => {
|
|
276
|
+
claudeCalls.push(label);
|
|
277
|
+
// Reviewer approves on first cycle
|
|
278
|
+
if (label.startsWith('review:')) {
|
|
279
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
280
|
+
entry.column = 'qa';
|
|
281
|
+
entry.moved_at = new Date().toISOString();
|
|
282
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
283
|
+
}
|
|
284
|
+
return 'mock debugger findings';
|
|
285
|
+
},
|
|
286
|
+
spawnCommand: async () => '',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
290
|
+
|
|
291
|
+
pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
|
|
292
|
+
|
|
293
|
+
await pipeline.run('feat_010');
|
|
294
|
+
|
|
295
|
+
// First call should be the debugger
|
|
296
|
+
assert.equal(claudeCalls[0], 'debug:feat_010');
|
|
297
|
+
// Then developer, then reviewer
|
|
298
|
+
assert.equal(claudeCalls[1], 'dev:feat_010');
|
|
299
|
+
assert.equal(claudeCalls[2], 'review:feat_010');
|
|
300
|
+
|
|
301
|
+
// The unaddressed note should now be marked as addressed
|
|
302
|
+
const entry = mockKanban._get('feat_010');
|
|
303
|
+
const note = entry.notes.find(n => n.id === 'n1');
|
|
304
|
+
assert.equal(note.addressed, true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('skips debugger when all notes are already addressed', async () => {
|
|
308
|
+
const claudeCalls = [];
|
|
309
|
+
const mockKanban = createMockKanban({
|
|
310
|
+
feat_010: {
|
|
311
|
+
column: 'in_progress',
|
|
312
|
+
rejection_count: 1,
|
|
313
|
+
notes: [
|
|
314
|
+
{ id: 'n1', text: 'Bug: fixed', addressed: true },
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const mockClaude = createMockClaudeService({
|
|
320
|
+
spawnClaude: async (prompt, cwd, label) => {
|
|
321
|
+
claudeCalls.push(label);
|
|
322
|
+
if (label.startsWith('review:')) {
|
|
323
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
324
|
+
entry.column = 'qa';
|
|
325
|
+
entry.moved_at = new Date().toISOString();
|
|
326
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
327
|
+
}
|
|
328
|
+
return 'mock output';
|
|
329
|
+
},
|
|
330
|
+
spawnCommand: async () => '',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
334
|
+
|
|
335
|
+
pipeline.pipelines.set('feat_010', { status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010', process: null, error: null });
|
|
336
|
+
|
|
337
|
+
await pipeline.run('feat_010');
|
|
338
|
+
|
|
339
|
+
// No debugger call — first call should be developer
|
|
340
|
+
assert.equal(claudeCalls[0], 'dev:feat_010');
|
|
341
|
+
assert.equal(claudeCalls.length, 2); // dev + reviewer only
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('pipeline task progress tracking', () => {
|
|
346
|
+
it('initializes tasks field in pipeline state on start', async () => {
|
|
347
|
+
const mockKanban = createMockKanban({
|
|
348
|
+
feat_010: { column: 'todo', rejection_count: 0, notes: [] },
|
|
349
|
+
});
|
|
350
|
+
const mockClaude = createMockClaudeService({
|
|
351
|
+
spawnClaude: () => new Promise(() => {}), // never resolves
|
|
352
|
+
spawnCommand: async () => '',
|
|
353
|
+
});
|
|
354
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
355
|
+
|
|
356
|
+
await pipeline.start('feat_010');
|
|
357
|
+
|
|
358
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
359
|
+
assert.deepEqual(state.tasks, { completed: 0, total: 0, items: [] });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('resets tasks at the start of each cycle', async () => {
|
|
363
|
+
let claudeCallCount = 0;
|
|
364
|
+
const mockKanban = createMockKanban({
|
|
365
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const mockClaude = createMockClaudeService({
|
|
369
|
+
spawnClaude: async (prompt, cwd, label, opts) => {
|
|
370
|
+
claudeCallCount++;
|
|
371
|
+
if (label.startsWith('dev:')) {
|
|
372
|
+
// Simulate TodoWrite callback if present
|
|
373
|
+
if (opts?.onToolUse) {
|
|
374
|
+
opts.onToolUse('TodoWrite', {
|
|
375
|
+
todos: [
|
|
376
|
+
{ content: 'Task A', status: 'completed', activeForm: 'Doing A' },
|
|
377
|
+
{ content: 'Task B', status: 'in_progress', activeForm: 'Doing B' },
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Reviewer rejects on first cycle, approves on second
|
|
383
|
+
if (label.startsWith('review:')) {
|
|
384
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
385
|
+
if (claudeCallCount <= 2) {
|
|
386
|
+
entry.column = 'todo';
|
|
387
|
+
entry.notes = [{ id: 'n1', text: 'Fix it' }];
|
|
388
|
+
} else {
|
|
389
|
+
entry.column = 'qa';
|
|
390
|
+
}
|
|
391
|
+
entry.moved_at = new Date().toISOString();
|
|
392
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
393
|
+
}
|
|
394
|
+
return 'mock output';
|
|
395
|
+
},
|
|
396
|
+
spawnCommand: async () => '',
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
400
|
+
pipeline.pipelines.set('feat_010', {
|
|
401
|
+
status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
|
|
402
|
+
process: null, error: null, tasks: { completed: 0, total: 0, items: [] },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await pipeline.run('feat_010');
|
|
406
|
+
|
|
407
|
+
// Pipeline should have completed after 2 cycles
|
|
408
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
409
|
+
assert.equal(state.status, 'completed');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('getStatus includes tasks when total > 0', async () => {
|
|
413
|
+
const mockKanban = createMockKanban({});
|
|
414
|
+
const mockClaude = createMockClaudeService();
|
|
415
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
416
|
+
|
|
417
|
+
pipeline.pipelines.set('feat_010', {
|
|
418
|
+
status: 'developing', cycle: 1, worktree_path: '/mock',
|
|
419
|
+
process: null, error: null,
|
|
420
|
+
tasks: { completed: 3, total: 7, items: [
|
|
421
|
+
{ name: 'Task A', status: 'completed' },
|
|
422
|
+
{ name: 'Task B', status: 'completed' },
|
|
423
|
+
{ name: 'Task C', status: 'completed' },
|
|
424
|
+
{ name: 'Task D', status: 'in_progress' },
|
|
425
|
+
{ name: 'Task E', status: 'pending' },
|
|
426
|
+
{ name: 'Task F', status: 'pending' },
|
|
427
|
+
{ name: 'Task G', status: 'pending' },
|
|
428
|
+
] },
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const result = await pipeline.getStatus('feat_010');
|
|
432
|
+
assert.equal(result.tasks.completed, 3);
|
|
433
|
+
assert.equal(result.tasks.total, 7);
|
|
434
|
+
assert.equal(result.tasks.items.length, 7);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('getStatus omits tasks when total is 0', async () => {
|
|
438
|
+
const mockKanban = createMockKanban({});
|
|
439
|
+
const mockClaude = createMockClaudeService();
|
|
440
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
441
|
+
|
|
442
|
+
pipeline.pipelines.set('feat_010', {
|
|
443
|
+
status: 'developing', cycle: 1, worktree_path: '/mock',
|
|
444
|
+
process: null, error: null,
|
|
445
|
+
tasks: { completed: 0, total: 0, items: [] },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const result = await pipeline.getStatus('feat_010');
|
|
449
|
+
assert.equal(result.tasks, undefined);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('pipeline tool call activity counters', () => {
|
|
454
|
+
it('initializes toolCalls field in pipeline state on start', async () => {
|
|
455
|
+
const mockKanban = createMockKanban({
|
|
456
|
+
feat_010: { column: 'todo', rejection_count: 0, notes: [] },
|
|
457
|
+
});
|
|
458
|
+
const mockClaude = createMockClaudeService({
|
|
459
|
+
spawnClaude: () => new Promise(() => {}),
|
|
460
|
+
spawnCommand: async () => '',
|
|
461
|
+
});
|
|
462
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
463
|
+
|
|
464
|
+
await pipeline.start('feat_010');
|
|
465
|
+
|
|
466
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
467
|
+
assert.deepEqual(state.toolCalls, { total: 0, write: 0, edit: 0, read: 0, bash: 0 });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('counts tool calls by type via onToolUse callback', async () => {
|
|
471
|
+
const mockKanban = createMockKanban({
|
|
472
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const mockClaude = createMockClaudeService({
|
|
476
|
+
spawnClaude: async (prompt, cwd, label, opts) => {
|
|
477
|
+
if (label.startsWith('dev:') && opts?.onToolUse) {
|
|
478
|
+
opts.onToolUse('Read', { file_path: '/some/file.js' });
|
|
479
|
+
opts.onToolUse('Read', { file_path: '/another/file.js' });
|
|
480
|
+
opts.onToolUse('Edit', { file_path: '/some/file.js', old_string: 'a', new_string: 'b' });
|
|
481
|
+
opts.onToolUse('Write', { file_path: '/new/file.js', content: '...' });
|
|
482
|
+
opts.onToolUse('Bash', { command: 'npm test' });
|
|
483
|
+
opts.onToolUse('Bash', { command: 'ls' });
|
|
484
|
+
opts.onToolUse('TodoWrite', { todos: [{ content: 'Task', status: 'pending', activeForm: 'Doing' }] });
|
|
485
|
+
opts.onToolUse('Grep', { pattern: 'foo' });
|
|
486
|
+
}
|
|
487
|
+
if (label.startsWith('review:')) {
|
|
488
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
489
|
+
entry.column = 'qa';
|
|
490
|
+
entry.moved_at = new Date().toISOString();
|
|
491
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
492
|
+
}
|
|
493
|
+
return 'mock output';
|
|
494
|
+
},
|
|
495
|
+
spawnCommand: async () => '',
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
499
|
+
pipeline.pipelines.set('feat_010', {
|
|
500
|
+
status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
|
|
501
|
+
process: null, error: null,
|
|
502
|
+
tasks: { completed: 0, total: 0, items: [] },
|
|
503
|
+
toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await pipeline.run('feat_010');
|
|
507
|
+
|
|
508
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
509
|
+
assert.equal(state.toolCalls.total, 8);
|
|
510
|
+
assert.equal(state.toolCalls.read, 2);
|
|
511
|
+
assert.equal(state.toolCalls.edit, 1);
|
|
512
|
+
assert.equal(state.toolCalls.write, 1);
|
|
513
|
+
assert.equal(state.toolCalls.bash, 2);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('resets toolCalls at the start of each cycle', async () => {
|
|
517
|
+
let claudeCallCount = 0;
|
|
518
|
+
const mockKanban = createMockKanban({
|
|
519
|
+
feat_010: { column: 'in_progress', rejection_count: 0, notes: [] },
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const mockClaude = createMockClaudeService({
|
|
523
|
+
spawnClaude: async (prompt, cwd, label, opts) => {
|
|
524
|
+
claudeCallCount++;
|
|
525
|
+
if (label.startsWith('dev:') && opts?.onToolUse) {
|
|
526
|
+
opts.onToolUse('Write', { file_path: '/f.js', content: '...' });
|
|
527
|
+
opts.onToolUse('Edit', { file_path: '/f.js', old_string: 'a', new_string: 'b' });
|
|
528
|
+
}
|
|
529
|
+
if (label.startsWith('review:')) {
|
|
530
|
+
const entry = await mockKanban.getKanbanEntry('feat_010');
|
|
531
|
+
if (claudeCallCount <= 2) {
|
|
532
|
+
entry.column = 'todo';
|
|
533
|
+
entry.notes = [{ id: 'n1', text: 'Fix' }];
|
|
534
|
+
} else {
|
|
535
|
+
entry.column = 'qa';
|
|
536
|
+
}
|
|
537
|
+
entry.moved_at = new Date().toISOString();
|
|
538
|
+
await mockKanban.saveKanbanEntry('feat_010', entry);
|
|
539
|
+
}
|
|
540
|
+
return 'mock output';
|
|
541
|
+
},
|
|
542
|
+
spawnCommand: async () => '',
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
546
|
+
pipeline.pipelines.set('feat_010', {
|
|
547
|
+
status: 'developing', cycle: 1, worktree_path: '/mock/worktrees/feat_010',
|
|
548
|
+
process: null, error: null,
|
|
549
|
+
tasks: { completed: 0, total: 0, items: [] },
|
|
550
|
+
toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await pipeline.run('feat_010');
|
|
554
|
+
|
|
555
|
+
// After 2 cycles (reject + approve), toolCalls should reflect only cycle 2
|
|
556
|
+
const state = pipeline.pipelines.get('feat_010');
|
|
557
|
+
assert.equal(state.toolCalls.total, 2);
|
|
558
|
+
assert.equal(state.toolCalls.write, 1);
|
|
559
|
+
assert.equal(state.toolCalls.edit, 1);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('getStatus includes toolCalls when total > 0', async () => {
|
|
563
|
+
const mockKanban = createMockKanban({});
|
|
564
|
+
const mockClaude = createMockClaudeService();
|
|
565
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
566
|
+
|
|
567
|
+
pipeline.pipelines.set('feat_010', {
|
|
568
|
+
status: 'developing', cycle: 1, worktree_path: '/mock',
|
|
569
|
+
process: null, error: null,
|
|
570
|
+
tasks: { completed: 0, total: 0, items: [] },
|
|
571
|
+
toolCalls: { total: 5, write: 1, edit: 2, read: 1, bash: 1 },
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const result = await pipeline.getStatus('feat_010');
|
|
575
|
+
assert.deepEqual(result.toolCalls, { total: 5, write: 1, edit: 2, read: 1, bash: 1 });
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('getStatus omits toolCalls when total is 0', async () => {
|
|
579
|
+
const mockKanban = createMockKanban({});
|
|
580
|
+
const mockClaude = createMockClaudeService();
|
|
581
|
+
const pipeline = createTestPipeline(mockClaude, mockKanban);
|
|
582
|
+
|
|
583
|
+
pipeline.pipelines.set('feat_010', {
|
|
584
|
+
status: 'developing', cycle: 1, worktree_path: '/mock',
|
|
585
|
+
process: null, error: null,
|
|
586
|
+
tasks: { completed: 0, total: 0, items: [] },
|
|
587
|
+
toolCalls: { total: 0, write: 0, edit: 0, read: 0, bash: 0 },
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const result = await pipeline.getStatus('feat_010');
|
|
591
|
+
assert.equal(result.toolCalls, undefined);
|
|
592
|
+
});
|
|
593
|
+
});
|