supipowers 0.4.0 → 0.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "OMP-native workflow extension inspired by Superpowers.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,32 +1,114 @@
1
1
  ---
2
2
  name: qa-strategy
3
- description: QA test planning for comprehensive coverage
3
+ description: E2E product testing strategy using Playwright — flow-based, autonomous, close to human interaction
4
4
  ---
5
5
 
6
- # QA Strategy Skill
6
+ # E2E Product Testing Strategy
7
7
 
8
- ## Test Pyramid
8
+ ## Core Principle
9
9
 
10
- 1. **Unit tests**: Fast, isolated, cover individual functions
11
- 2. **Integration tests**: Test component interactions
12
- 3. **E2E tests**: Test user-facing flows end-to-end
10
+ Test the product the way a user uses it. Every test simulates a real user flow — navigating, clicking, filling forms, waiting for responses. If a human wouldn't do it, don't test it here.
13
11
 
14
- ## When to Write What
12
+ **This is NOT for unit or integration tests.** This pipeline tests complete user journeys through the running application.
15
13
 
16
- - New function → unit test
17
- - New API endpoint → integration test
18
- - New user flow → E2E test
19
- - Bug fix → regression test at the appropriate level
14
+ ## Flow Discovery
20
15
 
21
- ## Coverage Priorities
16
+ Before writing tests, understand what the product does:
22
17
 
23
- Focus testing effort on:
24
- 1. Business logic (highest value)
25
- 2. Error handling paths
26
- 3. Edge cases in input validation
27
- 4. Integration points (API boundaries, DB queries)
18
+ 1. **Scan routes and pages** — every URL a user can visit is a potential flow entry point
19
+ 2. **Identify forms** login, signup, search, create, edit — these are high-value interaction points
20
+ 3. **Map navigation** — how does a user get from page A to page B? What's the happy path?
21
+ 4. **Find auth boundaries** what's public vs protected? Test both sides
22
+ 5. **Check CRUD operations** — can you create, read, update, delete the core entities?
28
23
 
