@ai-qa/workflow 2.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 (48) hide show
  1. package/.github/agents/playwright-test-generator.agent.md +33 -0
  2. package/.github/agents/playwright-test-healer.agent.md +36 -0
  3. package/.github/agents/playwright-test-planner.agent.md +44 -0
  4. package/.opencode/agents/qa-generator.md +19 -0
  5. package/.opencode/agents/qa-healer.md +25 -0
  6. package/.opencode/agents/qa-planner.md +20 -0
  7. package/.qa-workflow.json +22 -0
  8. package/README.md +365 -0
  9. package/ai-qa-workflow.js +330 -0
  10. package/cli.js +7 -0
  11. package/docs/application-context.md +20 -0
  12. package/install.js +303 -0
  13. package/opencode.json +31 -0
  14. package/package.json +30 -0
  15. package/prompts/QAe2eprompt.md +513 -0
  16. package/prompts/general_prompt.md +13 -0
  17. package/qa-dashboard/.env +3 -0
  18. package/qa-dashboard/app.js +46 -0
  19. package/qa-dashboard/package.json +18 -0
  20. package/qa-dashboard/public/css/style.css +266 -0
  21. package/qa-dashboard/public/js/main.js +6 -0
  22. package/qa-dashboard/routes/analytics.js +52 -0
  23. package/qa-dashboard/routes/export.js +153 -0
  24. package/qa-dashboard/routes/index.js +10 -0
  25. package/qa-dashboard/routes/projects.js +92 -0
  26. package/qa-dashboard/routes/runs.js +66 -0
  27. package/qa-dashboard/routes/stories.js +101 -0
  28. package/qa-dashboard/routes/test-data.js +82 -0
  29. package/qa-dashboard/services/cli-bridge.js +143 -0
  30. package/qa-dashboard/services/project-manager.js +61 -0
  31. package/qa-dashboard/views/analytics.ejs +188 -0
  32. package/qa-dashboard/views/error.ejs +8 -0
  33. package/qa-dashboard/views/index.ejs +83 -0
  34. package/qa-dashboard/views/layouts/main.ejs +28 -0
  35. package/qa-dashboard/views/project-add.ejs +23 -0
  36. package/qa-dashboard/views/project.ejs +97 -0
  37. package/qa-dashboard/views/projects.ejs +29 -0
  38. package/qa-dashboard/views/run.ejs +84 -0
  39. package/qa-dashboard/views/runs.ejs +64 -0
  40. package/qa-dashboard/views/stories.ejs +53 -0
  41. package/qa-dashboard/views/story.ejs +63 -0
  42. package/qa-dashboard/views/test-data.ejs +117 -0
  43. package/scripts/executor.js +142 -0
  44. package/scripts/generator.js +130 -0
  45. package/scripts/healer.js +207 -0
  46. package/scripts/planner.js +142 -0
  47. package/scripts/reporter.js +190 -0
  48. package/scripts/utils.js +244 -0
