pi-harness-runtime 1.1.16 → 1.1.26
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/CHANGELOG.md +26 -0
- package/README.md +76 -18
- package/harness/e2e/auto-test-loop.ts +815 -0
- package/harness/e2e/tools-detector.test.ts +156 -0
- package/harness/e2e/tools-detector.ts +801 -0
- package/harness/glm-quota-logger.ts +20 -23
- package/harness/index.ts +42 -0
- package/harness/loop-runtime.ts +49 -13
- package/harness/skill-sync.test.ts +57 -0
- package/harness/skill-sync.ts +247 -0
- package/harness/ssh-hang-recovery.ts +280 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +28 -0
- package/packages/skill-mcp-client/src/client.ts +171 -0
- package/packages/skill-mcp-client/src/errors.ts +75 -0
- package/packages/skill-mcp-client/src/index.ts +13 -0
- package/packages/skill-mcp-client/src/types.ts +123 -0
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Auto-Run and Fix Loop — RFC-0101
|
|
3
|
+
*
|
|
4
|
+
* Automatically runs E2E tests, analyzes failures, fixes code, and retries
|
|
5
|
+
* until tests pass or environment limitation is detected.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* Smart Decision (from tools-detector.ts)
|
|
9
|
+
* ↓
|
|
10
|
+
* Decision: auto_run → Run Tests
|
|
11
|
+
* ↓
|
|
12
|
+
* Tests Pass? → Continue / Commit
|
|
13
|
+
* ↓ (No)
|
|
14
|
+
* Analyze Failure
|
|
15
|
+
* ↓
|
|
16
|
+
* Can Auto-Fix? → Fix Code → Re-run Tests
|
|
17
|
+
* ↓ (No)
|
|
18
|
+
* Environment Limitation? → Ask Human to Run Manually
|
|
19
|
+
* ↓ (No)
|
|
20
|
+
* Unknown Failure → Retry (max 3x) → Ask Human
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join, extname } from "node:path";
|
|
25
|
+
import { execSync } from "child_process";
|
|
26
|
+
import {
|
|
27
|
+
makeSmartDecision,
|
|
28
|
+
getE2EToolsConfig,
|
|
29
|
+
type ProjectType,
|
|
30
|
+
type E2EToolsConfig,
|
|
31
|
+
} from "./tools-detector.js";
|
|
32
|
+
import { logInfo, logWarn, logError } from "../glm-quota-logger.js";
|
|
33
|
+
|
|
34
|
+
export interface E2ETestResult {
|
|
35
|
+
passed: boolean;
|
|
36
|
+
output: string;
|
|
37
|
+
duration: number;
|
|
38
|
+
failedTests: string[];
|
|
39
|
+
errorMessage?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AutoTestConfig {
|
|
43
|
+
projectRoot: string;
|
|
44
|
+
maxRetries: number;
|
|
45
|
+
autoFixEnabled: boolean;
|
|
46
|
+
generateStubs: boolean;
|
|
47
|
+
notifyOnEscalation: boolean;
|
|
48
|
+
/** Global timeout for entire test run (default: 30 minutes) */
|
|
49
|
+
globalTimeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AutoTestResult {
|
|
53
|
+
success: boolean;
|
|
54
|
+
testsRun: boolean;
|
|
55
|
+
testsPassed: boolean;
|
|
56
|
+
retriesAttempted: number;
|
|
57
|
+
escalatedToHuman: boolean;
|
|
58
|
+
escalationReason?: string;
|
|
59
|
+
output: string;
|
|
60
|
+
duration: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_CONFIG: AutoTestConfig = {
|
|
64
|
+
projectRoot: ".",
|
|
65
|
+
maxRetries: 3,
|
|
66
|
+
autoFixEnabled: true,
|
|
67
|
+
generateStubs: true,
|
|
68
|
+
notifyOnEscalation: true,
|
|
69
|
+
globalTimeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run the full E2E auto-test loop
|
|
74
|
+
*/
|
|
75
|
+
export async function runAutoTestLoop(
|
|
76
|
+
config: Partial<AutoTestConfig> = {},
|
|
77
|
+
): Promise<AutoTestResult> {
|
|
78
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
const retriesAttempted = 0;
|
|
81
|
+
|
|
82
|
+
logInfo(null, `Starting E2E Auto-Test Loop for: ${cfg.projectRoot}`);
|
|
83
|
+
|
|
84
|
+
// Step 1: Detect project and tools
|
|
85
|
+
const toolsConfig = getE2EToolsConfig(cfg.projectRoot);
|
|
86
|
+
logInfo(
|
|
87
|
+
null,
|
|
88
|
+
`Project type: ${toolsConfig.projectType}, Tools: ${toolsConfig.detectedTools.filter((t) => t.installed).length} installed`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Step 2: Make smart decision
|
|
92
|
+
const decision = makeSmartDecision(
|
|
93
|
+
cfg.projectRoot,
|
|
94
|
+
toolsConfig.projectType,
|
|
95
|
+
toolsConfig.detectedTools,
|
|
96
|
+
);
|
|
97
|
+
logInfo(
|
|
98
|
+
null,
|
|
99
|
+
`Smart decision: ${decision.suggestedAction} (confidence: ${decision.confidence})`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Handle decision
|
|
103
|
+
switch (decision.suggestedAction) {
|
|
104
|
+
case "skip":
|
|
105
|
+
logInfo(null, "Skipping E2E tests - not needed right now");
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
testsRun: false,
|
|
109
|
+
testsPassed: true,
|
|
110
|
+
retriesAttempted: 0,
|
|
111
|
+
escalatedToHuman: false,
|
|
112
|
+
output: decision.reason,
|
|
113
|
+
duration: Date.now() - startTime,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
case "ask_human":
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
testsRun: false,
|
|
120
|
+
testsPassed: false,
|
|
121
|
+
retriesAttempted: 0,
|
|
122
|
+
escalatedToHuman: true,
|
|
123
|
+
escalationReason: decision.reason,
|
|
124
|
+
output: decision.reason,
|
|
125
|
+
duration: Date.now() - startTime,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
case "generate_stubs":
|
|
129
|
+
if (cfg.generateStubs) {
|
|
130
|
+
logInfo(null, "Generating test stubs...");
|
|
131
|
+
await generateTestStubs(cfg.projectRoot, toolsConfig.projectType);
|
|
132
|
+
}
|
|
133
|
+
// Fall through to run tests
|
|
134
|
+
|
|
135
|
+
case "auto_run":
|
|
136
|
+
default:
|
|
137
|
+
// Run the test loop
|
|
138
|
+
return await runTestLoop(cfg, toolsConfig, retriesAttempted, startTime);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Main test loop with retry logic
|
|
144
|
+
*/
|
|
145
|
+
async function runTestLoop(
|
|
146
|
+
cfg: AutoTestConfig,
|
|
147
|
+
toolsConfig: E2EToolsConfig,
|
|
148
|
+
retriesAttempted: number,
|
|
149
|
+
startTime: number,
|
|
150
|
+
): Promise<AutoTestResult> {
|
|
151
|
+
let lastResult: E2ETestResult | null = null;
|
|
152
|
+
|
|
153
|
+
while (retriesAttempted <= cfg.maxRetries) {
|
|
154
|
+
// Check global timeout
|
|
155
|
+
const elapsedMs = Date.now() - startTime;
|
|
156
|
+
if (elapsedMs > (cfg.globalTimeoutMs ?? 30 * 60 * 1000)) {
|
|
157
|
+
logWarn(
|
|
158
|
+
null,
|
|
159
|
+
`⏱️ Global timeout reached (${elapsedMs}ms). Escalating to human.`,
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
testsRun: true,
|
|
164
|
+
testsPassed: false,
|
|
165
|
+
retriesAttempted,
|
|
166
|
+
escalatedToHuman: true,
|
|
167
|
+
escalationReason: `Global timeout reached after ${Math.round(elapsedMs / 1000)}s`,
|
|
168
|
+
output: lastResult?.output || "",
|
|
169
|
+
duration: elapsedMs,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logInfo(
|
|
174
|
+
null,
|
|
175
|
+
`Running E2E tests (attempt ${retriesAttempted + 1}/${cfg.maxRetries + 1})...`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Run tests
|
|
179
|
+
lastResult = await runTests(cfg.projectRoot, toolsConfig);
|
|
180
|
+
|
|
181
|
+
if (lastResult && lastResult.passed) {
|
|
182
|
+
logInfo(null, "✅ All E2E tests passed!");
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
testsRun: true,
|
|
186
|
+
testsPassed: true,
|
|
187
|
+
retriesAttempted,
|
|
188
|
+
escalatedToHuman: false,
|
|
189
|
+
output: (lastResult && lastResult.output) || "",
|
|
190
|
+
duration: Date.now() - startTime,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Tests failed
|
|
195
|
+
logWarn(
|
|
196
|
+
null,
|
|
197
|
+
`❌ E2E tests failed: ${(lastResult && lastResult.errorMessage) || " error"}`,
|
|
198
|
+
);
|
|
199
|
+
retriesAttempted++;
|
|
200
|
+
|
|
201
|
+
// Analyze failure
|
|
202
|
+
const analysis = analyzeFailure(
|
|
203
|
+
lastResult!,
|
|
204
|
+
cfg.projectRoot,
|
|
205
|
+
toolsConfig.projectType,
|
|
206
|
+
);
|
|
207
|
+
logInfo(
|
|
208
|
+
null,
|
|
209
|
+
`Failure analysis: ${analysis.canFix ? "Can auto-fix" : "Needs manual intervention"}`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (analysis.canFix && cfg.autoFixEnabled) {
|
|
213
|
+
// Try to fix
|
|
214
|
+
const fixResult = await attemptAutoFix(analysis, cfg.projectRoot);
|
|
215
|
+
if (fixResult.fixed) {
|
|
216
|
+
logInfo(null, `Auto-fixed: ${fixResult.description}`);
|
|
217
|
+
continue; // Retry tests
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if environment limitation
|
|
222
|
+
if (isEnvironmentLimitation(lastResult!)) {
|
|
223
|
+
logWarn(null, "Environment limitation detected - escalating to human");
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
testsRun: true,
|
|
227
|
+
testsPassed: false,
|
|
228
|
+
retriesAttempted,
|
|
229
|
+
escalatedToHuman: true,
|
|
230
|
+
escalationReason: `Environment limitation: ${(lastResult && lastResult.errorMessage) || "Unable to run tests in current environment"}`,
|
|
231
|
+
output: (lastResult && lastResult.output) || "",
|
|
232
|
+
duration: Date.now() - startTime,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check max retries
|
|
237
|
+
if (retriesAttempted > cfg.maxRetries) {
|
|
238
|
+
logWarn(null, "Max retries reached - escalating to human");
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
testsRun: true,
|
|
242
|
+
testsPassed: false,
|
|
243
|
+
retriesAttempted,
|
|
244
|
+
escalatedToHuman: true,
|
|
245
|
+
escalationReason: `Failed after ${cfg.maxRetries} attempts. Last error: ${lastResult?.errorMessage ?? "Unknown"}`,
|
|
246
|
+
output: (lastResult && lastResult.output) || "",
|
|
247
|
+
duration: Date.now() - startTime,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Should not reach here, but safety net
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
testsRun: true,
|
|
256
|
+
testsPassed: false,
|
|
257
|
+
retriesAttempted,
|
|
258
|
+
escalatedToHuman: true,
|
|
259
|
+
escalationReason: "Unexpected loop exit",
|
|
260
|
+
output: lastResult?.output || "",
|
|
261
|
+
duration: Date.now() - startTime,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Run tests based on project type
|
|
267
|
+
*/
|
|
268
|
+
async function runTests(
|
|
269
|
+
projectRoot: string,
|
|
270
|
+
toolsConfig: E2EToolsConfig,
|
|
271
|
+
): Promise<E2ETestResult> {
|
|
272
|
+
const startTime = Date.now();
|
|
273
|
+
const projectType = toolsConfig.projectType;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
let command: string;
|
|
277
|
+
let workingDir = projectRoot;
|
|
278
|
+
|
|
279
|
+
switch (projectType) {
|
|
280
|
+
case "tauri":
|
|
281
|
+
// Try hardware-e2e first, then regular e2e
|
|
282
|
+
if (existsSync(join(projectRoot, "hardware-e2e"))) {
|
|
283
|
+
command = "wdio run ./hardware-e2e/wdio.conf.ts";
|
|
284
|
+
workingDir = join(projectRoot, "hardware-e2e");
|
|
285
|
+
} else {
|
|
286
|
+
command = "npx playwright test";
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case "ios":
|
|
291
|
+
case "android":
|
|
292
|
+
// Native apps need special setup
|
|
293
|
+
return {
|
|
294
|
+
passed: false,
|
|
295
|
+
output: "",
|
|
296
|
+
duration: 0,
|
|
297
|
+
failedTests: [],
|
|
298
|
+
errorMessage:
|
|
299
|
+
"Native app testing requires manual setup. Run tests manually.",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
case "flutter":
|
|
303
|
+
command = "flutter test integration_test";
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case "nextjs":
|
|
307
|
+
case "react_vite":
|
|
308
|
+
case "generic":
|
|
309
|
+
command = "npx playwright test";
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case "django":
|
|
313
|
+
command = "python manage.py test";
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case "laravel":
|
|
317
|
+
command = "php artisan dusk";
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
default:
|
|
321
|
+
return {
|
|
322
|
+
passed: false,
|
|
323
|
+
output: "",
|
|
324
|
+
duration: 0,
|
|
325
|
+
failedTests: [],
|
|
326
|
+
errorMessage: "Unknown project type",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Run the command
|
|
331
|
+
logInfo(null, `Running: ${command} (in ${workingDir})`);
|
|
332
|
+
|
|
333
|
+
const output = execSync(command, {
|
|
334
|
+
cwd: workingDir,
|
|
335
|
+
encoding: "utf-8",
|
|
336
|
+
timeout: 300000, // 5 minute timeout
|
|
337
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const passed = output.includes("passed") && !output.includes("failed");
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
passed,
|
|
344
|
+
output,
|
|
345
|
+
duration: Date.now() - startTime,
|
|
346
|
+
failedTests: extractFailedTests(output),
|
|
347
|
+
errorMessage: passed ? undefined : "Tests failed",
|
|
348
|
+
};
|
|
349
|
+
} catch (error: any) {
|
|
350
|
+
const output = error.stdout || error.message || "";
|
|
351
|
+
return {
|
|
352
|
+
passed: false,
|
|
353
|
+
output,
|
|
354
|
+
duration: Date.now() - startTime,
|
|
355
|
+
failedTests: extractFailedTests(output),
|
|
356
|
+
errorMessage: error.message || "Test execution failed",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface FailureAnalysis {
|
|
362
|
+
canFix: boolean;
|
|
363
|
+
reason: string;
|
|
364
|
+
failedTests: string[];
|
|
365
|
+
likelyCause?: string;
|
|
366
|
+
suggestedFix?: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Analyze a test failure to determine if it can be auto-fixed
|
|
371
|
+
*/
|
|
372
|
+
function analyzeFailure(
|
|
373
|
+
result: E2ETestResult,
|
|
374
|
+
_projectRoot: string,
|
|
375
|
+
_projectType: ProjectType,
|
|
376
|
+
): FailureAnalysis {
|
|
377
|
+
const failedTests = result.failedTests || [];
|
|
378
|
+
|
|
379
|
+
// Check for common auto-fixable issues
|
|
380
|
+
|
|
381
|
+
// 1. Missing test file - can generate
|
|
382
|
+
if (
|
|
383
|
+
failedTests.some(
|
|
384
|
+
(t: string) => t.includes("Cannot find") || t.includes("not found"),
|
|
385
|
+
)
|
|
386
|
+
) {
|
|
387
|
+
return {
|
|
388
|
+
canFix: true,
|
|
389
|
+
reason: "Missing test file - can generate stubs",
|
|
390
|
+
failedTests,
|
|
391
|
+
likelyCause: "Test file doesn't exist",
|
|
392
|
+
suggestedFix: "Generate test stubs based on source files",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 2. Import errors - can fix
|
|
397
|
+
if (
|
|
398
|
+
result.output.includes("Cannot find module") ||
|
|
399
|
+
result.output.includes("import error")
|
|
400
|
+
) {
|
|
401
|
+
return {
|
|
402
|
+
canFix: true,
|
|
403
|
+
reason: "Import error - may need npm install",
|
|
404
|
+
failedTests,
|
|
405
|
+
likelyCause: "Missing dependencies",
|
|
406
|
+
suggestedFix: "Run npm install",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 3. Locator errors - can fix (re-generate selectors)
|
|
411
|
+
if (
|
|
412
|
+
failedTests.some(
|
|
413
|
+
(t: string) => t.includes("locator") || t.includes("selector"),
|
|
414
|
+
)
|
|
415
|
+
) {
|
|
416
|
+
return {
|
|
417
|
+
canFix: true,
|
|
418
|
+
reason: "UI locator changed - need to update selectors",
|
|
419
|
+
failedTests,
|
|
420
|
+
likelyCause: "CSS class or element changed in UI",
|
|
421
|
+
suggestedFix: "Update test selectors to match new UI",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 4. Auth/session issues - might be environment
|
|
426
|
+
if (
|
|
427
|
+
failedTests.some(
|
|
428
|
+
(t: string) =>
|
|
429
|
+
t.includes("auth") || t.includes("login") || t.includes("session"),
|
|
430
|
+
)
|
|
431
|
+
) {
|
|
432
|
+
return {
|
|
433
|
+
canFix: false,
|
|
434
|
+
reason: "Authentication issue - may need test credentials",
|
|
435
|
+
failedTests,
|
|
436
|
+
likelyCause: "Test needs valid credentials",
|
|
437
|
+
suggestedFix: "Set up test user credentials",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 5. Timeout issues - might be environment
|
|
442
|
+
if (result.output.includes("timeout") || result.output.includes("Timed out")) {
|
|
443
|
+
return {
|
|
444
|
+
canFix: false,
|
|
445
|
+
reason: "Timeout - may be environment issue",
|
|
446
|
+
failedTests,
|
|
447
|
+
likelyCause: "Server slow or not running",
|
|
448
|
+
suggestedFix: "Start dev server before running tests",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Default: unknown failure
|
|
453
|
+
return {
|
|
454
|
+
canFix: false,
|
|
455
|
+
reason: "Unknown failure - manual investigation needed",
|
|
456
|
+
failedTests,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
interface FixResult {
|
|
461
|
+
fixed: boolean;
|
|
462
|
+
description: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface FailureAnalysis {
|
|
466
|
+
canFix: boolean;
|
|
467
|
+
reason: string;
|
|
468
|
+
failedTests: string[];
|
|
469
|
+
likelyCause?: string;
|
|
470
|
+
suggestedFix?: string;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Attempt to auto-fix the failure
|
|
475
|
+
*/
|
|
476
|
+
async function attemptAutoFix(
|
|
477
|
+
analysis: FailureAnalysis,
|
|
478
|
+
projectRoot: string,
|
|
479
|
+
): Promise<FixResult> {
|
|
480
|
+
// Fix 1: Generate missing test stubs
|
|
481
|
+
if (analysis.reason.includes("Missing test file")) {
|
|
482
|
+
const stubsGenerated = await generateTestStubs(projectRoot, "generic");
|
|
483
|
+
if (stubsGenerated) {
|
|
484
|
+
return { fixed: true, description: "Generated test stubs" };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Fix 2: Run npm install
|
|
489
|
+
if (
|
|
490
|
+
analysis.reason.includes("import error") ||
|
|
491
|
+
analysis.reason.includes("Missing dependencies")
|
|
492
|
+
) {
|
|
493
|
+
try {
|
|
494
|
+
execSync("npm install", { cwd: projectRoot, stdio: "ignore" });
|
|
495
|
+
return { fixed: true, description: "Ran npm install" };
|
|
496
|
+
} catch {
|
|
497
|
+
return { fixed: false, description: "npm install failed" };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Fix 3: Start dev server (for web apps)
|
|
502
|
+
if (
|
|
503
|
+
analysis.reason.includes("Timeout") ||
|
|
504
|
+
analysis.reason.includes("Server slow")
|
|
505
|
+
) {
|
|
506
|
+
try {
|
|
507
|
+
// Check if dev server is already running
|
|
508
|
+
const port = 3000;
|
|
509
|
+
const isRunning = checkPortInUse(port);
|
|
510
|
+
if (!isRunning) {
|
|
511
|
+
logInfo(null, "Dev server not running - tests may fail until started");
|
|
512
|
+
}
|
|
513
|
+
// Can't auto-start server, but report the issue
|
|
514
|
+
return { fixed: false, description: "Dev server not running" };
|
|
515
|
+
} catch {
|
|
516
|
+
return { fixed: false, description: "Could not check server status" };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return { fixed: false, description: "Could not auto-fix" };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Check if a port is in use
|
|
525
|
+
*/
|
|
526
|
+
function checkPortInUse(port: number): boolean {
|
|
527
|
+
try {
|
|
528
|
+
execSync(`lsof -i :${port}`, { stdio: "ignore" });
|
|
529
|
+
return true;
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Determine if failure is due to environment limitation
|
|
537
|
+
*/
|
|
538
|
+
function isEnvironmentLimitation(result: E2ETestResult): boolean {
|
|
539
|
+
const output = result.output + (result.errorMessage || "");
|
|
540
|
+
|
|
541
|
+
// Check for environment-specific issues
|
|
542
|
+
const envIssues = [
|
|
543
|
+
"no device",
|
|
544
|
+
"adb not found",
|
|
545
|
+
"no android",
|
|
546
|
+
"no ios simulator",
|
|
547
|
+
"xcode not found",
|
|
548
|
+
"platform-tools not found",
|
|
549
|
+
"cannot connect to server",
|
|
550
|
+
"connection refused",
|
|
551
|
+
"ECONNREFUSED",
|
|
552
|
+
"ETIMEDOUT",
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
return envIssues.some((issue) =>
|
|
556
|
+
output.toLowerCase().includes(issue.toLowerCase()),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Extract failed test names from output
|
|
562
|
+
*/
|
|
563
|
+
function extractFailedTests(output: string): string[] {
|
|
564
|
+
const tests: string[] = [];
|
|
565
|
+
|
|
566
|
+
// Playwright format
|
|
567
|
+
const playwrightMatch = output.matchAll(/FAIL (\S+)/g);
|
|
568
|
+
for (const match of playwrightMatch) {
|
|
569
|
+
tests.push(match[1]);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Jest/Vitest format
|
|
573
|
+
const jestMatch = output.matchAll(/✕ (.+)/g);
|
|
574
|
+
for (const match of jestMatch) {
|
|
575
|
+
tests.push(match[1]);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Django format
|
|
579
|
+
const djangoMatch = output.matchAll(/FAIL: (\S+)/g);
|
|
580
|
+
for (const match of djangoMatch) {
|
|
581
|
+
tests.push(match[1]);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return tests;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate test stubs for a project
|
|
589
|
+
*/
|
|
590
|
+
async function generateTestStubs(
|
|
591
|
+
projectRoot: string,
|
|
592
|
+
projectType: ProjectType,
|
|
593
|
+
): Promise<boolean> {
|
|
594
|
+
const testDir = getTestDirectory(projectRoot, projectType);
|
|
595
|
+
|
|
596
|
+
// Create test directory if not exists
|
|
597
|
+
if (!existsSync(testDir)) {
|
|
598
|
+
try {
|
|
599
|
+
const { mkdirSync } = require("fs");
|
|
600
|
+
mkdirSync(testDir, { recursive: true });
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Find source files to generate tests for
|
|
607
|
+
const srcDir = getSourceDirectory(projectRoot, projectType);
|
|
608
|
+
if (!existsSync(srcDir)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const sourceFiles = findSourceFiles(srcDir, projectType);
|
|
614
|
+
|
|
615
|
+
// Generate a basic test file for each source file
|
|
616
|
+
for (const sourceFile of sourceFiles.slice(0, 5)) {
|
|
617
|
+
// Limit to 5 files
|
|
618
|
+
const testFile = createTestFilePath(sourceFile, testDir, projectType);
|
|
619
|
+
if (!existsSync(testFile)) {
|
|
620
|
+
const testContent = generateTestContent(sourceFile, projectType);
|
|
621
|
+
writeFileSync(testFile, testContent);
|
|
622
|
+
logInfo(null, `Generated test stub: ${testFile}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return true;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
logError(null, "Failed to generate test stubs", error);
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Get test directory for project type
|
|
635
|
+
*/
|
|
636
|
+
function getTestDirectory(root: string, projectType: ProjectType): string {
|
|
637
|
+
switch (projectType) {
|
|
638
|
+
case "tauri":
|
|
639
|
+
return join(root, "e2e");
|
|
640
|
+
case "nextjs":
|
|
641
|
+
case "react_vite":
|
|
642
|
+
case "generic":
|
|
643
|
+
return join(root, "tests");
|
|
644
|
+
case "flutter":
|
|
645
|
+
return join(root, "integration_test");
|
|
646
|
+
case "django":
|
|
647
|
+
return join(root, "tests");
|
|
648
|
+
case "laravel":
|
|
649
|
+
return join(root, "tests", "Browser");
|
|
650
|
+
default:
|
|
651
|
+
return join(root, "tests");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Get source directory for project type
|
|
657
|
+
*/
|
|
658
|
+
function getSourceDirectory(root: string, projectType: ProjectType): string {
|
|
659
|
+
switch (projectType) {
|
|
660
|
+
case "tauri":
|
|
661
|
+
return join(root, "src");
|
|
662
|
+
case "nextjs":
|
|
663
|
+
return join(root, "app");
|
|
664
|
+
case "react_vite":
|
|
665
|
+
return join(root, "src");
|
|
666
|
+
case "flutter":
|
|
667
|
+
return join(root, "lib");
|
|
668
|
+
case "django":
|
|
669
|
+
return join(root, "app");
|
|
670
|
+
default:
|
|
671
|
+
return join(root, "src");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Find source files to generate tests for
|
|
677
|
+
*/
|
|
678
|
+
function findSourceFiles(srcDir: string, projectType: ProjectType): string[] {
|
|
679
|
+
const extensions = {
|
|
680
|
+
tauri: [".ts", ".tsx", ".rs"],
|
|
681
|
+
nextjs: [".ts", ".tsx"],
|
|
682
|
+
react_vite: [".ts", ".tsx"],
|
|
683
|
+
flutter: [".dart"],
|
|
684
|
+
django: [".py"],
|
|
685
|
+
laravel: [".php"],
|
|
686
|
+
generic: [".ts", ".tsx", ".js", ".jsx"],
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const exts = extensions[projectType as keyof typeof extensions] || [
|
|
690
|
+
".ts",
|
|
691
|
+
".tsx",
|
|
692
|
+
];
|
|
693
|
+
const files: string[] = [];
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const entries = readdirSync(srcDir, { withFileTypes: true });
|
|
697
|
+
for (const entry of entries) {
|
|
698
|
+
if (entry.isFile() && exts.includes(extname(entry.name))) {
|
|
699
|
+
files.push(join(srcDir, entry.name));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
// Ignore errors
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return files;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Create test file path from source file path
|
|
711
|
+
*/
|
|
712
|
+
function createTestFilePath(
|
|
713
|
+
sourceFile: string,
|
|
714
|
+
testDir: string,
|
|
715
|
+
projectType: ProjectType,
|
|
716
|
+
): string {
|
|
717
|
+
const base = require("path").basename(sourceFile, extname(sourceFile));
|
|
718
|
+
const ext = {
|
|
719
|
+
tauri: ".spec.ts",
|
|
720
|
+
nextjs: ".test.ts",
|
|
721
|
+
react_vite: ".test.ts",
|
|
722
|
+
flutter: "_test.dart",
|
|
723
|
+
django: "_test.py",
|
|
724
|
+
laravel: ".test.php",
|
|
725
|
+
generic: ".test.ts",
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
return join(
|
|
729
|
+
testDir,
|
|
730
|
+
`${base}${ext[projectType as keyof typeof ext] || ".test.ts"}`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Generate basic test content
|
|
736
|
+
*/
|
|
737
|
+
function generateTestContent(
|
|
738
|
+
sourceFile: string,
|
|
739
|
+
projectType: ProjectType,
|
|
740
|
+
): string {
|
|
741
|
+
const base = require("path").basename(sourceFile, extname(sourceFile));
|
|
742
|
+
|
|
743
|
+
switch (projectType) {
|
|
744
|
+
case "nextjs":
|
|
745
|
+
case "react_vite":
|
|
746
|
+
case "generic":
|
|
747
|
+
return `/**
|
|
748
|
+
* Auto-generated test stub for ${base}
|
|
749
|
+
* Generated by pi-harness-runtime E2E Auto-Test Loop
|
|
750
|
+
*/
|
|
751
|
+
|
|
752
|
+
import { test, expect } from '@playwright/test';
|
|
753
|
+
|
|
754
|
+
test.describe('${base}', () => {
|
|
755
|
+
test.beforeEach(async ({ page }) => {
|
|
756
|
+
// TODO: Add setup
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('should render', async ({ page }) => {
|
|
760
|
+
// TODO: Add assertion
|
|
761
|
+
await expect(page).toHaveTitle(/.*/);
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
`;
|
|
765
|
+
|
|
766
|
+
case "flutter":
|
|
767
|
+
return `/**
|
|
768
|
+
* Auto-generated test stub for ${base}
|
|
769
|
+
* Generated by pi-harness-runtime E2E Auto-Test Loop
|
|
770
|
+
*/
|
|
771
|
+
|
|
772
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
773
|
+
|
|
774
|
+
void main() {
|
|
775
|
+
group('${base}', () {
|
|
776
|
+
testWidgets('should render', (WidgetTester tester) async {
|
|
777
|
+
// TODO: Add widget test
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
`;
|
|
782
|
+
|
|
783
|
+
case "django":
|
|
784
|
+
return `'''
|
|
785
|
+
Auto-generated test stub for ${base}
|
|
786
|
+
Generated by pi-harness-runtime E2E Auto-Test Loop
|
|
787
|
+
'''
|
|
788
|
+
|
|
789
|
+
from django.test import TestCase
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class ${base.charAt(0).toUpperCase() + base.slice(1)}TestCase(TestCase):
|
|
793
|
+
def setUp(self):
|
|
794
|
+
# TODO: Add setup
|
|
795
|
+
pass
|
|
796
|
+
|
|
797
|
+
def test_should_render(self):
|
|
798
|
+
# TODO: Add assertion
|
|
799
|
+
pass
|
|
800
|
+
`;
|
|
801
|
+
|
|
802
|
+
default:
|
|
803
|
+
return `/**
|
|
804
|
+
* Auto-generated test stub for ${base}
|
|
805
|
+
* Generated by pi-harness-runtime E2E Auto-Test Loop
|
|
806
|
+
*/
|
|
807
|
+
|
|
808
|
+
describe('${base}', () => {
|
|
809
|
+
it('should work', () => {
|
|
810
|
+
expect(true).toBe(true);
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
}
|