29
- Don't test:
30
- - Framework boilerplate
31
- - Simple getters/setters
32
- - Third-party library behavior
24
+ ## Flow Prioritization
25
+
26
+ | Priority | Description | Examples |
27
+ |----------|-------------|---------|
28
+ | **Critical** | Revenue or access blocking | Login, checkout, payment |
29
+ | **High** | Core product value | Create/edit main entities, dashboard |
30
+ | **Medium** | Secondary features | Settings, profile, search |
31
+ | **Low** | Nice-to-have | Theme toggle, tooltips |
32
+
33
+ Test critical and high flows first. Skip low flows if hitting the token budget.
34
+
35
+ ## Playwright Best Practices
36
+
37
+ ### Locators (prefer resilient selectors)
38
+
39
+ ```typescript
40
+ // GOOD — role-based, resilient to styling changes
41
+ page.getByRole('button', { name: 'Submit' })
42
+ page.getByLabel('Email')
43
+ page.getByText('Welcome back')
44
+ page.getByTestId('user-avatar')
45
+
46
+ // BAD — fragile, breaks on refactoring
47
+ page.locator('.btn-primary')
48
+ page.locator('#submit-btn')
49
+ page.locator('div > form > button:nth-child(2)')
50
+ ```
51
+
52
+ ### Assertions
53
+
54
+ ```typescript
55
+ // Wait for navigation
56
+ await expect(page).toHaveURL('/dashboard');
57
+
58
+ // Wait for element visibility
59
+ await expect(page.getByText('Success')).toBeVisible();
60
+
61
+ // Wait for element to disappear (loading states)
62
+ await expect(page.getByText('Loading...')).not.toBeVisible();
63
+ ```
64
+
65
+ ### Waiting
66
+
67
+ ```typescript
68
+ // GOOD — wait for specific condition
69
+ await page.waitForResponse(resp => resp.url().includes('/api/users'));
70
+ await page.waitForLoadState('networkidle');
71
+
72
+ // BAD — arbitrary delays
73
+ await page.waitForTimeout(3000);
74
+ ```
75
+
76
+ ### Test Structure
77
+
78
+ One flow per file. Each test in the flow tests a step or variant:
79
+
80
+ ```typescript
81
+ test.describe('Checkout flow', () => {
82
+ test('adds item to cart', async ({ page }) => { ... });
83
+ test('fills shipping info', async ({ page }) => { ... });
84
+ test('completes payment', async ({ page }) => { ... });
85
+ test('shows confirmation', async ({ page }) => { ... });
86
+ });
87
+ ```
88
+
89
+ ## What Makes a Good E2E Test
90
+
91
+ | Quality | Good | Bad |
92
+ |---------|------|-----|
93
+ | **User-centric** | Tests what a user would do | Tests implementation details |
94
+ | **Independent** | Each test can run alone | Tests depend on previous test state |
95
+ | **Resilient** | Uses role/label selectors | Uses CSS classes or DOM structure |
96
+ | **Fast-failing** | Fails clearly on the broken step | Fails on a timeout with no context |
97
+ | **Readable** | Test name describes the user action | Test name is a technical description |
98
+
99
+ ## Common Pitfalls
100
+
101
+ 1. **Testing internal state** — don't check Redux store, localStorage, or cookies directly. Test what the user sees.
102
+ 2. **Flaky waits** — use `waitForResponse` or `waitForSelector`, never `waitForTimeout`.
103
+ 3. **Shared state** — each test should set up its own state. Don't rely on test execution order.
104
+ 4. **Over-testing** — one flow per critical path. Don't test every permutation of a form.
105
+ 5. **Ignoring error states** — test what happens when the API returns an error, the network is slow, or the user enters invalid data.
106
+
107
+ ## Regression Analysis
108
+
109
+ When a previously-passing test fails:
110
+
111
+ 1. **Read the error** — what element wasn't found? What URL didn't match?
112
+ 2. **Check if the app changed** — did a route move? Did a button get renamed?
113
+ 3. **Distinguish bug from change** — if the app intentionally changed, the test needs updating. If not, it's a regression.
114
+ 4. **Record the finding** — update the flow matrix with the new status and reasoning.
@@ -1,184 +1,268 @@
1
1
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
- import { detectAndCache } from "../qa/detector.js";
3
- import { notifyInfo, notifyError } from "../notifications/renderer.js";
4
- import { findActiveSession, findSessionWithFailures } from "../storage/qa-sessions.js";
5
- import {
6
- createNewSession,
7
- advancePhase,
8
- getFailedTests,
9
- getNextPhase,
10
- getPhaseStatusLine,
11
- } from "../qa/session.js";
12
- import { buildDiscoveryPrompt } from "../qa/phases/discovery.js";
13
- import { buildMatrixPrompt } from "../qa/phases/matrix.js";
14
- import { buildExecutionPrompt } from "../qa/phases/execution.js";
15
- import { buildReportingPrompt } from "../qa/phases/reporting.js";
16
- import type { QaPhase, QaSessionLedger } from "../types.js";
17
-
18
- const PHASE_LABELS: Record<QaPhase, string> = {
19
- discovery: "Discovery — Scan for test cases",
20
- matrix: "Matrix — Build traceability matrix",
21
- execution: "Execution — Run tests",
22
- reporting: "Reporting — Generate summary",
23
- };
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { notifyInfo, notifyError, notifyWarning } from "../notifications/renderer.js";
5
+ import { loadE2eQaConfig, saveE2eQaConfig, DEFAULT_E2E_QA_CONFIG } from "../qa/config.js";
6
+ import { loadE2eMatrix } from "../qa/matrix.js";
7
+ import { createNewE2eSession } from "../qa/session.js";
8
+ import { buildE2eOrchestratorPrompt } from "../qa/prompt-builder.js";
9
+ import { findActiveSession, getSessionDir } from "../storage/qa-sessions.js";
10
+ import type { E2eQaConfig, AppType, E2eRegression } from "../qa/types.js";
24
11
 
