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 +1 -1
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/qa.ts +232 -148
- package/src/commands/supi.ts +1 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/qa/config.ts +43 -0
- package/src/qa/matrix.ts +84 -0
- package/src/qa/prompt-builder.ts +212 -0
- package/src/qa/scripts/detect-app-type.sh +68 -0
- package/src/qa/scripts/discover-routes.sh +143 -0
- package/src/qa/scripts/ensure-playwright.sh +38 -0
- package/src/qa/scripts/run-e2e-tests.sh +99 -0
- package/src/qa/scripts/start-dev-server.sh +46 -0
- package/src/qa/scripts/stop-dev-server.sh +36 -0
- package/src/qa/session.ts +39 -55
- package/src/qa/types.ts +97 -0
- package/src/storage/qa-sessions.ts +9 -9
- package/src/types.ts +1 -70
- package/src/qa/detector.ts +0 -61
- package/src/qa/phases/discovery.ts +0 -34
- package/src/qa/phases/execution.ts +0 -65
- package/src/qa/phases/matrix.ts +0 -41
- package/src/qa/phases/reporting.ts +0 -71
- package/src/qa/report.ts +0 -22
- package/src/qa/runner.ts +0 -46
package/package.json
CHANGED
|
@@ -1,32 +1,114 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qa-strategy
|
|
3
|
-
description:
|
|
3
|
+
description: E2E product testing strategy using Playwright — flow-based, autonomous, close to human interaction
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# E2E Product Testing Strategy
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Core Principle
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
12
|
+
**This is NOT for unit or integration tests.** This pipeline tests complete user journeys through the running application.
|
|
15
13
|
|
|
16
|
-
|
|
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
|
-
|
|
16
|
+
Before writing tests, understand what the product does:
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
package/src/commands/qa.ts
CHANGED
|
@@ -1,184 +1,268 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
36
|
+
const BROWSER_OPTIONS = [
|
|
37
|
+
"chromium (recommended)",
|
|
38
|
+
"firefox",
|
|
39
|
+
"webkit",
|
|
40
|
+
];
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
218
|
+
if (!discoveredRoutes) {
|
|
219
|
+
discoveredRoutes = '{"path": "/", "file": "unknown", "type": "page", "hasForm": false}';
|
|
160
220
|
}
|
|
161
221
|
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
});
|
package/src/commands/supi.ts
CHANGED
|
@@ -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 —
|
|
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",
|
package/src/config/defaults.ts
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -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()]),
|
package/src/qa/config.ts
ADDED
|
@@ -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
|
+
}
|