@ai-qa/workflow 2.0.11 → 2.0.14

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.
@@ -1,130 +1,24 @@
1
- const { DIRS, CONFIG, readMarkdown, writeMarkdown, slugify, log } = require('./utils');
2
- const path = require('path');
3
- const fs = require('fs');
4
-
5
- function generateTestSpec(planName) {
6
- const planPath = path.join(DIRS.specs, planName);
7
- if (!fs.existsSync(planPath)) {
8
- console.error(`Test plan not found: ${planPath}`);
9
- console.log(`Available plans: ${require('./utils').listTestPlans().join(', ')}`);
10
- process.exit(1);
11
- }
12
-
13
- const content = readMarkdown(planPath);
14
- const specName = planName.replace('-test-plan.md', '');
15
- const specPath = path.join(DIRS.tests, `${specName}.spec.ts`);
16
-
17
- log('GENERATOR', `Generating test spec from: ${planName}`);
18
-
19
- const spec = generateSpecContent(content, specName, planName);
20
-
21
- writeMarkdown(specPath, spec);
22
- log('GENERATOR', `Test spec saved: tests/${specName}.spec.ts`);
23
-
24
- return { specName, specPath };
25
- }
26
-
27
- function generateSpecContent(planContent, specName, planFileName) {
28
- const featureMatch = planContent.match(/\*\*Feature\*\*:\s*(.+)/);
29
- const feature = featureMatch ? featureMatch[1].trim() : specName;
30
-
31
- const scenarios = extractScenarios(planContent);
32
-
33
- const browserType = CONFIG.browser.type || 'chromium';
34
- const cdpPort = CONFIG.browser.cdpPort || 9222;
35
- const cdpImport = browserType === 'edge' ? "import { startEdgeCDP } from '../cdpSession';" : `import { chromium } from '@playwright/test';`;
36
-
37
- let setupBlock;
38
- if (browserType === 'edge') {
39
- setupBlock = ` // const { page } = await startEdgeCDP();\n // global.page = page;`;
40
- } else {
41
- setupBlock = ` // const browser = await chromium.launch({ headless: !process.env.HEADED });\n // const page = await browser.newPage();\n // global.page = page;`;
42
- }
43
-
44
- let spec = `import { test, expect } from '@playwright/test';
45
- ${cdpImport}
46
-
47
- test.describe('${feature.replace(/'/g, "\\'")}', () => {
48
-
49
- test.beforeEach(async () => {
50
- ${setupBlock}
51
- });
52
-
53
- test.afterEach(async () => {
54
- // Cleanup if needed
55
- });
56
-
57
- `;
58
-
59
- scenarios.forEach((scenario, idx) => {
60
- const safeTitle = scenario.title.replace(/['"]/g, "'");
61
- spec += ` test('${safeTitle}', async () => {\n`;
62
- spec += ` // TODO: Implement test steps from plan\n`;
63
- spec += ` // Reference: specs/${planFileName} (Scenario ${idx + 3}.1)\n`;
64
- spec += ` // \n`;
65
-
66
- scenario.steps.forEach((step, si) => {
67
- const truncated = step.length > 100 ? step.substring(0, 97) + '...' : step;
68
- spec += ` // Step ${si + 1}: ${truncated}\n`;
69
- });
70
-
71
- spec += ` // \n`;
72
- spec += ` // Expected: ${scenario.expected || 'Workflow completes successfully'}\n`;
73
- spec += ` \n`;
74
- spec += ` });\n\n`;
75
- });
76
-
77
- spec += `});\n`;
78
-
79
- return spec;
80
- }
81
-
82
- function extractScenarios(content) {
83
- const scenarios = [];
84
- const lines = content.split('\n');
85
- let current = null;
86
-
87
- for (let i = 0; i < lines.length; i++) {
88
- const line = lines[i];
89
-
90
- const scenarioMatch = line.match(/###\s+\d+\.\d+\s+Scenario:\s*(.+)/i);
91
- if (scenarioMatch) {
92
- if (current) scenarios.push(current);
93
- current = { title: scenarioMatch[1].trim(), steps: [], expected: '' };
94
- continue;
95
- }
96
-
97
- if (current) {
98
- const stepMatch = line.match(/^\d+\.\s+(.+)/);
99
- if (stepMatch && !line.match(/^\d+\.\s+\*\*/)) {
100
- current.steps.push(stepMatch[1].trim());
101
- }
102
-
103
- const expectedMatch = line.match(/Expected Results?[:\]]\s*(.+)/i);
104
- if (expectedMatch) {
105
- current.expected = expectedMatch[1].trim();
106
- }
107
-
108
- if (line.match(/---/) && current.steps.length > 0) {
109
- scenarios.push(current);
110
- current = null;
111
- }
112
- }
113
- }
114
-
115
- if (current) scenarios.push(current);
116
-
117
- return scenarios;
1
+ function generateTestSpec() {
2
+ console.log('');
3
+ console.log('╔══════════════════════════════════════════════════════════╗');
4
+ console.log('║ TEST GENERATION is handled by the AI agent ║');
5
+ console.log('║ ║');
6
+ console.log('║ The AI reads playwright-test-generator.agent.md, ║');
7
+ console.log('║ uses Playwright MCP to capture real selectors, and ║');
8
+ console.log('║ writes complete Playwright tests directly to tests/. ║');
9
+ console.log('║ ║');
10
+ console.log('║ The AI will STOP and ask for your approval before ║');
11
+ console.log('║ proceeding to execution. ║');
12
+ console.log('║ ║');
13
+ console.log('║ Tell your AI agent: ║');
14
+ console.log('║ "Generate tests for the plan in specs/" ║');
15
+ console.log('╚══════════════════════════════════════════════════════════╝');
16
+ console.log('');
17
+ process.exit(0);
118
18
  }
119
19
 
120
20
  if (require.main === module) {
121
- const planName = process.argv[2];
122
- if (!planName) {
123
- console.log('Usage: node scripts/generator.js <test-plan-file.md>');
124
- console.log(`Available: ${require('./utils').listTestPlans().join(', ')}`);
125
- process.exit(1);
126
- }
127
- generateTestSpec(planName);
21
+ generateTestSpec();
128
22
  }
129
23
 
130
24
  module.exports = { generateTestSpec };
package/scripts/healer.js CHANGED
@@ -1,192 +1,114 @@
1
- const { DIRS, ensureDir, timestamp, log } = require('./utils');
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
- const KNOWN_BUG_PATTERNS = [
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('HEALER', 'No execution results found to heal');
34
- return { healed: [], remaining: [], defects: [] };
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('HEALER', 'No execution result found');
40
- return { healed: [], remaining: [], defects: [] };
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('HEALER', 'All tests passed - nothing to heal');
47
- return { healed: [], remaining: [], defects: [] };
28
+ log('RETRY', 'All tests passed nothing to retry');
29
+ return { rerun: [], stillFailing: [] };
48
30
  }
49
31
 
50
- const screenshotDir = path.join(DIRS.testResults, 'screenshots', projectName || 'unknown');
51
- ensureDir(screenshotDir);
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
- healingLog.push({ attempt, healed, stillFailing, timestamp: new Date().toISOString() });
110
- remainingFailures = stillFailing;
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
- remainingFailures.forEach(f => {
114
- if (!defects.find(d => d.test === f.test)) {
115
- defects.push({
116
- test: f.test || f.file,
117
- error: f.error ? f.error.substring(0, 200) : 'Failed after healing',
118
- classification: 'unresolved',
119
- verdict: 'UNSTABLE',
120
- action: 'Requires manual investigation',
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
- originalResult: { total: result.failedTests ? result.failedTests.length : 0 },
130
- healingAttempts: healingLog,
131
- totalHealed: healingLog.reduce((sum, a) => sum + a.healed.length, 0),
132
- totalRemaining: remainingFailures.length,
133
- totalDefects: defects.length,
134
- defects,
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
- const reportPath = path.join(runDir, 'healing-report.json');
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
- return { healed: healingLog.flatMap(a => a.healed), remaining: remainingFailures, defects, reportPath };
160
- }
77
+ if (storyName) {
78
+ context.phaseComplete('heal', storyName);
79
+ if (rerun.length > 0) context.setHealed(storyName, path.basename(runDir));
80
+ }
161
81
 
162
- function attemptHealStrategy(failure, attempt, runDir) {
163
- const strategies = [
164
- { name: 'retry-standard' },
165
- { name: 'retry-longer-timeout', timeout: 60000 },
166
- ];
167
-
168
- const strategy = strategies[Math.min(attempt - 1, strategies.length - 1)];
169
- const testFile = failure.file || failure.test || '';
170
-
171
- try {
172
- const args = ['npx', 'playwright', 'test', testFile, '--reporter=list'];
173
- if (strategy.timeout) args.push(`--timeout=${strategy.timeout}`);
174
-
175
- require('child_process').execSync(args.join(' '), {
176
- cwd: DIRS.ROOT,
177
- encoding: 'utf-8',
178
- timeout: 120000,
179
- stdio: 'pipe',
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 slugify(text) {
189
- return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 80);
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
- const projectName = process.argv[3];
204
- selfHeal(runId, projectName);
125
+ selfHeal(runId);
205
126
  }
206
127
 
207
128
  module.exports = { selfHeal };
@@ -1,142 +1,24 @@
1
- const { DIRS, CONFIG, parseUserStory, readMarkdown, writeMarkdown, slugify, log } = require('./utils');
2
- const path = require('path');
3
-
4
- function generateTestPlan(storyName) {
5
- const storyPath = path.join(DIRS.userStory, storyName);
6
- if (!require('fs').existsSync(storyPath)) {
7
- console.error(`User story not found: ${storyPath}`);
8
- console.log(`Available stories: ${require('./utils').listUserStories().join(', ')}`);
9
- process.exit(1);
10
- }
11
-
12
- const story = parseUserStory(storyPath);
13
- const planId = slugify(story.id || story.title || storyName.replace('.md', ''));
14
- const planPath = path.join(DIRS.specs, `${planId}-test-plan.md`);
15
-
16
- log('PLANNER', `Generating test plan from: ${storyName}`);
17
- log('PLANNER', `Story: ${story.metaTitle || story.title}`);
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
- const storyName = process.argv[2];
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 };
@@ -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) {
package/qa-dashboard/.env DELETED
@@ -1,3 +0,0 @@
1
- PORT=4000
2
- SESSION_SECRET=qa-dashboard-secret
3
- DEFAULT_PROJECT_PATH=C:\Users\aitbe\test_wakil