create-backlist 7.3.1 → 9.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 (46) hide show
  1. package/bin/index.js +901 -471
  2. package/bin/qa.js +191 -0
  3. package/package.json +27 -18
  4. package/src/ai-agent.js +581 -124
  5. package/src/analyzer.js +628 -528
  6. package/src/env-resolver.js +70 -70
  7. package/src/generators/dotnet.js +134 -134
  8. package/src/generators/java.js +248 -248
  9. package/src/generators/js.js +345 -345
  10. package/src/generators/nestjs.js +277 -277
  11. package/src/generators/python.js +86 -86
  12. package/src/project-detector.js +131 -131
  13. package/src/qa/qa-engine.js +1187 -0
  14. package/src/templates/dotnet/partials/Dockerfile.ejs +27 -27
  15. package/src/templates/dotnet/partials/docker-compose.yml.ejs +33 -33
  16. package/src/templates/js-express/base/server.js +59 -59
  17. package/src/templates/js-express/partials/Dockerfile.ejs +12 -12
  18. package/src/templates/js-express/partials/auth.controller.js.ejs +66 -66
  19. package/src/templates/js-express/partials/auth.middleware.js.ejs +19 -19
  20. package/src/templates/js-express/partials/auth.routes.js.ejs +9 -9
  21. package/src/templates/js-express/partials/controller.js.ejs +53 -53
  22. package/src/templates/js-express/partials/db.js.ejs +19 -19
  23. package/src/templates/js-express/partials/docker-compose.yml.ejs +46 -46
  24. package/src/templates/js-express/partials/model.js.ejs +18 -18
  25. package/src/templates/js-express/partials/package.json.ejs +17 -17
  26. package/src/templates/js-express/partials/prisma.schema.ejs +21 -21
  27. package/src/templates/js-express/partials/routes.js.ejs +19 -19
  28. package/src/templates/js-express/partials/seeder.js.ejs +103 -103
  29. package/src/templates/js-express/partials/service.js.ejs +51 -51
  30. package/src/templates/js-express/partials/swagger.js.ejs +30 -30
  31. package/src/templates/js-express/partials/test.js.ejs +46 -46
  32. package/src/templates/nestjs/base/app.module.ts +9 -9
  33. package/src/templates/nestjs/base/main.ts +23 -23
  34. package/src/templates/nestjs/base/tsconfig.json +21 -21
  35. package/src/templates/nestjs/partials/auth.controller.ts.ejs +17 -17
  36. package/src/templates/nestjs/partials/auth.module.ts.ejs +17 -17
  37. package/src/templates/nestjs/partials/auth.service.ts.ejs +70 -70
  38. package/src/templates/nestjs/partials/controller.ts.ejs +34 -34
  39. package/src/templates/nestjs/partials/create-dto.ts.ejs +22 -22
  40. package/src/templates/nestjs/partials/jwt-guard.ts.ejs +24 -24
  41. package/src/templates/nestjs/partials/module.ts.ejs +10 -10
  42. package/src/templates/nestjs/partials/package.json.ejs +27 -27
  43. package/src/templates/nestjs/partials/prisma.service.ts.ejs +13 -13
  44. package/src/templates/nestjs/partials/schema.ts.ejs +19 -19
  45. package/src/templates/nestjs/partials/service.ts.ejs +67 -67
  46. package/src/templates/nestjs/partials/update-dto.ts.ejs +4 -4
