@ai-qa/workflow 2.0.17 → 2.0.18

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.
@@ -5,7 +5,6 @@ const pm = require('../services/project-manager');
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
8
  const project = projects.length > 0 ? projects[0] : null;
10
9
 
11
10
  let stats = {
@@ -15,7 +14,10 @@ router.get('/', (req, res) => {
15
14
  runs: 0,
16
15
  passedRuns: 0,
17
16
  passRate: 0,
18
- lastRunStatus: null, // 'passed' | 'failed' | null
17
+ lastRunStatus: null,
18
+ storiesList: [],
19
+ latestRun: null,
20
+ hasAllure: false,
19
21
  };
20
22
 
21
23
  if (project) {
@@ -27,6 +29,18 @@ router.get('/', (req, res) => {
27
29
  stats.plans = list.plans.length;
28
30
  stats.specs = list.specs.length;
29
31
  stats.runs = runDirs.length;
32
+ stats.hasAllure = bridge.hasAllureReport();
33
+
34
+ stats.storiesList = list.stories.map(s => {
35
+ const baseName = s.replace(/\.md$/i, '');
36
+ return {
37
+ name: s,
38
+ hasPlan: list.plans.some(p => p.toLowerCase().includes(baseName.toLowerCase())),
39
+ planName: list.plans.find(p => p.toLowerCase().includes(baseName.toLowerCase())),
40
+ hasSpec: list.specs.some(sp => sp.toLowerCase().includes(baseName.toLowerCase())),
41
+ specName: list.specs.find(sp => sp.toLowerCase().includes(baseName.toLowerCase())),
42
+ };
43
+ });
30
44
 
31
45
  if (runDirs.length > 0) {
32
46
  const results = runDirs.map(d => bridge.getRunResult(d.name)).filter(Boolean);
@@ -35,9 +49,17 @@ router.get('/', (req, res) => {
35
49
  ? Math.round((stats.passedRuns / results.length) * 100)
36
50
  : 0;
37
51
 
38
- // Most recent run result
39
52
  const latest = bridge.getRunResult(runDirs[0].name);
40
- if (latest) stats.lastRunStatus = latest.success ? 'passed' : 'failed';
53
+ if (latest) {
54
+ stats.lastRunStatus = latest.success ? 'passed' : 'failed';
55
+ stats.latestRun = {
56
+ id: runDirs[0].name,
57
+ duration: latest.duration,
58
+ passedCount: latest.passedTests ? latest.passedTests.length : 0,
59
+ failedCount: latest.failedTests ? latest.failedTests.length : 0,
60
+ testName: latest.test,
61
+ };
62
+ }
41
63
  }
42
64
  }
43
65
 
@@ -29,7 +29,31 @@ router.get('/:id', (req, res) => {
29
29
  const statusResult = bridge.status();
30
30
  const list = bridge.listAll();
31
31
 
32
- res.render('project', { project, status: statusResult, list });
32
+ const storiesWithStatus = list.stories.map(s => {
33
+ const baseName = s.replace(/\.md$/i, '');
34
+ const planName = list.plans.find(p => p.toLowerCase().includes(baseName.toLowerCase()));
35
+ const specName = list.specs.find(sp => sp.toLowerCase().includes(baseName.toLowerCase()));
36
+ const runDirs = bridge.getRunDirs();
37
+ const hasRun = runDirs.some(d => {
38
+ const result = bridge.getRunResult(d.name);
39
+ return result && result.test && result.test.toLowerCase().includes(baseName.toLowerCase());
40
+ });
41
+ return {
42
+ name: s,
43
+ baseName,
44
+ hasPlan: !!planName,
45
+ planName,
46
+ hasSpec: !!specName,
47
+ specName,
48
+ hasRun,
49
+ };
50
+ });
51
+
52
+ const hasAllure = bridge.hasAllureReport();
53
+ const runDirs = bridge.getRunDirs();
54
+ const latestRun = runDirs.length > 0 ? runDirs[0].name : null;
55
+
56
+ res.render('project', { project, status: statusResult, list, storiesWithStatus, hasAllure, latestRun });
33
57
  });
34
58
 
35
59
  router.post('/:id/remove', (req, res) => {
@@ -115,4 +115,42 @@ router.get('/:runId/compare/:otherRunId', (req, res) => {
115
115
  ] });
116
116
  });
117
117
 
118
+ // Rerun a specific test from a previous run
119
+ router.post('/:runId/rerun', (req, res) => {
120
+ const projectId = req.query.project;
121
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
122
+ if (!project) return res.status(404).json({ error: 'Project not found' });
123
+
124
+ const bridge = pm.getBridge(project.id);
125
+ const result = bridge.getRunResult(req.params.runId);
126
+ if (!result) return res.status(404).json({ error: 'Run not found' });
127
+
128
+ const testName = result.test || '';
129
+ if (!testName || testName === 'all') {
130
+ // Rerun all tests
131
+ const runResult = bridge.execute('');
132
+ return res.json({ success: runResult.success, output: runResult.output, error: runResult.error });
133
+ }
134
+
135
+ const runResult = bridge.execute(testName);
136
+ res.json({ success: runResult.success, output: runResult.output, error: runResult.error, runId: extractRunId(runResult.output) });
137
+ });
138
+
139
+ // Regenerate Allure report
140
+ router.post('/allure-regenerate', (req, res) => {
141
+ const projectId = req.query.project;
142
+ const project = projectId ? pm.get(projectId) : pm.getAll()[0];
143
+ if (!project) return res.status(404).json({ error: 'Project not found' });
144
+
145
+ const bridge = pm.getBridge(project.id);
146
+ const result = bridge.runCommand('npx allure generate allure-results --clean -o allure-report');
147
+ res.json({ success: result.success, output: result.output, error: result.error });
148
+ });
149
+
150
+ function extractRunId(output) {
151
+ if (!output) return null;
152
+ const match = output.match(/run-[\d-T-]+/);
153
+ return match ? match[0] : null;
154
+ }
155
+
118
156
  module.exports = router;
@@ -44,7 +44,24 @@ router.get('/:storyName', (req, res) => {
44
44
  const plan = planName ? bridge.getPlanContent(planName) : null;
45
45
  const spec = specName ? bridge.getSpecContent(specName) : null;
46
46
 
47
- res.render('story', { project, story: { name: req.params.storyName, content, plan, planName, spec, specName } });
47
+ const runDirs = bridge.getRunDirs();
48
+ const results = runDirs.map(d => {
49
+ const r = bridge.getRunResult(d.name);
50
+ if (!r) return null;
51
+ const testMatch = specName ? r.test && r.test.toLowerCase().includes(baseName.toLowerCase()) : true;
52
+ return testMatch ? {
53
+ id: d.name,
54
+ success: r.success,
55
+ duration: r.duration,
56
+ passedCount: r.passedTests ? r.passedTests.length : 0,
57
+ failedCount: r.failedTests ? r.failedTests.length : 0,
58
+ } : null;
59
+ }).filter(Boolean);
60
+
61
+ const hasRun = results.length > 0;
62
+ const hasAllure = bridge.hasAllureReport();
63
+
64
+ res.render('story', { project, story: { name: req.params.storyName, content, plan, planName, spec, specName, results, hasRun, hasAllure } });
48
65
  });
49
66
 
50
67
  // Helper to stream CLI command output asynchronously to client
@@ -0,0 +1,51 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const pm = require('../services/project-manager');
4
+ const { spawn } = require('child_process');
5
+
6
+ router.get('/', (req, res) => {
7
+ const projects = pm.getAll();
8
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
9
+ const project = projects.find(p => p.id === projectId);
10
+ res.render('terminal', { project, projects });
11
+ });
12
+
13
+ // Execute a command asynchronously with streaming output
14
+ router.post('/run', (req, res) => {
15
+ const projects = pm.getAll();
16
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
17
+ const project = projects.find(p => p.id === projectId);
18
+ if (!project) return res.status(404).send('Project not found');
19
+
20
+ const { command } = req.body;
21
+ if (!command) return res.status(400).send('No command provided');
22
+
23
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
24
+ res.setHeader('Transfer-Encoding', 'chunked');
25
+ res.setHeader('X-Content-Type-Options', 'nosniff');
26
+
27
+ const child = spawn(command, [], {
28
+ cwd: project.path,
29
+ shell: true,
30
+ env: { ...process.env, FORCE_COLOR: '0' },
31
+ });
32
+
33
+ child.stdout.on('data', (data) => res.write(data.toString()));
34
+ child.stderr.on('data', (data) => res.write(data.toString()));
35
+
36
+ child.on('close', (code) => {
37
+ res.write(`\n\n--- [FINISHED: ${code === 0 ? 'SUCCESS' : 'FAILED'}] (exit code: ${code}) ---\n`);
38
+ res.end();
39
+ });
40
+
41
+ child.on('error', (err) => {
42
+ res.write(`\nError: ${err.message}\n`);
43
+ res.end();
44
+ });
45
+
46
+ req.on('close', () => {
47
+ if (child && !child.killed) child.kill('SIGINT');
48
+ });
49
+ });
50
+
51
+ module.exports = router;
@@ -23,7 +23,51 @@ router.get('/', (req, res) => {
23
23
  });
24
24
  }
25
25
 
26
- res.render('test-data', { project, projects, dataFiles });
26
+ let testSpecs = [];
27
+ let testPlans = [];
28
+ if (project) {
29
+ const bridge = pm.getBridge(project.id);
30
+ testSpecs = bridge.listTestSpecs().map(specName => ({
31
+ name: specName,
32
+ content: bridge.getSpecContent(specName),
33
+ }));
34
+ testPlans = bridge.listTestPlans().map(planName => ({
35
+ name: planName,
36
+ content: bridge.getPlanContent(planName),
37
+ }));
38
+ }
39
+
40
+ res.render('test-data', { project, projects, dataFiles, testSpecs, testPlans });
41
+ });
42
+
43
+ router.get('/spec/:specName', (req, res) => {
44
+ const projects = pm.getAll();
45
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
46
+ const project = projects.find(p => p.id === projectId);
47
+ if (!project) return res.status(404).send('Project not found');
48
+
49
+ const bridge = pm.getBridge(project.id);
50
+ const content = bridge.getSpecContent(req.params.specName);
51
+ if (!content) return res.status(404).send('Spec not found');
52
+
53
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
54
+ res.setHeader('Content-Disposition', `attachment; filename="${req.params.specName}"`);
55
+ res.send(content);
56
+ });
57
+
58
+ router.get('/plan/:planName', (req, res) => {
59
+ const projects = pm.getAll();
60
+ const projectId = req.query.project || (projects.length > 0 ? projects[0].id : null);
61
+ const project = projects.find(p => p.id === projectId);
62
+ if (!project) return res.status(404).send('Project not found');
63
+
64
+ const bridge = pm.getBridge(project.id);
65
+ const content = bridge.getPlanContent(req.params.planName);
66
+ if (!content) return res.status(404).send('Plan not found');
67
+
68
+ res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
69
+ res.setHeader('Content-Disposition', `attachment; filename="${req.params.planName}"`);
70
+ res.send(content);
27
71
  });
28
72
 
29
73
  router.post('/generate', (req, res) => {
@@ -79,4 +123,4 @@ router.delete('/:file', (req, res) => {
79
123
  }
80
124
  });
81
125
 
82
- module.exports = router;
126
+ module.exports = router;
@@ -173,6 +173,10 @@ class CliBridge {
173
173
  return fs.readFileSync(reportPath, 'utf-8');
174
174
  }
175
175
 
176
+ runCommand(command) {
177
+ return this._run(command);
178
+ }
179
+
176
180
  hasAllureReport() {
177
181
  return fs.existsSync(path.join(this.projectPath, 'allure-report', 'index.html'));
178
182
  }