@ai-qa/workflow 2.0.10 → 2.0.13
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/.github/agents/playwright-test-generator.agent.md +49 -0
- package/.github/agents/playwright-test-healer.agent.md +32 -3
- package/.github/agents/playwright-test-planner.agent.md +26 -0
- package/.github/copilot-instructions.md +44 -2
- package/.opencode/agents/qa-generator.md +16 -0
- package/.opencode/agents/qa-healer.md +18 -0
- package/.opencode/agents/qa-planner.md +17 -0
- package/.opencode/rules.md +66 -2
- package/.qa-context/auth.json +29 -0
- package/.qa-context/heal-history.json +40 -0
- package/.qa-context/pipeline.json +34 -0
- package/.qa-context/selectors.json +64 -0
- package/.qa-context/traceability.json +30 -0
- package/README.md +399 -196
- package/ai-qa-workflow.js +82 -104
- package/install.js +9 -14
- package/package.json +5 -7
- package/prompting_template.md +283 -0
- package/qa-dashboard/app.js +1 -0
- package/qa-dashboard/routes/review.js +114 -0
- package/qa-dashboard/views/layouts/main.ejs +1 -0
- package/qa-dashboard/views/review.ejs +201 -0
- package/router.md +109 -29
- package/scripts/auth-manager.js +186 -0
- package/scripts/context-manager.js +226 -0
- package/scripts/executor.js +18 -7
- package/scripts/generator.js +18 -124
- package/scripts/healer.js +78 -157
- package/scripts/planner.js +18 -136
- package/scripts/reporter.js +21 -1
- package/scripts/utils.js +2 -0
package/scripts/healer.js
CHANGED
|
@@ -1,192 +1,114 @@
|
|
|
1
|
-
const { DIRS,
|
|
1
|
+
const { DIRS, log } = require('./utils');
|
|
2
|
+
const context = require('./context-manager');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const fs = require('fs');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
{ pattern: /element\(s\) not found/i, type: 'selector' },
|
|
7
|
-
{ pattern: /strict mode violation/i, type: 'selector' },
|
|
8
|
-
{ pattern: /Target closed/i, type: 'environment' },
|
|
9
|
-
{ pattern: /net::ERR_/i, type: 'environment' },
|
|
10
|
-
{ pattern: /timeout/i, type: 'timing' },
|
|
11
|
-
{ pattern: /page\.goto/i, type: 'navigation' },
|
|
12
|
-
{ pattern: /Cannot find module/i, type: 'test-syntax' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function classifyFailure(errorMessage) {
|
|
16
|
-
if (!errorMessage) return { type: 'unknown', fixable: false };
|
|
17
|
-
for (const rule of KNOWN_BUG_PATTERNS) {
|
|
18
|
-
if (rule.pattern.test(errorMessage)) {
|
|
19
|
-
return { type: rule.type, fixable: rule.type !== 'environment' };
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return { type: 'unknown', fixable: false };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function selfHeal(runId, projectName, options = {}) {
|
|
26
|
-
const { maxAttempts = 2 } = options;
|
|
27
|
-
|
|
7
|
+
function selfHeal(runId) {
|
|
28
8
|
const runDir = runId
|
|
29
9
|
? path.join(DIRS.testResults, runId)
|
|
30
10
|
: findLatestRunDir();
|
|
31
11
|
|
|
32
12
|
if (!runDir || !fs.existsSync(runDir)) {
|
|
33
|
-
log('
|
|
34
|
-
return {
|
|
13
|
+
log('RETRY', 'No execution results found');
|
|
14
|
+
return { rerun: [], stillFailing: [] };
|
|
35
15
|
}
|
|
36
16
|
|
|
37
17
|
const resultPath = path.join(runDir, 'execution-result.json');
|
|
38
18
|
if (!fs.existsSync(resultPath)) {
|
|
39
|
-
log('
|
|
40
|
-
return {
|
|
19
|
+
log('RETRY', 'No execution result found');
|
|
20
|
+
return { rerun: [], stillFailing: [] };
|
|
41
21
|
}
|
|
42
22
|
|
|
43
23
|
const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
|
|
44
24
|
|
|
25
|
+
const storyName = result.story || null;
|
|
26
|
+
|
|
45
27
|
if (result.success) {
|
|
46
|
-
log('
|
|
47
|
-
return {
|
|
28
|
+
log('RETRY', 'All tests passed — nothing to retry');
|
|
29
|
+
return { rerun: [], stillFailing: [] };
|
|
48
30
|
}
|
|
49
31
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const defects = [];
|
|
54
|
-
let remainingFailures = [...(result.failedTests || [])];
|
|
55
|
-
|
|
56
|
-
log('HEALER', `Found ${remainingFailures.length} failed test(s)`);
|
|
57
|
-
log('HEALER', `Screenshots will be saved to: test-results/screenshots/${projectName || 'unknown'}/`);
|
|
58
|
-
|
|
59
|
-
remainingFailures = remainingFailures.filter(failure => {
|
|
60
|
-
const classification = classifyFailure(failure.error);
|
|
61
|
-
const screenshotPath = path.join(screenshotDir, `${slugify(failure.test || 'failure')}.png`);
|
|
62
|
-
|
|
63
|
-
defects.push({
|
|
64
|
-
test: failure.test || failure.file,
|
|
65
|
-
error: failure.error ? failure.error.substring(0, 200) : 'Unknown',
|
|
66
|
-
classification: classification.type,
|
|
67
|
-
verdict: classification.fixable ? 'FIXABLE' : 'BUG',
|
|
68
|
-
action: classification.fixable
|
|
69
|
-
? 'Attempting auto-heal'
|
|
70
|
-
: 'Cannot auto-fix — reporting as defect',
|
|
71
|
-
screenshot: screenshotPath,
|
|
72
|
-
});
|
|
73
|
-
log('HEALER', ` ${classification.fixable ? '⚠️' : '⛔'} ${failure.test || failure.file} — ${classification.type}${classification.fixable ? '' : ' (BUG)'}`);
|
|
74
|
-
|
|
75
|
-
if (!classification.fixable) return false;
|
|
76
|
-
return true;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const healingLog = [];
|
|
80
|
-
let attempt = 0;
|
|
81
|
-
|
|
82
|
-
while (attempt < maxAttempts && remainingFailures.length > 0) {
|
|
83
|
-
attempt++;
|
|
84
|
-
log('HEALER', `Healing attempt ${attempt}/${maxAttempts}`);
|
|
85
|
-
|
|
86
|
-
const healed = [];
|
|
87
|
-
const stillFailing = [];
|
|
88
|
-
|
|
89
|
-
for (const failure of remainingFailures) {
|
|
90
|
-
log('HEALER', ` Re-running: ${failure.test || failure.file}`);
|
|
91
|
-
const healedResult = attemptHealStrategy(failure, attempt, runDir);
|
|
92
|
-
|
|
93
|
-
if (healedResult.success) {
|
|
94
|
-
healed.push({ ...failure, healedOnAttempt: attempt, strategy: healedResult.strategy });
|
|
95
|
-
const defect = defects.find(d => d.test === failure.test);
|
|
96
|
-
if (defect) defect.healed = true;
|
|
97
|
-
log('HEALER', ` ✓ Healed: ${failure.test || failure.file}`);
|
|
98
|
-
} else {
|
|
99
|
-
stillFailing.push(failure);
|
|
100
|
-
log('HEALER', ` ✗ Still failing: ${failure.test || failure.file}`);
|
|
101
|
-
const defect = defects.find(d => d.test === failure.test);
|
|
102
|
-
if (defect) {
|
|
103
|
-
defect.verdict = 'UNSTABLE';
|
|
104
|
-
defect.action = 'Requires manual investigation';
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
32
|
+
const failedTests = result.failedTests || [];
|
|
33
|
+
log('RETRY', `Found ${failedTests.length} failed test(s)`);
|
|
108
34
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
35
|
+
const rerun = [];
|
|
36
|
+
const stillFailing = [];
|
|
37
|
+
const healingAttempts = [];
|
|
38
|
+
|
|
39
|
+
for (const failure of failedTests) {
|
|
40
|
+
const testFile = failure.file || failure.test || '';
|
|
41
|
+
log('RETRY', ` Re-running: ${testFile}`);
|
|
112
42
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
43
|
+
const startMs = Date.now();
|
|
44
|
+
const args = ['npx', 'playwright', 'test', testFile, '--reporter=list', '--timeout=60000'];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
execSync(args.join(' '), {
|
|
48
|
+
cwd: DIRS.ROOT,
|
|
49
|
+
encoding: 'utf-8',
|
|
50
|
+
timeout: 120000,
|
|
51
|
+
stdio: 'pipe',
|
|
121
52
|
});
|
|
53
|
+
rerun.push({ test: testFile, status: 'passed' });
|
|
54
|
+
log('RETRY', ` ✓ Passed on retry: ${testFile}`);
|
|
55
|
+
healingAttempts.push({ test: testFile, status: 'passed', duration_ms: Date.now() - startMs, strategy: 'timeout_retry' });
|
|
56
|
+
} catch {
|
|
57
|
+
stillFailing.push({ test: testFile, status: 'failed', error: failure.error || '' });
|
|
58
|
+
log('RETRY', ` ✗ Still failing: ${testFile}`);
|
|
59
|
+
healingAttempts.push({ test: testFile, status: 'failed', duration_ms: Date.now() - startMs, strategy: 'timeout_retry' });
|
|
122
60
|
}
|
|
123
|
-
}
|
|
61
|
+
}
|
|
124
62
|
|
|
125
63
|
const healingReport = {
|
|
126
|
-
projectName: projectName || 'unknown',
|
|
127
|
-
screenshotDir: `test-results/screenshots/${projectName || 'unknown'}/`,
|
|
128
64
|
runId: path.basename(runDir),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
totalHealed:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
65
|
+
story: storyName,
|
|
66
|
+
totalAttempts: healingAttempts.length,
|
|
67
|
+
totalHealed: rerun.length,
|
|
68
|
+
remainingFailures: stillFailing.length,
|
|
69
|
+
healingAttempts,
|
|
70
|
+
healedTests: rerun.map(r => ({ test: r.test, status: 'passed', healedOnAttempt: 1, strategy: 'timeout_retry' })),
|
|
71
|
+
stillFailingTests: stillFailing.map(f => ({ test: f.test, status: 'failed', error: f.error })),
|
|
135
72
|
timestamp: new Date().toISOString(),
|
|
136
|
-
finalStatus: remainingFailures.length === 0 && defects.filter(d => d.verdict !== 'FIXABLE').length === 0 ? 'all-passed'
|
|
137
|
-
: remainingFailures.length === 0 ? 'all-passed-with-defects'
|
|
138
|
-
: 'partial',
|
|
139
73
|
};
|
|
140
74
|
|
|
141
|
-
|
|
142
|
-
fs.writeFileSync(reportPath, JSON.stringify(healingReport, null, 2), 'utf-8');
|
|
143
|
-
|
|
144
|
-
log('HEALER', `Healing complete:`);
|
|
145
|
-
log('HEALER', ` Healed: ${healingReport.totalHealed}`);
|
|
146
|
-
log('HEALER', ` Defects found: ${defects.filter(d => d.verdict === 'BUG').length}`);
|
|
147
|
-
log('HEALER', ` Screenshots: test-results/screenshots/${projectName || 'unknown'}/`);
|
|
148
|
-
|
|
149
|
-
if (defects.length > 0) {
|
|
150
|
-
console.log('\n Defects:');
|
|
151
|
-
defects.forEach(d => {
|
|
152
|
-
const icon = d.verdict === 'BUG' ? '⛔ BUG' : d.verdict === 'UNSTABLE' ? '⚠️ UNSTABLE' : '✓ FIXED';
|
|
153
|
-
console.log(` ${icon} ${d.test}`);
|
|
154
|
-
console.log(` → ${d.classification}: ${d.error.substring(0, 100)}`);
|
|
155
|
-
console.log(` → Screenshot: ${d.screenshot}`);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
75
|
+
writeHealingReport(runDir, healingReport);
|
|
158
76
|
|
|
159
|
-
|
|
160
|
-
|
|
77
|
+
if (storyName) {
|
|
78
|
+
context.phaseComplete('heal', storyName);
|
|
79
|
+
if (rerun.length > 0) context.setHealed(storyName, path.basename(runDir));
|
|
80
|
+
}
|
|
161
81
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
return { success: true, strategy: strategy.name };
|
|
183
|
-
} catch {
|
|
184
|
-
return { success: false, strategy: strategy.name };
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log('╔════════════════════════════════════════════════════════════════╗');
|
|
84
|
+
console.log('║ DEEPER DIAGNOSIS is handled by the AI agent ║');
|
|
85
|
+
console.log('║ ║');
|
|
86
|
+
console.log('║ The AI reads playwright-test-healer.agent.md, debugs with ║');
|
|
87
|
+
console.log('║ Playwright MCP (test_debug, browser_snapshot, etc.), and ║');
|
|
88
|
+
console.log('║ fixes selectors/timing issues or marks real bugs. ║');
|
|
89
|
+
console.log('║ ║');
|
|
90
|
+
console.log('║ Tell your AI agent: ║');
|
|
91
|
+
console.log('║ "Debug and heal the failing tests in test-results/' + path.basename(runDir) + '"');
|
|
92
|
+
console.log('╚════════════════════════════════════════════════════════════════╝');
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
if (rerun.length > 0) {
|
|
96
|
+
console.log(` Re-ran ${rerun.length} test(s) with longer timeout`);
|
|
97
|
+
console.log(` Passed on retry: ${rerun.length}`);
|
|
98
|
+
}
|
|
99
|
+
if (stillFailing.length > 0) {
|
|
100
|
+
console.log(` Still failing: ${stillFailing.length}`);
|
|
101
|
+
stillFailing.forEach(f => console.log(` - ${f.test}`));
|
|
185
102
|
}
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
return { rerun, stillFailing };
|
|
186
106
|
}
|
|
187
107
|
|
|
188
|
-
function
|
|
189
|
-
|
|
108
|
+
function writeHealingReport(runDir, report) {
|
|
109
|
+
const reportPath = path.join(runDir, 'healing-report.json');
|
|
110
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
111
|
+
log('RETRY', `Healing report saved: ${reportPath}`);
|
|
190
112
|
}
|
|
191
113
|
|
|
192
114
|
function findLatestRunDir() {
|
|
@@ -200,8 +122,7 @@ function findLatestRunDir() {
|
|
|
200
122
|
|
|
201
123
|
if (require.main === module) {
|
|
202
124
|
const runId = process.argv[2];
|
|
203
|
-
|
|
204
|
-
selfHeal(runId, projectName);
|
|
125
|
+
selfHeal(runId);
|
|
205
126
|
}
|
|
206
127
|
|
|
207
128
|
module.exports = { selfHeal };
|
package/scripts/planner.js
CHANGED
|
@@ -1,142 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
log('
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const plan = generatePlanContent(story, planId);
|
|
20
|
-
|
|
21
|
-
writeMarkdown(planPath, plan);
|
|
22
|
-
log('PLANNER', `Test plan saved: specs/${planId}-test-plan.md`);
|
|
23
|
-
|
|
24
|
-
return { planId, planPath };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function generatePlanContent(story, planId) {
|
|
28
|
-
const date = new Date().toISOString().split('T')[0];
|
|
29
|
-
|
|
30
|
-
let plan = `# Test Plan: ${story.metaTitle || story.title}
|
|
31
|
-
|
|
32
|
-
**ID**: PLAN-${(story.id || planId).toUpperCase()}
|
|
33
|
-
**Feature**: ${story.title || story.metaTitle || 'N/A'}
|
|
34
|
-
**Status**: Draft
|
|
35
|
-
**User Story Ref**: ${story.id || 'N/A'}
|
|
36
|
-
**Generated**: ${date}
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## 1. Overview
|
|
41
|
-
|
|
42
|
-
${story.description || 'Automated test plan derived from user story.'}
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 2. Preconditions
|
|
47
|
-
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
|
-
if (story.preconditions && story.preconditions.length > 0) {
|
|
51
|
-
story.preconditions.forEach(p => { plan += `- ${p}\n`; });
|
|
52
|
-
} else {
|
|
53
|
-
plan += `- Environment: ${CONFIG.project.url}\n`;
|
|
54
|
-
plan += `- Browser: ${CONFIG.browser.type === 'edge' ? 'Microsoft Edge via CDP' : 'Chromium'} (Port ${CONFIG.browser.cdpPort})\n`;
|
|
55
|
-
plan += `- User credentials as specified in user story\n`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
plan += `
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## 3. Test Scenarios
|
|
63
|
-
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
if (story.acceptanceCriteria) {
|
|
67
|
-
const sections = story.acceptanceCriteria.split(/(?=###\s+\d)/);
|
|
68
|
-
sections.forEach((section, idx) => {
|
|
69
|
-
const trimmed = section.trim();
|
|
70
|
-
if (!trimmed) return;
|
|
71
|
-
|
|
72
|
-
const titleMatch = trimmed.match(/###\s+\d+\.\s*(.+?)(?:\n|$)/);
|
|
73
|
-
const title = titleMatch ? titleMatch[1].trim() : `Scenario ${idx + 1}`;
|
|
74
|
-
|
|
75
|
-
const steps = parseSteps(trimmed);
|
|
76
|
-
|
|
77
|
-
plan += `### ${idx + 3}.1 Scenario: ${title}\n`;
|
|
78
|
-
plan += `**Steps**:\n`;
|
|
79
|
-
steps.forEach((s, i) => { plan += `${i + 1}. ${s}\n`; });
|
|
80
|
-
plan += `\n**Expected Results**:\n`;
|
|
81
|
-
plan += `- Feature behaves as described in acceptance criteria\n`;
|
|
82
|
-
plan += `- No errors or unexpected behavior\n`;
|
|
83
|
-
plan += `\n---\n\n`;
|
|
84
|
-
});
|
|
85
|
-
} else {
|
|
86
|
-
plan += `### 3.1 Scenario: Happy Path\n`;
|
|
87
|
-
plan += `**Steps**:\n`;
|
|
88
|
-
plan += `1. Login with the specified user credentials\n`;
|
|
89
|
-
plan += `2. Navigate to the feature\n`;
|
|
90
|
-
plan += `3. Execute the primary workflow\n`;
|
|
91
|
-
plan += `4. Verify success state\n\n`;
|
|
92
|
-
plan += `**Expected Results**:\n`;
|
|
93
|
-
plan += `- Workflow completes successfully\n\n`;
|
|
94
|
-
plan += `---\n\n`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
plan += `## 4. Success Criteria\n`;
|
|
98
|
-
plan += `- All scenarios pass without errors\n`;
|
|
99
|
-
plan += `- Screenshots are captured for key verification points\n`;
|
|
100
|
-
plan += `- Allure report is generated with test results\n\n`;
|
|
101
|
-
|
|
102
|
-
plan += `---\n\n`;
|
|
103
|
-
plan += `> **AI Instructions**: Review this test plan. Verify scenarios cover all acceptance criteria.\n`;
|
|
104
|
-
plan += `> Add edge cases, negative scenarios, and boundary tests where applicable.\n`;
|
|
105
|
-
plan += `> Update status from "Draft" to "Reviewed" once validated.\n`;
|
|
106
|
-
|
|
107
|
-
return plan;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseSteps(text) {
|
|
111
|
-
const steps = [];
|
|
112
|
-
const lines = text.split('\n');
|
|
113
|
-
let inSteps = false;
|
|
114
|
-
|
|
115
|
-
for (const line of lines) {
|
|
116
|
-
const trimmed = line.trim();
|
|
117
|
-
if (/^\d+\.\s+/.test(trimmed)) {
|
|
118
|
-
inSteps = true;
|
|
119
|
-
steps.push(trimmed.replace(/^\d+\.\s+/, ''));
|
|
120
|
-
} else if (inSteps && trimmed.startsWith('-')) {
|
|
121
|
-
steps.push(trimmed.replace(/^- /, ''));
|
|
122
|
-
} else if (inSteps && trimmed === '') {
|
|
123
|
-
continue;
|
|
124
|
-
} else if (inSteps) {
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return steps.length > 0 ? steps : ['Execute the workflow as described in the user story.'];
|
|
1
|
+
function generateTestPlan() {
|
|
2
|
+
console.log('');
|
|
3
|
+
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
4
|
+
console.log('║ PLANNING is handled by the AI agent ║');
|
|
5
|
+
console.log('║ ║');
|
|
6
|
+
console.log('║ The AI reads playwright-test-planner.agent.md, ║');
|
|
7
|
+
console.log('║ explores your app with Playwright MCP, and writes ║');
|
|
8
|
+
console.log('║ the test plan directly to specs/. ║');
|
|
9
|
+
console.log('║ ║');
|
|
10
|
+
console.log('║ The AI will STOP and ask for your approval before ║');
|
|
11
|
+
console.log('║ proceeding to test generation. ║');
|
|
12
|
+
console.log('║ ║');
|
|
13
|
+
console.log('║ Tell your AI agent: ║');
|
|
14
|
+
console.log('║ "Read router.md and plan tests for my-story.md" ║');
|
|
15
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
16
|
+
console.log('');
|
|
17
|
+
process.exit(0);
|
|
130
18
|
}
|
|
131
19
|
|
|
132
20
|
if (require.main === module) {
|
|
133
|
-
|
|
134
|
-
if (!storyName) {
|
|
135
|
-
console.log('Usage: node scripts/planner.js <user-story-file.md>');
|
|
136
|
-
console.log(`Available: ${require('./utils').listUserStories().join(', ')}`);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
generateTestPlan(storyName);
|
|
21
|
+
generateTestPlan();
|
|
140
22
|
}
|
|
141
23
|
|
|
142
24
|
module.exports = { generateTestPlan };
|
package/scripts/reporter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { DIRS, ensureDir, readMarkdown, writeMarkdown, log } = require('./utils');
|
|
2
|
+
const context = require('./context-manager');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
|
|
@@ -26,6 +27,13 @@ function generateReport(runId) {
|
|
|
26
27
|
const resultPath = path.join(runDir, 'execution-result.json');
|
|
27
28
|
if (fs.existsSync(resultPath)) {
|
|
28
29
|
report.execution = JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
|
|
30
|
+
report.storyName = report.execution.story || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (report.storyName) {
|
|
34
|
+
const trace = context.getTraceability();
|
|
35
|
+
const storyTrace = trace.stories[report.storyName];
|
|
36
|
+
if (storyTrace) report.traceability = storyTrace;
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
const healingPath = path.join(runDir, 'healing-report.json');
|
|
@@ -56,6 +64,10 @@ function generateReport(runId) {
|
|
|
56
64
|
|
|
57
65
|
log('REPORTER', `Report saved: test-results/${path.basename(runDir)}/final-test-report.md`);
|
|
58
66
|
|
|
67
|
+
if (report.storyName) {
|
|
68
|
+
context.phaseComplete('report', report.storyName);
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
return {
|
|
60
72
|
reportPath,
|
|
61
73
|
stats: report.stats,
|
|
@@ -77,13 +89,21 @@ function listTestFilesWithStatus() {
|
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
function generateMarkdownReport(report) {
|
|
80
|
-
const { runId, stats, execution, healing, testFiles } = report;
|
|
92
|
+
const { runId, stats, execution, healing, testFiles, storyName, traceability } = report;
|
|
81
93
|
|
|
82
94
|
let md = `# AI QA Execution Report
|
|
83
95
|
|
|
84
96
|
**Run ID**: ${runId}
|
|
85
97
|
**Generated**: ${report.generatedAt}
|
|
86
98
|
**Status**: ${stats.failed === 0 ? '✅ PASSED' : '❌ FAILED'}
|
|
99
|
+
${storyName ? `**Story**: ${storyName}` : ''}
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Traceability
|
|
104
|
+
|
|
105
|
+
${storyName && traceability ? `| Story | Plan | Spec | Status |\n|---|---|---|---|\n| ${storyName} | ${traceability.plan || '—'} | ${traceability.spec || '—'} | ${traceability.status || '—'} |\n` : `No traceability data available.\n`}
|
|
106
|
+
${traceability && traceability.runs && traceability.runs.length > 1 ? `\n**Run History:** ${traceability.runs.length} run(s)\n` : ''}
|
|
87
107
|
|
|
88
108
|
---
|
|
89
109
|
|
package/scripts/utils.js
CHANGED
|
@@ -159,6 +159,8 @@ const DIRS = {
|
|
|
159
159
|
scratch: path.join(ROOT, 'scratch'),
|
|
160
160
|
docs: path.join(ROOT, 'docs'),
|
|
161
161
|
scripts: path.join(ROOT, 'scripts'),
|
|
162
|
+
qaContext: path.join(ROOT, '.qa-context'),
|
|
163
|
+
auth: path.join(ROOT, '.auth'),
|
|
162
164
|
};
|
|
163
165
|
|
|
164
166
|
function ensureDir(dir) {
|