25
- export function registerQaCommand(pi: ExtensionAPI): void {
26
- pi.registerCommand("supi:qa", {
27
- description: "Run QA pipeline with session management (discovery → matrix → execution → reporting)",
28
- async handler(args, ctx) {
29
- const framework = detectAndCache(ctx.cwd);
30
-
31
- if (!framework) {
32
- notifyError(
33
- ctx,
34
- "No test framework detected",
35
- "Configure manually via /supi:config"
36
- );
37
- return;
38
- }
12
+ function getScriptsDir(): string {
13
+ return path.join(path.dirname(new URL(import.meta.url).pathname), "..", "qa", "scripts");
14
+ }
39
15
 
40
- // ── Step 1: Session selection ──────────────────────────────────
41
- let ledger: QaSessionLedger | null = null;
16
+ function findSkillPath(skillName: string): string | null {
17
+ const candidates = [
18
+ path.join(process.cwd(), "skills", skillName, "SKILL.md"),
19
+ path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "skills", skillName, "SKILL.md"),
20
+ ];
21
+ for (const p of candidates) {
22
+ if (fs.existsSync(p)) return p;
23
+ }
24
+ return null;
25
+ }
42
26
 
43
- const activeSession = findActiveSession(ctx.cwd);
44
- const failedSession = findSessionWithFailures(ctx.cwd);
27
+ const APP_TYPE_OPTIONS = [
28
+ "nextjs-app Next.js App Router",
29
+ "nextjs-pages — Next.js Pages Router",
30
+ "react-router — React with React Router",
31
+ "vite — Vite-based app",
32
+ "express — Express.js server",
33
+ "generic — Other web app",
34
+ ];
45
35
 
46
- if (ctx.hasUI && !args?.trim()) {
47
- const sessionOptions: string[] = [];
36
+ const BROWSER_OPTIONS = [
37
+ "chromium (recommended)",
38
+ "firefox",
39
+ "webkit",
40
+ ];
48
41
 
49
- if (failedSession) {
50
- const failCount = failedSession.results.filter((r) => r.status === "fail").length;
51
- sessionOptions.push(`Resume ${failedSession.id} (${failCount} failed test${failCount !== 1 ? "s" : ""})`);
52
- } else if (activeSession) {
53
- const next = getNextPhase(activeSession);
54
- sessionOptions.push(`Resume ${activeSession.id} (${next ?? "all phases done"} pending)`);
55
- }
42
+ const RETRY_OPTIONS = [
43
+ "1",
44
+ "2 (recommended)",
45
+ "3",
46
+ ];
56
47
 
57
- sessionOptions.push("Start new session");
48
+ async function runSetupWizard(
49
+ ctx: any,
50
+ detectedAppType: string | null,
51
+ detectedDevCommand: string | null,
52
+ detectedPort: number | null,
53
+ ): Promise<E2eQaConfig | null> {
54
+ // 1. App type
55
+ const appTypeChoice = await ctx.ui.select(
56
+ "App type",
57
+ APP_TYPE_OPTIONS,
58
+ { helpText: detectedAppType ? `Auto-detected: ${detectedAppType}` : "Select your web app framework" },
59
+ );
60
+ if (!appTypeChoice) return null;
61
+ const appType = appTypeChoice.split(" ")[0] as AppType;
58
62
 
59
- if (sessionOptions.length > 1) {
60
- const choice = await ctx.ui.select(
61
- "QA Session",
62
- sessionOptions,
63
- { helpText: "Select session · Esc to cancel" },
64
- );
65
- if (!choice) return;
63
+ // 2. Dev command
64
+ const defaultDev = detectedDevCommand || "npm run dev";
65
+ const devCommand = await ctx.ui.input(
66
+ "Dev server command",
67
+ defaultDev,
68
+ { helpText: "Command to start your development server" },
69
+ );
70
+ if (devCommand === undefined) return null;
66
71
 
67
- if (choice.startsWith("Resume")) {
68
- ledger = failedSession ?? activeSession;
69
- }
70
- }
71
- }
72
+ // 3. Port
73
+ const defaultPort = String(detectedPort || 3000);
74
+ const portStr = await ctx.ui.input(
75
+ "Dev server port",
76
+ defaultPort,
77
+ { helpText: "Port your dev server runs on" },
78
+ );
79
+ if (portStr === undefined) return null;
80
+ const port = parseInt(portStr, 10) || 3000;
72
81
 
73
- // Create new session if none selected
74
- if (!ledger) {
75
- ledger = createNewSession(ctx.cwd, framework.name);
76
- notifyInfo(ctx, "QA session created", ledger.id);
77
- }
82
+ // 4. Browser
83
+ const browserChoice = await ctx.ui.select(
84
+ "Browser for E2E tests",
85
+ BROWSER_OPTIONS,
86
+ { helpText: "Playwright browser to use" },
87
+ );
88
+ if (!browserChoice) return null;
89
+ const browser = browserChoice.split(" ")[0] as "chromium" | "firefox" | "webkit";
78
90
 
79
- // ── Step 2: Phase selection ────────────────────────────────────
80
- type PhaseAction =
81
- | { type: "run-phase"; phase: QaPhase }
82
- | { type: "rerun-failed" };
91
+ // 5. Max retries
92
+ const retryChoice = await ctx.ui.select(
93
+ "Max test retries",
94
+ RETRY_OPTIONS,
95
+ { helpText: "How many times to retry failing tests" },
96
+ );
97
+ if (!retryChoice) return null;
98
+ const maxRetries = parseInt(retryChoice, 10);
83
99
 
84
- let action: PhaseAction | null = null;
85
- const nextPhase = getNextPhase(ledger);
86
- const failedTests = getFailedTests(ledger);
100
+ return {
101
+ app: {
102
+ type: appType,
103
+ devCommand: devCommand || defaultDev,
104
+ port,
105
+ baseUrl: `http://localhost:${port}`,
106
+ },
107
+ playwright: {
108
+ browser,
109
+ headless: true,
110
+ timeout: 30000,
111
+ },
112
+ execution: {
113
+ maxRetries,
114
+ maxFlows: 20,
115
+ },
116
+ };
117
+ }
87
118
 
88
- if (ctx.hasUI && !args?.trim()) {
89
- const phaseOptions: string[] = [];
119
+ export function registerQaCommand(pi: ExtensionAPI): void {
120
+ pi.registerCommand("supi:qa", {
121
+ description: "Run autonomous E2E product testing pipeline with playwright",
122
+ async handler(args, ctx) {
123
+ const scriptsDir = getScriptsDir();
90
124
 
91
- // Offer re-run failed if there are failures
92
- if (failedTests.length > 0) {
93
- phaseOptions.push(`Re-run ${failedTests.length} failed test${failedTests.length !== 1 ? "s" : ""} only`);
125
+ // ── Step 1: Detect app type ─────────────────────────────────────
126
+ let detectedType: string | null = null;
127
+ let detectedDevCommand: string | null = null;
128
+ let detectedPort: number | null = null;
129
+
130
+ try {
131
+ const detectResult = await pi.exec("bash", [
132
+ path.join(scriptsDir, "detect-app-type.sh"),
133
+ ctx.cwd,
134
+ ], { cwd: ctx.cwd });
135
+
136
+ if (detectResult.code === 0) {
137
+ const detected = JSON.parse(detectResult.stdout.trim());
138
+ detectedType = detected.type;
139
+ detectedDevCommand = detected.devCommand;
140
+ detectedPort = detected.port;
94
141
  }
142
+ } catch { /* proceed without detection */ }
143
+
144
+ // ── Step 2: Ensure playwright ────────────────────────────────────
145
+ try {
146
+ const pwResult = await pi.exec("bash", [
147
+ path.join(scriptsDir, "ensure-playwright.sh"),
148
+ ctx.cwd,
149
+ ], { cwd: ctx.cwd });
95
150
 
96
- // Offer starting from next pending phase
97
- if (nextPhase) {
98
- phaseOptions.push(PHASE_LABELS[nextPhase]);
151
+ if (pwResult.code !== 0) {
152
+ notifyWarning(ctx, "Playwright setup issue", "Could not verify playwright installation. The agent will handle it.");
99
153
  }
154
+ } catch {
155
+ notifyWarning(ctx, "Playwright check skipped", "Will be handled during execution");
156
+ }
157
+
158
+ // ── Step 3: Load or create config ────────────────────────────────
159
+ let config = loadE2eQaConfig(ctx.cwd);
160
+
161
+ if (!config && ctx.hasUI) {
162
+ config = await runSetupWizard(ctx, detectedType, detectedDevCommand, detectedPort);
163
+ if (!config) return; // user cancelled
164
+ saveE2eQaConfig(ctx.cwd, config);
165
+ ctx.ui.notify("E2E QA config saved to .omp/supipowers/e2e-qa.json", "info");
166
+ }
167
+
168
+ if (!config) {
169
+ // Use defaults with detected values
170
+ config = {
171
+ ...DEFAULT_E2E_QA_CONFIG,
172
+ app: {
173
+ type: (detectedType as AppType) || "generic",
174
+ devCommand: detectedDevCommand || "npm run dev",
175
+ port: detectedPort || 3000,
176
+ baseUrl: `http://localhost:${detectedPort || 3000}`,
177
+ },
178
+ };
179
+ }
100
180
 
101
- if (phaseOptions.length > 1) {
102
- const statusLine = getPhaseStatusLine(ledger);
103
- const choice = await ctx.ui.select(
104
- `QA Phase · ${statusLine}`,
105
- phaseOptions,
106
- { helpText: "Select action · Esc to cancel" },
107
- );
108
- if (!choice) return;
109
-
110
- if (choice.startsWith("Re-run")) {
111
- action = { type: "rerun-failed" };
112
- } else {
113
- // Extract phase from label
114
- const selectedPhase = (Object.entries(PHASE_LABELS) as [QaPhase, string][])
115
- .find(([, label]) => label === choice)?.[0];
116
- if (selectedPhase) {
117
- action = { type: "run-phase", phase: selectedPhase };
118
- }
181
+ // ── Step 4: Check for unresolved regressions ─────────────────────
182
+ const activeSession = findActiveSession(ctx.cwd);
183
+ if (activeSession && activeSession.regressions.length > 0 && ctx.hasUI) {
184
+ const unresolvedRegressions = activeSession.regressions.filter((r: E2eRegression) => !r.resolution);
185
+ if (unresolvedRegressions.length > 0) {
186
+ for (const regression of unresolvedRegressions) {
187
+ const choice = await ctx.ui.select(
188
+ `Regression: ${regression.flowName}`,
189
+ [
190
+ "This is a bug — needs fixing",
191
+ "Behavior changed intentionally update the test",
192
+ "Skip for now",
193
+ ],
194
+ { helpText: `Was passing, now fails: ${regression.error}` },
195
+ );
196
+ if (!choice) continue;
197
+ if (choice.startsWith("This is a bug")) regression.resolution = "bug";
198
+ else if (choice.startsWith("Behavior changed")) regression.resolution = "intentional-change";
199
+ else regression.resolution = "skipped";
119
200
  }
120
- } else if (nextPhase) {
121
- // Only one option — just run the next phase
122
- action = { type: "run-phase", phase: nextPhase };
123
201
  }
124
- } else if (nextPhase) {
125
- action = { type: "run-phase", phase: nextPhase };
126
202
  }
127
203
 
128
- if (!action) {
129
- notifyInfo(ctx, "QA pipeline complete", getPhaseStatusLine(ledger));
130
- return;
131
- }
204
+ // ── Step 5: Route discovery ──────────────────────────────────────
205
+ let discoveredRoutes = "";
206
+ try {
207
+ const routeResult = await pi.exec("bash", [
208
+ path.join(scriptsDir, "discover-routes.sh"),
209
+ ctx.cwd,
210
+ config.app.type,
211
+ ], { cwd: ctx.cwd });
132
212
 
133
- // ── Step 3: Execute ────────────────────────────────────────────
134
- let prompt: string;
135
-
136
- if (action.type === "rerun-failed") {
137
- ledger = advancePhase(ctx.cwd, ledger, "execution", "running");
138
- prompt = buildExecutionPrompt(ledger, { failedOnly: true, failedTests });
139
- notifyInfo(ctx, "QA re-running failed tests", `${failedTests.length} test(s)`);
140
- } else {
141
- const phase = action.phase;
142
- ledger = advancePhase(ctx.cwd, ledger, phase, "running");
143
-
144
- switch (phase) {
145
- case "discovery":
146
- prompt = buildDiscoveryPrompt(framework, ctx.cwd);
147
- break;
148
- case "matrix":
149
- prompt = buildMatrixPrompt(ledger);
150
- break;
151
- case "execution":
152
- prompt = buildExecutionPrompt(ledger);
153
- break;
154
- case "reporting":
155
- prompt = buildReportingPrompt(ledger);
156
- break;
213
+ if (routeResult.code === 0 && routeResult.stdout.trim()) {
214
+ discoveredRoutes = routeResult.stdout.trim();
157
215
  }
216
+ } catch { /* agent will discover routes manually */ }
158
217
 
159
- notifyInfo(ctx, `QA phase: ${phase}`, `session: ${ledger.id}`);
218
+ if (!discoveredRoutes) {
219
+ discoveredRoutes = '{"path": "/", "file": "unknown", "type": "page", "hasForm": false}';
160
220
  }
161
221
 
162
- // Include session context for the sub-agent
163
- const sessionContext = [
164
- `\n\n## QA Session Context`,
165
- ``,
166
- `Session ID: ${ledger.id}`,
167
- `Session ledger path: .omp/supipowers/qa-sessions/${ledger.id}/ledger.json`,
168
- ``,
169
- `Current ledger state:`,
170
- "```json",
171
- JSON.stringify(ledger, null, 2),
172
- "```",
173
- ].join("\n");
222
+ // ── Step 6: Load previous matrix ─────────────────────────────────
223
+ const previousMatrix = loadE2eMatrix(ctx.cwd);
224
+ const matrixJson = previousMatrix ? JSON.stringify(previousMatrix, null, 2) : null;
225
+
226
+ // ── Step 7: Create session ───────────────────────────────────────
227
+ const ledger = createNewE2eSession(ctx.cwd, config);
228
+ const sessionDir = getSessionDir(ctx.cwd, ledger.id);
229
+
230
+ // ── Step 8: Load skill ───────────────────────────────────────────
231
+ let skillContent = "";
232
+ const skillPath = findSkillPath("qa-strategy");
233
+ if (skillPath) {
234
+ try {
235
+ skillContent = fs.readFileSync(skillPath, "utf-8");
236
+ } catch { /* proceed without */ }
237
+ }
238
+
239
+ // ── Step 9: Build and send prompt ────────────────────────────────
240
+ const routeCount = discoveredRoutes.split("\n").filter(Boolean).length;
241
+
242
+ const prompt = buildE2eOrchestratorPrompt({
243
+ cwd: ctx.cwd,
244
+ appType: config.app,
245
+ sessionDir,
246
+ scriptsDir,
247
+ config,
248
+ discoveredRoutes,
249
+ previousMatrix: matrixJson,
250
+ skillContent,
251
+ });
174
252
 
175
253
  pi.sendMessage(
176
254
  {
177
255
  customType: "supi-qa",
178
- content: [{ type: "text", text: prompt + sessionContext }],
256
+ content: [{ type: "text", text: prompt }],
179
257
  display: "none",
180
258
  },
181
- { deliverAs: "steer" }
259
+ { deliverAs: "steer" },
260
+ );
261
+
262
+ notifyInfo(
263
+ ctx,
264
+ `E2E QA started: ${config.app.type}`,
265
+ `${routeCount} routes discovered | session ${ledger.id}`,
182
266
  );
183
267
  },
184
268
  });
@@ -15,7 +15,7 @@ export function handleSupi(pi: ExtensionAPI, ctx: ExtensionContext): void {
15
15
  "/supi:plan — Start collaborative planning",
16
16
  "/supi:run — Execute a plan with sub-agents",
17
17
  "/supi:review — Run quality gates",
18
- "/supi:qa — Run QA pipeline",
18
+ "/supi:qa — E2E product testing with Playwright",
19
19
  "/supi:fix-pr — Fix PR review comments",
20
20
  "/supi:release — Release automation",
21
21
  "/supi:config — Manage configuration",
@@ -19,6 +19,7 @@ export const DEFAULT_CONFIG: SupipowersConfig = {
19
19
  qa: {
20
20
  framework: null,
21
21
  command: null,
22
+ e2e: false,
22
23
  },
23
24
  release: {
24
25
  pipeline: null,
@@ -25,6 +25,7 @@ const ConfigSchema = Type.Object({
25
25
  qa: Type.Object({
26
26
  framework: Type.Union([Type.String(), Type.Null()]),
27
27
  command: Type.Union([Type.String(), Type.Null()]),
28
+ e2e: Type.Boolean(),
28
29
  }),
29
30
  release: Type.Object({
30
31
  pipeline: Type.Union([Type.String(), Type.Null()]),
@@ -0,0 +1,43 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { E2eQaConfig } from "./types.js";
4
+
5
+ const CONFIG_FILENAME = "e2e-qa.json";
6
+
7
+ function getConfigPath(cwd: string): string {
8
+ return path.join(cwd, ".omp", "supipowers", CONFIG_FILENAME);
9
+ }
10
+
11
+ export const DEFAULT_E2E_QA_CONFIG: E2eQaConfig = {
12
+ app: {
13
+ type: "generic",
14
+ devCommand: "npm run dev",
15
+ port: 3000,
16
+ baseUrl: "http://localhost:3000",
17
+ },
18
+ playwright: {
19
+ browser: "chromium",
20
+ headless: true,
21
+ timeout: 30000,
22
+ },
23
+ execution: {
24
+ maxRetries: 2,
25
+ maxFlows: 20,
26
+ },
27
+ };
28
+
29
+ export function loadE2eQaConfig(cwd: string): E2eQaConfig | null {
30
+ const configPath = getConfigPath(cwd);
31
+ if (!fs.existsSync(configPath)) return null;
32
+ try {
33
+ return JSON.parse(fs.readFileSync(configPath, "utf-8")) as E2eQaConfig;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function saveE2eQaConfig(cwd: string, config: E2eQaConfig): void {
40
+ const configPath = getConfigPath(cwd);
41
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
42
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
43
+ }