@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.
Files changed (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Tests for SidePanel class — node detail side panel.
3
+ * Uses node:test built-in runner.
4
+ */
5
+ import { describe, it, mock } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ class SidePanel {
8
+ panelEl: any;
9
+ titleEl: any;
10
+ bodyEl: any;
11
+ markdownRenderer: any;
12
+ apiClient: any;
13
+ onEdgeClick: any;
14
+ graphData: any = null;
15
+
16
+ constructor(panelEl: any, titleEl: any, bodyEl: any, markdownRenderer: any, apiClient: any, onEdgeClick: any) {
17
+ this.panelEl = panelEl;
18
+ this.titleEl = titleEl;
19
+ this.bodyEl = bodyEl;
20
+ this.markdownRenderer = markdownRenderer;
21
+ this.apiClient = apiClient;
22
+ this.onEdgeClick = onEdgeClick;
23
+ }
24
+
25
+ setGraphData(data: any) { this.graphData = data; }
26
+
27
+ async open(node: any) {
28
+ try {
29
+ const detail = await this.apiClient.fetchNode(node.id);
30
+ this.renderContent(node, detail.content);
31
+ this.panelEl.classList.add('open');
32
+ } catch { /* fail silently */ }
33
+ }
34
+
35
+ close() { this.panelEl.classList.remove('open'); }
36
+
37
+ findEdges(nodeId: string) {
38
+ if (!this.graphData) return [];
39
+ const edges: any[] = [];
40
+ this.graphData.edges.forEach((e: any) => {
41
+ if (e.from === nodeId) edges.push({ neighborId: e.to, relation: e.relation, direction: 'outgoing' });
42
+ else if (e.to === nodeId) edges.push({ neighborId: e.from, relation: e.relation, direction: 'incoming' });
43
+ });
44
+ return edges;
45
+ }
46
+
47
+ buildStatusSection(node: any) {
48
+ const statusLabel = (node.status || 'draft').replace(/_/g, ' ');
49
+ const completeness = Math.round((node.completeness || 0) * 100);
50
+ const typeLabel = node.type ? node.type.replace(/_/g, ' ') : '';
51
+ return `<div class="panel-status-section"><span class="panel-type-badge">${typeLabel}</span><span class="panel-status-badge panel-status-${node.status || 'draft'}">${statusLabel}</span><span class="panel-completeness">${completeness}% complete</span></div>`;
52
+ }
53
+
54
+ buildEdgesSection(nodeId: string) {
55
+ const edges = this.findEdges(nodeId);
56
+ if (edges.length === 0) return '';
57
+ let html = '<div class="panel-edges-section"><h3>Relationships</h3><ul class="panel-edge-list">';
58
+ for (const edge of edges) {
59
+ const neighborNode = this.graphData?.nodes.find((n: any) => n.id === edge.neighborId);
60
+ const name = neighborNode ? neighborNode.name : edge.neighborId;
61
+ const direction = edge.direction === 'outgoing' ? '\u2192' : '\u2190';
62
+ html += `<li class="panel-edge-item"><span class="panel-edge-direction">${direction}</span><span class="panel-edge-relation">${edge.relation.replace(/_/g, ' ')}</span><a class="panel-edge-link" href="#">${name}</a><span class="panel-edge-id">${edge.neighborId}</span></li>`;
63
+ }
64
+ html += '</ul></div>';
65
+ return html;
66
+ }
67
+
68
+ renderMarkdown(md: string) {
69
+ const stripped = md.replace(/^---[\s\S]*?---\n*/m, '');
70
+ if (this.markdownRenderer) {
71
+ try { return this.markdownRenderer(stripped); } catch { return stripped; }
72
+ }
73
+ // Basic fallback renderer
74
+ let html = stripped
75
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
76
+ .replace(/^- (.+)$/gm, '<li>$1</li>');
77
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => `<ul>${match}</ul>`);
78
+ return html;
79
+ }
80
+
81
+ renderContent(node: any, content: string) {
82
+ if (this.titleEl) this.titleEl.textContent = `${node.name} (${node.id})`;
83
+ if (this.bodyEl) {
84
+ const statusHtml = this.buildStatusSection(node);
85
+ const mdHtml = this.renderMarkdown(content);
86
+ const edgesHtml = this.buildEdgesSection(node.id);
87
+ this.bodyEl.innerHTML = statusHtml + mdHtml + edgesHtml;
88
+ }
89
+ }
90
+ }
91
+
92
+ // --- Mock helpers ---
93
+
94
+ const createMockPanel = () => ({ classList: { add: mock.fn(), remove: mock.fn() } });
95
+ const createMockTitle = () => ({ textContent: '' });
96
+ const createMockBody = () => ({
97
+ innerHTML: '',
98
+ querySelectorAll: mock.fn(() => []),
99
+ });
100
+
101
+ const SAMPLE_GRAPH_DATA = {
102
+ nodes: [
103
+ { id: 'feat_001', name: 'Node CRUD', type: 'feature', status: 'defined', completeness: 1, open_questions_count: 0 },
104
+ { id: 'comp_001', name: 'GraphStore', type: 'component', status: 'defined', completeness: 0.8, open_questions_count: 0 },
105
+ { id: 'dec_001', name: 'JSON Storage', type: 'decision', status: 'draft', completeness: 0.5, open_questions_count: 2 },
106
+ ],
107
+ edges: [
108
+ { from: 'feat_001', to: 'comp_001', relation: 'contains' },
109
+ { from: 'feat_001', to: 'dec_001', relation: 'governed_by' },
110
+ ],
111
+ };
112
+
113
+ const SAMPLE_MARKDOWN = `---
114
+ id: feat_001
115
+ type: feature
116
+ ---
117
+ ## Description
118
+ This is a test node.
119
+
120
+ ## Acceptance Criteria
121
+ - Criterion one
122
+ - Criterion two
123
+ `;
124
+
125
+ // --- Tests ---
126
+
127
+ describe('SidePanel', () => {
128
+
129
+ describe('constructor', () => {
130
+ it('stores all injected dependencies', () => {
131
+ const panelEl = createMockPanel();
132
+ const titleEl = createMockTitle();
133
+ const bodyEl = createMockBody();
134
+ const renderer = mock.fn();
135
+ const apiClient = {};
136
+ const onEdgeClick = mock.fn();
137
+
138
+ const panel = new SidePanel(panelEl, titleEl, bodyEl, renderer, apiClient, onEdgeClick);
139
+
140
+ assert.equal(panel.panelEl, panelEl);
141
+ assert.equal(panel.titleEl, titleEl);
142
+ assert.equal(panel.bodyEl, bodyEl);
143
+ assert.equal(panel.markdownRenderer, renderer);
144
+ assert.equal(panel.apiClient, apiClient);
145
+ assert.equal(panel.onEdgeClick, onEdgeClick);
146
+ });
147
+ });
148
+
149
+ describe('setGraphData', () => {
150
+ it('stores graph data for edge lookups', () => {
151
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
152
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
153
+ assert.equal(panel.graphData, SAMPLE_GRAPH_DATA);
154
+ });
155
+ });
156
+
157
+ describe('open', () => {
158
+ it('fetches node content and adds open class', async () => {
159
+ const panelEl = createMockPanel();
160
+ const bodyEl = createMockBody();
161
+ const mockApi = { fetchNode: mock.fn(async () => ({ id: 'feat_001', content: SAMPLE_MARKDOWN })) };
162
+
163
+ const panel = new SidePanel(panelEl, createMockTitle(), bodyEl, null, mockApi, null);
164
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
165
+
166
+ const node = SAMPLE_GRAPH_DATA.nodes[0];
167
+ await panel.open(node);
168
+
169
+ assert.equal(mockApi.fetchNode.mock.calls.length, 1);
170
+ assert.equal(mockApi.fetchNode.mock.calls[0].arguments[0], 'feat_001');
171
+ assert.equal(panelEl.classList.add.mock.calls.length, 1);
172
+ assert.equal(panelEl.classList.add.mock.calls[0].arguments[0], 'open');
173
+ });
174
+
175
+ it('handles API errors gracefully', async () => {
176
+ const panelEl = createMockPanel();
177
+ const mockApi = { fetchNode: mock.fn(async () => { throw new Error('404'); }) };
178
+
179
+ const panel = new SidePanel(panelEl, createMockTitle(), createMockBody(), null, mockApi, null);
180
+
181
+ await panel.open({ id: 'feat_999', name: 'Missing' });
182
+ // Should not throw, panel should NOT open
183
+ assert.equal(panelEl.classList.add.mock.calls.length, 0);
184
+ });
185
+ });
186
+
187
+ describe('close', () => {
188
+ it('removes open class from panel', () => {
189
+ const panelEl = createMockPanel();
190
+ const panel = new SidePanel(panelEl, createMockTitle(), createMockBody(), null, {}, null);
191
+
192
+ panel.close();
193
+ assert.equal(panelEl.classList.remove.mock.calls.length, 1);
194
+ assert.equal(panelEl.classList.remove.mock.calls[0].arguments[0], 'open');
195
+ });
196
+ });
197
+
198
+ describe('buildStatusSection', () => {
199
+ it('renders status badge and completeness', () => {
200
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
201
+
202
+ const node = { status: 'defined', completeness: 0.85, type: 'feature' };
203
+ const html = panel.buildStatusSection(node);
204
+
205
+ assert.ok(html.includes('panel-status-defined'));
206
+ assert.ok(html.includes('defined'));
207
+ assert.ok(html.includes('85% complete'));
208
+ assert.ok(html.includes('feature'));
209
+ });
210
+
211
+ it('handles partially_defined status', () => {
212
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
213
+
214
+ const node = { status: 'partially_defined', completeness: 0.5, type: 'decision' };
215
+ const html = panel.buildStatusSection(node);
216
+
217
+ assert.ok(html.includes('panel-status-partially_defined'));
218
+ assert.ok(html.includes('partially defined'));
219
+ assert.ok(html.includes('50% complete'));
220
+ });
221
+
222
+ it('defaults to draft when status is missing', () => {
223
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
224
+
225
+ const html = panel.buildStatusSection({});
226
+ assert.ok(html.includes('panel-status-draft'));
227
+ assert.ok(html.includes('0% complete'));
228
+ });
229
+ });
230
+
231
+ describe('findEdges', () => {
232
+ it('finds outgoing edges', () => {
233
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
234
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
235
+
236
+ const edges = panel.findEdges('feat_001');
237
+ assert.equal(edges.length, 2);
238
+ assert.equal(edges[0].neighborId, 'comp_001');
239
+ assert.equal(edges[0].direction, 'outgoing');
240
+ assert.equal(edges[0].relation, 'contains');
241
+ });
242
+
243
+ it('finds incoming edges', () => {
244
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
245
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
246
+
247
+ const edges = panel.findEdges('comp_001');
248
+ assert.equal(edges.length, 1);
249
+ assert.equal(edges[0].neighborId, 'feat_001');
250
+ assert.equal(edges[0].direction, 'incoming');
251
+ });
252
+
253
+ it('returns empty array when no graph data', () => {
254
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
255
+ const edges = panel.findEdges('feat_001');
256
+ assert.deepEqual(edges, []);
257
+ });
258
+
259
+ it('returns empty array for node with no edges', () => {
260
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
261
+ panel.setGraphData({ nodes: [], edges: [] });
262
+
263
+ const edges = panel.findEdges('feat_999');
264
+ assert.deepEqual(edges, []);
265
+ });
266
+ });
267
+
268
+ describe('buildEdgesSection', () => {
269
+ it('renders edge list with neighbor names', () => {
270
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
271
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
272
+
273
+ const html = panel.buildEdgesSection('feat_001');
274
+ assert.ok(html.includes('GraphStore'));
275
+ assert.ok(html.includes('JSON Storage'));
276
+ assert.ok(html.includes('comp_001'));
277
+ assert.ok(html.includes('dec_001'));
278
+ assert.ok(html.includes('panel-edge-link'));
279
+ assert.ok(html.includes('contains'));
280
+ assert.ok(html.includes('governed by'));
281
+ });
282
+
283
+ it('returns empty string when no graph data', () => {
284
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
285
+ const html = panel.buildEdgesSection('feat_001');
286
+ assert.equal(html, '');
287
+ });
288
+
289
+ it('returns empty string for node with no edges', () => {
290
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
291
+ panel.setGraphData({ nodes: [], edges: [] });
292
+
293
+ const html = panel.buildEdgesSection('feat_999');
294
+ assert.equal(html, '');
295
+ });
296
+ });
297
+
298
+ describe('renderMarkdown', () => {
299
+ it('uses injected markdown renderer when provided', () => {
300
+ const mockRenderer = mock.fn((md) => `<p>rendered: ${md}</p>`);
301
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), mockRenderer, {}, null);
302
+
303
+ const result = panel.renderMarkdown(SAMPLE_MARKDOWN);
304
+ assert.equal(mockRenderer.mock.calls.length, 1);
305
+ assert.ok(result.includes('rendered:'));
306
+ // Should strip frontmatter before passing to renderer
307
+ assert.ok(!result.includes('id: feat_001'));
308
+ });
309
+
310
+ it('falls back to basic renderer when no marked.js', () => {
311
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), createMockBody(), null, {}, null);
312
+
313
+ const result = panel.renderMarkdown(SAMPLE_MARKDOWN);
314
+ assert.ok(result.includes('<h2>Description</h2>'));
315
+ assert.ok(result.includes('<li>Criterion one</li>'));
316
+ // Should strip frontmatter
317
+ assert.ok(!result.includes('id: feat_001'));
318
+ });
319
+ });
320
+
321
+ describe('renderContent', () => {
322
+ it('sets title with node name and id', () => {
323
+ const titleEl = createMockTitle();
324
+ const bodyEl = createMockBody();
325
+ const panel = new SidePanel(createMockPanel(), titleEl, bodyEl, null, {}, null);
326
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
327
+
328
+ panel.renderContent(SAMPLE_GRAPH_DATA.nodes[0], SAMPLE_MARKDOWN);
329
+ assert.equal(titleEl.textContent, 'Node CRUD (feat_001)');
330
+ });
331
+
332
+ it('includes status section, markdown, and edges in body', () => {
333
+ const bodyEl = createMockBody();
334
+ const panel = new SidePanel(createMockPanel(), createMockTitle(), bodyEl, null, {}, null);
335
+ panel.setGraphData(SAMPLE_GRAPH_DATA);
336
+
337
+ panel.renderContent(SAMPLE_GRAPH_DATA.nodes[0], SAMPLE_MARKDOWN);
338
+
339
+ assert.ok(bodyEl.innerHTML.includes('panel-status-section'));
340
+ assert.ok(bodyEl.innerHTML.includes('Description'));
341
+ assert.ok(bodyEl.innerHTML.includes('Relationships'));
342
+ assert.ok(bodyEl.innerHTML.includes('GraphStore'));
343
+ });
344
+ });
345
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tests for feat_056: Label Spec Completeness Bar on Kanban Cards.
3
+ * Verifies that the completeness bar displays a 'Spec' prefix label
4
+ * to distinguish spec completeness from pipeline task progress.
5
+ * Uses node:test built-in runner.
6
+ */
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+
10
+ /**
11
+ * The completeness bar label formatting logic extracted from kanban_renderer.js.
12
+ * The renderer creates a prefix label 'Spec' and a percentage label like '75%'.
13
+ */
14
+ const formatCompletenessLabel = (completeness) => {
15
+ const pct = Math.round((completeness || 0) * 100);
16
+ return `${pct}%`;
17
+ };
18
+
19
+ const SPEC_PREFIX = 'Spec';
20
+
21
+ describe('Spec completeness label on kanban cards', () => {
22
+ describe('prefix label', () => {
23
+ it('uses "Spec" as the prefix text', () => {
24
+ assert.equal(SPEC_PREFIX, 'Spec');
25
+ });
26
+ });
27
+
28
+ describe('formatCompletenessLabel', () => {
29
+ it('formats 100% completeness', () => {
30
+ assert.equal(formatCompletenessLabel(1), '100%');
31
+ });
32
+
33
+ it('formats 0% completeness', () => {
34
+ assert.equal(formatCompletenessLabel(0), '0%');
35
+ });
36
+
37
+ it('formats partial completeness', () => {
38
+ assert.equal(formatCompletenessLabel(0.75), '75%');
39
+ });
40
+
41
+ it('rounds fractional completeness', () => {
42
+ assert.equal(formatCompletenessLabel(0.333), '33%');
43
+ });
44
+
45
+ it('handles null completeness', () => {
46
+ assert.equal(formatCompletenessLabel(null), '0%');
47
+ });
48
+
49
+ it('handles undefined completeness', () => {
50
+ assert.equal(formatCompletenessLabel(undefined), '0%');
51
+ });
52
+ });
53
+
54
+ describe('label disambiguates from pipeline progress', () => {
55
+ it('spec label and task progress use different formats', () => {
56
+ // Spec completeness: "Spec 75%"
57
+ const specDisplay = `${SPEC_PREFIX} ${formatCompletenessLabel(0.75)}`;
58
+ assert.equal(specDisplay, 'Spec 75%');
59
+
60
+ // Pipeline task progress: "3/7 tasks" (from feat_052/055)
61
+ const taskProgress = '3/7 tasks';
62
+
63
+ // They are clearly different formats
64
+ assert.notEqual(specDisplay, taskProgress);
65
+ assert.ok(specDisplay.startsWith('Spec'), 'spec display starts with Spec prefix');
66
+ assert.ok(!taskProgress.startsWith('Spec'), 'task progress does not start with Spec');
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Tests for URL-based view routing (feat_032).
3
+ * Tests the hash parsing and tab validation logic.
4
+ *
5
+ * Run: node --test ui/url_routing_test.js
6
+ */
7
+
8
+ import { describe, it, beforeEach } from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+
11
+ /**
12
+ * Extracts the tab name from a URL hash string.
13
+ * Mirrors the logic in App.getTabFromHash.
14
+ */
15
+ const getTabFromHash = (hash, validTabs) => {
16
+ const name = hash.replace('#', '');
17
+ return validTabs.has(name) ? name : 'graph';
18
+ };
19
+
20
+ const VALID_TABS = new Set(['graph', 'kanban']);
21
+
22
+ describe('URL routing — getTabFromHash', () => {
23
+ it('returns "graph" for empty hash', () => {
24
+ assert.equal(getTabFromHash('', VALID_TABS), 'graph');
25
+ });
26
+
27
+ it('returns "graph" for hash "#graph"', () => {
28
+ assert.equal(getTabFromHash('#graph', VALID_TABS), 'graph');
29
+ });
30
+
31
+ it('returns "kanban" for hash "#kanban"', () => {
32
+ assert.equal(getTabFromHash('#kanban', VALID_TABS), 'kanban');
33
+ });
34
+
35
+ it('returns "graph" for unknown hash value', () => {
36
+ assert.equal(getTabFromHash('#settings', VALID_TABS), 'graph');
37
+ });
38
+
39
+ it('returns "graph" for hash with just "#"', () => {
40
+ assert.equal(getTabFromHash('#', VALID_TABS), 'graph');
41
+ });
42
+
43
+ it('returns "graph" for hash with invalid characters', () => {
44
+ assert.equal(getTabFromHash('#kanban/extra', VALID_TABS), 'graph');
45
+ });
46
+ });
47
+
48
+ describe('URL routing — switchTab hash update', () => {
49
+ let hashValue;
50
+ let activeTab;
51
+ let tabClasses;
52
+ let containerDisplays;
53
+
54
+ const mockSwitchTab = (tabName, { updateHash = true } = {}) => {
55
+ activeTab = tabName;
56
+ if (updateHash) {
57
+ hashValue = tabName;
58
+ }
59
+ tabClasses = {
60
+ graph: tabName === 'graph' ? 'active' : '',
61
+ kanban: tabName === 'kanban' ? 'active' : '',
62
+ };
63
+ containerDisplays = {
64
+ graph: tabName === 'graph' ? 'block' : 'none',
65
+ kanban: tabName === 'graph' ? 'none' : 'flex',
66
+ legend: tabName === 'graph' ? 'block' : 'none',
67
+ };
68
+ };
69
+
70
+ beforeEach(() => {
71
+ hashValue = '';
72
+ activeTab = 'graph';
73
+ tabClasses = {};
74
+ containerDisplays = {};
75
+ });
76
+
77
+ it('updates hash when switching to kanban', () => {
78
+ mockSwitchTab('kanban');
79
+ assert.equal(hashValue, 'kanban');
80
+ assert.equal(activeTab, 'kanban');
81
+ });
82
+
83
+ it('updates hash when switching to graph', () => {
84
+ mockSwitchTab('kanban');
85
+ mockSwitchTab('graph');
86
+ assert.equal(hashValue, 'graph');
87
+ assert.equal(activeTab, 'graph');
88
+ });
89
+
90
+ it('does not update hash when updateHash is false', () => {
91
+ hashValue = 'original';
92
+ mockSwitchTab('kanban', { updateHash: false });
93
+ assert.equal(hashValue, 'original');
94
+ assert.equal(activeTab, 'kanban');
95
+ });
96
+
97
+ it('sets correct active tab classes for graph', () => {
98
+ mockSwitchTab('graph');
99
+ assert.equal(tabClasses.graph, 'active');
100
+ assert.equal(tabClasses.kanban, '');
101
+ });
102
+
103
+ it('sets correct active tab classes for kanban', () => {
104
+ mockSwitchTab('kanban');
105
+ assert.equal(tabClasses.graph, '');
106
+ assert.equal(tabClasses.kanban, 'active');
107
+ });
108
+
109
+ it('shows graph container and legend when graph tab active', () => {
110
+ mockSwitchTab('graph');
111
+ assert.equal(containerDisplays.graph, 'block');
112
+ assert.equal(containerDisplays.kanban, 'none');
113
+ assert.equal(containerDisplays.legend, 'block');
114
+ });
115
+
116
+ it('shows kanban container and hides legend when kanban tab active', () => {
117
+ mockSwitchTab('kanban');
118
+ assert.equal(containerDisplays.graph, 'none');
119
+ assert.equal(containerDisplays.kanban, 'flex');
120
+ assert.equal(containerDisplays.legend, 'none');
121
+ });
122
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Unit tests for User Login (feat_065).
3
+ * Tests login route validation logic and data flow
4
+ * without requiring a real database or HTTP server.
5
+ */
6
+ import { describe, it } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+
9
+ // --- Login validation tests ---
10
+
11
+ describe('Login route validation', () => {
12
+ it('rejects missing email', () => {
13
+ const body = { password: 'TestPassword123' };
14
+ const isValid = body.email && body.password;
15
+ assert.equal(isValid, undefined);
16
+ });
17
+
18
+ it('rejects missing password', () => {
19
+ const body = { email: 'test@example.com' };
20
+ const isValid = body.email && (body as any).password;
21
+ assert.equal(isValid, undefined);
22
+ });
23
+
24
+ it('rejects non-string email', () => {
25
+ const email = 123;
26
+ assert.equal(typeof email === 'string', false);
27
+ });
28
+
29
+ it('rejects non-string password', () => {
30
+ const password = true;
31
+ assert.equal(typeof password === 'string', false);
32
+ });
33
+ });
34
+
35
+ // --- Login credential verification tests ---
36
+
37
+ describe('Login credential verification', () => {
38
+ it('verifyPassword returns true for correct password', async () => {
39
+ const { hash, compare } = await import('bcryptjs');
40
+ const password = 'CorrectPassword123';
41
+ const hashed = await hash(password, 12);
42
+ const result = await compare(password, hashed);
43
+ assert.equal(result, true);
44
+ });
45
+
46
+ it('verifyPassword returns false for wrong password', async () => {
47
+ const { hash, compare } = await import('bcryptjs');
48
+ const hashed = await hash('CorrectPassword123', 12);
49
+ const result = await compare('WrongPassword', hashed);
50
+ assert.equal(result, false);
51
+ });
52
+
53
+ it('email lookup is case-insensitive via toLowerCase', () => {
54
+ const inputEmail = 'Test@EXAMPLE.com';
55
+ const storedEmail = 'test@example.com';
56
+ assert.equal(inputEmail.toLowerCase(), storedEmail);
57
+ });
58
+ });
59
+
60
+ // --- Login response tests ---
61
+
62
+ describe('Login response shape', () => {
63
+ it('returns user object with id, email, and role on success', () => {
64
+ const user = { id: 'uuid-123', email: 'test@example.com', passwordHash: '$2b$12$xxx', role: 'user' };
65
+ const response = {
66
+ user: { id: user.id, email: user.email, role: user.role },
67
+ };
68
+ assert.ok(response.user.id);
69
+ assert.ok(response.user.email);
70
+ assert.equal(response.user.role, 'user');
71
+ assert.equal((response.user as any).passwordHash, undefined);
72
+ });
73
+
74
+ it('returns 401 for non-existent user (same message as wrong password)', () => {
75
+ const userFound = undefined;
76
+ const statusCode = !userFound ? 401 : 200;
77
+ assert.equal(statusCode, 401);
78
+ });
79
+
80
+ it('returns 401 for wrong password (same message as non-existent user)', () => {
81
+ const passwordValid = false;
82
+ const statusCode = !passwordValid ? 401 : 200;
83
+ assert.equal(statusCode, 401);
84
+ });
85
+
86
+ it('uses generic error message to prevent user enumeration', () => {
87
+ const errorMessage = 'Invalid email or password';
88
+ // Same message whether user doesn't exist or password is wrong
89
+ assert.equal(errorMessage, 'Invalid email or password');
90
+ });
91
+ });
92
+
93
+ // --- Token generation on login ---
94
+
95
+ describe('Login token generation', () => {
96
+ it('generates access token with user claims after successful login', async () => {
97
+ const { SignJWT, jwtVerify } = await import('jose');
98
+ const secret = new TextEncoder().encode('test-secret-key-for-unit-tests');
99
+
100
+ const user = { id: 'user-456', email: 'login@example.com', role: 'user' };
101
+ const token = await new SignJWT({ sub: user.id, email: user.email, role: user.role })
102
+ .setProtectedHeader({ alg: 'HS256' })
103
+ .setIssuedAt()
104
+ .setExpirationTime('30m')
105
+ .sign(secret);
106
+
107
+ const { payload } = await jwtVerify(token, secret);
108
+ assert.equal(payload.sub, 'user-456');
109
+ assert.equal(payload.email, 'login@example.com');
110
+ assert.equal(payload.role, 'user');
111
+ });
112
+
113
+ it('generates refresh token with type=refresh claim', async () => {
114
+ const { SignJWT, jwtVerify } = await import('jose');
115
+ const secret = new TextEncoder().encode('test-secret-key-for-unit-tests');
116
+
117
+ const token = await new SignJWT({ sub: 'user-456', type: 'refresh' })
118
+ .setProtectedHeader({ alg: 'HS256' })
119
+ .setIssuedAt()
120
+ .setExpirationTime('7d')
121
+ .sign(secret);
122
+
123
+ const { payload } = await jwtVerify(token, secret);
124
+ assert.equal(payload.sub, 'user-456');
125
+ assert.equal(payload.type, 'refresh');
126
+ });
127
+
128
+ it('stores refresh token hash (not raw token) in DB', async () => {
129
+ const { hash } = await import('bcryptjs');
130
+ const refreshToken = 'eyJhbGciOiJIUzI1NiJ9.fake.token';
131
+ const tokenHash = await hash(refreshToken, 12);
132
+ // Hash should not equal the raw token
133
+ assert.notEqual(tokenHash, refreshToken);
134
+ assert.ok(tokenHash.startsWith('$2'));
135
+ });
136
+ });
137
+
138
+ // --- Login cookie settings ---
139
+
140
+ describe('Login cookie settings', () => {
141
+ it('sets HTTP-only cookies', () => {
142
+ const cookieOptions = { httpOnly: true, secure: true, sameSite: 'strict' as const };
143
+ assert.equal(cookieOptions.httpOnly, true);
144
+ });
145
+
146
+ it('refresh token cookie path is restricted to /api/auth', () => {
147
+ const refreshCookiePath = '/api/auth';
148
+ assert.equal(refreshCookiePath, '/api/auth');
149
+ });
150
+ });