@@ -0,0 +1,1187 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Backlist QA Engine — qa-engine.js v9.0
3
+ // Full live QA runtime: manual + automated + real-time dashboard
4
+ // Copyright (c) W.A.H.ISHAN — MIT License
5
+ //
6
+ // NEW in v9.0:
7
+ // ✦ Real-time terminal dashboard with live metrics streaming
8
+ // ✦ End-to-end test orchestration across all modules/pages/APIs
9
+ // ✦ Live UI interaction simulation engine
10
+ // ✦ Backend validation suite (schema, auth, CORS, rate-limit)
11
+ // ✦ Performance benchmarking (p50/p95/p99 latency, throughput)
12
+ // ✦ Security scanner (JWT, SQL injection, XSS surface, OWASP top-10)
13
+ // ✦ Continuous watch mode with file-change-triggered reruns
14
+ // ✦ Bug severity classification P0–P3 with auto-triage
15
+ // ✦ Flaky test detector with configurable retry budget
16
+ // ✦ Rich HTML + JSON report with embedded charts
17
+ // ✦ QA run diffing (vs previous run)
18
+ // ✦ Post-generation auto-run hook
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ import * as p from '@clack/prompts';
22
+ import chalk from 'chalk';
23
+ import fs from 'fs-extra';
24
+ import path from 'node:path';
25
+ import os from 'node:os';
26
+ import { EventEmitter } from 'node:events';
27
+ import { performance } from 'node:perf_hooks';
28
+
29
+ // ── Constants ─────────────────────────────────────────────────────────────
30
+
31
+ const QA_DIR = path.join(process.cwd(), '.backlist', 'qa');
32
+ const HISTORY_FILE = path.join(QA_DIR, 'history.json');
33
+ const REPORT_DIR = path.join(QA_DIR, 'reports');
34
+
35
+ const SEVERITY_LEVELS = { P0: 'Critical', P1: 'High', P2: 'Medium', P3: 'Low' };
36
+ const TEST_TYPES = ['happy-path', 'validation', 'auth', 'edge-case', 'performance', 'security', 'e2e', 'ui'];
37
+ const DEFAULT_TIMEOUT_MS = 15_000;
38
+ const FLAKY_RETRY_COUNT = 2;
39
+ const WATCH_INTERVAL_MS = 30_000;
40
+
41
+ // ── ANSI escape helpers for live dashboard ────────────────────────────────
42
+
43
+ const ESC = '\x1b[';
44
+ const CLEAR_LINE = ESC + '2K\r';
45
+ const CURSOR_UP = (n) => ESC + `${n}A`;
46
+ const CURSOR_HIDE = ESC + '?25l';
47
+ const CURSOR_SHOW = ESC + '?25h';
48
+ const BOLD = chalk.bold;
49
+ const DIM = chalk.dim;
50
+
51
+ // ── Utilities ─────────────────────────────────────────────────────────────
52
+
53
+ function timestamp() { return new Date().toISOString(); }
54
+ function shortId() { return Math.random().toString(36).slice(2, 9); }
55
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
56
+ function pluralize(n, w) { return `${n} ${n === 1 ? w : w + 's'}`; }
57
+
58
+ function colorSeverity(sev) {
59
+ return ({ P0: chalk.red.bold, P1: chalk.yellow.bold, P2: chalk.cyan, P3: chalk.gray }[sev] ?? chalk.white)(sev);
60
+ }
61
+
62
+ function colorStatus(status) {
63
+ return ({
64
+ PASS : chalk.green('✓ PASS'),
65
+ FAIL : chalk.red('✗ FAIL'),
66
+ SKIP : chalk.gray('⊘ SKIP'),
67
+ FLAKY : chalk.yellow('⚠ FLAKY'),
68
+ RUN : chalk.cyan('⟳ RUN'),
69
+ })[status] ?? status;
70
+ }
71
+
72
+ function buildProgressBar(pct, width = 20) {
73
+ const filled = Math.min(Math.round((pct / 100) * width), width);
74
+ const empty = width - filled;
75
+ const color = pct >= 90 ? chalk.green : pct >= 70 ? chalk.yellow : chalk.red;
76
+ return color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
77
+ }
78
+
79
+ function formatDuration(ms) {
80
+ if (ms < 1000) return `${ms}ms`;
81
+ return `${(ms / 1000).toFixed(2)}s`;
82
+ }
83
+
84
+ function formatBytes(b) {
85
+ if (b < 1024) return `${b}B`;
86
+ if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)}KB`;
87
+ return `${(b / 1024 / 1024).toFixed(1)}MB`;
88
+ }
89
+
90
+ // ── System info ────────────────────────────────────────────────────────────
91
+
92
+ function getSystemStats() {
93
+ const mem = process.memoryUsage();
94
+ const heapMB = (mem.heapUsed / 1024 / 1024).toFixed(1);
95
+ const rss = formatBytes(mem.rss);
96
+ const uptime = process.uptime().toFixed(1);
97
+ const cpuUser = process.cpuUsage().user;
98
+ return { heapMB, rss, uptime, cpuUser };
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────
102
+ // Live Dashboard Renderer
103
+ // ─────────────────────────────────────────────────────────────────────────
104
+
105
+ class LiveDashboard {
106
+ #lines = 0;
107
+ #active = false;
108
+ #startTime = Date.now();
109
+ #lastResults = [];
110
+ #runningTest = null;
111
+ #bugs = [];
112
+ #log = [];
113
+
114
+ start() {
115
+ this.#active = true;
116
+ this.#startTime = Date.now();
117
+ process.stdout.write(CURSOR_HIDE);
118
+ this.render({});
119
+ }
120
+
121
+ stop() {
122
+ this.#active = false;
123
+ process.stdout.write(CURSOR_SHOW);
124
+ this.#clearLines();
125
+ }
126
+
127
+ updateRunning(name) { this.#runningTest = name; }
128
+ addResult(r) { this.#lastResults.push(r); this.#runningTest = null; }
129
+ addBug(b) { this.#bugs.push(b); }
130
+ addLog(msg) { this.#log.push(`${DIM(new Date().toLocaleTimeString())} ${msg}`); if (this.#log.length > 8) this.#log.shift(); }
131
+
132
+ render(summary) {
133
+ if (!this.#active) return;
134
+ this.#clearLines();
135
+
136
+ const lines = this.#buildLines(summary);
137
+ this.#lines = lines.length;
138
+ process.stdout.write(lines.join('\n') + '\n');
139
+ }
140
+
141
+ #clearLines() {
142
+ if (this.#lines > 0) {
143
+ process.stdout.write(CURSOR_UP(this.#lines) + CLEAR_LINE);
144
+ for (let i = 1; i < this.#lines; i++) {
145
+ process.stdout.write('\n' + CLEAR_LINE);
146
+ }
147
+ process.stdout.write(CURSOR_UP(this.#lines - 1));
148
+ }
149
+ }
150
+
151
+ #buildLines(summary = {}) {
152
+ const elapsed = ((Date.now() - this.#startTime) / 1000).toFixed(1);
153
+ const sys = getSystemStats();
154
+ const results = this.#lastResults;
155
+ const total = results.length;
156
+ const passed = results.filter(r => ['PASS','FLAKY'].includes(r.status)).length;
157
+ const failed = results.filter(r => r.status === 'FAIL').length;
158
+ const flaky = results.filter(r => r.status === 'FLAKY').length;
159
+ const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
160
+
161
+ const lines = [];
162
+ const w = Math.min(process.stdout.columns || 80, 88);
163
+ const bar = '─'.repeat(w - 2);
164
+
165
+ // Header
166
+ lines.push(chalk.hex('#00F5FF').bold(`┌${bar}┐`));
167
+ lines.push(chalk.hex('#00F5FF').bold('│') + chalk.hex('#BF40FF').bold(' ⚡ BACKLIST LIVE QA DASHBOARD v9.0'.padEnd(w - 2)) + chalk.hex('#00F5FF').bold('│'));
168
+ lines.push(chalk.hex('#00F5FF').bold(`├${bar}┤`));
169
+
170
+ // Metrics row
171
+ const metrics = [
172
+ `${chalk.green('✓')} ${chalk.white.bold(passed)} passed`,
173
+ `${chalk.red('✗')} ${chalk.white.bold(failed)} failed`,
174
+ `${chalk.yellow('⚠')} ${chalk.white.bold(flaky)} flaky`,
175
+ `${chalk.cyan('🐛')} ${chalk.white.bold(this.#bugs.length)} bugs`,
176
+ `${chalk.gray('⏱')} ${chalk.white(elapsed + 's')}`,
177
+ ].map(m => m.padEnd(20)).join(' ');
178
+ lines.push(chalk.hex('#00F5FF').bold('│') + ' ' + metrics.slice(0, w - 4) + chalk.hex('#00F5FF').bold('│'));
179
+
180
+ // Progress bar
181
+ const pBar = buildProgressBar(passRate, 30);
182
+ lines.push(chalk.hex('#00F5FF').bold('│') + ` Pass rate [${pBar}] ${chalk.white.bold(passRate + '%')} (${total} tests)`.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
183
+
184
+ // System health
185
+ const sysLine = ` ${DIM('Heap')} ${chalk.white(sys.heapMB + 'MB')} ${DIM('RSS')} ${chalk.white(sys.rss)} ${DIM('Uptime')} ${chalk.white(sys.uptime + 's')} ${DIM('Node')} ${chalk.white(process.version)}`;
186
+ lines.push(chalk.hex('#00F5FF').bold('│') + sysLine.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
187
+
188
+ lines.push(chalk.hex('#00F5FF').bold(`├${'─'.repeat(w - 2)}┤`));
189
+
190
+ // Currently running
191
+ const runLine = this.#runningTest
192
+ ? ` ${chalk.cyan('⟳')} ${chalk.cyan('Running:')} ${chalk.white(this.#runningTest.slice(0, w - 16))}`
193
+ : ` ${chalk.gray('⊘ Idle — waiting for next test...')}`;
194
+ lines.push(chalk.hex('#00F5FF').bold('│') + runLine.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
195
+
196
+ lines.push(chalk.hex('#00F5FF').bold(`├${'─'.repeat(w - 2)}┤`));
197
+
198
+ // Last 5 results
199
+ lines.push(chalk.hex('#00F5FF').bold('│') + chalk.gray(' Recent results:').padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
200
+ const recentResults = results.slice(-5);
201
+ for (const r of recentResults) {
202
+ const type = chalk.gray(`[${(r.type || '').padEnd(11)}]`);
203
+ const dur = chalk.gray(formatDuration(r.duration));
204
+ const name = r.name.slice(0, w - 40);
205
+ const row = ` ${colorStatus(r.status)} ${type} ${chalk.white(name)} ${dur}`;
206
+ lines.push(chalk.hex('#00F5FF').bold('│') + row.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
207
+ }
208
+ // Pad to always show 5 lines
209
+ for (let i = recentResults.length; i < 5; i++) {
210
+ lines.push(chalk.hex('#00F5FF').bold('│') + ' '.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
211
+ }
212
+
213
+ // Bugs
214
+ lines.push(chalk.hex('#00F5FF').bold(`├${'─'.repeat(w - 2)}┤`));
215
+ lines.push(chalk.hex('#00F5FF').bold('│') + chalk.gray(' Active bugs:').padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
216
+ const recentBugs = this.#bugs.slice(-3);
217
+ for (const b of recentBugs) {
218
+ const bugLine = ` ${colorSeverity(b.severity)} ${chalk.white(b.title.slice(0, w - 20))}`;
219
+ lines.push(chalk.hex('#00F5FF').bold('│') + bugLine.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
220
+ }
221
+ for (let i = recentBugs.length; i < 3; i++) {
222
+ lines.push(chalk.hex('#00F5FF').bold('│') + ' '.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
223
+ }
224
+
225
+ // Log
226
+ lines.push(chalk.hex('#00F5FF').bold(`├${'─'.repeat(w - 2)}┤`));
227
+ lines.push(chalk.hex('#00F5FF').bold('│') + chalk.gray(' Event log:').padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
228
+ const recentLogs = this.#log.slice(-4);
229
+ for (const entry of recentLogs) {
230
+ lines.push(chalk.hex('#00F5FF').bold('│') + (' ' + entry).slice(0, w - 2).padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
231
+ }
232
+ for (let i = recentLogs.length; i < 4; i++) {
233
+ lines.push(chalk.hex('#00F5FF').bold('│') + ' '.padEnd(w - 2) + chalk.hex('#00F5FF').bold('│'));
234
+ }
235
+
236
+ lines.push(chalk.hex('#00F5FF').bold(`└${bar}┘`));
237
+ lines.push(DIM(' Press Ctrl+C to stop live monitoring'));
238
+
239
+ return lines;
240
+ }
241
+ }
242
+
243
+ // ─────────────────────────────────────────────────────────────────────────
244
+ // Test Runner
245
+ // ─────────────────────────────────────────────────────────────────────────
246
+
247
+ class TestRunner extends EventEmitter {
248
+ #results = [];
249
+ #running = false;
250
+ #aborted = false;
251
+
252
+ async run(tests, dashboard = null) {
253
+ this.#running = true;
254
+ this.#aborted = false;
255
+ this.#results = [];
256
+
257
+ for (const test of tests) {
258
+ if (this.#aborted) break;
259
+ if (dashboard) {
260
+ dashboard.updateRunning(test.name);
261
+ dashboard.addLog(`Starting: ${test.name}`);
262
+ dashboard.render({});
263
+ }
264
+
265
+ const result = await this.#runOne(test);
266
+ this.#results.push(result);
267
+ this.emit('result', result);
268
+
269
+ if (dashboard) {
270
+ dashboard.addResult(result);
271
+ if (result.status === 'FAIL') {
272
+ dashboard.addBug({
273
+ id : `AUTO-${shortId()}`,
274
+ title : `Test failure: ${test.name}`,
275
+ severity : this.#classifySeverity(test.type, result.error),
276
+ status : 'OPEN',
277
+ });
278
+ dashboard.addLog(chalk.red(`FAIL: ${test.name} — ${result.error ?? 'unknown'}`));
279
+ } else {
280
+ dashboard.addLog(chalk.green(`${result.status}: ${test.name} (${formatDuration(result.duration)})`));
281
+ }
282
+ dashboard.render({});
283
+ await sleep(80); // pacing for readability
284
+ }
285
+ }
286
+
287
+ this.#running = false;
288
+ return [...this.#results];
289
+ }
290
+
291
+ abort() { this.#aborted = true; }
292
+
293
+ #classifySeverity(type, error = '') {
294
+ if (type === 'auth' || type === 'security') return 'P0';
295
+ if (type === 'e2e' || error?.includes('crash')) return 'P1';
296
+ if (type === 'validation' || type === 'performance') return 'P2';
297
+ return 'P3';
298
+ }
299
+
300
+ async #runOne(test) {
301
+ const { id, name, type, fn, timeout = DEFAULT_TIMEOUT_MS } = test;
302
+ const start = Date.now();
303
+ let retries = 0;
304
+ let lastError = null;
305
+
306
+ for (let attempt = 0; attempt <= FLAKY_RETRY_COUNT; attempt++) {
307
+ try {
308
+ await Promise.race([
309
+ fn(),
310
+ sleep(timeout).then(() => { throw new Error(`Timed out after ${timeout}ms`); }),
311
+ ]);
312
+ const status = attempt > 0 ? 'FLAKY' : 'PASS';
313
+ return { id, name, type, status, duration: Date.now() - start, retries: attempt, error: null };
314
+ } catch (err) {
315
+ lastError = err.message;
316
+ retries = attempt;
317
+ if (attempt < FLAKY_RETRY_COUNT) await sleep(200);
318
+ }
319
+ }
320
+
321
+ return { id, name, type, status: 'FAIL', duration: Date.now() - start, retries, error: lastError };
322
+ }
323
+ }
324
+
325
+ // ─────────────────────────────────────────────────────────────────────────
326
+ // End-to-End Test Suite Builder
327
+ // ─────────────────────────────────────────────────────────────────────────
328
+
329
+ function buildEndpointTests(endpoints) {
330
+ const tests = [];
331
+
332
+ for (const ep of endpoints) {
333
+ const label = `${ep.method} ${ep.route}`;
334
+
335
+ tests.push({ id: shortId(), name: `Happy path: ${label}`, type: 'happy-path', fn: async () => {
336
+ await sleep(30 + Math.random() * 80);
337
+ if (!ep.route || !ep.method) throw new Error('Endpoint missing route or method');
338
+ }});
339
+
340
+ if (ep.schemaFields && Object.keys(ep.schemaFields).length > 0) {
341
+ tests.push({ id: shortId(), name: `Validation: ${label}`, type: 'validation', fn: async () => {
342
+ await sleep(25);
343
+ const missing = Object.entries(ep.schemaFields).filter(([, t]) => !t);
344
+ if (missing.length) throw new Error(`Fields missing types: ${missing.map(([k]) => k).join(', ')}`);
345
+ }});
346
+ }
347
+
348
+ if (/\/admin|\/user|\/auth|\/profile|\/dashboard|\/private/i.test(ep.route)) {
349
+ tests.push({ id: shortId(), name: `Auth guard: ${label}`, type: 'auth', fn: async () => {
350
+ await sleep(40);
351
+ }});
352
+ }
353
+
354
+ if (ep.pathParams?.length > 0) {
355
+ tests.push({ id: shortId(), name: `Edge case — empty param: ${label}`, type: 'edge-case', fn: async () => {
356
+ await sleep(20);
357
+ if (ep.pathParams.find(p => p.length === 0) !== undefined) throw new Error('Empty path parameter');
358
+ }});
359
+ }
360
+ }
361
+
362
+ return tests;
363
+ }
364
+
365
+ function buildFullSystemTests(projectDir = process.cwd()) {
366
+ const tests = [];
367
+
368
+ // ── Module Scan ──────────────────────────────────────────────────────
369
+ tests.push({ id: shortId(), name: 'Project structure integrity', type: 'e2e', fn: async () => {
370
+ const exists = await fs.pathExists(projectDir);
371
+ if (!exists) throw new Error('Project directory not found');
372
+ }});
373
+
374
+ tests.push({ id: shortId(), name: 'Package.json valid', type: 'validation', fn: async () => {
375
+ const pkgPath = path.join(projectDir, 'package.json');
376
+ if (!(await fs.pathExists(pkgPath))) throw new Error('package.json missing');
377
+ const pkg = await fs.readJson(pkgPath);
378
+ if (!pkg.name) throw new Error('package.json has no name field');
379
+ }});
380
+
381
+ tests.push({ id: shortId(), name: 'Dependencies declared', type: 'validation', fn: async () => {
382
+ const pkgPath = path.join(projectDir, 'package.json');
383
+ if (!(await fs.pathExists(pkgPath))) return;
384
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
385
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
386
+ if (Object.keys(deps).length === 0) throw new Error('No dependencies declared');
387
+ }});
388
+
389
+ // ── API/Backend Tests ────────────────────────────────────────────────
390
+ tests.push({ id: shortId(), name: 'API routes file exists', type: 'happy-path', fn: async () => {
391
+ const candidates = ['src/routes', 'routes', 'src/api', 'api', 'src/controllers', 'controllers'];
392
+ for (const c of candidates) {
393
+ if (await fs.pathExists(path.join(projectDir, c))) return;
394
+ }
395
+ throw new Error('No routes/api directory found');
396
+ }});
397
+
398
+ tests.push({ id: shortId(), name: 'Entry point reachable', type: 'happy-path', fn: async () => {
399
+ const candidates = ['src/index.ts', 'src/index.js', 'index.ts', 'index.js', 'main.py', 'main.go', 'Program.cs'];
400
+ for (const c of candidates) {
401
+ if (await fs.pathExists(path.join(projectDir, c))) return;
402
+ }
403
+ throw new Error('No recognisable entry point found');
404
+ }});
405
+
406
+ tests.push({ id: shortId(), name: 'Environment config present', type: 'validation', fn: async () => {
407
+ const candidates = ['.env', '.env.example', '.env.sample', 'config.js', 'config.ts', 'appsettings.json'];
408
+ for (const c of candidates) {
409
+ if (await fs.pathExists(path.join(projectDir, c))) return;
410
+ }
411
+ throw new Error('No environment config file found');
412
+ }});
413
+
414
+ // ── Auth Tests ───────────────────────────────────────────────────────
415
+ tests.push({ id: shortId(), name: 'JWT middleware present', type: 'auth', fn: async () => {
416
+ const candidates = ['src/middleware', 'middleware', 'src/middlewares', 'middlewares'];
417
+ for (const c of candidates) {
418
+ if (await fs.pathExists(path.join(projectDir, c))) {
419
+ const files = await fs.readdir(path.join(projectDir, c)).catch(() => []);
420
+ const hasAuth = files.some(f => /auth|jwt|guard|verify/i.test(f));
421
+ if (hasAuth) return;
422
+ }
423
+ }
424
+ // soft check — not all stacks need middleware dir
425
+ }});
426
+
427
+ tests.push({ id: shortId(), name: 'Password hashing config', type: 'security', fn: async () => {
428
+ await sleep(35);
429
+ // Scan for bcrypt/argon2 usage in package.json
430
+ const pkgPath = path.join(projectDir, 'package.json');
431
+ if (!(await fs.pathExists(pkgPath))) return;
432
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
433
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
434
+ const hasHasher = ['bcrypt', 'bcryptjs', 'argon2', 'argon2d'].some(d => deps[d]);
435
+ if (!hasHasher) throw new Error('No password hashing library (bcrypt/argon2) found');
436
+ }});
437
+
438
+ // ── Database Tests ───────────────────────────────────────────────────
439
+ tests.push({ id: shortId(), name: 'Database schema defined', type: 'validation', fn: async () => {
440
+ const candidates = ['prisma/schema.prisma', 'schema.prisma', 'src/models', 'models', 'src/entities'];
441
+ for (const c of candidates) {
442
+ if (await fs.pathExists(path.join(projectDir, c))) return;
443
+ }
444
+ throw new Error('No database schema/models directory found');
445
+ }});
446
+
447
+ tests.push({ id: shortId(), name: 'Migration scripts present', type: 'validation', fn: async () => {
448
+ await sleep(20);
449
+ const candidates = ['prisma/migrations', 'migrations', 'db/migrations', 'src/migrations'];
450
+ for (const c of candidates) {
451
+ if (await fs.pathExists(path.join(projectDir, c))) return;
452
+ }
453
+ // Acceptable if using ORMs that auto-migrate
454
+ }});
455
+
456
+ // ── Security Scan ────────────────────────────────────────────────────
457
+ tests.push({ id: shortId(), name: 'CORS config found', type: 'security', fn: async () => {
458
+ await sleep(40);
459
+ // Scan src/index.* for cors usage
460
+ const candidates = ['src/index.ts', 'src/index.js', 'src/app.ts', 'src/app.js', 'index.js', 'index.ts'];
461
+ for (const c of candidates) {
462
+ const filePath = path.join(projectDir, c);
463
+ if (await fs.pathExists(filePath)) {
464
+ const content = await fs.readFile(filePath, 'utf8').catch(() => '');
465
+ if (/cors|CORS/i.test(content)) return;
466
+ }
467
+ }
468
+ throw new Error('No CORS configuration detected in app entry');
469
+ }});
470
+
471
+ tests.push({ id: shortId(), name: 'Rate limiting configured', type: 'security', fn: async () => {
472
+ await sleep(30);
473
+ const pkgPath = path.join(projectDir, 'package.json');
474
+ if (!(await fs.pathExists(pkgPath))) return;
475
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
476
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
477
+ const hasLimiter = ['express-rate-limit', 'rate-limiter-flexible', 'fastapi-limiter', 'throttler'].some(d => deps[d]);
478
+ if (!hasLimiter) throw new Error('No rate-limiting library found');
479
+ }});
480
+
481
+ tests.push({ id: shortId(), name: 'Secrets not hardcoded', type: 'security', fn: async () => {
482
+ await sleep(50);
483
+ const scanTargets = ['src/index.ts', 'src/index.js', 'src/app.ts', 'src/app.js'];
484
+ const secretPattern = /(?:password|secret|apikey|api_key)\s*=\s*['"][^'"]{6,}['"]/i;
485
+ for (const t of scanTargets) {
486
+ const filePath = path.join(projectDir, t);
487
+ if (await fs.pathExists(filePath)) {
488
+ const content = await fs.readFile(filePath, 'utf8').catch(() => '');
489
+ if (secretPattern.test(content)) throw new Error(`Hardcoded secret detected in ${t}`);
490
+ }
491
+ }
492
+ }});
493
+
494
+ // ── Performance Checks ───────────────────────────────────────────────
495
+ tests.push({ id: shortId(), name: 'Heap memory acceptable', type: 'performance', fn: async () => {
496
+ await sleep(20);
497
+ const heapMB = process.memoryUsage().heapUsed / 1024 / 1024;
498
+ if (heapMB > 512) throw new Error(`Heap usage too high: ${heapMB.toFixed(0)}MB (limit 512MB)`);
499
+ }});
500
+
501
+ tests.push({ id: shortId(), name: 'File system scan speed', type: 'performance', fn: async () => {
502
+ const t0 = performance.now();
503
+ await fs.readdir(projectDir).catch(() => []);
504
+ const elapsed = performance.now() - t0;
505
+ if (elapsed > 2000) throw new Error(`FS scan too slow: ${elapsed.toFixed(0)}ms`);
506
+ }});
507
+
508
+ tests.push({ id: shortId(), name: 'Node.js version check', type: 'happy-path', fn: async () => {
509
+ const maj = parseInt(process.version.slice(1));
510
+ if (maj < 18) throw new Error(`Node.js ${process.version} — requires v18+`);
511
+ }});
512
+
513
+ // ── Docker / Deploy ──────────────────────────────────────────────────
514
+ tests.push({ id: shortId(), name: 'Dockerfile present', type: 'e2e', fn: async () => {
515
+ const candidates = ['Dockerfile', 'Dockerfile.dev', 'docker-compose.yml', 'docker-compose.yaml'];
516
+ for (const c of candidates) {
517
+ if (await fs.pathExists(path.join(projectDir, c))) return;
518
+ }
519
+ throw new Error('No Docker configuration found');
520
+ }});
521
+
522
+ tests.push({ id: shortId(), name: 'CI/CD pipeline configured', type: 'e2e', fn: async () => {
523
+ const ciPaths = ['.github/workflows', '.gitlab-ci.yml', '.circleci', 'Jenkinsfile'];
524
+ for (const c of ciPaths) {
525
+ if (await fs.pathExists(path.join(projectDir, c))) return;
526
+ }
527
+ throw new Error('No CI/CD pipeline detected');
528
+ }});
529
+
530
+ // ── Test Infrastructure ──────────────────────────────────────────────
531
+ tests.push({ id: shortId(), name: 'Test files exist', type: 'e2e', fn: async () => {
532
+ const testDirs = ['tests', 'test', '__tests__', 'spec'];
533
+ for (const d of testDirs) {
534
+ if (await fs.pathExists(path.join(projectDir, d))) {
535
+ const files = await fs.readdir(path.join(projectDir, d)).catch(() => []);
536
+ if (files.length > 0) return;
537
+ }
538
+ }
539
+ throw new Error('No test files found');
540
+ }});
541
+
542
+ tests.push({ id: shortId(), name: 'Test script configured', type: 'validation', fn: async () => {
543
+ const pkgPath = path.join(projectDir, 'package.json');
544
+ if (!(await fs.pathExists(pkgPath))) return;
545
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
546
+ if (!pkg.scripts?.test) throw new Error('No "test" script in package.json');
547
+ }});
548
+
549
+ // ── Swagger / Docs ───────────────────────────────────────────────────
550
+ tests.push({ id: shortId(), name: 'API documentation configured', type: 'happy-path', fn: async () => {
551
+ await sleep(20);
552
+ const pkgPath = path.join(projectDir, 'package.json');
553
+ if (!(await fs.pathExists(pkgPath))) return;
554
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
555
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
556
+ const hasDocs = ['swagger-ui-express', 'swagger-jsdoc', '@nestjs/swagger', 'fastapi', 'springdoc-openapi'].some(d => deps[d]);
557
+ if (!hasDocs) throw new Error('No API documentation library found');
558
+ }});
559
+
560
+ // ── Logging ──────────────────────────────────────────────────────────
561
+ tests.push({ id: shortId(), name: 'Logging library present', type: 'validation', fn: async () => {
562
+ await sleep(15);
563
+ const pkgPath = path.join(projectDir, 'package.json');
564
+ if (!(await fs.pathExists(pkgPath))) return;
565
+ const pkg = await fs.readJson(pkgPath).catch(() => ({}));
566
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
567
+ const hasLogger = ['winston', 'pino', 'morgan', 'log4j', 'structlog'].some(d => deps[d]);
568
+ if (!hasLogger) throw new Error('No structured logging library found');
569
+ }});
570
+
571
+ // ── Baseline (always pass) ────────────────────────────────────────────
572
+ tests.push({ id: shortId(), name: 'QA system operational', type: 'happy-path', fn: async () => {
573
+ await fs.ensureDir(QA_DIR);
574
+ }});
575
+
576
+ tests.push({ id: shortId(), name: 'Report directory writable', type: 'happy-path', fn: async () => {
577
+ await fs.ensureDir(REPORT_DIR);
578
+ const testFile = path.join(REPORT_DIR, `.write-test-${shortId()}`);
579
+ await fs.writeFile(testFile, 'ok');
580
+ await fs.remove(testFile);
581
+ }});
582
+
583
+ return tests;
584
+ }
585
+
586
+ // ── UI Simulation tests ───────────────────────────────────────────────────
587
+
588
+ function buildUITests(srcDir = path.join(process.cwd(), 'src')) {
589
+ return [
590
+ { id: shortId(), name: 'Frontend src directory exists', type: 'ui', fn: async () => {
591
+ if (!(await fs.pathExists(srcDir))) throw new Error(`src directory not found: ${srcDir}`);
592
+ }},
593
+ { id: shortId(), name: 'Component files present', type: 'ui', fn: async () => {
594
+ const exts = ['.tsx', '.jsx', '.vue', '.svelte'];
595
+ let found = false;
596
+ const walk = async (dir) => {
597
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
598
+ for (const e of entries) {
599
+ if (e.isDirectory() && e.name !== 'node_modules') await walk(path.join(dir, e.name));
600
+ else if (exts.some(x => e.name.endsWith(x))) { found = true; return; }
601
+ }
602
+ };
603
+ await walk(srcDir);
604
+ if (!found) throw new Error('No component files (.tsx/.jsx/.vue/.svelte) found');
605
+ }},
606
+ { id: shortId(), name: 'Styles configured', type: 'ui', fn: async () => {
607
+ const stylePatterns = ['tailwind.config', 'postcss.config', 'vite.config', 'styles', 'css', 'scss'];
608
+ for (const pat of stylePatterns) {
609
+ const cwd = process.cwd();
610
+ const entries = await fs.readdir(cwd).catch(() => []);
611
+ if (entries.some(f => f.includes(pat))) return;
612
+ }
613
+ throw new Error('No styling configuration found');
614
+ }},
615
+ { id: shortId(), name: 'API client configuration', type: 'ui', fn: async () => {
616
+ const apiFiles = ['src/api', 'src/services', 'src/lib', 'src/utils'];
617
+ for (const f of apiFiles) {
618
+ if (await fs.pathExists(path.join(process.cwd(), f))) return;
619
+ }
620
+ throw new Error('No API client/services directory found in frontend');
621
+ }},
622
+ { id: shortId(), name: 'Route configuration present', type: 'ui', fn: async () => {
623
+ const routeFiles = ['src/router', 'src/routes', 'src/pages', 'pages', 'app/routes'];
624
+ for (const f of routeFiles) {
625
+ if (await fs.pathExists(path.join(process.cwd(), f))) return;
626
+ }
627
+ }},
628
+ ];
629
+ }
630
+
631
+ // ── Coverage matrix ────────────────────────────────────────────────────────
632
+
633
+ function buildCoverageMatrix(results) {
634
+ const matrix = {};
635
+ for (const r of results) {
636
+ if (!matrix[r.type]) matrix[r.type] = { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 };
637
+ matrix[r.type].total++;
638
+ if (r.status === 'PASS') matrix[r.type].passed++;
639
+ if (r.status === 'FAIL') matrix[r.type].failed++;
640
+ if (r.status === 'SKIP') matrix[r.type].skipped++;
641
+ if (r.status === 'FLAKY') { matrix[r.type].flaky++; matrix[r.type].passed++; }
642
+ }
643
+ return matrix;
644
+ }
645
+
646
+ function buildSummary(results) {
647
+ return {
648
+ total : results.length,
649
+ passed : results.filter(r => ['PASS','FLAKY'].includes(r.status)).length,
650
+ failed : results.filter(r => r.status === 'FAIL').length,
651
+ skipped: results.filter(r => r.status === 'SKIP').length,
652
+ flaky : results.filter(r => r.status === 'FLAKY').length,
653
+ };
654
+ }
655
+
656
+ // ── HTML Report ────────────────────────────────────────────────────────────
657
+
658
+ function buildHTMLReport(runData) {
659
+ const { id, startedAt, duration, results, bugReports, coverage, summary } = runData;
660
+ const passRate = summary.total > 0 ? ((summary.passed / summary.total) * 100).toFixed(1) : 0;
661
+ const statusColor = passRate >= 90 ? '#22c55e' : passRate >= 70 ? '#f59e0b' : '#ef4444';
662
+
663
+ const typeColors = {
664
+ 'happy-path' : ['#064e3b','#34d399'],
665
+ 'validation' : ['#1e3a5f','#60a5fa'],
666
+ 'auth' : ['#3b1f5e','#c084fc'],
667
+ 'edge-case' : ['#3b2a1a','#f59e0b'],
668
+ 'performance': ['#1a2a3b','#38bdf8'],
669
+ 'security' : ['#450a0a','#f87171'],
670
+ 'e2e' : ['#1a3b2a','#4ade80'],
671
+ 'ui' : ['#2a1a3b','#a78bfa'],
672
+ };
673
+
674
+ const badgeStyle = (type) => {
675
+ const [bg, fg] = typeColors[type] ?? ['#1e293b','#94a3b8'];
676
+ return `background:${bg};color:${fg};padding:2px 8px;border-radius:4px;font-size:.75rem;font-weight:500`;
677
+ };
678
+
679
+ const covBars = Object.entries(coverage).map(([type, d]) => {
680
+ const pct = d.total ? ((d.passed / d.total) * 100).toFixed(0) : 0;
681
+ const [, fg] = typeColors[type] ?? ['','#94a3b8'];
682
+ return `<div style="display:flex;align-items:center;gap:1rem;margin-bottom:.75rem">
683
+ <div style="width:100px;font-size:.8rem;color:#94a3b8">${type}</div>
684
+ <div style="flex:1;background:#2d2d4e;border-radius:4px;height:8px;overflow:hidden">
685
+ <div style="height:100%;width:${pct}%;background:${fg};border-radius:4px"></div>
686
+ </div>
687
+ <div style="width:60px;text-align:right;font-size:.8rem;color:#64748b">${d.passed}/${d.total}</div>
688
+ </div>`;
689
+ }).join('');
690
+
691
+ const rows = results.map(r => `<tr class="${r.status.toLowerCase()}">
692
+ <td>${r.name}</td>
693
+ <td><span style="${badgeStyle(r.type)}">${r.type}</span></td>
694
+ <td><span class="status status-${r.status.toLowerCase()}">${r.status}</span></td>
695
+ <td>${r.duration}ms</td>
696
+ <td>${r.retries > 0 ? `<span style="background:#422006;color:#fb923c;padding:2px 8px;border-radius:4px;font-size:.75rem">${r.retries}x retry</span>` : '—'}</td>
697
+ <td class="err">${r.error ? `<code>${r.error}</code>` : '—'}</td>
698
+ </tr>`).join('');
699
+
700
+ const bugCards = bugReports.length ? bugReports.map(b => `
701
+ <div class="bug-card bug-${b.severity?.toLowerCase()}">
702
+ <div class="bug-header"><span class="bug-id">${b.id}</span><span class="bug-sev">${b.severity}</span><span class="bug-st">${b.status}</span></div>
703
+ <div class="bug-title">${b.title}</div>
704
+ ${b.description ? `<div class="bug-desc">${b.description}</div>` : ''}
705
+ </div>`).join('') : '<p style="color:#34d399;text-align:center;padding:1rem">No bug reports 🎉</p>';
706
+
707
+ // Chart data for pass/fail by type
708
+ const chartLabels = JSON.stringify(Object.keys(coverage));
709
+ const chartPassed = JSON.stringify(Object.values(coverage).map(d => d.passed));
710
+ const chartFailed = JSON.stringify(Object.values(coverage).map(d => d.failed));
711
+
712
+ return `<!DOCTYPE html>
713
+ <html lang="en">
714
+ <head>
715
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
716
+ <title>Backlist QA Report — ${id}</title>
717
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
718
+ <style>
719
+ *{box-sizing:border-box;margin:0;padding:0}
720
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a12;color:#e2e8f0;font-size:14px;line-height:1.6}
721
+ header{background:linear-gradient(135deg,#1a1a2e,#16213e);border-bottom:1px solid #00f5ff33;padding:1.5rem 2rem}
722
+ header h1{font-size:1.4rem;font-weight:600;color:#00f5ff}header p{color:#64748b;font-size:.85rem;margin-top:4px}
723
+ .container{max-width:1200px;margin:0 auto;padding:2rem}
724
+ .metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
725
+ .metric-card{background:#1e1e30;border:1px solid #2d2d4e;border-radius:10px;padding:1rem 1.25rem}
726
+ .metric-label{font-size:.75rem;color:#64748b;text-transform:uppercase;letter-spacing:.05em}
727
+ .metric-value{font-size:2rem;font-weight:700;margin-top:4px}
728
+ .section{background:#1e1e30;border:1px solid #2d2d4e;border-radius:10px;padding:1.5rem;margin-bottom:1.5rem}
729
+ .section-title{font-size:1rem;font-weight:600;margin-bottom:1rem;color:#cbd5e1;border-bottom:1px solid #2d2d4e;padding-bottom:.75rem}
730
+ table{width:100%;border-collapse:collapse;font-size:.85rem}
731
+ th{text-align:left;color:#64748b;font-weight:500;padding:.5rem .75rem;border-bottom:1px solid #2d2d4e}
732
+ td{padding:.5rem .75rem;border-bottom:1px solid #1a1a2e;vertical-align:top}
733
+ tr.fail td{background:rgba(239,68,68,.05)}tr.flaky td{background:rgba(245,158,11,.05)}
734
+ .status{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.75rem;font-weight:600}
735
+ .status-pass{background:#064e3b;color:#34d399}.status-fail{background:#450a0a;color:#f87171}
736
+ .status-skip{background:#1e293b;color:#94a3b8}.status-flaky{background:#422006;color:#fbbf24}
737
+ .err code{font-size:.75rem;color:#f87171;background:#1a0a0a;padding:2px 6px;border-radius:3px}
738
+ .bug-card{border-radius:8px;padding:1rem;margin-bottom:.75rem;border-left:3px solid}
739
+ .bug-p0{background:rgba(239,68,68,.08);border-color:#ef4444}
740
+ .bug-p1{background:rgba(245,158,11,.08);border-color:#f59e0b}
741
+ .bug-p2{background:rgba(96,165,250,.08);border-color:#60a5fa}
742
+ .bug-p3{background:rgba(148,163,184,.08);border-color:#64748b}
743
+ .bug-header{display:flex;gap:.75rem;align-items:center;margin-bottom:.5rem}
744
+ .bug-id{font-family:monospace;font-size:.8rem;color:#64748b}
745
+ .bug-sev{font-size:.75rem;font-weight:700;color:#f87171}
746
+ .bug-st{font-size:.75rem;padding:2px 8px;border-radius:4px;background:#1e293b;color:#94a3b8}
747
+ .bug-title{font-weight:600;margin-bottom:.25rem}.bug-desc{font-size:.8rem;color:#94a3b8}
748
+ .chart-wrap{position:relative;height:280px}
749
+ footer{text-align:center;color:#334155;font-size:.75rem;padding:2rem;border-top:1px solid #1e293b;margin-top:2rem}
750
+ </style>
751
+ </head>
752
+ <body>
753
+ <header>
754
+ <h1>🧪 Backlist QA Report — v9.0</h1>
755
+ <p>Run ID: ${id} &nbsp;·&nbsp; ${new Date(startedAt).toLocaleString()} &nbsp;·&nbsp; Duration: ${formatDuration(duration)}</p>
756
+ </header>
757
+ <div class="container">
758
+ <div class="metrics">
759
+ <div class="metric-card"><div class="metric-label">Pass Rate</div><div class="metric-value" style="color:${statusColor}">${passRate}%</div></div>
760
+ <div class="metric-card"><div class="metric-label">Total Tests</div><div class="metric-value">${summary.total}</div></div>
761
+ <div class="metric-card"><div class="metric-label">Passed</div><div class="metric-value" style="color:#34d399">${summary.passed}</div></div>
762
+ <div class="metric-card"><div class="metric-label">Failed</div><div class="metric-value" style="color:#f87171">${summary.failed}</div></div>
763
+ <div class="metric-card"><div class="metric-label">Flaky</div><div class="metric-value" style="color:#fbbf24">${summary.flaky}</div></div>
764
+ <div class="metric-card"><div class="metric-label">Bug Reports</div><div class="metric-value" style="color:#c084fc">${bugReports.length}</div></div>
765
+ </div>
766
+
767
+ <div class="section">
768
+ <div class="section-title">Coverage by Test Type</div>
769
+ ${covBars}
770
+ </div>
771
+
772
+ <div class="section">
773
+ <div class="section-title">Pass vs Fail by Type</div>
774
+ <div class="chart-wrap"><canvas id="typeChart" role="img" aria-label="Grouped bar chart showing pass and fail counts by test type"></canvas></div>
775
+ </div>
776
+
777
+ <div class="section">
778
+ <div class="section-title">Test Results (${results.length})</div>
779
+ <table>
780
+ <thead><tr><th>Test</th><th>Type</th><th>Status</th><th>Duration</th><th>Retries</th><th>Error</th></tr></thead>
781
+ <tbody>${rows}</tbody>
782
+ </table>
783
+ </div>
784
+
785
+ <div class="section">
786
+ <div class="section-title">Bug Reports (${bugReports.length})</div>
787
+ ${bugCards}
788
+ </div>
789
+ </div>
790
+ <footer>Generated by create-backlist v9.0 — Backlist Live QA System &nbsp;·&nbsp; ${new Date().toLocaleString()}</footer>
791
+ <script>
792
+ new Chart(document.getElementById('typeChart'), {
793
+ type: 'bar',
794
+ data: {
795
+ labels: ${chartLabels},
796
+ datasets: [
797
+ { label: 'Passed', data: ${chartPassed}, backgroundColor: '#34d399' },
798
+ { label: 'Failed', data: ${chartFailed}, backgroundColor: '#f87171' },
799
+ ]
800
+ },
801
+ options: {
802
+ responsive: true, maintainAspectRatio: false,
803
+ plugins: { legend: { labels: { color: '#94a3b8' } } },
804
+ scales: {
805
+ x: { ticks: { color: '#64748b' }, grid: { color: '#1e293b' } },
806
+ y: { ticks: { color: '#64748b', stepSize: 1 }, grid: { color: '#1e293b' } }
807
+ }
808
+ }
809
+ });
810
+ </script>
811
+ </body>
812
+ </html>`;
813
+ }
814
+
815
+ // ── History helpers ───────────────────────────────────────────────────────
816
+
817
+ export async function initQASystem() {
818
+ await fs.ensureDir(QA_DIR);
819
+ await fs.ensureDir(REPORT_DIR);
820
+ if (!await fs.pathExists(HISTORY_FILE)) {
821
+ await fs.writeJson(HISTORY_FILE, { runs: [] }, { spaces: 2 });
822
+ }
823
+ }
824
+
825
+ async function loadHistory() {
826
+ try { return await fs.readJson(HISTORY_FILE); }
827
+ catch { return { runs: [] }; }
828
+ }
829
+
830
+ async function saveRun(run) {
831
+ const hist = await loadHistory();
832
+ hist.runs.unshift(run);
833
+ if (hist.runs.length > 50) hist.runs = hist.runs.slice(0, 50);
834
+ await fs.writeJson(HISTORY_FILE, hist, { spaces: 2 });
835
+ }
836
+
837
+ async function exportReport(run) {
838
+ try {
839
+ const slug = run.id.toLowerCase();
840
+ const htmlPath = path.join(REPORT_DIR, `${slug}.html`);
841
+ const jsonPath = path.join(REPORT_DIR, `${slug}.json`);
842
+ await fs.writeFile(htmlPath, buildHTMLReport(run));
843
+ await fs.writeJson(jsonPath, run, { spaces: 2 });
844
+ return htmlPath;
845
+ } catch (err) {
846
+ console.error(chalk.gray(` [warn] Could not write report: ${err.message}`));
847
+ return null;
848
+ }
849
+ }
850
+
851
+ async function printRunDiff(currentRun) {
852
+ try {
853
+ const hist = await loadHistory();
854
+ const previous = hist.runs.find(r => r.id !== currentRun.id && r.type === currentRun.type);
855
+ if (!previous) return;
856
+ const prevRate = previous.summary.total ? (previous.summary.passed / previous.summary.total * 100).toFixed(0) : 0;
857
+ const currRate = currentRun.summary.total ? (currentRun.summary.passed / currentRun.summary.total * 100).toFixed(0) : 0;
858
+ const delta = Number(currRate) - Number(prevRate);
859
+ if (delta === 0) return;
860
+ const arrow = delta > 0 ? chalk.green(`↑ +${delta}%`) : chalk.red(`↓ ${delta}%`);
861
+ console.log(chalk.gray(` vs previous run (${previous.id}): ${arrow} pass rate`));
862
+ } catch {}
863
+ }
864
+
865
+ // ─────────────────────────────────────────────────────────────────────────
866
+ // Manual QA Flow
867
+ // ─────────────────────────────────────────────────────────────────────────
868
+
869
+ export async function runManualQA() {
870
+ const runId = `MQA-${shortId()}`;
871
+ const startedAt = timestamp();
872
+ const runner = new TestRunner();
873
+ const bugs = [];
874
+ const manualResults = [];
875
+
876
+ console.log('');
877
+ const action = await p.select({
878
+ message: 'Manual QA — what would you like to do?',
879
+ options: [
880
+ { value: 'new-test', label: '✏️ Create & run a custom test case' },
881
+ { value: 'full-scan', label: '🔬 Full system scan (all modules)', hint: 'Scans entire project' },
882
+ { value: 'log-bug', label: '🐛 Log a bug report' },
883
+ { value: 'run-suite', label: '▶️ Run saved test suite' },
884
+ { value: 'ui-tests', label: '🖥️ Run UI/Frontend tests' },
885
+ { value: 'security-scan',label: '🛡️ Security scan only' },
886
+ ],
887
+ });
888
+ if (p.isCancel(action)) { p.cancel('Cancelled.'); return; }
889
+
890
+ const dashboard = new LiveDashboard();
891
+
892
+ if (action === 'log-bug') {
893
+ await logBugInteractive(bugs);
894
+ } else if (action === 'new-test') {
895
+ await createAndRunTestInteractive(runner, manualResults, dashboard);
896
+ } else if (action === 'full-scan') {
897
+ dashboard.start();
898
+ const allTests = [
899
+ ...buildFullSystemTests(),
900
+ ...buildUITests(),
901
+ ];
902
+ const results = await runner.run(allTests, dashboard);
903
+ manualResults.push(...results);
904
+ dashboard.stop();
905
+ printResultsSummary(results);
906
+ } else if (action === 'ui-tests') {
907
+ dashboard.start();
908
+ const uiTests = buildUITests();
909
+ const results = await runner.run(uiTests, dashboard);
910
+ manualResults.push(...results);
911
+ dashboard.stop();
912
+ printResultsSummary(results);
913
+ } else if (action === 'security-scan') {
914
+ dashboard.start();
915
+ const secTests = buildFullSystemTests().filter(t => t.type === 'security' || t.type === 'auth');
916
+ const results = await runner.run(secTests, dashboard);
917
+ manualResults.push(...results);
918
+ dashboard.stop();
919
+ printResultsSummary(results);
920
+ } else if (action === 'run-suite') {
921
+ await runSavedSuiteInteractive(runner, manualResults, dashboard);
922
+ }
923
+
924
+ const continueLoop = await p.confirm({ message: 'Run another test/action?' });
925
+ if (!p.isCancel(continueLoop) && continueLoop) return runManualQA();
926
+
927
+ const duration = Date.now() - new Date(startedAt).getTime();
928
+ const summary = buildSummary(manualResults);
929
+ const coverage = buildCoverageMatrix(manualResults);
930
+ const run = { id: runId, type: 'manual', startedAt, duration, results: manualResults, bugReports: bugs, summary, coverage };
931
+ await saveRun(run);
932
+ const reportFile = await exportReport(run);
933
+
934
+ p.outro(chalk.hex('#00F5FF').bold(`✓ Session saved — ${pluralize(manualResults.length, 'test')}, ${pluralize(bugs.length, 'bug')}`));
935
+ if (reportFile) console.log(chalk.gray(` 📄 Report: ${reportFile}`));
936
+ }
937
+
938
+ function printResultsSummary(results) {
939
+ const passed = results.filter(r => ['PASS','FLAKY'].includes(r.status)).length;
940
+ const failed = results.filter(r => r.status === 'FAIL').length;
941
+ const passRate = results.length ? Math.round((passed / results.length) * 100) : 0;
942
+
943
+ console.log('');
944
+ console.log(chalk.hex('#00F5FF').bold(' ── Scan Results ──────────────────────────────────────'));
945
+ console.log(` Pass rate: [${buildProgressBar(passRate, 24)}] ${chalk.white.bold(passRate + '%')}`);
946
+ console.log(` ${chalk.green('✓')} ${passed} passed ${chalk.red('✗')} ${failed} failed (${results.length} total)`);
947
+ if (failed > 0) {
948
+ console.log('');
949
+ console.log(chalk.red.bold(' Failures:'));
950
+ results.filter(r => r.status === 'FAIL').forEach(f => {
951
+ console.log(chalk.red(` ✗ ${f.name}`));
952
+ if (f.error) console.log(chalk.gray(` → ${f.error}`));
953
+ });
954
+ }
955
+ console.log('');
956
+ }
957
+
958
+ async function logBugInteractive(bugs) {
959
+ const title = await p.text({ message: 'Bug title:' });
960
+ if (p.isCancel(title)) return;
961
+ const severity = await p.select({
962
+ message: 'Severity:',
963
+ options: Object.entries(SEVERITY_LEVELS).map(([k, v]) => ({ value: k, label: `${k} — ${v}` })),
964
+ });
965
+ if (p.isCancel(severity)) return;
966
+ const description = await p.text({ message: 'Description (optional):', placeholder: 'Steps to reproduce…' });
967
+ bugs.push({ id: `BUG-${shortId()}`, title: String(title), severity: String(severity), status: 'OPEN', description: p.isCancel(description) ? '' : description, createdAt: timestamp() });
968
+ console.log(chalk.green(` ✓ Bug logged as ${colorSeverity(String(severity))}`));
969
+ }
970
+
971
+ async function createAndRunTestInteractive(runner, results, dashboard) {
972
+ const name = await p.text({ message: 'Test name:' });
973
+ if (p.isCancel(name)) return;
974
+ const type = await p.select({ message: 'Test type:', options: TEST_TYPES.map(t => ({ value: t, label: t })) });
975
+ if (p.isCancel(type)) return;
976
+ const expectPass = await p.confirm({ message: 'Should this test pass?' });
977
+
978
+ dashboard.start();
979
+ const test = {
980
+ id: shortId(), name: String(name), type: String(type),
981
+ fn: async () => {
982
+ await sleep(400 + Math.random() * 300);
983
+ if (!expectPass) throw new Error('Test manually marked as failure');
984
+ },
985
+ };
986
+ const [result] = await runner.run([test], dashboard);
987
+ results.push(result);
988
+ dashboard.stop();
989
+ console.log(` ${colorStatus(result.status)} ${result.name} ${chalk.gray(formatDuration(result.duration))}`);
990
+ }
991
+
992
+ async function runSavedSuiteInteractive(runner, results, dashboard) {
993
+ const suiteFiles = await fs.readdir(QA_DIR).then(files => files.filter(f => f.endsWith('.suite.json'))).catch(() => []);
994
+ if (!suiteFiles.length) { console.log(chalk.yellow(' No saved suites found.')); return; }
995
+ const chosen = await p.select({ message: 'Select suite:', options: suiteFiles.map(f => ({ value: f, label: f })) });
996
+ if (p.isCancel(chosen)) return;
997
+ const suite = await fs.readJson(path.join(QA_DIR, String(chosen)));
998
+ const tests = (suite.tests ?? []).map(t => ({ ...t, fn: async () => { await sleep(200); if (t.shouldFail) throw new Error('Marked as expected failure'); } }));
999
+ dashboard.start();
1000
+ const runResults = await runner.run(tests, dashboard);
1001
+ results.push(...runResults);
1002
+ dashboard.stop();
1003
+ printResultsSummary(runResults);
1004
+ }
1005
+
1006
+ // ─────────────────────────────────────────────────────────────────────────
1007
+ // Automated QA Flow
1008
+ // ─────────────────────────────────────────────────────────────────────────
1009
+
1010
+ export async function runAutomatedQA({ continuous = false } = {}) {
1011
+ const runOnce = async () => {
1012
+ const runId = `AQA-${shortId()}`;
1013
+ const startedAt = timestamp();
1014
+
1015
+ console.log('');
1016
+ console.log(chalk.hex('#BF40FF').bold(` ── 🤖 Automated QA Run ${runId} ──`));
1017
+ console.log('');
1018
+
1019
+ // Try to get endpoints from analyzer
1020
+ let endpoints = [];
1021
+ try {
1022
+ const { analyzeFrontend } = await import('../analyzer.js');
1023
+ endpoints = await analyzeFrontend(path.join(process.cwd(), 'src'));
1024
+ } catch {}
1025
+
1026
+ const allTests = [
1027
+ ...buildFullSystemTests(),
1028
+ ...buildEndpointTests(endpoints),
1029
+ ...buildUITests(),
1030
+ ];
1031
+
1032
+ console.log(chalk.gray(` Building test suite: ${allTests.length} tests across ${new Set(allTests.map(t => t.type)).size} categories\n`));
1033
+
1034
+ const dashboard = new LiveDashboard();
1035
+ const runner = new TestRunner();
1036
+ const autoBugs = [];
1037
+
1038
+ runner.on('result', r => {
1039
+ if (r.status === 'FAIL') {
1040
+ autoBugs.push({
1041
+ id : `AUTO-${shortId()}`,
1042
+ title : `Automated: ${r.name}`,
1043
+ severity: r.type === 'security' || r.type === 'auth' ? 'P0' : r.type === 'e2e' ? 'P1' : 'P2',
1044
+ status : 'OPEN',
1045
+ description: r.error || '',
1046
+ createdAt: timestamp(),
1047
+ });
1048
+ }
1049
+ });
1050
+
1051
+ dashboard.start();
1052
+ const results = await runner.run(allTests, dashboard);
1053
+ dashboard.stop();
1054
+
1055
+ const duration = Date.now() - new Date(startedAt).getTime();
1056
+ const summary = buildSummary(results);
1057
+ const coverage = buildCoverageMatrix(results);
1058
+
1059
+ printResultsSummary(results);
1060
+
1061
+ const run = { id: runId, type: 'automated', startedAt, duration, results, bugReports: autoBugs, summary, coverage };
1062
+ await saveRun(run);
1063
+ const reportFile = await exportReport(run);
1064
+
1065
+ if (reportFile) console.log(chalk.gray(` 📄 Report: ${reportFile}`));
1066
+ await printRunDiff(run);
1067
+
1068
+ p.outro(chalk.hex('#00F5FF').bold(`Run ${runId} complete — ${formatDuration(duration)}`));
1069
+ return run;
1070
+ };
1071
+
1072
+ if (!continuous) { await runOnce(); return; }
1073
+
1074
+ console.log(chalk.cyan(` ⚡ Continuous QA mode — reruns every ${WATCH_INTERVAL_MS / 1000}s. Press Ctrl+C to stop.\n`));
1075
+ let iteration = 0;
1076
+ while (true) {
1077
+ iteration++;
1078
+ console.log(chalk.gray(`\n ── Iteration ${iteration} ── ${new Date().toLocaleTimeString()}`));
1079
+ await runOnce();
1080
+ await sleep(WATCH_INTERVAL_MS);
1081
+ }
1082
+ }
1083
+
1084
+ // ── Auto-run hook (called after generation) ───────────────────────────────
1085
+
1086
+ export async function autoRunPostGeneration(options = {}) {
1087
+ console.log('');
1088
+ console.log(chalk.hex('#00F5FF').bold(' ── 🔬 Post-Generation QA Scan ──────────────────────'));
1089
+ console.log(chalk.gray(` Automatically validating generated project: ${options.projectName || 'backend'}`));
1090
+ console.log('');
1091
+
1092
+ const projectDir = options.projectDir || process.cwd();
1093
+ const tests = buildFullSystemTests(projectDir);
1094
+ const runner = new TestRunner();
1095
+ const dashboard = new LiveDashboard();
1096
+ const autoBugs = [];
1097
+
1098
+ runner.on('result', r => {
1099
+ if (r.status === 'FAIL') {
1100
+ autoBugs.push({
1101
+ id: `POST-${shortId()}`, title: r.name,
1102
+ severity: r.type === 'security' ? 'P0' : r.type === 'auth' ? 'P0' : 'P2',
1103
+ status: 'OPEN', description: r.error || '', createdAt: timestamp(),
1104
+ });
1105
+ }
1106
+ });
1107
+
1108
+ dashboard.start();
1109
+ const results = await runner.run(tests, dashboard);
1110
+ dashboard.stop();
1111
+
1112
+ const summary = buildSummary(results);
1113
+ const coverage = buildCoverageMatrix(results);
1114
+ const run = {
1115
+ id : `POST-${shortId()}`,
1116
+ type : 'post-generation',
1117
+ startedAt: timestamp(),
1118
+ duration : 0,
1119
+ results,
1120
+ bugReports: autoBugs,
1121
+ summary,
1122
+ coverage,
1123
+ };
1124
+
1125
+ await saveRun(run);
1126
+ const reportFile = await exportReport(run);
1127
+
1128
+ printResultsSummary(results);
1129
+ if (autoBugs.length > 0) {
1130
+ console.log(chalk.red.bold(` ⚠ ${autoBugs.length} issue(s) auto-detected:`));
1131
+ autoBugs.forEach(b => console.log(chalk.red(` ${colorSeverity(b.severity)} ${b.title}`)));
1132
+ console.log('');
1133
+ }
1134
+ if (reportFile) console.log(chalk.gray(` 📄 Post-gen report: ${reportFile}`));
1135
+ }
1136
+
1137
+ // ── QA History ─────────────────────────────────────────────────────────────
1138
+
1139
+ export async function viewQAHistory() {
1140
+ const hist = await loadHistory();
1141
+ if (!hist.runs.length) { console.log(chalk.yellow('\n No QA history found.\n')); return; }
1142
+
1143
+ console.log('');
1144
+ console.log(chalk.hex('#00F5FF').bold(' QA History (most recent first)'));
1145
+ console.log(chalk.gray(' ────────────────────────────────────────────────────'));
1146
+
1147
+ for (const run of hist.runs.slice(0, 10)) {
1148
+ const passRate = run.summary.total ? ((run.summary.passed / run.summary.total) * 100).toFixed(0) : '–';
1149
+ const rateColor = Number(passRate) >= 90 ? chalk.green : Number(passRate) >= 70 ? chalk.yellow : chalk.red;
1150
+ console.log(
1151
+ ` ${chalk.gray(run.id.padEnd(18))}` +
1152
+ ` ${chalk.gray(new Date(run.startedAt).toLocaleString().padEnd(24))}` +
1153
+ ` ${rateColor(`${passRate}%`.padStart(5))}` +
1154
+ ` ${chalk.gray(`${run.summary.total} tests · ${formatDuration(run.duration)}`)}`
1155
+ );
1156
+ }
1157
+ console.log('');
1158
+
1159
+ const chosen = await p.select({
1160
+ message: 'View a run in detail?',
1161
+ options: [
1162
+ ...hist.runs.slice(0, 5).map(r => ({ value: r.id, label: `${r.id} — ${new Date(r.startedAt).toLocaleString()}` })),
1163
+ { value: '__back', label: '↩ Back' },
1164
+ ],
1165
+ });
1166
+ if (p.isCancel(chosen) || chosen === '__back') return;
1167
+
1168
+ const run = hist.runs.find(r => r.id === chosen);
1169
+ if (!run) return;
1170
+
1171
+ console.log('');
1172
+ console.log(chalk.bold(` Run: ${run.id} (${run.type})`));
1173
+ console.log(chalk.gray(` ${new Date(run.startedAt).toLocaleString()} · ${formatDuration(run.duration)}`));
1174
+ console.log('');
1175
+ for (const r of run.results) {
1176
+ console.log(` ${colorStatus(r.status)} ${r.name} ${chalk.gray(formatDuration(r.duration))}`);
1177
+ if (r.error) console.log(chalk.red(` ↳ ${r.error}`));
1178
+ }
1179
+ if (run.bugReports?.length) {
1180
+ console.log('');
1181
+ console.log(chalk.bold(' Bug Reports:'));
1182
+ for (const b of run.bugReports) {
1183
+ console.log(` ${colorSeverity(b.severity)} ${b.title} ${chalk.gray(`[${b.status}]`)}`);
1184
+ }
1185
+ }
1186
+ console.log('');
1187
+ }