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.
Files changed (105) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +76 -18
  3. package/harness/e2e/auto-test-loop.ts +815 -0
  4. package/harness/e2e/tools-detector.test.ts +156 -0
  5. package/harness/e2e/tools-detector.ts +801 -0
  6. package/harness/glm-quota-logger.ts +20 -23
  7. package/harness/index.ts +42 -0
  8. package/harness/loop-runtime.ts +49 -13
  9. package/harness/skill-sync.test.ts +57 -0
  10. package/harness/skill-sync.ts +247 -0
  11. package/harness/ssh-hang-recovery.ts +280 -0
  12. package/package.json +1 -1
  13. package/packages/a2a-adapter/package.json +1 -1
  14. package/packages/architecture-generator/package.json +1 -1
  15. package/packages/auth/package.json +1 -1
  16. package/packages/autonomous-refactor/package.json +1 -1
  17. package/packages/autonomous-runtime/package.json +1 -1
  18. package/packages/cache-strategy/package.json +1 -1
  19. package/packages/capability-registry/package.json +1 -1
  20. package/packages/checkpoint/package.json +1 -1
  21. package/packages/cli-plugin-sdk/package.json +1 -1
  22. package/packages/clipboard/package.json +1 -1
  23. package/packages/clipboard-plugin/package.json +1 -1
  24. package/packages/code-generation/package.json +1 -1
  25. package/packages/code-review/package.json +1 -1
  26. package/packages/codex-adapter/package.json +1 -1
  27. package/packages/config-capture/package.json +1 -1
  28. package/packages/context-compiler/package.json +1 -1
  29. package/packages/context-discovery/package.json +1 -1
  30. package/packages/context-manager/package.json +1 -1
  31. package/packages/cookie-sanitizer/package.json +1 -1
  32. package/packages/cost-optimizer/package.json +1 -1
  33. package/packages/dependency-analyzer/package.json +1 -1
  34. package/packages/django-plugin/package.json +1 -1
  35. package/packages/doc-generator/package.json +1 -1
  36. package/packages/evaluation-engine/package.json +1 -1
  37. package/packages/evaluation-runner/package.json +1 -1
  38. package/packages/event-bus/package.json +1 -1
  39. package/packages/event-store/package.json +1 -1
  40. package/packages/experience-replay/package.json +1 -1
  41. package/packages/feedback-collector/package.json +1 -1
  42. package/packages/file-copy-helper/package.json +1 -1
  43. package/packages/framework-detector/package.json +1 -1
  44. package/packages/framework-plugin-sdk/package.json +1 -1
  45. package/packages/frappe-plugin/package.json +1 -1
  46. package/packages/generic-web-plugin/package.json +1 -1
  47. package/packages/health-monitor/package.json +1 -1
  48. package/packages/intent-analyzer/package.json +1 -1
  49. package/packages/knowledge-graph/package.json +1 -1
  50. package/packages/knowledge-retrieval/package.json +1 -1
  51. package/packages/laravel-plugin/package.json +1 -1
  52. package/packages/learning-engine/package.json +1 -1
  53. package/packages/mcp-adapter/package.json +1 -1
  54. package/packages/memory-engine/package.json +1 -1
  55. package/packages/milestone-manager/package.json +1 -1
  56. package/packages/model-registry/package.json +1 -1
  57. package/packages/nextjs-plugin/package.json +1 -1
  58. package/packages/notification/package.json +1 -1
  59. package/packages/observability/package.json +1 -1
  60. package/packages/okf-indexer/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/release-manager/package.json +1 -1
  76. package/packages/requirement-compiler/package.json +1 -1
  77. package/packages/runtime/package.json +1 -1
  78. package/packages/scheduler/package.json +1 -1
  79. package/packages/scheduler-adapter/package.json +1 -1
  80. package/packages/session/package.json +1 -1
  81. package/packages/session-api/package.json +1 -1
  82. package/packages/session-export/package.json +1 -1
  83. package/packages/shared-context/package.json +1 -1
  84. package/packages/skill-mcp-client/package.json +28 -0
  85. package/packages/skill-mcp-client/src/client.ts +171 -0
  86. package/packages/skill-mcp-client/src/errors.ts +75 -0
  87. package/packages/skill-mcp-client/src/index.ts +13 -0
  88. package/packages/skill-mcp-client/src/types.ts +123 -0
  89. package/packages/skill-registry/package.json +1 -1
  90. package/packages/sprint-planner/package.json +1 -1
  91. package/packages/subscription-engine/package.json +1 -1
  92. package/packages/task-compiler/package.json +1 -1
  93. package/packages/tencentdb-memory/package.json +1 -1
  94. package/packages/tencentdb-sync/package.json +1 -1
  95. package/packages/test-data-generator/package.json +1 -1
  96. package/packages/test-generator/package.json +1 -1
  97. package/packages/todo-bd-sync/package.json +1 -1
  98. package/packages/token-estimation/package.json +1 -1
  99. package/packages/token-optimizer/package.json +1 -1
  100. package/packages/tui/package.json +1 -1
  101. package/packages/types/package.json +1 -1
  102. package/packages/workflow-events/package.json +1 -1
  103. package/packages/workspace-scanner/package.json +1 -1
  104. package/packages/worktree/package.json +1 -1
  105. 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
+ }