@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.
package/router.md CHANGED
@@ -1,29 +1,109 @@
1
- # 🧠 AI QA Workflow - Agent Router (Core Engine)
2
-
3
- Tu es l'Intelligence Artificielle (Copilot, RooCode, Cursor,opencode, Antigravity, Gemini, etc.) assignée à ce projet d'automatisation QA.
4
- Ce projet contient plusieurs profils d'agents (dans `.github/agents/` ou `agents/`) et plusieurs instructions (dans `prompts/`).
5
-
6
- **RÈGLE STRICTE :** Tu DOIS analyser l'intention de l'utilisateur et lire automatiquement le fichier correspondant avant de générer ta réponse ou d'exécuter du code.
7
-
8
- ## 🧭 Routage Automatique du Contexte
9
-
10
- ### 1. Planification et Exploration (Intention : "Teste ce lien", "Explore cette app", "Fais un plan pour cette story")
11
- * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-planner.agent.md`
12
- * **Comportement attendu :** Utilise tes outils MCP Playwright pour naviguer sur l'application, faire des snapshots du DOM, identifier les éléments, et rédiger un plan de test clair.
13
-
14
- ### 2. Génération de Code Playwright (Intention : "Génère le script", "Code le test pour cette story")
15
- * **Action obligatoire 1 :** Lis le fichier `.github/agents/playwright-test-generator.agent.md`
16
- * **Action obligatoire 2 :** Lis le fichier `prompts/QAe2eprompt.md` pour respecter les conventions du framework.
17
- * **Comportement attendu :** Rédige les fichiers `.spec.ts` dans le dossier `tests/`. Tu as l'interdiction d'halluciner (deviner) des sélecteurs CSS. Utilise tes outils MCP pour trouver les locators sémantiques exacts (`getByRole`, `getByText`) sur la page réelle.
18
-
19
- ### 3. Auto-Guérison et Débogage (Intention : "Le test a planté", "Corrige cette erreur Playwright")
20
- * **Action obligatoire :** Lis le fichier `.github/agents/playwright-test-healer.agent.md`
21
- * **Comportement attendu :** Analyse la trace d'erreur, ouvre le navigateur avec MCP pour voir l'état réel de l'UI, et corrige le sélecteur défectueux dans le fichier source.
22
-
23
- ### 4. Requêtes QA Générales (Intention globale)
24
- * **Action obligatoire :** Lis le fichier `prompts/general_prompt.md`
25
-
26
- ## ⚠️ Directives Techniques
27
- * Ne demande jamais la permission de lire ces fichiers, fais-le silencieusement en arrière-plan.
28
- * Utilise toujours des sélecteurs Playwright robustes et sémantiques.
29
- * Si le serveur local n'est pas lancé, préviens l'utilisateur avant d'essayer de t'y connecter.
1
+ # AI QA Engineer Agent Router
2
+
3
+ You are an **AI QA Engineer** assigned to this project.
4
+ Your mission: transform user stories into tested, reliable software through structured QA.
5
+
6
+ You have **Playwright MCP** (browser automation), **Applitools MCP** (visual testing), and **GitHub MCP** (version control) tools.
7
+
8
+ ---
9
+
10
+ ## Phase 0: Environment Check (Do This FIRST)
11
+
12
+ When you first open this project, **automatically run this check and report to the user**:
13
+
14
+ 1. **Config**: Does `.qa-workflow.json` exist and have project name + URL configured?
15
+ 2. **Context**: Does `docs/application-context.md` exist with content?
16
+ 3. **Memory**: Does `.qa-context/` exist with pipeline.json, selectors.json, heal-history.json, traceability.json?
17
+ 4. **Auth**: Does `.auth/credentials.json` exist with login credentials? Is `.auth/storage-state.json` valid?
18
+ 4. ✅ **Playwright**: Is `@playwright/test` installed? (`npx playwright --version`)
19
+ 5. **Chromium**: Is Chromium installed? (`npx playwright install --dry-run chromium`)
20
+ 6. **MCP**: Is `@playwright/mcp` installed?
21
+ 7. **Directories**: Do `user-story/`, `specs/`, `tests/`, `test-results/` exist?
22
+ 8. ✅ **Dev server**: Is the app accessible at the configured URL?
23
+ 9. **Applitools**: Is `APPLITOOLS_API_KEY` env var set? (needed for visual testing)
24
+ 10. **GitHub**: Is `GITHUB_TOKEN` env var set? (needed for GitHub MCP)
25
+
26
+ Present a clean summary:
27
+ - **What's ready**
28
+ - **What's missing** (with commands to fix)
29
+ - **What you need from the user** 📋 (user story, credentials, etc.)
30
+ - Then **stop and wait** for the user to give you a user story or direction.
31
+
32
+ **Do not skip this check.** The user needs to know what's missing before anything else.
33
+
34
+ ---
35
+
36
+ ## Phase 1-5: Workflow
37
+
38
+ Once the user provides a user story or testing request, follow this workflow. **Every phase requires human approval before proceeding.**
39
+
40
+ ### Phase 1: Plan & Explore
41
+ User says: *"Plan tests for this user story"*
42
+ 1. Read `.github/agents/playwright-test-planner.agent.md`
43
+ 2. Explore the app with Playwright MCP
44
+ 3. Write test plan to `specs/`
45
+ 4. **STOP** — present the plan to the user
46
+ 5. Wait for approval before continuing
47
+
48
+ ### Phase 2: Generate Tests
49
+ User says: *"Generate tests from the plan"*
50
+ 1. Read `.github/agents/playwright-test-generator.agent.md`
51
+ 2. Read `prompts/QAe2eprompt.md` for conventions
52
+ 3. Write Playwright test files to `tests/`
53
+ 4. **STOP** — present the generated tests to the user
54
+ 5. Wait for approval before executing
55
+
56
+ ### Phase 3: Execute
57
+ User says: *"Execute the tests"*
58
+ 1. User runs `npm run qa:execute` (or you suggest it)
59
+ 2. Check results — did they pass or fail?
60
+
61
+ ### Phase 4: Heal Failures
62
+ If tests fail, user says: *"Fix the failing tests"*
63
+ 1. Read `.github/agents/playwright-test-healer.agent.md`
64
+ 2. Debug with Playwright MCP (`test_debug`, `browser_snapshot`, etc.)
65
+ 3. Present diagnosis to the user (root cause + proposed 1-3 line fix)
66
+ 4. **STOP** — wait for user to approve the fix
67
+ 5. Apply fix and re-run
68
+ 6. If still failing, mark `test.fixme()` and classify as defect
69
+
70
+ ### Phase 5: Report
71
+ User says: *"Generate the report"*
72
+ 1. User runs `npm run qa:report` (or you suggest it)
73
+ 2. Present the report summary to the user
74
+
75
+ ---
76
+
77
+ ## ⛔ Human Supervision (Mandatory)
78
+
79
+ | Phase | You do this | Then wait for approval |
80
+ |-------|------------|----------------------|
81
+ | Plan | Explore app, write plan | User reviews plan |
82
+ | Generate | Write Playwright tests | User reviews code |
83
+ | Execute | Run tests (or tell user to) | User checks results |
84
+ | Heal | Diagnose failure, propose fix | User approves fix |
85
+ | Report | Generate and present | User reviews |
86
+
87
+ **You never proceed to the next phase without explicit user approval.**
88
+
89
+ ---
90
+
91
+ ## 🧭 Routing Quick Reference
92
+
93
+ | User intent | Read this |
94
+ |------------|-----------|
95
+ | "Plan tests / Explore the app / Write a test plan" | `.github/agents/playwright-test-planner.agent.md` |
96
+ | "Generate tests / Write test code / Create spec files" | `.github/agents/playwright-test-generator.agent.md` + `prompts/QAe2eprompt.md` |
97
+ | "Fix failing tests / Debug / Heal" | `.github/agents/playwright-test-healer.agent.md` |
98
+ | General QA requests | `prompts/general_prompt.md` |
99
+
100
+ ---
101
+
102
+ ## ⚠️ Technical Rules
103
+
104
+ - Never hallucinate CSS selectors — use Playwright MCP to find real ones
105
+ - Priority: `data-testid` > `aria-label` > `role` > `text` > `xpath` (last resort)
106
+ - 1 fix attempt max per test. If still failing, it's a defect.
107
+ - 1-3 line targeted edits only. No full file rewrites.
108
+ - If the dev server isn't running, warn the user before trying to connect.
109
+ - Token efficiency: keep responses focused and minimal.
@@ -0,0 +1,186 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { DIRS, ensureDir } = require('./utils');
4
+
5
+ const AUTH_CONFIG_PATH = path.join(DIRS.qaContext, 'auth.json');
6
+ const CREDENTIALS_PATH = path.join(DIRS.auth, 'credentials.json');
7
+ const STORAGE_PATH = path.join(DIRS.auth, 'storage-state.json');
8
+
9
+ function readJSON(filePath) {
10
+ if (!fs.existsSync(filePath)) return null;
11
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
12
+ }
13
+
14
+ function writeJSON(filePath, data) {
15
+ ensureDir(path.dirname(filePath));
16
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
17
+ }
18
+
19
+ const auth = {
20
+
21
+ // ── Auth Config (.qa-context/auth.json) ──
22
+
23
+ getConfig() {
24
+ return readJSON(AUTH_CONFIG_PATH) || {
25
+ methods: [],
26
+ session: { storage_path: STORAGE_PATH, last_refreshed: null },
27
+ };
28
+ },
29
+
30
+ saveConfig(config) {
31
+ writeJSON(AUTH_CONFIG_PATH, config);
32
+ },
33
+
34
+ updateMethod(method) {
35
+ const config = this.getConfig();
36
+ const idx = config.methods.findIndex(m => m.id === method.id);
37
+ if (idx >= 0) config.methods[idx] = method;
38
+ else config.methods.push(method);
39
+ this.saveConfig(config);
40
+ return config;
41
+ },
42
+
43
+ hasMethod() {
44
+ const config = this.getConfig();
45
+ return config.methods.length > 0;
46
+ },
47
+
48
+ // ── Credentials (.auth/credentials.json) ──
49
+
50
+ saveCredentials(username, password, options = {}) {
51
+ const creds = { username, password, url: options.url || '', updated_at: new Date().toISOString() };
52
+ writeJSON(CREDENTIALS_PATH, creds);
53
+
54
+ const config = this.getConfig();
55
+ if (!config.methods.find(m => m.id === 'main-login')) {
56
+ this.updateMethod({
57
+ id: 'main-login',
58
+ type: 'form',
59
+ detected_at: new Date().toISOString(),
60
+ login_url: options.url || '/login',
61
+ fields: [
62
+ { name: 'username', selector: '', type: 'text' },
63
+ { name: 'password', selector: '', type: 'password' },
64
+ ],
65
+ submit: '',
66
+ success_indicator: '',
67
+ last_verified: new Date().toISOString(),
68
+ status: 'configured',
69
+ });
70
+ }
71
+
72
+ return creds;
73
+ },
74
+
75
+ getCredentials() {
76
+ return readJSON(CREDENTIALS_PATH) || null;
77
+ },
78
+
79
+ hasCredentials() {
80
+ return this.getCredentials() !== null;
81
+ },
82
+
83
+ clearCredentials() {
84
+ if (fs.existsSync(CREDENTIALS_PATH)) fs.unlinkSync(CREDENTIALS_PATH);
85
+ if (fs.existsSync(STORAGE_PATH)) fs.unlinkSync(STORAGE_PATH);
86
+ },
87
+
88
+ // ── Session (.auth/storage-state.json) ──
89
+
90
+ saveSession(page) {
91
+ throw new Error('saveSession requires Playwright page — use from AI agent or Playwright context');
92
+ },
93
+
94
+ getStorageStatePath() {
95
+ return STORAGE_PATH;
96
+ },
97
+
98
+ hasSession() {
99
+ return fs.existsSync(STORAGE_PATH);
100
+ },
101
+
102
+ clearSession() {
103
+ if (fs.existsSync(STORAGE_PATH)) fs.unlinkSync(STORAGE_PATH);
104
+ },
105
+
106
+ // ── Login Steps Generator ──
107
+
108
+ getLoginSteps(methodId) {
109
+ const config = this.getConfig();
110
+ const method = config.methods.find(m => m.id === (methodId || 'main-login'));
111
+ if (!method) return null;
112
+
113
+ const creds = this.getCredentials();
114
+ if (!creds) return null;
115
+
116
+ return {
117
+ url: method.login_url || creds.url || '/login',
118
+ fields: method.fields.map(f => ({
119
+ selector: f.selector,
120
+ value: f.name === 'username' || f.name === 'email' ? creds.username : creds.password,
121
+ })),
122
+ submit: method.submit,
123
+ success: method.success_indicator,
124
+ };
125
+ },
126
+
127
+ generateSetupCode() {
128
+ const config = this.getConfig();
129
+ const method = config.methods.find(m => m.id === 'main-login');
130
+ const creds = this.getCredentials();
131
+ if (!method || !creds) return null;
132
+
133
+ const storagePath = STORAGE_PATH.replace(/\\/g, '/');
134
+ const fields = method.fields.map(f => {
135
+ const val = f.name === 'username' || f.name === 'email' ? 'credentials.username' : 'credentials.password';
136
+ return ` await page.locator('${f.selector || ''}').fill(${val});`;
137
+ }).join('\n');
138
+
139
+ return `import { test as setup, expect } from '@playwright/test';
140
+ import path from 'path';
141
+
142
+ const AUTH_FILE = path.resolve(__dirname, '..', '${storagePath}');
143
+ const CREDENTIALS = require('${CREDENTIALS_PATH.replace(/\\/g, '/')}');
144
+
145
+ setup('authenticate', async ({ page }) => {
146
+ await page.goto('${method.login_url || creds.url || '/'}');
147
+ ${fields}
148
+ await page.locator('${method.submit || ''}').click();
149
+ await page.waitForURL('**${method.success_indicator || ''}**');
150
+ await page.context().storageState({ path: AUTH_FILE });
151
+ });`;
152
+ },
153
+
154
+ // ── Auth Failure Detection ──
155
+
156
+ isAuthFailure(error) {
157
+ if (!error) return false;
158
+ const msg = typeof error === 'string' ? error : (error.message || error.error || '');
159
+ const patterns = [
160
+ 'login', 'unauthorized', 'unauthenticated', '401', '403',
161
+ 'session expired', 'invalid token', 'redirected to login',
162
+ 'sign in', 'authentication required', 'credentials',
163
+ ];
164
+ return patterns.some(p => msg.toLowerCase().includes(p));
165
+ },
166
+
167
+ // ── Init ──
168
+
169
+ init() {
170
+ ensureDir(DIRS.auth);
171
+
172
+ if (!fs.existsSync(AUTH_CONFIG_PATH)) {
173
+ this.saveConfig({
174
+ methods: [],
175
+ session: { storage_path: STORAGE_PATH, last_refreshed: null },
176
+ });
177
+ }
178
+
179
+ const gitignorePath = path.join(DIRS.auth, '.gitignore');
180
+ if (!fs.existsSync(gitignorePath)) {
181
+ fs.writeFileSync(gitignorePath, '# Auth credentials and Playwright storage state\n*\n', 'utf-8');
182
+ }
183
+ },
184
+ };
185
+
186
+ module.exports = auth;
@@ -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;