@@ -0,0 +1,266 @@
1
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ :root {
3
+ --bg: #0f1117;
4
+ --surface: #1a1d27;
5
+ --surface2: #232734;
6
+ --border: #2d3140;
7
+ --text: #e4e6f0;
8
+ --text-muted: #8b8fa3;
9
+ --primary: #6366f1;
10
+ --primary-hover: #818cf8;
11
+ --success: #22c55e;
12
+ --danger: #ef4444;
13
+ --warning: #f59e0b;
14
+ --radius: 8px;
15
+ }
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ background: var(--bg);
19
+ color: var(--text);
20
+ line-height: 1.6;
21
+ }
22
+ .container { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
23
+
24
+ /* Navbar */
25
+ .navbar {
26
+ background: var(--surface);
27
+ border-bottom: 1px solid var(--border);
28
+ padding: 0 1.5rem;
29
+ position: sticky; top: 0; z-index: 100;
30
+ }
31
+ .nav-inner {
32
+ max-width: 1200px; margin: 0 auto;
33
+ display: flex; align-items: center; justify-content: space-between;
34
+ height: 56px;
35
+ }
36
+ .nav-brand {
37
+ font-size: 1.2rem; font-weight: 700;
38
+ color: var(--primary); text-decoration: none;
39
+ }
40
+ .nav-links { display: flex; gap: 1.5rem; }
41
+ .nav-links a {
42
+ color: var(--text-muted); text-decoration: none;
43
+ font-size: 0.9rem; transition: color 0.2s;
44
+ }
45
+ .nav-links a:hover { color: var(--text); }
46
+
47
+ /* Hero */
48
+ .hero {
49
+ text-align: center; padding: 3rem 0 2rem;
50
+ }
51
+ .hero h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem; }
52
+ .hero p { color: var(--text-muted); font-size: 1.1rem; }
53
+
54
+ /* Stats */
55
+ .stats-row { display: flex; gap: 1rem; justify-content: center; margin: 2rem 0; }
56
+ .stat-card {
57
+ background: var(--surface); border: 1px solid var(--border);
58
+ border-radius: var(--radius); padding: 1.5rem 2rem; text-align: center;
59
+ min-width: 150px;
60
+ }
61
+ .stat-number { font-size: 2rem; font-weight: 700; color: var(--primary); }
62
+ .stat-label { color: var(--text-muted); font-size: 0.85rem; margin-top: 0.25rem; }
63
+
64
+ /* Cards */
65
+ .card {
66
+ background: var(--surface); border: 1px solid var(--border);
67
+ border-radius: var(--radius); padding: 1.5rem;
68
+ margin-bottom: 1rem;
69
+ }
70
+ .card h3 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--primary); }
71
+
72
+ /* Pipeline Flow */
73
+ .pipeline-flow { margin: 2rem 0; }
74
+ .pipeline-flow h2 { margin-bottom: 1rem; }
75
+ .flow-steps {
76
+ display: flex; align-items: center; gap: 0.75rem;
77
+ flex-wrap: wrap; justify-content: center;
78
+ }
79
+ .flow-step {
80
+ background: var(--surface2); border: 1px solid var(--border);
81
+ border-radius: var(--radius); padding: 1rem 1.25rem;
82
+ text-align: center; min-width: 110px;
83
+ }
84
+ .step-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
85
+ .step-name { font-weight: 600; font-size: 0.85rem; }
86
+ .step-desc { color: var(--text-muted); font-size: 0.75rem; }
87
+ .flow-arrow { color: var(--text-muted); font-size: 1.25rem; }
88
+
89
+ /* Grid */
90
+ .project-grid, .row { display: flex; gap: 1rem; flex-wrap: wrap; }
91
+ .col { flex: 1; min-width: 200px; }
92
+ .col-30 { flex: 0 0 30%; }
93
+ .col-40 { flex: 0 0 38%; }
94
+ .col-60 { flex: 0 0 58%; }
95
+ .col-70 { flex: 0 0 68%; }
96
+
97
+ /* Project Card */
98
+ .project-card {
99
+ background: var(--surface); border: 1px solid var(--border);
100
+ border-radius: var(--radius); padding: 1.25rem;
101
+ text-decoration: none; color: var(--text);
102
+ display: block; transition: border-color 0.2s;
103
+ min-width: 250px; flex: 1;
104
+ }
105
+ .project-card:hover { border-color: var(--primary); }
106
+ .project-name { font-weight: 600; font-size: 1.1rem; }
107
+ .project-path { color: var(--text-muted); font-size: 0.8rem; word-break: break-all; }
108
+ .project-meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.5rem; }
109
+ .project-actions { margin-top: 0.75rem; display: flex; gap: 0.5rem; }
110
+
111
+ /* Page Header */
112
+ .page-header {
113
+ display: flex; align-items: center; justify-content: space-between;
114
+ margin-bottom: 1.5rem; flex-wrap: wrap; gap: 0.75rem;
115
+ }
116
+ .page-header h1 { font-size: 1.5rem; }
117
+ .header-actions { display: flex; gap: 0.5rem; }
118
+
119
+ /* Buttons */
120
+ .btn {
121
+ display: inline-block; padding: 0.5rem 1rem;
122
+ border-radius: var(--radius); border: 1px solid var(--border);
123
+ background: var(--surface2); color: var(--text);
124
+ font-size: 0.85rem; cursor: pointer; text-decoration: none;
125
+ transition: all 0.2s;
126
+ }
127
+ .btn:hover { border-color: var(--primary); color: var(--primary); }
128
+ .btn-primary { background: var(--primary); border-color: var(--primary); color: #fff; }
129
+ .btn-primary:hover { background: var(--primary-hover); color: #fff; }
130
+ .btn-sm { padding: 0.35rem 0.75rem; font-size: 0.8rem; }
131
+ .btn-danger { border-color: var(--danger); color: var(--danger); }
132
+ .btn-danger:hover { background: var(--danger); color: #fff; }
133
+
134
+ /* Forms */
135
+ .form { max-width: 500px; }
136
+ .form-group { margin-bottom: 1rem; }
137
+ .form-group label { display: block; margin-bottom: 0.35rem; font-size: 0.9rem; }
138
+ .form-group small { color: var(--text-muted); font-size: 0.8rem; }
139
+ .form-input, .form input[type="text"], .form select {
140
+ width: 100%; padding: 0.6rem 0.75rem;
141
+ background: var(--surface2); border: 1px solid var(--border);
142
+ border-radius: var(--radius); color: var(--text); font-size: 0.9rem;
143
+ }
144
+
145
+ /* Badges */
146
+ .badge {
147
+ display: inline-block; padding: 0.2rem 0.6rem;
148
+ border-radius: 100px; font-size: 0.75rem; font-weight: 600;
149
+ }
150
+ .badge-project { background: var(--surface2); color: var(--text-muted); border: 1px solid var(--border); }
151
+ .badge-plan { background: #1e3a5f; color: #60a5fa; }
152
+ .badge-spec { background: #14532d; color: #4ade80; }
153
+ .badge-draft { background: #422006; color: #fbbf24; }
154
+ .badge-pass { background: #14532d; color: #4ade80; }
155
+ .badge-fail { background: #450a0a; color: #f87171; }
156
+
157
+ /* Stories */
158
+ .story-list { display: flex; flex-direction: column; gap: 1rem; }
159
+ .story-card {
160
+ background: var(--surface); border: 1px solid var(--border);
161
+ border-radius: var(--radius); padding: 1.25rem;
162
+ }
163
+ .story-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
164
+ .story-title { font-weight: 600; color: var(--primary); text-decoration: none; font-size: 1.05rem; }
165
+ .story-title:hover { text-decoration: underline; }
166
+ .story-badges { display: flex; gap: 0.35rem; }
167
+ .story-preview { color: var(--text-muted); font-size: 0.85rem; }
168
+ .story-preview p { margin: 0.15rem 0; }
169
+ .story-actions { margin-top: 0.75rem; }
170
+ .story-action-output { margin-left: 0.5rem; font-size: 0.85rem; }
171
+
172
+ /* Runs */
173
+ .run-list { display: flex; flex-direction: column; gap: 0.75rem; }
174
+ .run-card {
175
+ display: flex; align-items: center; gap: 1rem;
176
+ background: var(--surface); border: 1px solid var(--border);
177
+ border-radius: var(--radius); padding: 1rem 1.25rem;
178
+ text-decoration: none; color: var(--text); transition: border-color 0.2s;
179
+ }
180
+ .run-card:hover { border-color: var(--primary); }
181
+ .run-passed { border-left: 3px solid var(--success); }
182
+ .run-failed { border-left: 3px solid var(--danger); }
183
+ .run-icon { font-size: 1.25rem; }
184
+ .run-name { font-weight: 600; font-size: 0.9rem; }
185
+ .run-test { color: var(--text-muted); font-size: 0.8rem; }
186
+ .run-meta { color: var(--text-muted); font-size: 0.75rem; margin-top: 0.2rem; }
187
+
188
+ /* Output */
189
+ .output-box {
190
+ margin-top: 0.75rem; padding: 0.75rem;
191
+ border-radius: var(--radius); font-size: 0.8rem;
192
+ max-height: 300px; overflow-y: auto; white-space: pre-wrap;
193
+ font-family: 'Consolas', 'Courier New', monospace;
194
+ }
195
+ .output-running { background: #1e1b4b; color: #a5b4fc; border: 1px solid #3730a3; }
196
+ .output-success { background: #052e16; color: #86efac; border: 1px solid #166534; }
197
+ .output-error { background: #450a0a; color: #fca5a5; border: 1px solid #991b1b; }
198
+
199
+ /* Action buttons */
200
+ .action-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
201
+ .action-buttons-vertical { display: flex; flex-direction: column; gap: 0.5rem; }
202
+
203
+ /* Info table */
204
+ .info-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
205
+ .info-table td { padding: 0.5rem 0.25rem; border-bottom: 1px solid var(--border); }
206
+ .info-table td:first-child { color: var(--text-muted); width: 40%; }
207
+
208
+ /* Code */
209
+ .code-block {
210
+ background: #0d0f16; border: 1px solid var(--border);
211
+ border-radius: var(--radius); padding: 1rem;
212
+ font-size: 0.78rem; overflow-x: auto; max-height: 400px;
213
+ font-family: 'Consolas', 'Courier New', monospace;
214
+ line-height: 1.5;
215
+ }
216
+
217
+ /* Markdown */
218
+ .markdown-content { font-size: 0.85rem; line-height: 1.6; }
219
+ .markdown-content br { margin-bottom: 0.25rem; }
220
+
221
+ /* Heal */
222
+ .heal-attempt {
223
+ background: var(--surface2); border: 1px solid var(--border);
224
+ border-radius: var(--radius); padding: 0.5rem 0.75rem;
225
+ margin: 0.35rem 0; font-size: 0.8rem;
226
+ }
227
+
228
+ /* Fail list */
229
+ .fail-list { list-style: none; }
230
+ .fail-list li {
231
+ background: #450a0a; border: 1px solid #991b1b;
232
+ border-radius: var(--radius); padding: 0.75rem;
233
+ margin-bottom: 0.5rem; font-size: 0.85rem;
234
+ }
235
+
236
+ /* Big number */
237
+ .big-number { font-size: 2.5rem; font-weight: 700; color: var(--primary); margin-bottom: 0.5rem; }
238
+
239
+ /* Empty state */
240
+ .empty-state { text-align: center; padding: 4rem 1rem; }
241
+ .empty-state h3 { margin-bottom: 0.5rem; }
242
+ .empty-state p { color: var(--text-muted); margin-bottom: 1rem; }
243
+
244
+ /* Alert */
245
+ .alert { padding: 0.75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: 0.85rem; }
246
+ .alert-error { background: #450a0a; border: 1px solid #991b1b; color: #fca5a5; }
247
+
248
+ /* Text colors */
249
+ .text-success { color: var(--success); }
250
+ .text-warning { color: var(--warning); }
251
+ .text-danger { color: var(--danger); }
252
+
253
+ /* Column helpers */
254
+ .col-50 { flex: 0 0 48%; }
255
+ .col-60 { flex: 0 0 58%; }
256
+ .col-40 { flex: 0 0 38%; }
257
+ @media (max-width: 768px) {
258
+ .col-50, .col-60, .col-40 { flex: 0 0 100%; }
259
+ }
260
+
261
+ /* Table header styling */
262
+ .info-table th { color: var(--text-muted); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; padding: 0.5rem 0.25rem; border-bottom: 2px solid var(--border); text-align: left; }
263
+
264
+ /* Refresh spinner */
265
+ .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.6s linear infinite; }
266
+ @keyframes spin { to { transform: rotate(360deg); } }
@@ -0,0 +1,6 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ document.querySelectorAll('.auto-refresh').forEach(el => {
3
+ const interval = el.dataset.interval || 10000;
4
+ setInterval(() => { location.reload(); }, interval);
5
+ });
6
+ });
@@ -0,0 +1,52 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ router.get('/', (req, res) => {
8
+ const projects = pm.getAll();
9
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
10
+ const project = projects.find(p => p.id === projectId);
11
+
12
+ if (!project || !pm.getBridge(projectId)) {
13
+ return res.render('analytics', { project: null, projects, runs: [], stats: { totalRuns: 0, passedRuns: 0, totalFailures: 0, totalHealed: 0, totalDefects: 0, avgDuration: 0, passRate: 0, healRate: 0 } });
14
+ }
15
+
16
+ const bridge = pm.getBridge(project.id);
17
+ const runDirs = bridge.getRunDirs();
18
+
19
+ const runs = runDirs.map(d => {
20
+ const result = bridge.getRunResult(d.name);
21
+ const healing = bridge.getHealingReport(d.name);
22
+ return {
23
+ id: d.name,
24
+ timestamp: d.name.replace('run-', '').replace(/-/g, ':').slice(0, 19),
25
+ success: result ? result.success : null,
26
+ duration: result ? result.duration : null,
27
+ total: result && result.failedTests ? result.failedTests.length + (result.passedTests ? result.passedTests.length : 0) : 0,
28
+ failures: result && result.failedTests ? result.failedTests.length : 0,
29
+ healed: healing ? healing.totalHealed || 0 : 0,
30
+ defects: healing && healing.defects ? healing.defects.length : 0,
31
+ testName: result ? result.test : 'N/A',
32
+ };
33
+ });
34
+
35
+ const totalRuns = runs.length;
36
+ const passedRuns = runs.filter(r => r.success).length;
37
+ const totalFailures = runs.reduce((s, r) => s + r.failures, 0);
38
+ const totalHealed = runs.reduce((s, r) => s + r.healed, 0);
39
+ const totalDefects = runs.reduce((s, r) => s + r.defects, 0);
40
+ const avgDuration = runs.length > 0 ? (runs.reduce((s, r) => s + (r.duration || 0), 0) / runs.length) : 0;
41
+ const passRate = totalRuns > 0 ? ((passedRuns / totalRuns) * 100).toFixed(1) : 0;
42
+ const healRate = totalFailures > 0 ? ((totalHealed / totalFailures) * 100).toFixed(1) : 0;
43
+
44
+ res.render('analytics', {
45
+ project,
46
+ projects,
47
+ runs: runs.reverse(),
48
+ stats: { totalRuns, passedRuns, totalFailures, totalHealed, totalDefects, avgDuration, passRate, healRate },
49
+ });
50
+ });
51
+
52
+ module.exports = router;
@@ -0,0 +1,153 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ function resolveProject(req) {
8
+ const projects = pm.getAll();
9
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
10
+ return projects.find(p => p.id === projectId);
11
+ }
12
+
13
+ function resolveRunId(runId, bridge) {
14
+ if (runId === 'latest') {
15
+ const dirs = bridge.getRunDirs();
16
+ return dirs.length > 0 ? dirs[0].name : null;
17
+ }
18
+ return runId;
19
+ }
20
+
21
+ router.get('/report/:runId', (req, res) => {
22
+ const project = resolveProject(req);
23
+ if (!project) return res.status(404).send('Project not found');
24
+
25
+ const bridge = pm.getBridge(project.id);
26
+ const runId = resolveRunId(req.params.runId, bridge);
27
+ if (!runId) return res.status(404).send('No runs found');
28
+
29
+ const report = bridge.getFinalReport(runId);
30
+ const result = bridge.getRunResult(runId);
31
+ const healing = bridge.getHealingReport(runId);
32
+
33
+ if (!result) return res.status(404).send('Run not found');
34
+
35
+ const html = `<!DOCTYPE html>
36
+ <html><head><meta charset="UTF-8"><title>QA Report - ${runId}</title>
37
+ <style>
38
+ body { font-family: Arial; max-width: 800px; margin: 2rem auto; padding: 1rem; color: #333; }
39
+ h1 { color: #6366f1; border-bottom: 2px solid #6366f1; padding-bottom: 0.5rem; }
40
+ .badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: bold; }
41
+ .pass { background: #dcfce7; color: #166534; }
42
+ .fail { background: #fecaca; color: #991b1b; }
43
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
44
+ td, th { border: 1px solid #ddd; padding: 8px; text-align: left; }
45
+ th { background: #f4f4f5; }
46
+ .section { margin: 2rem 0; }
47
+ .stats { display: flex; gap: 1rem; flex-wrap: wrap; }
48
+ .stat-card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; min-width: 120px; text-align: center; }
49
+ .stat-num { font-size: 24px; font-weight: bold; color: #6366f1; }
50
+ .stat-label { font-size: 12px; color: #666; }
51
+ .footer { margin-top: 2rem; font-size: 12px; color: #999; border-top: 1px solid #eee; padding-top: 1rem; }
52
+ </style></head><body>
53
+ <h1>AI QA Execution Report</h1>
54
+ <p>Run: ${runId} | ${new Date().toLocaleDateString()}</p>
55
+ <span class="badge ${result.success ? 'pass' : 'fail'}">${result.success ? 'PASSED' : 'FAILED'}</span>
56
+
57
+ <div class="section">
58
+ <h2>Summary</h2>
59
+ <div class="stats">
60
+ <div class="stat-card"><div class="stat-num">${result.failedTests ? result.failedTests.length : 0}</div><div class="stat-label">Failures</div></div>
61
+ <div class="stat-card"><div class="stat-num">${healing ? healing.totalHealed || 0 : 0}</div><div class="stat-label">Healed</div></div>
62
+ <div class="stat-card"><div class="stat-num">${healing && healing.defects ? healing.defects.length : 0}</div><div class="stat-label">Defects</div></div>
63
+ <div class="stat-card"><div class="stat-num">${result.duration ? (result.duration / 1000).toFixed(1) + 's' : 'N/A'}</div><div class="stat-label">Duration</div></div>
64
+ </div>
65
+ </div>
66
+
67
+ ${healing && healing.defects && healing.defects.length > 0 ? `
68
+ <div class="section">
69
+ <h2>Defects</h2>
70
+ <table><tr><th>Test</th><th>Type</th><th>Verdict</th></tr>
71
+ ${healing.defects.map(d => `<tr><td>${d.test}</td><td>${d.classification}</td><td>${d.verdict}</td></tr>`).join('')}
72
+ </table>
73
+ </div>` : ''}
74
+
75
+ <div class="section">
76
+ <h2>Full Report</h2>
77
+ <pre style="background:#f4f4f5;padding:1rem;border-radius:8px;font-size:12px;max-height:400px;overflow:auto;">${(report || result.output || 'No detailed report').substring(0, 5000)}</pre>
78
+ </div>
79
+
80
+ <div class="footer">
81
+ Generated by AI QA Pipeline | ${new Date().toISOString()}
82
+ </div>
83
+ </body></html>`;
84
+
85
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
86
+ res.setHeader('Content-Disposition', 'inline');
87
+ res.send(html);
88
+ });
89
+
90
+ router.get('/pdf/:runId', (req, res) => {
91
+ const project = resolveProject(req);
92
+ if (!project) return res.status(404).send('Project not found');
93
+
94
+ const bridge = pm.getBridge(project.id);
95
+ const runId = resolveRunId(req.params.runId, bridge);
96
+ if (!runId) return res.status(404).send('No runs found');
97
+
98
+ const result = bridge.getRunResult(runId);
99
+ if (!result) return res.status(404).send('Run not found');
100
+
101
+ const report = bridge.getFinalReport(runId);
102
+ const healing = bridge.getHealingReport(runId);
103
+
104
+ const lines = [];
105
+ lines.push('='.repeat(60));
106
+ lines.push('AI QA EXECUTION REPORT');
107
+ lines.push('='.repeat(60));
108
+ lines.push('');
109
+ lines.push(`Run ID: ${runId}`);
110
+ lines.push(`Date: ${new Date().toLocaleDateString()}`);
111
+ lines.push(`Status: ${result.success ? 'PASSED' : 'FAILED'}`);
112
+ lines.push(`Duration: ${result.duration ? (result.duration / 1000).toFixed(1) + 's' : 'N/A'}`);
113
+ lines.push('');
114
+ lines.push('-'.repeat(60));
115
+ lines.push('RESULTS');
116
+ lines.push('-'.repeat(60));
117
+ lines.push(`Failures: ${result.failedTests ? result.failedTests.length : 0}`);
118
+ lines.push(`Healed: ${healing ? healing.totalHealed || 0 : 0}`);
119
+ lines.push(`Defects: ${healing && healing.defects ? healing.defects.length : 0}`);
120
+ lines.push('');
121
+
122
+ if (healing && healing.defects && healing.defects.length > 0) {
123
+ lines.push('-'.repeat(60));
124
+ lines.push('DEFECTS');
125
+ lines.push('-'.repeat(60));
126
+ healing.defects.forEach(d => {
127
+ lines.push(` [${d.verdict}] ${d.test}`);
128
+ lines.push(` Type: ${d.classification}`);
129
+ lines.push(` ${d.error ? d.error.substring(0, 150) : ''}`);
130
+ if (d.screenshot) lines.push(` Screenshot: ${d.screenshot}`);
131
+ lines.push('');
132
+ });
133
+ }
134
+
135
+ if (report) {
136
+ lines.push('-'.repeat(60));
137
+ lines.push('FULL REPORT');
138
+ lines.push('-'.repeat(60));
139
+ lines.push(report.substring(0, 3000));
140
+ }
141
+
142
+ lines.push('');
143
+ lines.push('='.repeat(60));
144
+ lines.push(`Generated: ${new Date().toISOString()}`);
145
+ lines.push('='.repeat(60));
146
+
147
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
148
+ res.setHeader('Content-Disposition', `attachment; filename="qa-report-${runId}.txt"`);
149
+ res.setHeader('Content-Description', 'AI QA Test Report');
150
+ res.send(lines.join('\n'));
151
+ });
152
+
153
+ module.exports = router;
@@ -0,0 +1,10 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+
5
+ router.get('/', (req, res) => {
6
+ const projects = pm.getAll();
7
+ res.render('index', { projects });
8
+ });
9
+
10
+ module.exports = router;
@@ -0,0 +1,92 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+ const fs = require('fs');
5
+
6
+ router.get('/', (req, res) => {
7
+ const projects = pm.getAll();
8
+ res.render('projects', { projects });
9
+ });
10
+
11
+ router.get('/add', (req, res) => {
12
+ res.render('project-add');
13
+ });
14
+
15
+ router.post('/add', (req, res) => {
16
+ const { name, path } = req.body;
17
+ if (!name || !path || !fs.existsSync(path)) {
18
+ return res.render('project-add', { error: 'Invalid project path or name' });
19
+ }
20
+ const project = pm.add(name, path);
21
+ res.redirect(`/projects/${project.id}`);
22
+ });
23
+
24
+ router.get('/:id', (req, res) => {
25
+ const project = pm.get(req.params.id);
26
+ if (!project) return res.status(404).render('error', { message: 'Project not found' });
27
+
28
+ const bridge = pm.getBridge(req.params.id);
29
+ const statusResult = bridge.status();
30
+ const list = bridge.listAll();
31
+
32
+ res.render('project', { project, status: statusResult, list });
33
+ });
34
+
35
+ router.post('/:id/remove', (req, res) => {
36
+ pm.remove(req.params.id);
37
+ res.redirect('/projects');
38
+ });
39
+
40
+ router.post('/:id/run-pipeline', (req, res) => {
41
+ const project = pm.get(req.params.id);
42
+ if (!project) return res.status(404).json({ error: 'Project not found' });
43
+
44
+ const bridge = pm.getBridge(req.params.id);
45
+ const { storyName } = req.body;
46
+
47
+ let result;
48
+ if (storyName) {
49
+ result = bridge.runFull(storyName);
50
+ } else {
51
+ result = bridge.status();
52
+ }
53
+
54
+ res.json({ success: result.success, output: result.output, error: result.error });
55
+ });
56
+
57
+ router.post('/:id/execute', (req, res) => {
58
+ const project = pm.get(req.params.id);
59
+ if (!project) return res.status(404).json({ error: 'Project not found' });
60
+
61
+ const bridge = pm.getBridge(req.params.id);
62
+ const { testName } = req.body;
63
+ const result = bridge.execute(testName);
64
+ res.json({ success: result.success, output: result.output, error: result.error, runId: extractRunId(result.output) });
65
+ });
66
+
67
+ router.post('/:id/heal', (req, res) => {
68
+ const project = pm.get(req.params.id);
69
+ if (!project) return res.status(404).json({ error: 'Project not found' });
70
+
71
+ const bridge = pm.getBridge(req.params.id);
72
+ const { runId } = req.body;
73
+ const result = bridge.heal(runId);
74
+ res.json({ success: result.success, output: result.output, error: result.error });
75
+ });
76
+
77
+ router.post('/:id/report', (req, res) => {
78
+ const project = pm.get(req.params.id);
79
+ if (!project) return res.status(404).json({ error: 'Project not found' });
80
+
81
+ const bridge = pm.getBridge(req.params.id);
82
+ const { runId } = req.body;
83
+ const result = bridge.report(runId);
84
+ res.json({ success: result.success, output: result.output, error: result.error });
85
+ });
86
+
87
+ function extractRunId(output) {
88
+ const match = output.match(/run-[\d-T-]+/);
89
+ return match ? match[0] : null;
90
+ }
91
+
92
+ module.exports = router;
@@ -0,0 +1,66 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+
5
+ router.get('/', (req, res) => {
6
+ const projectId = req.query.project;
7
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
8
+ if (!project) return res.redirect('/projects');
9
+
10
+ const bridge = pm.getBridge(project.id);
11
+ const runDirs = bridge.getRunDirs();
12
+
13
+ const hasAllure = bridge.hasAllureReport();
14
+ const runs = runDirs.map(d => {
15
+ const result = bridge.getRunResult(d.name);
16
+ const healing = bridge.getHealingReport(d.name);
17
+ return {
18
+ id: d.name,
19
+ timestamp: d.name.replace('run-', '').replace(/-/g, ':').slice(0, 19),
20
+ success: result ? result.success : null,
21
+ duration: result ? result.duration : null,
22
+ failedCount: result && result.failedTests ? result.failedTests.length : 0,
23
+ passedCount: result && result.passedTests ? result.passedTests.length : 0,
24
+ healedCount: healing ? healing.totalHealed || 0 : 0,
25
+ hasReport: !!bridge.getFinalReport(d.name),
26
+ hasAllure,
27
+ testName: result ? result.test : 'Unknown',
28
+ };
29
+ });
30
+
31
+ res.render('runs', { project, runs });
32
+ });
33
+
34
+ router.get('/:runId', (req, res) => {
35
+ const projectId = req.query.project;
36
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
37
+ if (!project) return res.redirect('/projects');
38
+
39
+ const bridge = pm.getBridge(project.id);
40
+ const result = bridge.getRunResult(req.params.runId);
41
+ if (!result) return res.status(404).render('error', { message: 'Run not found' });
42
+
43
+ const healing = bridge.getHealingReport(req.params.runId);
44
+ const report = bridge.getFinalReport(req.params.runId);
45
+ const hasAllure = bridge.hasAllureReport();
46
+
47
+ res.render('run', { project, run: { id: req.params.runId, result, healing, report, hasAllure } });
48
+ });
49
+
50
+ router.get('/:runId/compare/:otherRunId', (req, res) => {
51
+ const projectId = req.query.project;
52
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
53
+ if (!project) return res.redirect('/projects');
54
+
55
+ const bridge = pm.getBridge(project.id);
56
+ const result1 = bridge.getRunResult(req.params.runId);
57
+ const result2 = bridge.getRunResult(req.params.otherRunId);
58
+ if (!result1 || !result2) return res.status(404).render('error', { message: 'Run not found' });
59
+
60
+ res.render('runs', { project, runs: [
61
+ { id: req.params.runId, success: result1.success, duration: result1.duration, testName: result1.test, failedCount: result1.failedTests ? result1.failedTests.length : 0, healedCount: 0, hasReport: !!bridge.getFinalReport(req.params.runId), timestamp: '' },
62
+ { id: req.params.otherRunId, success: result2.success, duration: result2.duration, testName: result2.test, failedCount: result2.failedTests ? result2.failedTests.length : 0, healedCount: 0, hasReport: !!bridge.getFinalReport(req.params.otherRunId), timestamp: '' },
63
+ ] });
64
+ });
65
+
66
+ module.exports = router;