@ai-qa/workflow 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +0 -0
- package/install.js +1 -0
- package/package.json +4 -3
- package/qa-dashboard/app.js +52 -0
- package/qa-dashboard/package-lock.json +1002 -0
- package/qa-dashboard/public/css/style.css +1062 -207
- package/qa-dashboard/public/js/main.js +3 -2
- package/qa-dashboard/routes/export.js +35 -16
- package/qa-dashboard/routes/index.js +38 -1
- package/qa-dashboard/routes/runs.js +58 -6
- package/qa-dashboard/routes/stories.js +59 -15
- package/qa-dashboard/services/cli-bridge.js +47 -3
- package/qa-dashboard/services/project-manager.js +26 -0
- package/qa-dashboard/views/analytics.ejs +226 -153
- package/qa-dashboard/views/index.ejs +241 -82
- package/qa-dashboard/views/layouts/main.ejs +18 -0
- package/qa-dashboard/views/project.ejs +49 -29
- package/qa-dashboard/views/projects.ejs +7 -5
- package/qa-dashboard/views/run.ejs +97 -37
- package/qa-dashboard/views/runs.ejs +29 -38
- package/qa-dashboard/views/stories.ejs +23 -25
- package/qa-dashboard/views/story.ejs +94 -15
- package/qa-dashboard/views/test-data.ejs +24 -24
- package/scripts/executor.js +11 -7
- package/specs/us-expense-01-test-plan.md +55 -0
- package/tests/us-expense-01.spec.ts +30 -0
- package/user-story/Demande-alimentation.md +24 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
// Auto-refresh for status pages (disabled by default, activate with data-autorefresh attribute)
|
|
1
2
|
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
-
document.querySelectorAll('
|
|
3
|
-
const interval = el.dataset.
|
|
3
|
+
document.querySelectorAll('[data-autorefresh]').forEach(el => {
|
|
4
|
+
const interval = el.dataset.autorefresh || 10000;
|
|
4
5
|
setInterval(() => { location.reload(); }, interval);
|
|
5
6
|
});
|
|
6
7
|
});
|
|
@@ -35,25 +35,44 @@ router.get('/report/:runId', (req, res) => {
|
|
|
35
35
|
const html = `<!DOCTYPE html>
|
|
36
36
|
<html><head><meta charset="UTF-8"><title>QA Report - ${runId}</title>
|
|
37
37
|
<style>
|
|
38
|
-
body { font-family: Arial; max-width: 800px; margin: 2rem auto; padding: 1rem; color: #333; }
|
|
38
|
+
body { font-family: Arial, sans-serif; max-width: 800px; margin: 2rem auto; padding: 1rem; color: #333; line-height: 1.5; }
|
|
39
39
|
h1 { color: #6366f1; border-bottom: 2px solid #6366f1; padding-bottom: 0.5rem; }
|
|
40
|
-
.badge { display: inline-block; padding:
|
|
41
|
-
.pass { background: #dcfce7; color: #166534; }
|
|
42
|
-
.fail { background: #fecaca; color: #991b1b; }
|
|
43
|
-
table { width: 100%; border-collapse: collapse; margin:
|
|
44
|
-
td, th { border: 1px solid #ddd; padding:
|
|
45
|
-
th { background: #f4f4f5; }
|
|
46
|
-
.section { margin:
|
|
47
|
-
.stats { display: flex; gap:
|
|
48
|
-
.stat-card { border: 1px solid #
|
|
49
|
-
.stat-num { font-size:
|
|
50
|
-
.stat-label { font-size:
|
|
51
|
-
.footer { margin-top:
|
|
40
|
+
.badge { display: inline-block; padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: bold; text-transform: uppercase; }
|
|
41
|
+
.pass { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; }
|
|
42
|
+
.fail { background: #fecaca; color: #991b1b; border: 1px solid #fca5a5; }
|
|
43
|
+
table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; }
|
|
44
|
+
td, th { border: 1px solid #ddd; padding: 10px; text-align: left; }
|
|
45
|
+
th { background: #f4f4f5; font-weight: 600; }
|
|
46
|
+
.section { margin: 2.5rem 0; }
|
|
47
|
+
.stats { display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: 1rem; }
|
|
48
|
+
.stat-card { border: 1px solid #e4e4e7; border-radius: 8px; padding: 1.25rem; min-width: 140px; text-align: center; background: #fafafa; }
|
|
49
|
+
.stat-num { font-size: 28px; font-weight: bold; color: #6366f1; }
|
|
50
|
+
.stat-label { font-size: 13px; color: #71717a; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
51
|
+
.footer { margin-top: 3rem; font-size: 12px; color: #999; border-top: 1px solid #eee; padding-top: 1rem; text-align: center; }
|
|
52
|
+
pre { white-space: pre-wrap; word-wrap: break-word; background: #f4f4f5; padding: 1.25rem; border-radius: 8px; font-size: 13px; font-family: monospace; border: 1px solid #e4e4e7; }
|
|
53
|
+
|
|
54
|
+
/* Print actions style */
|
|
55
|
+
.actions-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; background: #f4f4f5; padding: 1rem; border-radius: 8px; border: 1px solid #e4e4e7; }
|
|
56
|
+
.btn-print { background: #6366f1; color: #fff; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; font-size: 14px; transition: background 0.2s; }
|
|
57
|
+
.btn-print:hover { background: #4f46e5; }
|
|
58
|
+
|
|
59
|
+
@media print {
|
|
60
|
+
.actions-bar { display: none; }
|
|
61
|
+
body { max-width: 100%; margin: 0; padding: 0; }
|
|
62
|
+
.stat-card { background: #fff !important; border: 1px solid #ddd; }
|
|
63
|
+
}
|
|
52
64
|
</style></head><body>
|
|
65
|
+
<div class="actions-bar">
|
|
66
|
+
<span style="font-weight: 600; color: #4b5563;">Rapport de Test Automatisé</span>
|
|
67
|
+
<button onclick="window.print()" class="btn-print">
|
|
68
|
+
🖨️ Enregistrer en PDF / Imprimer
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
53
72
|
<h1>AI QA Execution Report</h1>
|
|
54
|
-
<p>Run:
|
|
73
|
+
<p style="color: #6b7280; font-size: 14px;">Run ID: <strong>${runId}</strong> | Date de génération : ${new Date().toLocaleString()}</p>
|
|
55
74
|
<span class="badge ${result.success ? 'pass' : 'fail'}">${result.success ? 'PASSED' : 'FAILED'}</span>
|
|
56
|
-
|
|
75
|
+
|
|
57
76
|
<div class="section">
|
|
58
77
|
<h2>Summary</h2>
|
|
59
78
|
<div class="stats">
|
|
@@ -74,7 +93,7 @@ router.get('/report/:runId', (req, res) => {
|
|
|
74
93
|
|
|
75
94
|
<div class="section">
|
|
76
95
|
<h2>Full Report</h2>
|
|
77
|
-
<pre
|
|
96
|
+
<pre>${report || result.output || 'No detailed report'}</pre>
|
|
78
97
|
</div>
|
|
79
98
|
|
|
80
99
|
<div class="footer">
|
|
@@ -4,7 +4,44 @@ const pm = require('../services/project-manager');
|
|
|
4
4
|
|
|
5
5
|
router.get('/', (req, res) => {
|
|
6
6
|
const projects = pm.getAll();
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
// Single-project mode: always use the first (and only) registered project
|
|
9
|
+
const project = projects.length > 0 ? projects[0] : null;
|
|
10
|
+
|
|
11
|
+
let stats = {
|
|
12
|
+
stories: 0,
|
|
13
|
+
plans: 0,
|
|
14
|
+
specs: 0,
|
|
15
|
+
runs: 0,
|
|
16
|
+
passedRuns: 0,
|
|
17
|
+
passRate: 0,
|
|
18
|
+
lastRunStatus: null, // 'passed' | 'failed' | null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (project) {
|
|
22
|
+
const bridge = pm.getBridge(project.id);
|
|
23
|
+
const list = bridge.listAll();
|
|
24
|
+
const runDirs = bridge.getRunDirs();
|
|
25
|
+
|
|
26
|
+
stats.stories = list.stories.length;
|
|
27
|
+
stats.plans = list.plans.length;
|
|
28
|
+
stats.specs = list.specs.length;
|
|
29
|
+
stats.runs = runDirs.length;
|
|
30
|
+
|
|
31
|
+
if (runDirs.length > 0) {
|
|
32
|
+
const results = runDirs.map(d => bridge.getRunResult(d.name)).filter(Boolean);
|
|
33
|
+
stats.passedRuns = results.filter(r => r.success).length;
|
|
34
|
+
stats.passRate = results.length > 0
|
|
35
|
+
? Math.round((stats.passedRuns / results.length) * 100)
|
|
36
|
+
: 0;
|
|
37
|
+
|
|
38
|
+
// Most recent run result
|
|
39
|
+
const latest = bridge.getRunResult(runDirs[0].name);
|
|
40
|
+
if (latest) stats.lastRunStatus = latest.success ? 'passed' : 'failed';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.render('index', { projects, project, stats });
|
|
8
45
|
});
|
|
9
46
|
|
|
10
47
|
module.exports = router;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const pm = require('../services/project-manager');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
|
|
5
7
|
router.get('/', (req, res) => {
|
|
6
8
|
const projectId = req.query.project;
|
|
@@ -19,8 +21,8 @@ router.get('/', (req, res) => {
|
|
|
19
21
|
timestamp: d.name.replace('run-', '').replace(/-/g, ':').slice(0, 19),
|
|
20
22
|
success: result ? result.success : null,
|
|
21
23
|
duration: result ? result.duration : null,
|
|
22
|
-
failedCount: result && result.failedTests ? result.failedTests.length : 0,
|
|
23
|
-
passedCount: result && result.passedTests ? result.passedTests.length : 0,
|
|
24
|
+
failedCount: result && Array.isArray(result.failedTests) ? result.failedTests.length : 0,
|
|
25
|
+
passedCount: result && Array.isArray(result.passedTests) ? result.passedTests.length : 0,
|
|
24
26
|
healedCount: healing ? healing.totalHealed || 0 : 0,
|
|
25
27
|
hasReport: !!bridge.getFinalReport(d.name),
|
|
26
28
|
hasAllure,
|
|
@@ -37,8 +39,34 @@ router.get('/:runId', (req, res) => {
|
|
|
37
39
|
if (!project) return res.redirect('/projects');
|
|
38
40
|
|
|
39
41
|
const bridge = pm.getBridge(project.id);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
// Validation de l'existence du dossier de run
|
|
44
|
+
const runDirs = bridge.getRunDirs();
|
|
45
|
+
const runExists = runDirs.some(d => d.name === req.params.runId);
|
|
46
|
+
if (!runExists) return res.status(404).render('error', { message: 'Run not found' });
|
|
47
|
+
|
|
48
|
+
// Récupération des résultats ou fallback dynamique (en cours d'exécution)
|
|
49
|
+
let result = bridge.getRunResult(req.params.runId);
|
|
50
|
+
if (!result) {
|
|
51
|
+
const outputPath = path.join(bridge.projectPath, 'test-results', req.params.runId, 'execution-output.json');
|
|
52
|
+
let outputText = 'No console output captured yet.';
|
|
53
|
+
if (fs.existsSync(outputPath)) {
|
|
54
|
+
try {
|
|
55
|
+
outputText = fs.readFileSync(outputPath, 'utf-8');
|
|
56
|
+
} catch (err) {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result = {
|
|
60
|
+
runId: req.params.runId,
|
|
61
|
+
success: null,
|
|
62
|
+
duration: null,
|
|
63
|
+
test: 'Running / In Progress...',
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
failedTests: [],
|
|
66
|
+
passedTests: [],
|
|
67
|
+
output: outputText
|
|
68
|
+
};
|
|
69
|
+
}
|
|
42
70
|
|
|
43
71
|
const healing = bridge.getHealingReport(req.params.runId);
|
|
44
72
|
const report = bridge.getFinalReport(req.params.runId);
|
|
@@ -57,9 +85,33 @@ router.get('/:runId/compare/:otherRunId', (req, res) => {
|
|
|
57
85
|
const result2 = bridge.getRunResult(req.params.otherRunId);
|
|
58
86
|
if (!result1 || !result2) return res.status(404).render('error', { message: 'Run not found' });
|
|
59
87
|
|
|
88
|
+
const hasAllure = bridge.hasAllureReport();
|
|
89
|
+
|
|
60
90
|
res.render('runs', { project, runs: [
|
|
61
|
-
{
|
|
62
|
-
|
|
91
|
+
{
|
|
92
|
+
id: req.params.runId,
|
|
93
|
+
success: result1.success,
|
|
94
|
+
duration: result1.duration,
|
|
95
|
+
testName: result1.test || 'Unknown',
|
|
96
|
+
failedCount: Array.isArray(result1.failedTests) ? result1.failedTests.length : 0,
|
|
97
|
+
passedCount: Array.isArray(result1.passedTests) ? result1.passedTests.length : 0,
|
|
98
|
+
healedCount: 0,
|
|
99
|
+
hasReport: !!bridge.getFinalReport(req.params.runId),
|
|
100
|
+
hasAllure,
|
|
101
|
+
timestamp: result1.timestamp ? result1.timestamp.replace('T', ' ').slice(0, 19) : ''
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: req.params.otherRunId,
|
|
105
|
+
success: result2.success,
|
|
106
|
+
duration: result2.duration,
|
|
107
|
+
testName: result2.test || 'Unknown',
|
|
108
|
+
failedCount: Array.isArray(result2.failedTests) ? result2.failedTests.length : 0,
|
|
109
|
+
passedCount: Array.isArray(result2.passedTests) ? result2.passedTests.length : 0,
|
|
110
|
+
healedCount: 0,
|
|
111
|
+
hasReport: !!bridge.getFinalReport(req.params.otherRunId),
|
|
112
|
+
hasAllure,
|
|
113
|
+
timestamp: result2.timestamp ? result2.timestamp.replace('T', ' ').slice(0, 19) : ''
|
|
114
|
+
},
|
|
63
115
|
] });
|
|
64
116
|
});
|
|
65
117
|
|
|
@@ -47,55 +47,99 @@ router.get('/:storyName', (req, res) => {
|
|
|
47
47
|
res.render('story', { project, story: { name: req.params.storyName, content, plan, planName, spec, specName } });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
// Helper to stream CLI command output asynchronously to client
|
|
51
|
+
function streamCommand(res, req, bridge, command) {
|
|
52
|
+
if (bridge.isRunning) {
|
|
53
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
54
|
+
res.write("Erreur : Un pipeline de test est déjà en cours d'exécution sur ce projet. Veuillez patienter.\n");
|
|
55
|
+
return res.end();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
59
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
60
|
+
|
|
61
|
+
let child;
|
|
62
|
+
try {
|
|
63
|
+
child = bridge.runAsync(
|
|
64
|
+
command,
|
|
65
|
+
(data) => {
|
|
66
|
+
res.write(data);
|
|
67
|
+
},
|
|
68
|
+
(success) => {
|
|
69
|
+
res.write(`\n\n--- [FINISHED: ${success ? 'SUCCESS' : 'FAILED'}] ---\n`);
|
|
70
|
+
res.end();
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
res.write(`Error: ${err.message}\n`);
|
|
75
|
+
return res.end();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prevent zombies: kill child process if user cancels/disconnects request
|
|
79
|
+
req.on('close', () => {
|
|
80
|
+
if (child && !child.killed) {
|
|
81
|
+
child.kill('SIGINT');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
router.post('/:storyName/plan', (req, res) => {
|
|
51
87
|
const projectId = req.query.project;
|
|
52
88
|
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
53
|
-
if (!project) return res.status(404).
|
|
89
|
+
if (!project) return res.status(404).send('Project not found');
|
|
54
90
|
|
|
55
91
|
const bridge = pm.getBridge(project.id);
|
|
56
|
-
const
|
|
57
|
-
res
|
|
92
|
+
const command = `node ai-qa-workflow.js plan "${req.params.storyName}"`;
|
|
93
|
+
streamCommand(res, req, bridge, command);
|
|
58
94
|
});
|
|
59
95
|
|
|
60
96
|
router.post('/:storyName/generate', (req, res) => {
|
|
61
97
|
const projectId = req.query.project;
|
|
62
98
|
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
63
|
-
if (!project) return res.status(404).
|
|
99
|
+
if (!project) return res.status(404).send('Project not found');
|
|
64
100
|
|
|
65
101
|
const bridge = pm.getBridge(project.id);
|
|
66
102
|
const baseName = req.params.storyName.replace(/\.md$/i, '');
|
|
67
103
|
const plans = bridge.listTestPlans();
|
|
68
104
|
const planName = plans.find(p => p.toLowerCase().includes(baseName.toLowerCase()));
|
|
69
|
-
if (!planName)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
if (!planName) {
|
|
106
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
107
|
+
res.write('Error: No test plan found. Run plan first.\n');
|
|
108
|
+
return res.end();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const command = `node ai-qa-workflow.js generate "${planName}"`;
|
|
112
|
+
streamCommand(res, req, bridge, command);
|
|
73
113
|
});
|
|
74
114
|
|
|
75
115
|
router.post('/:storyName/execute', (req, res) => {
|
|
76
116
|
const projectId = req.query.project;
|
|
77
117
|
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
78
|
-
if (!project) return res.status(404).
|
|
118
|
+
if (!project) return res.status(404).send('Project not found');
|
|
79
119
|
|
|
80
120
|
const bridge = pm.getBridge(project.id);
|
|
81
121
|
const baseName = req.params.storyName.replace(/\.md$/i, '');
|
|
82
122
|
const specs = bridge.listTestSpecs();
|
|
83
123
|
const specName = specs.find(sp => sp.toLowerCase().includes(baseName.toLowerCase()));
|
|
84
|
-
if (!specName)
|
|
124
|
+
if (!specName) {
|
|
125
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
126
|
+
res.write(`Error: No test spec found for "${req.params.storyName}". Generate one first.\n`);
|
|
127
|
+
return res.end();
|
|
128
|
+
}
|
|
85
129
|
|
|
86
130
|
const testName = specName.replace(/\.spec\.ts$/i, '');
|
|
87
|
-
const
|
|
88
|
-
res
|
|
131
|
+
const command = `node ai-qa-workflow.js execute "${testName}"`;
|
|
132
|
+
streamCommand(res, req, bridge, command);
|
|
89
133
|
});
|
|
90
134
|
|
|
91
135
|
router.post('/:storyName/full-pipeline', (req, res) => {
|
|
92
136
|
const projectId = req.query.project;
|
|
93
137
|
const project = projectId ? pm.get(projectId) : pm.getAll()[0];
|
|
94
|
-
if (!project) return res.status(404).
|
|
138
|
+
if (!project) return res.status(404).send('Project not found');
|
|
95
139
|
|
|
96
140
|
const bridge = pm.getBridge(project.id);
|
|
97
|
-
const
|
|
98
|
-
res
|
|
141
|
+
const command = `node ai-qa-workflow.js run "${req.params.storyName}"`;
|
|
142
|
+
streamCommand(res, req, bridge, command);
|
|
99
143
|
});
|
|
100
144
|
|
|
101
145
|
module.exports = router;
|
|
@@ -1,10 +1,44 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
1
|
+
const { execSync, spawn } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
|
|
5
5
|
class CliBridge {
|
|
6
6
|
constructor(projectPath) {
|
|
7
7
|
this.projectPath = projectPath;
|
|
8
|
+
this.isRunning = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
runAsync(command, onData, onEnd) {
|
|
12
|
+
if (this.isRunning) {
|
|
13
|
+
throw new Error('Un pipeline de test est déjà en cours d\'exécution sur ce projet.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.isRunning = true;
|
|
17
|
+
|
|
18
|
+
// Split command into executable and args
|
|
19
|
+
const args = command.split(' ');
|
|
20
|
+
const cmd = args.shift();
|
|
21
|
+
|
|
22
|
+
const child = spawn(cmd, args, {
|
|
23
|
+
cwd: this.projectPath,
|
|
24
|
+
shell: true,
|
|
25
|
+
env: { ...process.env, FORCE_COLOR: '0' } // Disable ANSI escape codes for clean browser logs
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.stdout.on('data', (data) => {
|
|
29
|
+
onData(data.toString());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.stderr.on('data', (data) => {
|
|
33
|
+
onData(data.toString());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.on('close', (code) => {
|
|
37
|
+
this.isRunning = false;
|
|
38
|
+
onEnd(code === 0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return child;
|
|
8
42
|
}
|
|
9
43
|
|
|
10
44
|
_run(command) {
|
|
@@ -114,13 +148,23 @@ class CliBridge {
|
|
|
114
148
|
getRunResult(runDirName) {
|
|
115
149
|
const resultPath = path.join(this.projectPath, 'test-results', runDirName, 'execution-result.json');
|
|
116
150
|
if (!fs.existsSync(resultPath)) return null;
|
|
117
|
-
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`Error parsing execution-result.json for ${runDirName}:`, err.message);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
118
157
|
}
|
|
119
158
|
|
|
120
159
|
getHealingReport(runDirName) {
|
|
121
160
|
const reportPath = path.join(this.projectPath, 'test-results', runDirName, 'healing-report.json');
|
|
122
161
|
if (!fs.existsSync(reportPath)) return null;
|
|
123
|
-
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(`Error parsing healing-report.json for ${runDirName}:`, err.message);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
getFinalReport(runDirName) {
|
|
@@ -8,6 +8,32 @@ class ProjectManager {
|
|
|
8
8
|
constructor() {
|
|
9
9
|
this.projects = [];
|
|
10
10
|
this._load();
|
|
11
|
+
if (this.projects.length === 0) {
|
|
12
|
+
this._autoRegister();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_autoRegister() {
|
|
17
|
+
// Scan parent directories up to 3 levels for ai-qa-workflow.js
|
|
18
|
+
let dir = path.resolve(__dirname, '..', '..');
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
const parent = path.resolve(dir, '..');
|
|
21
|
+
if (parent === dir) break;
|
|
22
|
+
dir = parent;
|
|
23
|
+
}
|
|
24
|
+
// Check current and parent dirs
|
|
25
|
+
const candidates = [
|
|
26
|
+
path.resolve(__dirname, '..', '..'), // 2 levels up (project root from qa-dashboard/services/)
|
|
27
|
+
path.resolve(__dirname, '..'), // qa-dashboard/
|
|
28
|
+
process.cwd(), // wherever node was called from
|
|
29
|
+
];
|
|
30
|
+
for (const c of [...new Set(candidates)]) {
|
|
31
|
+
if (c && fs.existsSync(path.join(c, 'ai-qa-workflow.js'))) {
|
|
32
|
+
const name = path.basename(c);
|
|
33
|
+
this.add(name, c);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
11
37
|
}
|
|
12
38
|
|
|
13
39
|
_load() {
|