pi-harness-runtime 1.1.16 → 1.1.18

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