supipowers 0.3.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/fix-pr/SKILL.md +99 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/qa.ts +232 -148
- package/src/commands/supi.ts +2 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/fix-pr/config.ts +36 -0
- package/src/fix-pr/prompt-builder.ts +201 -0
- package/src/fix-pr/scripts/diff-comments.sh +33 -0
- package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
- package/src/fix-pr/scripts/trigger-review.sh +36 -0
- package/src/fix-pr/scripts/wait-and-check.sh +37 -0
- package/src/fix-pr/types.ts +71 -0
- package/src/index.ts +2 -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/fix-pr-sessions.ts +59 -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/src/qa/session.ts
CHANGED
|
@@ -1,37 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { E2ePhase, E2ePhaseStatus, E2eSessionLedger, E2eQaConfig } from "./types.js";
|
|
2
4
|
import { generateSessionId, createSession, updateSession } from "../storage/qa-sessions.js";
|
|
3
5
|
|
|
4
|
-
const PHASE_ORDER:
|
|
6
|
+
const PHASE_ORDER: E2ePhase[] = ["flow-discovery", "test-generation", "execution", "reporting"];
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
const PHASE_LABELS: Record<E2ePhase, string> = {
|
|
9
|
+
"flow-discovery": "Discovery",
|
|
10
|
+
"test-generation": "Generation",
|
|
11
|
+
"execution": "Execution",
|
|
12
|
+
"reporting": "Reporting",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Create a new E2E QA session with all phases pending */
|
|
16
|
+
export function createNewE2eSession(cwd: string, config: E2eQaConfig): E2eSessionLedger {
|
|
8
17
|
const now = new Date().toISOString();
|
|
9
|
-
const ledger:
|
|
18
|
+
const ledger: E2eSessionLedger = {
|
|
10
19
|
id: generateSessionId(),
|
|
11
20
|
createdAt: now,
|
|
12
21
|
updatedAt: now,
|
|
13
|
-
|
|
22
|
+
appType: config.app.type,
|
|
23
|
+
baseUrl: config.app.baseUrl,
|
|
14
24
|
phases: {
|
|
15
|
-
discovery: { status: "pending" },
|
|
16
|
-
|
|
17
|
-
execution: { status: "pending" },
|
|
18
|
-
reporting: { status: "pending" },
|
|
25
|
+
"flow-discovery": { status: "pending" },
|
|
26
|
+
"test-generation": { status: "pending" },
|
|
27
|
+
"execution": { status: "pending" },
|
|
28
|
+
"reporting": { status: "pending" },
|
|
19
29
|
},
|
|
20
|
-
|
|
21
|
-
matrix: [],
|
|
30
|
+
flows: [],
|
|
22
31
|
results: [],
|
|
32
|
+
regressions: [],
|
|
33
|
+
config,
|
|
23
34
|
};
|
|
35
|
+
|
|
36
|
+
// Create session with subdirectories
|
|
24
37
|
createSession(cwd, ledger);
|
|
38
|
+
|
|
39
|
+
const sessionDir = path.join(cwd, ".omp", "supipowers", "qa-sessions", ledger.id);
|
|
40
|
+
fs.mkdirSync(path.join(sessionDir, "tests"), { recursive: true });
|
|
41
|
+
fs.mkdirSync(path.join(sessionDir, "screenshots"), { recursive: true });
|
|
42
|
+
|
|
25
43
|
return ledger;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/** Update a phase's status and timestamps, persist to disk */
|
|
29
|
-
export function
|
|
47
|
+
export function advanceE2ePhase(
|
|
30
48
|
cwd: string,
|
|
31
|
-
ledger:
|
|
32
|
-
phase:
|
|
33
|
-
status:
|
|
34
|
-
):
|
|
49
|
+
ledger: E2eSessionLedger,
|
|
50
|
+
phase: E2ePhase,
|
|
51
|
+
status: E2ePhaseStatus,
|
|
52
|
+
): E2eSessionLedger {
|
|
35
53
|
const now = new Date().toISOString();
|
|
36
54
|
const record = { ...ledger.phases[phase] };
|
|
37
55
|
|
|
@@ -43,7 +61,7 @@ export function advancePhase(
|
|
|
43
61
|
record.completedAt = now;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
const updated:
|
|
64
|
+
const updated: E2eSessionLedger = {
|
|
47
65
|
...ledger,
|
|
48
66
|
updatedAt: now,
|
|
49
67
|
phases: { ...ledger.phases, [phase]: record },
|
|
@@ -52,42 +70,8 @@ export function advancePhase(
|
|
|
52
70
|
return updated;
|
|
53
71
|
}
|
|
54
72
|
|
|
55
|
-
/** Merge new test results into the ledger, upserting by testId */
|
|
56
|
-
export function mergeTestResults(
|
|
57
|
-
ledger: QaSessionLedger,
|
|
58
|
-
newResults: QaTestResult[]
|
|
59
|
-
): QaSessionLedger {
|
|
60
|
-
const resultMap = new Map(ledger.results.map((r) => [r.testId, r]));
|
|
61
|
-
|
|
62
|
-
for (const incoming of newResults) {
|
|
63
|
-
const existing = resultMap.get(incoming.testId);
|
|
64
|
-
if (existing) {
|
|
65
|
-
resultMap.set(incoming.testId, {
|
|
66
|
-
...incoming,
|
|
67
|
-
retryCount: existing.retryCount + 1,
|
|
68
|
-
});
|
|
69
|
-
} else {
|
|
70
|
-
resultMap.set(incoming.testId, incoming);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
...ledger,
|
|
76
|
-
updatedAt: new Date().toISOString(),
|
|
77
|
-
results: Array.from(resultMap.values()),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Get test cases whose latest result is "fail" */
|
|
82
|
-
export function getFailedTests(ledger: QaSessionLedger): QaTestCase[] {
|
|
83
|
-
const failedIds = new Set(
|
|
84
|
-
ledger.results.filter((r) => r.status === "fail").map((r) => r.testId)
|
|
85
|
-
);
|
|
86
|
-
return ledger.tests.filter((t) => failedIds.has(t.id));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
73
|
/** Get the next phase that is not completed */
|
|
90
|
-
export function
|
|
74
|
+
export function getNextE2ePhase(ledger: E2eSessionLedger): E2ePhase | null {
|
|
91
75
|
for (const phase of PHASE_ORDER) {
|
|
92
76
|
if (ledger.phases[phase].status !== "completed") return phase;
|
|
93
77
|
}
|
|
@@ -95,9 +79,9 @@ export function getNextPhase(ledger: QaSessionLedger): QaPhase | null {
|
|
|
95
79
|
}
|
|
96
80
|
|
|
97
81
|
/** Format phase status for TUI display */
|
|
98
|
-
export function
|
|
82
|
+
export function getE2ePhaseStatusLine(ledger: E2eSessionLedger): string {
|
|
99
83
|
return PHASE_ORDER.map((phase) => {
|
|
100
|
-
const label = phase
|
|
84
|
+
const label = PHASE_LABELS[phase];
|
|
101
85
|
const done = ledger.phases[phase].status === "completed";
|
|
102
86
|
return done ? `[done] ${label}` : `[ ] ${label}`;
|
|
103
87
|
}).join(" · ");
|
package/src/qa/types.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ── App Detection ──────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type AppType = "nextjs-app" | "nextjs-pages" | "react-router" | "vite" | "express" | "generic";
|
|
4
|
+
|
|
5
|
+
export interface AppTypeInfo {
|
|
6
|
+
type: AppType;
|
|
7
|
+
devCommand: string;
|
|
8
|
+
port: number;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Configuration ──────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface PlaywrightConfig {
|
|
15
|
+
browser: "chromium" | "firefox" | "webkit";
|
|
16
|
+
headless: boolean;
|
|
17
|
+
timeout: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ExecutionConfig {
|
|
21
|
+
maxRetries: number;
|
|
22
|
+
maxFlows: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface E2eQaConfig {
|
|
26
|
+
app: AppTypeInfo;
|
|
27
|
+
playwright: PlaywrightConfig;
|
|
28
|
+
execution: ExecutionConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Persistent Flow Matrix ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface E2eFlowRecord {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
entryRoute: string;
|
|
37
|
+
steps: string[];
|
|
38
|
+
priority: "critical" | "high" | "medium" | "low";
|
|
39
|
+
lastStatus: "pass" | "fail" | "untested";
|
|
40
|
+
lastTestedAt: string | null;
|
|
41
|
+
lastError?: string;
|
|
42
|
+
addedAt: string;
|
|
43
|
+
removedAt?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface E2eMatrix {
|
|
47
|
+
version: string;
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
appType: string;
|
|
50
|
+
flows: E2eFlowRecord[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Session ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export type E2ePhase = "flow-discovery" | "test-generation" | "execution" | "reporting";
|
|
56
|
+
export type E2ePhaseStatus = "pending" | "running" | "completed" | "failed";
|
|
57
|
+
|
|
58
|
+
export interface E2eFlow {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
entryRoute: string;
|
|
62
|
+
steps: string[];
|
|
63
|
+
priority: "critical" | "high" | "medium" | "low";
|
|
64
|
+
testFile?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface E2eTestResult {
|
|
68
|
+
flowId: string;
|
|
69
|
+
testFile: string;
|
|
70
|
+
status: "pass" | "fail" | "skip";
|
|
71
|
+
duration?: number;
|
|
72
|
+
error?: string;
|
|
73
|
+
screenshot?: string;
|
|
74
|
+
retryCount: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface E2eRegression {
|
|
78
|
+
flowId: string;
|
|
79
|
+
flowName: string;
|
|
80
|
+
previousStatus: "pass";
|
|
81
|
+
currentStatus: "fail";
|
|
82
|
+
error: string;
|
|
83
|
+
resolution?: "bug" | "intentional-change" | "skipped";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface E2eSessionLedger {
|
|
87
|
+
id: string;
|
|
88
|
+
createdAt: string;
|
|
89
|
+
updatedAt: string;
|
|
90
|
+
appType: string;
|
|
91
|
+
baseUrl: string;
|
|
92
|
+
phases: Record<E2ePhase, { status: E2ePhaseStatus; startedAt?: string; completedAt?: string }>;
|
|
93
|
+
flows: E2eFlow[];
|
|
94
|
+
results: E2eTestResult[];
|
|
95
|
+
regressions: E2eRegression[];
|
|
96
|
+
config: E2eQaConfig;
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { FixPrSessionLedger } from "../fix-pr/types.js";
|
|
4
|
+
|
|
5
|
+
const SESSIONS_DIR = "fix-pr-sessions";
|
|
6
|
+
|
|
7
|
+
function getBaseDir(cwd: string): string {
|
|
8
|
+
return path.join(cwd, ".omp", "supipowers", SESSIONS_DIR);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getSessionDir(cwd: string, sessionId: string): string {
|
|
12
|
+
return path.join(getBaseDir(cwd), sessionId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function generateFixPrSessionId(): string {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
18
|
+
const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
|
|
19
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
20
|
+
return `fpr-${date}-${time}-${rand}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createFixPrSession(cwd: string, ledger: FixPrSessionLedger): void {
|
|
24
|
+
const sessionDir = getSessionDir(cwd, ledger.id);
|
|
25
|
+
fs.mkdirSync(path.join(sessionDir, "snapshots"), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(sessionDir, "ledger.json"), JSON.stringify(ledger, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadFixPrSession(cwd: string, sessionId: string): FixPrSessionLedger | null {
|
|
30
|
+
const ledgerPath = path.join(getSessionDir(cwd, sessionId), "ledger.json");
|
|
31
|
+
if (!fs.existsSync(ledgerPath)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(ledgerPath, "utf-8")) as FixPrSessionLedger;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function updateFixPrSession(cwd: string, ledger: FixPrSessionLedger): void {
|
|
40
|
+
const ledgerPath = path.join(getSessionDir(cwd, ledger.id), "ledger.json");
|
|
41
|
+
ledger.updatedAt = new Date().toISOString();
|
|
42
|
+
fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findActiveFixPrSession(cwd: string): FixPrSessionLedger | null {
|
|
46
|
+
const baseDir = getBaseDir(cwd);
|
|
47
|
+
if (!fs.existsSync(baseDir)) return null;
|
|
48
|
+
|
|
49
|
+
const dirs = fs.readdirSync(baseDir)
|
|
50
|
+
.filter((d) => d.startsWith("fpr-"))
|
|
51
|
+
.sort()
|
|
52
|
+
.reverse();
|
|
53
|
+
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
const ledger = loadFixPrSession(cwd, dir);
|
|
56
|
+
if (ledger && ledger.status === "running") return ledger;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
3
|
+
import type { E2eSessionLedger } from "../qa/types.js";
|
|
4
4
|
|
|
5
5
|
const SESSIONS_DIR = [".omp", "supipowers", "qa-sessions"];
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ function getSessionsDir(cwd: string): string {
|
|
|
8
8
|
return path.join(cwd, ...SESSIONS_DIR);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function getSessionDir(cwd: string, sessionId: string): string {
|
|
11
|
+
export function getSessionDir(cwd: string, sessionId: string): string {
|
|
12
12
|
return path.join(getSessionsDir(cwd), sessionId);
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -22,17 +22,17 @@ export function generateSessionId(): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Create a new QA session */
|
|
25
|
-
export function createSession(cwd: string, ledger:
|
|
25
|
+
export function createSession(cwd: string, ledger: E2eSessionLedger): void {
|
|
26
26
|
const sessionDir = getSessionDir(cwd, ledger.id);
|
|
27
27
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
28
28
|
fs.writeFileSync(
|
|
29
29
|
path.join(sessionDir, "ledger.json"),
|
|
30
|
-
JSON.stringify(ledger, null, 2) + "\n"
|
|
30
|
+
JSON.stringify(ledger, null, 2) + "\n",
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Load a QA session ledger */
|
|
35
|
-
export function loadSession(cwd: string, sessionId: string):
|
|
35
|
+
export function loadSession(cwd: string, sessionId: string): E2eSessionLedger | null {
|
|
36
36
|
const filePath = path.join(getSessionDir(cwd, sessionId), "ledger.json");
|
|
37
37
|
if (!fs.existsSync(filePath)) return null;
|
|
38
38
|
try {
|
|
@@ -43,7 +43,7 @@ export function loadSession(cwd: string, sessionId: string): QaSessionLedger | n
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Update a QA session ledger */
|
|
46
|
-
export function updateSession(cwd: string, ledger:
|
|
46
|
+
export function updateSession(cwd: string, ledger: E2eSessionLedger): void {
|
|
47
47
|
const filePath = path.join(getSessionDir(cwd, ledger.id), "ledger.json");
|
|
48
48
|
fs.writeFileSync(filePath, JSON.stringify(ledger, null, 2) + "\n");
|
|
49
49
|
}
|
|
@@ -60,12 +60,12 @@ export function listSessions(cwd: string): string[] {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Find the latest session with incomplete phases */
|
|
63
|
-
export function findActiveSession(cwd: string):
|
|
63
|
+
export function findActiveSession(cwd: string): E2eSessionLedger | null {
|
|
64
64
|
for (const sessionId of listSessions(cwd)) {
|
|
65
65
|
const ledger = loadSession(cwd, sessionId);
|
|
66
66
|
if (!ledger) continue;
|
|
67
67
|
const allCompleted = Object.values(ledger.phases).every(
|
|
68
|
-
(p) => p.status === "completed"
|
|
68
|
+
(p) => p.status === "completed",
|
|
69
69
|
);
|
|
70
70
|
if (!allCompleted) return ledger;
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ export function findActiveSession(cwd: string): QaSessionLedger | null {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Find the latest session with failed test results */
|
|
76
|
-
export function findSessionWithFailures(cwd: string):
|
|
76
|
+
export function findSessionWithFailures(cwd: string): E2eSessionLedger | null {
|
|
77
77
|
for (const sessionId of listSessions(cwd)) {
|
|
78
78
|
const ledger = loadSession(cwd, sessionId);
|
|
79
79
|
if (!ledger) continue;
|
package/src/types.ts
CHANGED
|
@@ -118,82 +118,13 @@ export interface SupipowersConfig {
|
|
|
118
118
|
qa: {
|
|
119
119
|
framework: string | null;
|
|
120
120
|
command: string | null;
|
|
121
|
+
e2e: boolean;
|
|
121
122
|
};
|
|
122
123
|
release: {
|
|
123
124
|
pipeline: string | null;
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// ── QA Session Management ──────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
/** QA pipeline phase */
|
|
130
|
-
export type QaPhase = "discovery" | "matrix" | "execution" | "reporting";
|
|
131
|
-
|
|
132
|
-
/** Phase completion status */
|
|
133
|
-
export type QaPhaseStatus = "pending" | "running" | "completed" | "failed";
|
|
134
|
-
|
|
135
|
-
/** Per-test result status */
|
|
136
|
-
export type TestResultStatus = "pass" | "fail" | "skip";
|
|
137
|
-
|
|
138
|
-
/** A discovered test case */
|
|
139
|
-
export interface QaTestCase {
|
|
140
|
-
id: string;
|
|
141
|
-
filePath: string;
|
|
142
|
-
testName: string;
|
|
143
|
-
suiteName?: string;
|
|
144
|
-
tags?: string[];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Traceability matrix entry — one requirement mapped to its tests */
|
|
148
|
-
export interface QaMatrixEntry {
|
|
149
|
-
requirement: string;
|
|
150
|
-
testIds: string[];
|
|
151
|
-
platforms?: string[];
|
|
152
|
-
coverage: "full" | "partial" | "none";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Per-test execution result */
|
|
156
|
-
export interface QaTestResult {
|
|
157
|
-
testId: string;
|
|
158
|
-
status: TestResultStatus;
|
|
159
|
-
duration?: number;
|
|
160
|
-
error?: string;
|
|
161
|
-
retryCount: number;
|
|
162
|
-
lastRunAt: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Report summary generated in the Reporting phase */
|
|
166
|
-
export interface QaReportSummary {
|
|
167
|
-
generatedAt: string;
|
|
168
|
-
total: number;
|
|
169
|
-
passed: number;
|
|
170
|
-
failed: number;
|
|
171
|
-
skipped: number;
|
|
172
|
-
passRate: number;
|
|
173
|
-
failedTests: { testId: string; testName: string; error?: string }[];
|
|
174
|
-
coverageSummary?: string;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Phase record within a session */
|
|
178
|
-
export interface QaPhaseRecord {
|
|
179
|
-
status: QaPhaseStatus;
|
|
180
|
-
startedAt?: string;
|
|
181
|
-
completedAt?: string;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** The full QA session ledger */
|
|
185
|
-
export interface QaSessionLedger {
|
|
186
|
-
id: string;
|
|
187
|
-
createdAt: string;
|
|
188
|
-
updatedAt: string;
|
|
189
|
-
framework: string;
|
|
190
|
-
phases: Record<QaPhase, QaPhaseRecord>;
|
|
191
|
-
tests: QaTestCase[];
|
|
192
|
-
matrix: QaMatrixEntry[];
|
|
193
|
-
results: QaTestResult[];
|
|
194
|
-
report?: QaReportSummary;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
128
|
/** Profile shape */
|
|
198
129
|
export interface Profile {
|
|
199
130
|
name: string;
|
package/src/qa/detector.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { updateConfig, loadConfig } from "../config/loader.js";
|
|
4
|
-
|
|
5
|
-
export interface DetectedFramework {
|
|
6
|
-
name: string;
|
|
7
|
-
command: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const FRAMEWORK_SIGNATURES: { name: string; files: string[]; command: string }[] = [
|
|
11
|
-
{ name: "vitest", files: ["vitest.config.ts", "vitest.config.js", "vitest.config.mts"], command: "npx vitest run" },
|
|
12
|
-
{ name: "jest", files: ["jest.config.ts", "jest.config.js", "jest.config.mjs"], command: "npx jest" },
|
|
13
|
-
{ name: "mocha", files: [".mocharc.yml", ".mocharc.json", ".mocharc.js"], command: "npx mocha" },
|
|
14
|
-
{ name: "pytest", files: ["pytest.ini", "pyproject.toml", "conftest.py"], command: "pytest" },
|
|
15
|
-
{ name: "cargo-test", files: ["Cargo.toml"], command: "cargo test" },
|
|
16
|
-
{ name: "go-test", files: ["go.mod"], command: "go test ./..." },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
export function detectFramework(cwd: string): DetectedFramework | null {
|
|
20
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
21
|
-
if (fs.existsSync(pkgPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
24
|
-
if (pkg.scripts?.test && pkg.scripts.test !== 'echo "Error: no test specified" && exit 1') {
|
|
25
|
-
const testScript = pkg.scripts.test;
|
|
26
|
-
for (const sig of FRAMEWORK_SIGNATURES) {
|
|
27
|
-
if (testScript.includes(sig.name)) {
|
|
28
|
-
return { name: sig.name, command: "npm test" };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { name: "npm-test", command: "npm test" };
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// continue to file-based detection
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
for (const sig of FRAMEWORK_SIGNATURES) {
|
|
39
|
-
for (const file of sig.files) {
|
|
40
|
-
if (fs.existsSync(path.join(cwd, file))) {
|
|
41
|
-
return { name: sig.name, command: sig.command };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function detectAndCache(cwd: string): DetectedFramework | null {
|
|
50
|
-
const config = loadConfig(cwd);
|
|
51
|
-
|
|
52
|
-
if (config.qa.framework && config.qa.command) {
|
|
53
|
-
return { name: config.qa.framework, command: config.qa.command };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const detected = detectFramework(cwd);
|
|
57
|
-
if (detected) {
|
|
58
|
-
updateConfig(cwd, { qa: { framework: detected.name, command: detected.command } });
|
|
59
|
-
}
|
|
60
|
-
return detected;
|
|
61
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { DetectedFramework } from "../detector.js";
|
|
2
|
-
|
|
3
|
-
export function buildDiscoveryPrompt(framework: DetectedFramework, cwd: string): string {
|
|
4
|
-
const sections: string[] = [
|
|
5
|
-
"# QA Phase: Test Discovery",
|
|
6
|
-
"",
|
|
7
|
-
`Project: ${cwd}`,
|
|
8
|
-
`Test framework: ${framework.name} (command: \`${framework.command}\`)`,
|
|
9
|
-
"",
|
|
10
|
-
"## Task",
|
|
11
|
-
"",
|
|
12
|
-
"Scan the project for all test files and enumerate every individual test case.",
|
|
13
|
-
"",
|
|
14
|
-
"1. Find all test files matching the framework's conventions",
|
|
15
|
-
"2. Parse each file to extract individual test/it/describe blocks",
|
|
16
|
-
"3. Classify each test with tags (unit, integration, e2e) based on file path or naming",
|
|
17
|
-
"",
|
|
18
|
-
"## Expected Output",
|
|
19
|
-
"",
|
|
20
|
-
"Write a JSON array to the QA session ledger's `tests` field. Each entry:",
|
|
21
|
-
"",
|
|
22
|
-
"```json",
|
|
23
|
-
'[{ "id": "<filePath>:<testName>", "filePath": "relative/path.test.ts", "testName": "test name", "suiteName": "describe block", "tags": ["unit"] }]',
|
|
24
|
-
"```",
|
|
25
|
-
"",
|
|
26
|
-
"The `id` must be deterministic: use `filePath:testName` so it stays stable across sessions.",
|
|
27
|
-
"",
|
|
28
|
-
"## After Completion",
|
|
29
|
-
"",
|
|
30
|
-
"Update the QA session ledger with the discovered tests, mark the discovery phase as completed, then invoke `/supi:qa` to continue to the next phase.",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
return sections.join("\n");
|
|
34
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { QaSessionLedger, QaTestCase } from "../../types.js";
|
|
2
|
-
|
|
3
|
-
export interface ExecutionOptions {
|
|
4
|
-
failedOnly: true;
|
|
5
|
-
failedTests: QaTestCase[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function buildExecutionPrompt(
|
|
9
|
-
ledger: QaSessionLedger,
|
|
10
|
-
options?: ExecutionOptions
|
|
11
|
-
): string {
|
|
12
|
-
const isRetry = options?.failedOnly === true;
|
|
13
|
-
const targetTests = isRetry ? options.failedTests : ledger.tests;
|
|
14
|
-
|
|
15
|
-
const testList = targetTests
|
|
16
|
-
.map((t) => `- ${t.id}: ${t.testName} (${t.filePath})`)
|
|
17
|
-
.join("\n");
|
|
18
|
-
|
|
19
|
-
const sections: string[] = [
|
|
20
|
-
"# QA Phase: Test Execution",
|
|
21
|
-
"",
|
|
22
|
-
`Framework: ${ledger.framework}`,
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
if (isRetry) {
|
|
26
|
-
sections.push(
|
|
27
|
-
"",
|
|
28
|
-
`## Re-running ${targetTests.length} failed test(s)`,
|
|
29
|
-
"",
|
|
30
|
-
"Run ONLY the following failed tests:",
|
|
31
|
-
"",
|
|
32
|
-
testList,
|
|
33
|
-
);
|
|
34
|
-
} else {
|
|
35
|
-
sections.push(
|
|
36
|
-
"",
|
|
37
|
-
`## Running all ${targetTests.length} test(s)`,
|
|
38
|
-
"",
|
|
39
|
-
testList,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
sections.push(
|
|
44
|
-
"",
|
|
45
|
-
"## Instructions",
|
|
46
|
-
"",
|
|
47
|
-
"1. Run the tests using the framework's CLI",
|
|
48
|
-
"2. Collect per-test results: pass, fail, or skip",
|
|
49
|
-
"3. For failures, capture the error message",
|
|
50
|
-
"",
|
|
51
|
-
"## Expected Output",
|
|
52
|
-
"",
|
|
53
|
-
"Write a JSON array to the QA session ledger's `results` field:",
|
|
54
|
-
"",
|
|
55
|
-
"```json",
|
|
56
|
-
'[{ "testId": "file.test.ts:test name", "status": "pass|fail|skip", "duration": 123, "error": "only if failed" }]',
|
|
57
|
-
"```",
|
|
58
|
-
"",
|
|
59
|
-
"## After Completion",
|
|
60
|
-
"",
|
|
61
|
-
"Update the QA session ledger with results, mark the execution phase as completed, then invoke `/supi:qa` to continue to the next phase.",
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return sections.join("\n");
|
|
65
|
-
}
|
package/src/qa/phases/matrix.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { QaSessionLedger } from "../../types.js";
|
|
2
|
-
|
|
3
|
-
export function buildMatrixPrompt(ledger: QaSessionLedger): string {
|
|
4
|
-
const testSummary = ledger.tests
|
|
5
|
-
.map((t) => `- ${t.id}: ${t.testName} (${t.filePath})${t.tags ? ` [${t.tags.join(", ")}]` : ""}`)
|
|
6
|
-
.join("\n");
|
|
7
|
-
|
|
8
|
-
const sections: string[] = [
|
|
9
|
-
"# QA Phase: Traceability Matrix",
|
|
10
|
-
"",
|
|
11
|
-
`Framework: ${ledger.framework}`,
|
|
12
|
-
`Discovered tests: ${ledger.tests.length}`,
|
|
13
|
-
"",
|
|
14
|
-
"## Discovered Tests",
|
|
15
|
-
"",
|
|
16
|
-
testSummary,
|
|
17
|
-
"",
|
|
18
|
-
"## Task",
|
|
19
|
-
"",
|
|
20
|
-
"Build a traceability matrix that maps requirements to test cases and platforms.",
|
|
21
|
-
"",
|
|
22
|
-
"1. Read the project's README, PR descriptions, code comments, and doc files to identify requirements",
|
|
23
|
-
"2. Map each requirement to the test case IDs that cover it",
|
|
24
|
-
"3. Identify target platforms (node, browser, CI) from project config",
|
|
25
|
-
"4. Assess coverage: full (all paths tested), partial (some paths), none (no tests)",
|
|
26
|
-
"",
|
|
27
|
-
"## Expected Output",
|
|
28
|
-
"",
|
|
29
|
-
"Write a JSON array to the QA session ledger's `matrix` field:",
|
|
30
|
-
"",
|
|
31
|
-
"```json",
|
|
32
|
-
'[{ "requirement": "User login validates email format", "testIds": ["auth.test.ts:validates email"], "platforms": ["node"], "coverage": "full" }]',
|
|
33
|
-
"```",
|
|
34
|
-
"",
|
|
35
|
-
"## After Completion",
|
|
36
|
-
"",
|
|
37
|
-
"Update the QA session ledger with the matrix, mark the matrix phase as completed, then invoke `/supi:qa` to continue to the next phase.",
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
return sections.join("\n");
|
|
41
|
-
}
|