supipowers 0.4.0 → 0.6.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 +3 -3
- package/skills/context-mode/SKILL.md +38 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/config.ts +23 -2
- package/src/commands/fix-pr.ts +1 -1
- package/src/commands/plan.ts +1 -1
- package/src/commands/qa.ts +232 -148
- package/src/commands/release.ts +1 -1
- package/src/commands/review.ts +1 -1
- package/src/commands/run.ts +9 -4
- package/src/commands/supi.ts +1 -1
- package/src/config/defaults.ts +11 -0
- package/src/config/schema.ts +11 -0
- package/src/context-mode/compressor.ts +200 -0
- package/src/context-mode/detector.ts +43 -0
- package/src/context-mode/event-extractor.ts +170 -0
- package/src/context-mode/event-store.ts +168 -0
- package/src/context-mode/hooks.ts +176 -0
- package/src/context-mode/installer.ts +71 -0
- package/src/context-mode/snapshot-builder.ts +127 -0
- package/src/discipline/debugging.ts +7 -7
- package/src/discipline/receiving-review.ts +5 -5
- package/src/discipline/tdd.ts +2 -2
- package/src/discipline/verification.ts +9 -9
- package/src/git/base-branch.ts +30 -0
- package/src/git/branch-finish.ts +12 -3
- package/src/git/sanitize.ts +19 -0
- package/src/git/worktree.ts +38 -11
- package/src/index.ts +8 -1
- package/src/orchestrator/agent-prompts.ts +15 -7
- package/src/orchestrator/conflict-resolver.ts +3 -2
- package/src/orchestrator/dispatcher.ts +76 -21
- package/src/orchestrator/prompts.ts +46 -6
- package/src/planning/plan-reviewer.ts +1 -1
- package/src/planning/plan-writer-prompt.ts +6 -9
- package/src/planning/prompt-builder.ts +17 -16
- package/src/planning/spec-reviewer.ts +2 -2
- 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 +22 -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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run playwright tests and produce a compact JSON summary.
|
|
3
|
+
# Usage: run-e2e-tests.sh <test_dir> <base_url> [test_filter]
|
|
4
|
+
# Output: Compact JSON summary on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
TEST_DIR="$1"
|
|
8
|
+
BASE_URL="$2"
|
|
9
|
+
TEST_FILTER="${3:-}"
|
|
10
|
+
RESULTS_DIR="${TEST_DIR}/../results"
|
|
11
|
+
SCREENSHOTS_DIR="${TEST_DIR}/../screenshots"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$RESULTS_DIR" "$SCREENSHOTS_DIR"
|
|
14
|
+
|
|
15
|
+
# Build playwright command
|
|
16
|
+
PW_ARGS=(
|
|
17
|
+
test
|
|
18
|
+
"$TEST_DIR"
|
|
19
|
+
--reporter=json
|
|
20
|
+
--output="$RESULTS_DIR"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if [ -n "$TEST_FILTER" ]; then
|
|
24
|
+
PW_ARGS+=(--grep "$TEST_FILTER")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Run playwright, capture JSON output
|
|
28
|
+
RAW_OUTPUT="$RESULTS_DIR/raw-results.json"
|
|
29
|
+
set +e
|
|
30
|
+
BASE_URL="$BASE_URL" npx playwright "${PW_ARGS[@]}" > "$RAW_OUTPUT" 2>/dev/null
|
|
31
|
+
PW_EXIT=$?
|
|
32
|
+
set -e
|
|
33
|
+
|
|
34
|
+
# If no JSON output was produced, create a minimal error report
|
|
35
|
+
if [ ! -s "$RAW_OUTPUT" ]; then
|
|
36
|
+
cat <<EOF
|
|
37
|
+
{"total": 0, "passed": 0, "failed": 0, "skipped": 0, "duration": 0, "failures": [], "error": "Playwright produced no output (exit code: $PW_EXIT)"}
|
|
38
|
+
EOF
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Parse the JSON output into compact summary using node (more reliable than jq)
|
|
43
|
+
node -e "
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync('$RAW_OUTPUT', 'utf-8'));
|
|
46
|
+
|
|
47
|
+
const suites = raw.suites || [];
|
|
48
|
+
const results = [];
|
|
49
|
+
|
|
50
|
+
function collectTests(suite, parentTitle) {
|
|
51
|
+
const title = parentTitle ? parentTitle + ' > ' + suite.title : suite.title;
|
|
52
|
+
for (const spec of (suite.specs || [])) {
|
|
53
|
+
for (const test of (spec.tests || [])) {
|
|
54
|
+
for (const result of (test.results || [])) {
|
|
55
|
+
results.push({
|
|
56
|
+
test: title + ' > ' + spec.title,
|
|
57
|
+
file: spec.file + (spec.line ? ':' + spec.line : ''),
|
|
58
|
+
status: result.status,
|
|
59
|
+
duration: result.duration || 0,
|
|
60
|
+
error: result.error?.message || null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const child of (suite.suites || [])) {
|
|
66
|
+
collectTests(child, title);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const suite of suites) {
|
|
71
|
+
collectTests(suite, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const passed = results.filter(r => r.status === 'passed').length;
|
|
75
|
+
const failed = results.filter(r => r.status === 'failed' || r.status === 'timedOut').length;
|
|
76
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
77
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
78
|
+
|
|
79
|
+
const failures = results
|
|
80
|
+
.filter(r => r.status === 'failed' || r.status === 'timedOut')
|
|
81
|
+
.map(r => ({
|
|
82
|
+
test: r.test,
|
|
83
|
+
file: r.file,
|
|
84
|
+
error: r.error || 'Unknown error',
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const summary = {
|
|
88
|
+
total: results.length,
|
|
89
|
+
passed,
|
|
90
|
+
failed,
|
|
91
|
+
skipped,
|
|
92
|
+
duration: totalDuration,
|
|
93
|
+
failures,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
console.log(JSON.stringify(summary));
|
|
97
|
+
" 2>/dev/null || cat <<EOF
|
|
98
|
+
{"total": 0, "passed": 0, "failed": 0, "skipped": 0, "duration": 0, "failures": [], "error": "Failed to parse playwright output"}
|
|
99
|
+
EOF
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start the dev server in the background and wait for it to be ready.
|
|
3
|
+
# Usage: start-dev-server.sh <cwd> <dev_command> <port> <timeout_seconds> <session_dir>
|
|
4
|
+
# Output: JSON on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CWD="$1"
|
|
8
|
+
DEV_COMMAND="$2"
|
|
9
|
+
PORT="$3"
|
|
10
|
+
TIMEOUT="${4:-60}"
|
|
11
|
+
SESSION_DIR="${5:-.}"
|
|
12
|
+
|
|
13
|
+
cd "$CWD"
|
|
14
|
+
|
|
15
|
+
# Check if port is already in use
|
|
16
|
+
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT" 2>/dev/null | grep -qE '^[0-9]'; then
|
|
17
|
+
echo "{\"pid\": null, \"url\": \"http://localhost:$PORT\", \"ready\": true, \"note\": \"Server already running\"}"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Start dev server in background
|
|
22
|
+
eval "$DEV_COMMAND" > "$SESSION_DIR/dev-server.log" 2>&1 &
|
|
23
|
+
PID=$!
|
|
24
|
+
echo "$PID" > "$SESSION_DIR/dev-server.pid"
|
|
25
|
+
|
|
26
|
+
# Wait for server to be ready
|
|
27
|
+
for i in $(seq 1 "$TIMEOUT"); do
|
|
28
|
+
# Check if process is still alive
|
|
29
|
+
if ! kill -0 "$PID" 2>/dev/null; then
|
|
30
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": false, \"error\": \"Server process exited\"}"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Check if port responds
|
|
35
|
+
if curl -s -o /dev/null "http://localhost:$PORT" 2>/dev/null; then
|
|
36
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": true}"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
sleep 1
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# Timeout — kill the server
|
|
44
|
+
kill "$PID" 2>/dev/null || true
|
|
45
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": false, \"error\": \"Timeout after ${TIMEOUT}s\"}"
|
|
46
|
+
exit 1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop the dev server started by start-dev-server.sh.
|
|
3
|
+
# Usage: stop-dev-server.sh <session_dir>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SESSION_DIR="${1:-.}"
|
|
7
|
+
PID_FILE="$SESSION_DIR/dev-server.pid"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$PID_FILE" ]; then
|
|
10
|
+
echo '{"stopped": false, "error": "No PID file found"}'
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
PID=$(cat "$PID_FILE")
|
|
15
|
+
|
|
16
|
+
if [ -z "$PID" ]; then
|
|
17
|
+
echo '{"stopped": false, "error": "Empty PID file"}'
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Kill the process and its children
|
|
22
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
23
|
+
# Kill process group if possible
|
|
24
|
+
kill -- -"$PID" 2>/dev/null || kill "$PID" 2>/dev/null || true
|
|
25
|
+
# Wait briefly for cleanup
|
|
26
|
+
sleep 1
|
|
27
|
+
# Force kill if still alive
|
|
28
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
29
|
+
kill -9 "$PID" 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
rm -f "$PID_FILE"
|
|
32
|
+
echo "{\"stopped\": true, \"pid\": $PID}"
|
|
33
|
+
else
|
|
34
|
+
rm -f "$PID_FILE"
|
|
35
|
+
echo "{\"stopped\": true, \"pid\": $PID, \"note\": \"Process was already dead\"}"
|
|
36
|
+
fi
|
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
|
+
}
|
|
@@ -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
|
@@ -99,6 +99,26 @@ export interface ReviewReport {
|
|
|
99
99
|
passed: boolean;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Context-mode integration settings */
|
|
103
|
+
export interface ContextModeConfig {
|
|
104
|
+
/** Master toggle for all context-mode integration (default: true) */
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
/** Byte threshold above which tool results are compressed (default: 4096) */
|
|
107
|
+
compressionThreshold: number;
|
|
108
|
+
/** Block curl/wget/HTTP commands and redirect to ctx_fetch_and_index (default: true) */
|
|
109
|
+
blockHttpCommands: boolean;
|
|
110
|
+
/** Inject routing instructions into system prompt when ctx_* tools detected (default: true) */
|
|
111
|
+
routingInstructions: boolean;
|
|
112
|
+
/** Track events from tool results in SQLite (default: true) */
|
|
113
|
+
eventTracking: boolean;
|
|
114
|
+
/** Inject session knowledge into compaction summaries (default: true) */
|
|
115
|
+
compaction: boolean;
|
|
116
|
+
/** Use LLM calls for summarizing very large outputs (default: false) */
|
|
117
|
+
llmSummarization: boolean;
|
|
118
|
+
/** Byte threshold above which LLM summarization is used instead of structural compression (default: 16384) */
|
|
119
|
+
llmThreshold: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
/** Config shape */
|
|
103
123
|
export interface SupipowersConfig {
|
|
104
124
|
version: string;
|
|
@@ -118,80 +138,12 @@ export interface SupipowersConfig {
|
|
|
118
138
|
qa: {
|
|
119
139
|
framework: string | null;
|
|
120
140
|
command: string | null;
|
|
141
|
+
e2e: boolean;
|
|
121
142
|
};
|
|
122
143
|
release: {
|
|
123
144
|
pipeline: string | null;
|
|
124
145
|
};
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
|
146
|
+
contextMode: ContextModeConfig;
|
|
195
147
|
}
|
|
196
148
|
|
|
197
149
|
/** Profile shape */
|
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
|
-
}
|