@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.
@@ -0,0 +1,226 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const ROOT = path.resolve(__dirname, '..');
5
+ const CONTEXT_DIR = path.join(ROOT, '.qa-context');
6
+
7
+ function ensureDir(dir) {
8
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
9
+ }
10
+
11
+ function readJSON(filePath) {
12
+ if (!fs.existsSync(filePath)) return null;
13
+ try {
14
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
15
+ } catch { return null; }
16
+ }
17
+
18
+ function writeJSON(filePath, data) {
19
+ ensureDir(path.dirname(filePath));
20
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
21
+ }
22
+
23
+ function pipelinePath() { return path.join(CONTEXT_DIR, 'pipeline.json'); }
24
+ function selectorsPath() { return path.join(CONTEXT_DIR, 'selectors.json'); }
25
+ function healHistoryPath() { return path.join(CONTEXT_DIR, 'heal-history.json'); }
26
+ function traceabilityPath() { return path.join(CONTEXT_DIR, 'traceability.json'); }
27
+
28
+ const context = {
29
+
30
+ // ── Pipeline State ──
31
+
32
+ getPipeline() {
33
+ return readJSON(pipelinePath()) || {
34
+ project_path: '',
35
+ phases: { plan: { completed: [], in_progress: null }, generate: { completed: [], in_progress: null }, execute: { completed: [], in_progress: null }, heal: { completed: [], in_progress: null }, report: { completed: [], in_progress: null } },
36
+ current_story: null,
37
+ last_run_id: null,
38
+ updated_at: '',
39
+ };
40
+ },
41
+
42
+ setPipeline(updates) {
43
+ const data = this.getPipeline();
44
+ Object.assign(data, updates);
45
+ data.updated_at = new Date().toISOString();
46
+ writeJSON(pipelinePath(), data);
47
+ },
48
+
49
+ phaseComplete(phase, item) {
50
+ const data = this.getPipeline();
51
+ if (!data.phases[phase]) data.phases[phase] = { completed: [], in_progress: null };
52
+ if (!data.phases[phase].completed.includes(item)) {
53
+ data.phases[phase].completed.push(item);
54
+ }
55
+ data.phases[phase].in_progress = null;
56
+ data.updated_at = new Date().toISOString();
57
+ writeJSON(pipelinePath(), data);
58
+ },
59
+
60
+ phaseInProgress(phase, item) {
61
+ const data = this.getPipeline();
62
+ if (!data.phases[phase]) data.phases[phase] = { completed: [], in_progress: null };
63
+ data.phases[phase].in_progress = item;
64
+ data.updated_at = new Date().toISOString();
65
+ writeJSON(pipelinePath(), data);
66
+ },
67
+
68
+ setCurrentStory(story) {
69
+ this.setPipeline({ current_story: story });
70
+ },
71
+
72
+ setLastRun(runId) {
73
+ this.setPipeline({ last_run_id: runId });
74
+ },
75
+
76
+ // ── Selectors Memory ──
77
+
78
+ getSelectors() {
79
+ return readJSON(selectorsPath()) || [];
80
+ },
81
+
82
+ addSelector(page, element, selectorValue, options = {}) {
83
+ const selectors = this.getSelectors();
84
+ let entry = selectors.find(s => s.page === page && s.element === element);
85
+ if (!entry) {
86
+ entry = { page, element, selectors: [], recommended: selectorValue };
87
+ selectors.push(entry);
88
+ }
89
+ const existing = entry.selectors.find(s => s.value === selectorValue);
90
+ if (existing) {
91
+ existing.last_used = new Date().toISOString();
92
+ if (options.reliability !== undefined) existing.reliability = options.reliability;
93
+ if (options.healed) existing.healed = true;
94
+ if (options.original) existing.original = options.original;
95
+ } else {
96
+ entry.selectors.push({
97
+ value: selectorValue,
98
+ type: options.type || 'css',
99
+ reliability: options.reliability || 0.5,
100
+ healed: options.healed || false,
101
+ original: options.original || null,
102
+ strategy: options.strategy || null,
103
+ healed_at: options.healed_at || null,
104
+ run_id: options.run_id || null,
105
+ first_seen: new Date().toISOString(),
106
+ last_used: new Date().toISOString(),
107
+ });
108
+ }
109
+ entry.selectors.sort((a, b) => b.reliability - a.reliability);
110
+ entry.recommended = entry.selectors[0].value;
111
+ writeJSON(selectorsPath(), selectors);
112
+ },
113
+
114
+ getRecommendedSelector(page, element) {
115
+ const selectors = this.getSelectors();
116
+ const entry = selectors.find(s => s.page === page && s.element === element);
117
+ return entry ? entry.recommended : null;
118
+ },
119
+
120
+ updateSelectorReliability(page, element, selectorValue, succeeded) {
121
+ const selectors = this.getSelectors();
122
+ const entry = selectors.find(s => s.page === page && s.element === element);
123
+ if (!entry) return;
124
+ const sel = entry.selectors.find(s => s.value === selectorValue);
125
+ if (!sel) return;
126
+ sel.reliability = succeeded
127
+ ? Math.min(1, sel.reliability + 0.1)
128
+ : Math.max(0, sel.reliability - 0.2);
129
+ sel.last_used = new Date().toISOString();
130
+ entry.selectors.sort((a, b) => b.reliability - a.reliability);
131
+ entry.recommended = entry.selectors[0].value;
132
+ writeJSON(selectorsPath(), selectors);
133
+ },
134
+
135
+ // ── Healing History ──
136
+
137
+ getHealHistory() {
138
+ return readJSON(healHistoryPath()) || [];
139
+ },
140
+
141
+ addHealAttempt(runId, story, test, element, attempts, success) {
142
+ const history = this.getHealHistory();
143
+ history.push({
144
+ run_id: runId,
145
+ story,
146
+ test,
147
+ element,
148
+ original_selector: attempts[0] && attempts[0].original ? attempts[0].original : null,
149
+ failure_reason: attempts[0] && attempts[0].error || null,
150
+ attempts: attempts.map(a => ({
151
+ selector: a.selector,
152
+ strategy: a.strategy || 'retry',
153
+ success: a.success,
154
+ duration_ms: a.duration_ms || 0,
155
+ })),
156
+ success,
157
+ duration_ms: attempts.reduce((s, a) => s + (a.duration_ms || 0), 0),
158
+ timestamp: new Date().toISOString(),
159
+ });
160
+ writeJSON(healHistoryPath(), history);
161
+ return history;
162
+ },
163
+
164
+ getHealHistoryForStory(story) {
165
+ return this.getHealHistory().filter(h => h.story === story);
166
+ },
167
+
168
+ // ── Traceability ──
169
+
170
+ getTraceability() {
171
+ return readJSON(traceabilityPath()) || { stories: {} };
172
+ },
173
+
174
+ addRunId(story, runId, success, testName) {
175
+ const data = this.getTraceability();
176
+ if (!data.stories[story]) {
177
+ data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
178
+ }
179
+ data.stories[story].runs.push({ run_id: runId, success, test: testName || null, timestamp: new Date().toISOString() });
180
+ data.stories[story].status = success ? 'stable' : 'unstable';
181
+ writeJSON(traceabilityPath(), data);
182
+ },
183
+
184
+ addPlan(story, planFile) {
185
+ const data = this.getTraceability();
186
+ if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
187
+ data.stories[story].plan = planFile;
188
+ writeJSON(traceabilityPath(), data);
189
+ },
190
+
191
+ addSpec(story, specFile) {
192
+ const data = this.getTraceability();
193
+ if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
194
+ data.stories[story].spec = specFile;
195
+ writeJSON(traceabilityPath(), data);
196
+ },
197
+
198
+ setHealed(story, runId) {
199
+ const data = this.getTraceability();
200
+ if (!data.stories[story]) data.stories[story] = { plan: null, spec: null, runs: [], latest_heal: null, status: 'pending' };
201
+ data.stories[story].latest_heal = runId;
202
+ writeJSON(traceabilityPath(), data);
203
+ },
204
+
205
+ // ── Init ──
206
+
207
+ init() {
208
+ ensureDir(CONTEXT_DIR);
209
+ for (const file of ['pipeline.json', 'selectors.json', 'heal-history.json', 'traceability.json']) {
210
+ const fp = path.join(CONTEXT_DIR, file);
211
+ if (!fs.existsSync(fp)) {
212
+ const defaults = {
213
+ 'pipeline.json': { project_path: '', phases: { plan: { completed: [], in_progress: null }, generate: { completed: [], in_progress: null }, execute: { completed: [], in_progress: null }, heal: { completed: [], in_progress: null }, report: { completed: [], in_progress: null } }, current_story: null, last_run_id: null, updated_at: '' },
214
+ 'selectors.json': [],
215
+ 'heal-history.json': [],
216
+ 'traceability.json': { stories: {} },
217
+ };
218
+ writeJSON(fp, defaults[file]);
219
+ }
220
+ }
221
+ },
222
+
223
+ contextDir: CONTEXT_DIR,
224
+ };
225
+
226
+ module.exports = context;
@@ -1,4 +1,5 @@
1
1
  const { DIRS, CONFIG, ensureDir, timestamp, log, writeMarkdown } = require('./utils');
