jettypod 4.4.120 → 4.4.121

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 (208) hide show
  1. package/.env +2 -1
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -1,307 +0,0 @@
1
- // Test results database layer
2
- // Ingests cucumber-results.json into SQLite with upsert semantics
3
- // so results survive git operations and file resets.
4
-
5
- import Database from 'better-sqlite3';
6
- import path from 'path';
7
- import fs from 'fs';
8
- import { execSync } from 'child_process';
9
-
10
- // eslint-disable-next-line @typescript-eslint/no-require-imports
11
- const { runMigrations } = require('./run-migrations');
12
-
13
- export interface TestResultRow {
14
- scenario_name: string;
15
- feature_file: string;
16
- status: 'passed' | 'failed' | 'pending';
17
- duration_ms: number;
18
- error_message: string | null;
19
- failed_step: string | null;
20
- run_at: string;
21
- }
22
-
23
- export interface TestRunRow {
24
- id: number;
25
- run_at: string;
26
- total_scenarios: number;
27
- passed: number;
28
- failed: number;
29
- pending: number;
30
- duration_ms: number;
31
- }
32
-
33
- // --- Database connection (reuses existing singleton pattern) ---
34
-
35
- let cachedDb: Database.Database | null = null;
36
- let cachedDbPath: string | null = null;
37
-
38
- function getProjectRoot(): string {
39
- if (process.env.JETTYPOD_PROJECT_PATH) {
40
- return process.env.JETTYPOD_PROJECT_PATH;
41
- }
42
- try {
43
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
44
- } catch {
45
- throw new Error('Not in a git repository and JETTYPOD_PROJECT_PATH not set');
46
- }
47
- }
48
-
49
- function getDb(): Database.Database {
50
- const dbPath = path.join(getProjectRoot(), '.jettypod', 'work.db');
51
-
52
- if (cachedDb && cachedDbPath !== dbPath) {
53
- cachedDb.close();
54
- cachedDb = null;
55
- cachedDbPath = null;
56
- }
57
-
58
- if (!cachedDb) {
59
- cachedDb = new Database(dbPath);
60
- cachedDbPath = dbPath;
61
- cachedDb.pragma('journal_mode = WAL');
62
- cachedDb.pragma('foreign_keys = ON');
63
- try {
64
- runMigrations(cachedDb);
65
- } catch (err) {
66
- console.error('[test-results-db] Failed to run migrations on', dbPath, err);
67
- }
68
- }
69
- return cachedDb;
70
- }
71
-
72
- // --- Ingestion ---
73
-
74
- interface CucumberStep {
75
- keyword?: string;
76
- name?: string;
77
- result?: {
78
- status: string;
79
- duration?: number;
80
- error_message?: string;
81
- };
82
- }
83
-
84
- interface CucumberElement {
85
- type: string;
86
- name: string;
87
- steps?: CucumberStep[];
88
- }
89
-
90
- interface CucumberFeature {
91
- uri: string;
92
- elements?: CucumberElement[];
93
- }
94
-
95
- /**
96
- * Ingest cucumber-results.json into SQLite.
97
- * Uses upsert semantics: each scenario's latest result is kept,
98
- * unrelated scenarios are not affected.
99
- *
100
- * @param jsonPath - Path to cucumber-results.json (defaults to project root)
101
- * @returns Number of scenarios ingested, or 0 if file doesn't exist
102
- */
103
- export function ingestCucumberResults(jsonPath?: string): number {
104
- const projectRoot = getProjectRoot();
105
- const resultsPath = jsonPath || path.join(projectRoot, 'cucumber-results.json');
106
-
107
- if (!fs.existsSync(resultsPath)) {
108
- return 0;
109
- }
110
-
111
- let cucumberJson: CucumberFeature[];
112
- try {
113
- const raw = fs.readFileSync(resultsPath, 'utf-8');
114
- cucumberJson = JSON.parse(raw);
115
- } catch {
116
- return 0;
117
- }
118
-
119
- if (!Array.isArray(cucumberJson) || cucumberJson.length === 0) {
120
- return 0;
121
- }
122
-
123
- return ingestCucumberJson(cucumberJson);
124
- }
125
-
126
- /**
127
- * Ingest parsed cucumber JSON data into SQLite.
128
- * Separated from file reading for testability.
129
- */
130
- export function ingestCucumberJson(cucumberJson: CucumberFeature[]): number {
131
- const db = getDb();
132
-
133
- // Parse scenarios from cucumber JSON
134
- const scenarios: {
135
- feature_file: string;
136
- scenario_name: string;
137
- status: 'passed' | 'failed' | 'pending';
138
- duration_ms: number;
139
- error_message: string | null;
140
- failed_step: string | null;
141
- }[] = [];
142
-
143
- for (const feature of cucumberJson) {
144
- for (const element of feature.elements || []) {
145
- if (element.type !== 'scenario') continue;
146
-
147
- const steps = element.steps || [];
148
- const failed = steps.some(s => s.result?.status === 'failed');
149
- const pending = steps.some(s =>
150
- s.result?.status === 'pending' || s.result?.status === 'undefined'
151
- );
152
- const totalDuration = steps.reduce(
153
- (sum, s) => sum + (s.result?.duration || 0), 0
154
- );
155
-
156
- let errorMessage: string | null = null;
157
- let failedStepName: string | null = null;
158
- if (failed) {
159
- const failedStepObj = steps.find(s => s.result?.status === 'failed');
160
- errorMessage = failedStepObj?.result?.error_message || null;
161
- failedStepName = failedStepObj
162
- ? `${failedStepObj.keyword || ''}${failedStepObj.name || ''}`.trim()
163
- : null;
164
- }
165
-
166
- scenarios.push({
167
- feature_file: feature.uri,
168
- scenario_name: element.name,
169
- status: failed ? 'failed' : pending ? 'pending' : 'passed',
170
- duration_ms: Math.round(totalDuration / 1000000), // nanoseconds → ms
171
- error_message: errorMessage,
172
- failed_step: failedStepName,
173
- });
174
- }
175
- }
176
-
177
- if (scenarios.length === 0) {
178
- return 0;
179
- }
180
-
181
- // Write in a transaction for atomicity
182
- const insertRun = db.prepare(`
183
- INSERT INTO test_runs (run_at, total_scenarios, passed, failed, pending, duration_ms)
184
- VALUES (datetime('now'), ?, ?, ?, ?, ?)
185
- `);
186
-
187
- const upsertScenario = db.prepare(`
188
- INSERT INTO test_scenarios (feature_file, scenario_name)
189
- VALUES (?, ?)
190
- ON CONFLICT(feature_file, scenario_name) DO NOTHING
191
- `);
192
-
193
- const getScenarioId = db.prepare(`
194
- SELECT id FROM test_scenarios WHERE feature_file = ? AND scenario_name = ?
195
- `);
196
-
197
- const insertResult = db.prepare(`
198
- INSERT INTO test_results (test_run_id, scenario_id, status, duration_ms, error_message, failed_step, run_at)
199
- VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
200
- `);
201
-
202
- const transaction = db.transaction(() => {
203
- const passed = scenarios.filter(s => s.status === 'passed').length;
204
- const failed = scenarios.filter(s => s.status === 'failed').length;
205
- const pending = scenarios.filter(s => s.status === 'pending').length;
206
- const totalDuration = scenarios.reduce((sum, s) => sum + s.duration_ms, 0);
207
-
208
- const runResult = insertRun.run(scenarios.length, passed, failed, pending, totalDuration);
209
- const runId = runResult.lastInsertRowid as number;
210
-
211
- for (const scenario of scenarios) {
212
- upsertScenario.run(scenario.feature_file, scenario.scenario_name);
213
- const row = getScenarioId.get(scenario.feature_file, scenario.scenario_name) as { id: number };
214
- insertResult.run(
215
- runId,
216
- row.id,
217
- scenario.status,
218
- scenario.duration_ms,
219
- scenario.error_message,
220
- scenario.failed_step
221
- );
222
- }
223
-
224
- return scenarios.length;
225
- });
226
-
227
- return transaction();
228
- }
229
-
230
- // --- Query functions ---
231
-
232
- /**
233
- * Get the latest result for each scenario.
234
- * Returns one row per scenario with its most recent test result.
235
- */
236
- export function getLatestResults(): TestResultRow[] {
237
- const db = getDb();
238
- return db.prepare(`
239
- SELECT
240
- ts.scenario_name,
241
- ts.feature_file,
242
- tr.status,
243
- tr.duration_ms,
244
- tr.error_message,
245
- tr.failed_step,
246
- tr.run_at
247
- FROM test_results tr
248
- INNER JOIN test_scenarios ts ON tr.scenario_id = ts.id
249
- WHERE tr.id = (
250
- SELECT tr2.id FROM test_results tr2
251
- WHERE tr2.scenario_id = tr.scenario_id
252
- ORDER BY tr2.run_at DESC
253
- LIMIT 1
254
- )
255
- ORDER BY ts.feature_file, ts.scenario_name
256
- `).all() as TestResultRow[];
257
- }
258
-
259
- /**
260
- * Get the latest result for a specific scenario by name.
261
- */
262
- export function getLatestResultForScenario(scenarioName: string): TestResultRow | null {
263
- const db = getDb();
264
- const row = db.prepare(`
265
- SELECT
266
- ts.scenario_name,
267
- ts.feature_file,
268
- tr.status,
269
- tr.duration_ms,
270
- tr.error_message,
271
- tr.failed_step,
272
- tr.run_at
273
- FROM test_results tr
274
- INNER JOIN test_scenarios ts ON tr.scenario_id = ts.id
275
- WHERE ts.scenario_name = ?
276
- ORDER BY tr.run_at DESC
277
- LIMIT 1
278
- `).get(scenarioName) as TestResultRow | undefined;
279
- return row || null;
280
- }
281
-
282
- /**
283
- * Get the most recent test run metadata.
284
- */
285
- export function getLatestTestRun(): TestRunRow | null {
286
- const db = getDb();
287
- const row = db.prepare(`
288
- SELECT id, run_at, total_scenarios, passed, failed, pending, duration_ms
289
- FROM test_runs
290
- ORDER BY run_at DESC
291
- LIMIT 1
292
- `).get() as TestRunRow | undefined;
293
- return row || null;
294
- }
295
-
296
- /**
297
- * Get all test runs (for history display).
298
- */
299
- export function getTestRuns(limit: number = 50): TestRunRow[] {
300
- const db = getDb();
301
- return db.prepare(`
302
- SELECT id, run_at, total_scenarios, passed, failed, pending, duration_ms
303
- FROM test_runs
304
- ORDER BY run_at DESC
305
- LIMIT ?
306
- `).all(limit) as TestRunRow[];
307
- }
@@ -1,282 +0,0 @@
1
- // Test data layer for the test dashboard
2
- // Reads BDD test structure from feature files and results from SQLite
3
-
4
- import path from 'path';
5
- import fs from 'fs';
6
- import { execSync } from 'child_process';
7
- import { getLatestResults, getLatestTestRun } from './test-results-db';
8
-
9
- // Types for test data structure
10
- export interface TestScenario {
11
- id: string;
12
- title: string;
13
- status: 'pass' | 'fail' | 'pending';
14
- duration: string;
15
- lastRun: string | null;
16
- error?: string;
17
- failedStep?: string;
18
- steps: string[];
19
- undefinedSteps: string[];
20
- }
21
-
22
- export interface TestFeature {
23
- id: string;
24
- title: string;
25
- description: string;
26
- featureFile: string;
27
- scenarios: TestScenario[];
28
- }
29
-
30
- export interface TestEpic {
31
- id: string;
32
- title: string;
33
- healthBadge: { passing: number; failing: number };
34
- features: TestFeature[];
35
- }
36
-
37
- export interface TestSummary {
38
- total: number;
39
- passing: number;
40
- failing: number;
41
- pending: number;
42
- lastRun: string | null;
43
- }
44
-
45
- export interface TestDashboardData {
46
- summary: TestSummary;
47
- epics: TestEpic[];
48
- standaloneFeatures: TestFeature[];
49
- }
50
-
51
- function getProjectRoot(): string {
52
- if (process.env.JETTYPOD_PROJECT_PATH) {
53
- return process.env.JETTYPOD_PROJECT_PATH;
54
- }
55
- try {
56
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
57
- } catch {
58
- throw new Error('Not in a git repository and JETTYPOD_PROJECT_PATH not set');
59
- }
60
- }
61
-
62
- // Parse a .feature file to extract scenarios with steps and description
63
- function parseFeatureFile(filePath: string): { title: string; description: string; scenarios: { name: string; steps: string[] }[] } {
64
- const content = fs.readFileSync(filePath, 'utf-8');
65
- const lines = content.split('\n');
66
-
67
- let title = '';
68
- const descriptionLines: string[] = [];
69
- const scenarios: { name: string; steps: string[] }[] = [];
70
- let inDescription = false;
71
- let currentScenario: { name: string; steps: string[] } | null = null;
72
-
73
- for (const line of lines) {
74
- const trimmed = line.trim();
75
- if (trimmed.startsWith('Feature:')) {
76
- title = trimmed.replace('Feature:', '').trim();
77
- inDescription = true;
78
- } else if (trimmed.startsWith('Scenario:')) {
79
- inDescription = false;
80
- if (currentScenario) scenarios.push(currentScenario);
81
- currentScenario = { name: trimmed.replace('Scenario:', '').trim(), steps: [] };
82
- } else if (currentScenario && (trimmed.startsWith('Given ') || trimmed.startsWith('When ') || trimmed.startsWith('Then ') || trimmed.startsWith('And ') || trimmed.startsWith('But '))) {
83
- currentScenario.steps.push(trimmed);
84
- } else if (inDescription && trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('@')) {
85
- descriptionLines.push(trimmed);
86
- }
87
- }
88
- if (currentScenario) scenarios.push(currentScenario);
89
-
90
- return { title, description: descriptionLines.join(' ').trim(), scenarios };
91
- }
92
-
93
- // Try to read Cucumber JSON results if available
94
- function getCucumberResults(projectRoot: string): Map<string, { status: string; duration: number; error?: string; failedStep?: string }> | null {
95
- const resultsPath = path.join(projectRoot, 'cucumber-results.json');
96
- if (!fs.existsSync(resultsPath)) {
97
- return null;
98
- }
99
-
100
- try {
101
- const results = JSON.parse(fs.readFileSync(resultsPath, 'utf-8'));
102
- const scenarioResults = new Map<string, { status: string; duration: number; error?: string; failedStep?: string }>();
103
-
104
- for (const feature of results) {
105
- for (const element of feature.elements || []) {
106
- if (element.type === 'scenario') {
107
- const steps = element.steps || [];
108
- const failed = steps.some((s: { result?: { status: string } }) => s.result?.status === 'failed');
109
- const pending = steps.some((s: { result?: { status: string } }) => s.result?.status === 'pending' || s.result?.status === 'undefined');
110
- const totalDuration = steps.reduce((sum: number, s: { result?: { duration: number } }) => sum + (s.result?.duration || 0), 0);
111
-
112
- let error: string | undefined;
113
- let failedStepName: string | undefined;
114
- if (failed) {
115
- const failedStepObj = steps.find((s: { result?: { status: string; error_message?: string }; keyword?: string; name?: string }) => s.result?.status === 'failed');
116
- error = failedStepObj?.result?.error_message;
117
- failedStepName = failedStepObj ? `${failedStepObj.keyword}${failedStepObj.name}`.trim() : undefined;
118
- }
119
-
120
- scenarioResults.set(element.name, {
121
- status: failed ? 'failed' : pending ? 'pending' : 'passed',
122
- duration: totalDuration / 1000000, // Convert nanoseconds to milliseconds
123
- error,
124
- failedStep: failedStepName,
125
- });
126
- }
127
- }
128
- }
129
-
130
- return scenarioResults;
131
- } catch {
132
- return null;
133
- }
134
- }
135
-
136
- // Get all feature files from the project
137
- function getFeatureFiles(projectRoot: string): string[] {
138
- const featuresDir = path.join(projectRoot, 'features');
139
- if (!fs.existsSync(featuresDir)) {
140
- return [];
141
- }
142
-
143
- const files: string[] = [];
144
- const entries = fs.readdirSync(featuresDir, { withFileTypes: true });
145
-
146
- for (const entry of entries) {
147
- if (entry.isFile() && entry.name.endsWith('.feature')) {
148
- files.push(path.join(featuresDir, entry.name));
149
- }
150
- }
151
-
152
- return files;
153
- }
154
-
155
- // Run cucumber dry-run to detect undefined steps (exported for separate async endpoint)
156
- export function getUndefinedSteps(projectRoot?: string): Record<string, string[]> {
157
- const root = projectRoot || getProjectRoot();
158
- const undefinedMap: Record<string, string[]> = {};
159
-
160
- try {
161
- const result = execSync(
162
- 'NODE_ENV=test npx cucumber-js --dry-run --format json 2>/dev/null',
163
- { cwd: root, encoding: 'utf-8', timeout: 30000 }
164
- );
165
-
166
- const parsed = JSON.parse(result);
167
- for (const feature of parsed) {
168
- for (const element of (feature.elements || [])) {
169
- if (element.type === 'scenario') {
170
- const steps = element.steps || [];
171
- const undefinedSteps: string[] = [];
172
- for (const step of steps) {
173
- if (step.result?.status === 'undefined') {
174
- undefinedSteps.push(`${step.keyword}${step.name}`.trim());
175
- }
176
- }
177
- if (undefinedSteps.length > 0) {
178
- undefinedMap[element.name] = undefinedSteps;
179
- }
180
- }
181
- }
182
- }
183
- } catch (err: unknown) {
184
- const error = err as { killed?: boolean; stderr?: string; message?: string };
185
- if (error.killed) {
186
- console.warn('[getUndefinedSteps] dry-run timed out');
187
- } else if (error.stderr && error.stderr.includes('ambiguous')) {
188
- console.warn('[getUndefinedSteps] ambiguous step definitions detected');
189
- } else {
190
- console.warn('[getUndefinedSteps] dry-run failed:', error.message || 'unknown error');
191
- }
192
- }
193
-
194
- return undefinedMap;
195
- }
196
-
197
- // Main function to get test dashboard data
198
- // Reads scenario structure from feature files, results from SQLite database
199
- export function getTestDashboardData(): TestDashboardData {
200
- const projectRoot = getProjectRoot();
201
- const featureFiles = getFeatureFiles(projectRoot);
202
-
203
- // Build a map of scenario name → latest DB result
204
- let dbResults: Map<string, { status: string; duration_ms: number; error_message: string | null; failed_step: string | null; run_at: string }>;
205
- let lastRun: string | null = null;
206
- try {
207
- const rows = getLatestResults();
208
- dbResults = new Map(rows.map(r => [r.scenario_name, r]));
209
- const latestRun = getLatestTestRun();
210
- lastRun = latestRun?.run_at || null;
211
- } catch {
212
- // DB not available (tables not created yet) — fall back to empty
213
- dbResults = new Map();
214
- }
215
-
216
- const features: TestFeature[] = [];
217
- let totalTests = 0;
218
- let passingTests = 0;
219
- let failingTests = 0;
220
- let pendingTests = 0;
221
-
222
- for (const filePath of featureFiles) {
223
- const { title, description, scenarios: parsedScenarios } = parseFeatureFile(filePath);
224
- const featureId = path.basename(filePath, '.feature');
225
-
226
- const scenarios: TestScenario[] = parsedScenarios.map((scenario, index) => {
227
- const result = dbResults.get(scenario.name);
228
- let status: 'pass' | 'fail' | 'pending' = 'pending';
229
- let duration = '0s';
230
- let error: string | undefined;
231
- let failedStep: string | undefined;
232
-
233
- if (result) {
234
- status = result.status === 'passed' ? 'pass' : result.status === 'failed' ? 'fail' : 'pending';
235
- duration = result.duration_ms < 1000 ? `${Math.round(result.duration_ms)}ms` : `${(result.duration_ms / 1000).toFixed(1)}s`;
236
- error = result.error_message || undefined;
237
- failedStep = result.failed_step || undefined;
238
- }
239
-
240
- totalTests++;
241
- if (status === 'pass') passingTests++;
242
- else if (status === 'fail') failingTests++;
243
- else pendingTests++;
244
-
245
- const undefinedSteps: string[] = [];
246
-
247
- return {
248
- id: `${featureId}-${index}`,
249
- title: scenario.name,
250
- status,
251
- duration,
252
- lastRun: result?.run_at || null,
253
- error,
254
- failedStep,
255
- steps: scenario.steps,
256
- undefinedSteps,
257
- };
258
- });
259
-
260
- features.push({
261
- id: featureId,
262
- title,
263
- description,
264
- featureFile: path.relative(projectRoot, filePath),
265
- scenarios,
266
- });
267
- }
268
-
269
- const summary: TestSummary = {
270
- total: totalTests,
271
- passing: passingTests,
272
- failing: failingTests,
273
- pending: pendingTests,
274
- lastRun,
275
- };
276
-
277
- return {
278
- summary,
279
- epics: [],
280
- standaloneFeatures: features,
281
- };
282
- }
@@ -1,66 +0,0 @@
1
- const path = require('path');
2
-
3
- /** @type {import('next').NextConfig} */
4
- const nextConfig = {
5
- // Externalize modules with native bindings or dynamic requires
6
- // - better-sqlite3: Used by dashboard's lib/db.ts (sync)
7
- // - sqlite3: Used by lib/database.js (async) via worktree-facade
8
- serverExternalPackages: ['better-sqlite3', 'sqlite3'],
9
-
10
- // Set monorepo root for correct file tracing
11
- outputFileTracingRoot: path.resolve(process.cwd(), '../..'),
12
-
13
- // Note: Turbopack disabled - it can't handle dynamic requires in lib/migrations/index.js
14
- // The webpack externals below properly externalize the jettypod lib chain
15
-
16
- webpack: (config, { isServer }) => {
17
- // Split @dnd-kit into a separate chunk so it doesn't block initial page parse.
18
- // The chunk loads in parallel and is only needed when KanbanBoard renders.
19
- if (!isServer) {
20
- config.optimization = config.optimization || {};
21
- config.optimization.splitChunks = config.optimization.splitChunks || {};
22
- config.optimization.splitChunks.cacheGroups = {
23
- ...config.optimization.splitChunks.cacheGroups,
24
- dndkit: {
25
- test: /[\\/]node_modules[\\/]@dnd-kit[\\/]/,
26
- name: 'dnd-kit',
27
- chunks: 'all',
28
- priority: 30,
29
- },
30
- };
31
- }
32
-
33
- if (isServer) {
34
- config.externals = config.externals || [];
35
- config.externals.push(({ request }, callback) => {
36
- // Externalize worktree-facade with RUNTIME path resolution.
37
- // Uses 'var' external type so the path expression evaluates at runtime,
38
- // not at build time (which would bake in the build machine's absolute path).
39
- // Packaged app: JETTYPOD_RESOURCES_PATH/bin/lib/<module>
40
- // Dev mode: process.cwd()/../../lib/<module>
41
- if (request && request.includes('lib/worktree-facade')) {
42
- const moduleName = request.split('lib/')[1];
43
- return callback(null,
44
- `var require(process.env.JETTYPOD_RESOURCES_PATH ` +
45
- `? require('path').join(process.env.JETTYPOD_RESOURCES_PATH, 'bin', 'lib', '${moduleName}') ` +
46
- `: require('path').resolve(process.cwd(), '../../lib', '${moduleName}'))`
47
- );
48
- }
49
- // Externalize run-migrations.js with RUNTIME path resolution.
50
- // It uses dynamic require() to load migration files, which webpack
51
- // replaces with a dead stub if bundled.
52
- // In both packaged and dev: process.cwd() is the dashboard dir,
53
- // and run-migrations.js is at lib/run-migrations.js relative to it.
54
- if (request && request.includes('run-migrations')) {
55
- return callback(null,
56
- `var require(require('path').join(process.cwd(), 'lib', 'run-migrations'))`
57
- );
58
- }
59
- callback(undefined);
60
- });
61
- }
62
- return config;
63
- },
64
- };
65
-
66
- module.exports = nextConfig;
@@ -1,7 +0,0 @@
1
- const config = {
2
- plugins: {
3
- "@tailwindcss/postcss": {},
4
- },
5
- };
6
-
7
- export default config;