2
+ const context = require('./context-manager');
2
3
  const path = require('path');
3
4
  const fs = require('fs');
4
5
  const { execSync, spawn } = require('child_process');
@@ -10,15 +11,18 @@ function executeTests(testName, options = {}) {
10
11
  timeout = testCfg.timeout || 120000,
11
12
  retries = testCfg.retries || 0,
12
13
  workers = testCfg.workers || 1,
13
- screenshots = 'off',
14
+ //screenshots = 'off',
14
15
  } = options;
15
16
 
16
17
  const runId = `run-${timestamp()}`;
17
18
  const runDir = path.join(DIRS.testResults, runId);
18
19
  ensureDir(runDir);
19
20
 
21
+ const storyName = testName ? testName.replace(/\.spec\.ts$/, '').split(/[\/\\]/).pop() : null;
22
+
20
23
  log('EXECUTOR', `Run ID: ${runId}`);
21
24
  log('EXECUTOR', `Test: ${testName || 'all tests'}`);
25
+ if (storyName) log('EXECUTOR', `Story: ${storyName}`);
22
26
 
23
27
  const args = ['playwright', 'test'];
24
28
 
@@ -37,12 +41,7 @@ function executeTests(testName, options = {}) {
37
41
  if (retries > 0) args.push('--retries', retries.toString());
38
42
  if (workers) args.push('--workers', workers.toString());
39
43
  if (timeout) args.push('--timeout', timeout.toString());
40
- if (screenshots === 'off') {
41
- args.push('--screenshot', 'off');
42
- }
43
- if (screenshots === 'only-on-failure') {
44
- args.push('--screenshot', 'only-on-failure');
45
- }
44
+ // Screenshots configured in playwright.config.ts — not a valid CLI arg
46
45
 
47
46
  const outputPath = path.join(runDir, 'execution-output.json');
48
47
  const resultPath = path.join(runDir, 'execution-result.json');
@@ -64,6 +63,7 @@ function executeTests(testName, options = {}) {
64
63
 
65
64
  const result = {
66
65
  runId,
66
+ story: storyName,
67
67
  test: testName || 'all',
68
68
  success: true,
69
69
  exitCode: 0,
@@ -75,6 +75,11 @@ function executeTests(testName, options = {}) {
75
75
  };
76
76
 
77
77
  writeMarkdown(resultPath, JSON.stringify(result, null, 2));
78
+ context.setLastRun(runId);
79
+ if (storyName) {
80
+ context.addRunId(storyName, runId, true, testName);
81
+ context.phaseComplete('execute', storyName);
82
+ }
78
83
  log('EXECUTOR', `Tests passed (${result.duration}ms)`);
79
84
 
80
85
  return result;
@@ -89,6 +94,7 @@ function executeTests(testName, options = {}) {
89
94
 
90
95
  const result = {
91
96
  runId,
97
+ story: storyName,
92
98
  test: testName || 'all',
93
99
  success: false,
94
100
  exitCode: err.status || 1,
@@ -100,6 +106,11 @@ function executeTests(testName, options = {}) {
100
106
  };
101
107
 
102
108
  writeMarkdown(resultPath, JSON.stringify(result, null, 2));
109
+ context.setLastRun(runId);
110
+ if (storyName) {
111
+ context.addRunId(storyName, runId, false, testName);
112
+ context.phaseComplete('execute', storyName);
113
+ }
103
114
  log('EXECUTOR', `Tests failed (${result.duration}ms) - ${failedTests.length} failure(s)`);
104
115
 
105
116
  return result;
@@ -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 };