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,801 @@
1
+ /**
2
+ * E2E Testing Tools Detector & Advisor — RFC-0101
3
+ *
4
+ * Detects project type and available testing tools, then presents
5
+ * relevant options to human or auto-decides based on code changes.
6
+ *
7
+ * Case-based mapping:
8
+ * - Tauri/Rust → WebdriverIO + @wdio/tauri-service, ADB, scrcpy
9
+ * - iOS/Swift → XCUITest, fastlane, simctl
10
+ * - Android/Kotlin → Espresso, ADB, Gradle
11
+ * - Web (Next.js/React) → Playwright, Playwright CLI
12
+ * - Django → Django test client, Playwright
13
+ * - Laravel → Laravel Dusk, Playwright
14
+ *
15
+ * Flow:
16
+ * Code Changed
17
+ * → Detect Project Type
18
+ * → Scan Available Tools
19
+ * → Smart Decision (auto vs ask human)
20
+ * → Generate Test Stubs (if needed)
21
+ * → Run Tests
22
+ * → Auto-fix if failed
23
+ * → Escalate if stuck
24
+ */
25
+
26
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
27
+ import { join, extname } from "node:path";
28
+ import { homedir } from "node:os";
29
+
30
+ export type NativeAppType =
31
+ | "tauri"
32
+ | "ios"
33
+ | "android"
34
+ | "electron"
35
+ | "flutter";
36
+ export type WebFramework =
37
+ | "nextjs"
38
+ | "react_vite"
39
+ | "django"
40
+ | "laravel"
41
+ | "generic";
42
+ export type ProjectType = NativeAppType | WebFramework | "unknown";
43
+
44
+ export interface TestingTool {
45
+ name: string;
46
+ description: string;
47
+ command?: string;
48
+ installed: boolean;
49
+ installHint?: string;
50
+ }
51
+
52
+ export interface E2EToolsConfig {
53
+ projectRoot: string;
54
+ projectType: ProjectType;
55
+ detectedTools: TestingTool[];
56
+ recommendedTools: TestingTool[];
57
+ needsManualSetup: boolean;
58
+ setupInstructions?: string;
59
+ }
60
+
61
+ export interface SmartDecision {
62
+ shouldAutoTest: boolean;
63
+ confidence: number; // 0-1
64
+ reason: string;
65
+ suggestedAction: "auto_run" | "ask_human" | "skip" | "generate_stubs";
66
+ }
67
+
68
+ /** Case-based testing tools mapping */
69
+ const TESTING_TOOLS_MAP: Record<
70
+ ProjectType,
71
+ {
72
+ autoTools: TestingTool[];
73
+ manualTools: TestingTool[];
74
+ detectionSignals: string[];
75
+ }
76
+ > = {
77
+ tauri: {
78
+ detectionSignals: [
79
+ "src-tauri/Cargo.toml",
80
+ "tauri.conf.json",
81
+ "src-tauri/src/lib.rs",
82
+ ],
83
+ autoTools: [
84
+ {
85
+ name: "WebdriverIO + @wdio/tauri-service",
86
+ description: "E2E tests via embedded WebDriver in Tauri app",
87
+ command: "wdio run ./hardware-e2e/wdio.conf.ts",
88
+ installed: false,
89
+ installHint: "npm install @wdio/cli @wdio/tauri-service",
90
+ },
91
+ {
92
+ name: "Playwright (for web components)",
93
+ description: "Test web views rendered in WebView",
94
+ command: "npx playwright test",
95
+ installed: false,
96
+ installHint: "npm install @playwright/test",
97
+ },
98
+ ],
99
+ manualTools: [
100
+ {
101
+ name: "ADB (Android Debug Bridge)",
102
+ description: "Device control: screenshot, tap, swipe, shell",
103
+ installed: false,
104
+ installHint: "Install Android SDK platform tools",
105
+ },
106
+ {
107
+ name: "scrcpy",
108
+ description: "Mirror Android screen to desktop",
109
+ installed: false,
110
+ installHint: "brew install scrcpy (macOS) or apt install scrcpy (Linux)",
111
+ },
112
+ ],
113
+ },
114
+ ios: {
115
+ detectionSignals: [
116
+ "*.xcodeproj",
117
+ "*.xcworkspace",
118
+ "Package.swift",
119
+ "ios/*.swift",
120
+ "App/*.swift",
121
+ ],
122
+ autoTools: [
123
+ {
124
+ name: "XCUITest",
125
+ description: "Native iOS UI testing",
126
+ installed: false,
127
+ installHint: "Built into Xcode",
128
+ },
129
+ {
130
+ name: "fastlane",
131
+ description: "Automate iOS testing and deployment",
132
+ command: "fastlane scan",
133
+ installed: false,
134
+ installHint: "gem install fastlane",
135
+ },
136
+ ],
137
+ manualTools: [
138
+ {
139
+ name: "simctl",
140
+ description: "Control iOS Simulator from command line",
141
+ installed: false,
142
+ installHint: "xcrun simctl list",
143
+ },
144
+ ],
145
+ },
146
+ android: {
147
+ detectionSignals: [
148
+ "android/app/build.gradle",
149
+ "app/build.gradle.kts",
150
+ "settings.gradle",
151
+ "settings.gradle.kts",
152
+ ],
153
+ autoTools: [
154
+ {
155
+ name: "Espresso",
156
+ description: "Android UI testing framework",
157
+ command: "./gradlew connectedAndroidTest",
158
+ installed: false,
159
+ installHint: "Included in Android SDK",
160
+ },
161
+ {
162
+ name: "ADB (Android Debug Bridge)",
163
+ description: "Device control: screenshot, tap, swipe, shell",
164
+ installed: false,
165
+ installHint: "Install Android SDK platform tools",
166
+ },
167
+ ],
168
+ manualTools: [
169
+ {
170
+ name: "Appium",
171
+ description: "Cross-platform mobile automation",
172
+ installed: false,
173
+ installHint: "npm install -g appium",
174
+ },
175
+ {
176
+ name: "scrcpy",
177
+ description: "Mirror Android screen to desktop",
178
+ installed: false,
179
+ installHint: "brew install scrcpy (macOS) or apt install scrcpy (Linux)",
180
+ },
181
+ ],
182
+ },
183
+ electron: {
184
+ detectionSignals: [
185
+ "electron-builder",
186
+ "electron/main.ts",
187
+ "electron/main.js",
188
+ "package.json", // with electron dependency
189
+ ],
190
+ autoTools: [
191
+ {
192
+ name: "Playwright",
193
+ description: "E2E testing for Electron apps",
194
+ command: "npx playwright test",
195
+ installed: false,
196
+ installHint: "npm install @playwright/test",
197
+ },
198
+ {
199
+ name: "Spectron (legacy)",
200
+ description: "Electron-specific testing (deprecated)",
201
+ installed: false,
202
+ installHint: "npm install @electron/spectron",
203
+ },
204
+ ],
205
+ manualTools: [],
206
+ },
207
+ flutter: {
208
+ detectionSignals: ["pubspec.yaml", "lib/main.dart", "test/*.dart"],
209
+ autoTools: [
210
+ {
211
+ name: "Flutter Test",
212
+ description: "Unit and widget testing",
213
+ command: "flutter test",
214
+ installed: false,
215
+ installHint: "flutter pub run test",
216
+ },
217
+ {
218
+ name: "Integration Test",
219
+ description: "Full app E2E testing",
220
+ command: "flutter test integration_test",
221
+ installed: false,
222
+ installHint: "flutter pub run integration_test",
223
+ },
224
+ ],
225
+ manualTools: [
226
+ {
227
+ name: " Patrol",
228
+ description: "Native E2E testing for Flutter",
229
+ installed: false,
230
+ installHint: "flutter pub add patrol",
231
+ },
232
+ ],
233
+ },
234
+ nextjs: {
235
+ detectionSignals: [
236
+ "next.config.ts",
237
+ "next.config.js",
238
+ "package.json", // with next dependency
239
+ "app/",
240
+ "pages/",
241
+ ],
242
+ autoTools: [
243
+ {
244
+ name: "Playwright",
245
+ description: "E2E testing for Next.js apps",
246
+ command: "npx playwright test",
247
+ installed: false,
248
+ installHint: "npm install @playwright/test && npx playwright install",
249
+ },
250
+ ],
251
+ manualTools: [],
252
+ },
253
+ react_vite: {
254
+ detectionSignals: [
255
+ "vite.config.ts",
256
+ "vite.config.js",
257
+ "package.json", // with vite dependency
258
+ "src/main.tsx",
259
+ "src/main.jsx",
260
+ ],
261
+ autoTools: [
262
+ {
263
+ name: "Playwright",
264
+ description: "E2E testing for Vite apps",
265
+ command: "npx playwright test",
266
+ installed: false,
267
+ installHint: "npm install @playwright/test && npx playwright install",
268
+ },
269
+ {
270
+ name: "Vitest",
271
+ description: "Unit testing (complements Playwright)",
272
+ command: "npx vitest",
273
+ installed: false,
274
+ installHint: "npm install vitest",
275
+ },
276
+ ],
277
+ manualTools: [],
278
+ },
279
+ django: {
280
+ detectionSignals: [
281
+ "manage.py",
282
+ "settings.py",
283
+ "requirements.txt", // with django
284
+ ],
285
+ autoTools: [
286
+ {
287
+ name: "Django Test Client",
288
+ description: "Built-in Django testing",
289
+ command: "python manage.py test",
290
+ installed: false,
291
+ },
292
+ {
293
+ name: "Playwright",
294
+ description: "Browser-based E2E testing",
295
+ command: "npx playwright test",
296
+ installed: false,
297
+ installHint: "npm install @playwright/test",
298
+ },
299
+ ],
300
+ manualTools: [],
301
+ },
302
+ laravel: {
303
+ detectionSignals: [
304
+ "artisan",
305
+ "database/seeders",
306
+ "composer.json", // with laravel
307
+ ],
308
+ autoTools: [
309
+ {
310
+ name: "Laravel Dusk",
311
+ description: "Browser testing for Laravel",
312
+ command: "php artisan dusk",
313
+ installed: false,
314
+ installHint: "composer require --dev laravel/dusk",
315
+ },
316
+ {
317
+ name: "Playwright",
318
+ description: "Browser-based E2E testing",
319
+ command: "npx playwright test",
320
+ installed: false,
321
+ installHint: "npm install @playwright/test",
322
+ },
323
+ ],
324
+ manualTools: [],
325
+ },
326
+ generic: {
327
+ detectionSignals: ["package.json"],
328
+ autoTools: [
329
+ {
330
+ name: "Playwright",
331
+ description: "Generic E2E testing",
332
+ command: "npx playwright test",
333
+ installed: false,
334
+ installHint: "npm install @playwright/test && npx playwright install",
335
+ },
336
+ ],
337
+ manualTools: [],
338
+ },
339
+ unknown: {
340
+ detectionSignals: [],
341
+ autoTools: [],
342
+ manualTools: [],
343
+ },
344
+ };
345
+
346
+ /**
347
+ * Detect project type from project root directory
348
+ */
349
+ export function detectProjectType(projectRoot: string): ProjectType {
350
+ // Check for Tauri/Rust
351
+ if (
352
+ hasFile(projectRoot, "src-tauri/Cargo.toml") ||
353
+ hasFile(projectRoot, "tauri.conf.json")
354
+ ) {
355
+ return "tauri";
356
+ }
357
+
358
+ // Check for iOS/Swift
359
+ if (
360
+ hasFileWithExt(projectRoot, ".xcodeproj") ||
361
+ hasFileWithExt(projectRoot, ".xcworkspace") ||
362
+ hasFile(projectRoot, "Package.swift") ||
363
+ hasFile(projectRoot, "ios/AppDelegate.swift")
364
+ ) {
365
+ return "ios";
366
+ }
367
+
368
+ // Check for Android/Kotlin
369
+ if (
370
+ hasFile(projectRoot, "android/app/build.gradle") ||
371
+ hasFile(projectRoot, "app/build.gradle.kts") ||
372
+ hasFile(projectRoot, "settings.gradle") ||
373
+ hasFile(projectRoot, "settings.gradle.kts")
374
+ ) {
375
+ return "android";
376
+ }
377
+
378
+ // Check for Flutter
379
+ if (
380
+ hasFile(projectRoot, "pubspec.yaml") &&
381
+ (hasFile(projectRoot, "lib/main.dart") || hasFile(projectRoot, "test/"))
382
+ ) {
383
+ return "flutter";
384
+ }
385
+
386
+ // Check for Electron
387
+ if (hasFile(projectRoot, "package.json")) {
388
+ const pkg = readPackageJson(projectRoot);
389
+ if (
390
+ pkg.dependencies?.["electron"] ||
391
+ pkg.devDependencies?.["electron"] ||
392
+ pkg.dependencies?.["electron-builder"]
393
+ ) {
394
+ return "electron";
395
+ }
396
+ }
397
+
398
+ // Check for Next.js
399
+ if (
400
+ hasFile(projectRoot, "next.config.ts") ||
401
+ hasFile(projectRoot, "next.config.js") ||
402
+ (hasFile(projectRoot, "package.json") &&
403
+ (hasFile(projectRoot, "app/") || hasFile(projectRoot, "pages/")))
404
+ ) {
405
+ const pkg = readPackageJson(projectRoot);
406
+ if (pkg.dependencies?.["next"] || pkg.devDependencies?.["next"]) {
407
+ return "nextjs";
408
+ }
409
+ }
410
+
411
+ // Check for React/Vite
412
+ if (
413
+ hasFile(projectRoot, "vite.config.ts") ||
414
+ hasFile(projectRoot, "vite.config.js")
415
+ ) {
416
+ return "react_vite";
417
+ }
418
+
419
+ // Check for Django
420
+ if (hasFile(projectRoot, "manage.py") || hasFile(projectRoot, "settings.py")) {
421
+ return "django";
422
+ }
423
+
424
+ // Check for Laravel
425
+ if (
426
+ hasFile(projectRoot, "artisan") ||
427
+ hasFile(projectRoot, "database/seeders")
428
+ ) {
429
+ return "laravel";
430
+ }
431
+
432
+ // Check for generic web project
433
+ if (hasFile(projectRoot, "package.json")) {
434
+ return "generic";
435
+ }
436
+
437
+ return "unknown";
438
+ }
439
+
440
+ /**
441
+ * Detect available testing tools in the project
442
+ */
443
+ export function detectAvailableTools(
444
+ projectRoot: string,
445
+ projectType: ProjectType,
446
+ ): TestingTool[] {
447
+ const tools: TestingTool[] = [];
448
+ const config = TESTING_TOOLS_MAP[projectType];
449
+
450
+ if (!config) return tools;
451
+
452
+ // Check for each tool
453
+ for (const tool of [...config.autoTools, ...config.manualTools]) {
454
+ const toolCopy = { ...tool };
455
+
456
+ // Check if installed via package.json
457
+ if (tool.installHint?.startsWith("npm")) {
458
+ const pkg = readPackageJson(projectRoot);
459
+ const pkgName = tool.installHint.match(/@[\w-]+/)?.[0];
460
+ if (pkgName) {
461
+ toolCopy.installed =
462
+ !!pkg.dependencies?.[pkgName] || !!pkg.devDependencies?.[pkgName];
463
+ }
464
+ }
465
+
466
+ // Check for CLI tools in PATH
467
+ if (tool.command) {
468
+ const cmd = tool.command.split(" ")[0];
469
+ toolCopy.installed = isCommandAvailable(cmd);
470
+ }
471
+
472
+ tools.push(toolCopy);
473
+ }
474
+
475
+ return tools;
476
+ }
477
+
478
+ /**
479
+ * Get recommended tools (auto-tools that are installed)
480
+ */
481
+ export function getRecommendedTools(tools: TestingTool[]): TestingTool[] {
482
+ return tools.filter((t) => t.installed);
483
+ }
484
+
485
+ /**
486
+ * Smart decision algorithm to determine if testing is needed
487
+ *
488
+ * Factors:
489
+ * - Project type (native apps need more manual setup)
490
+ * - Available tools
491
+ * - Recent code changes (heuristic)
492
+ * - Test coverage (if tests exist)
493
+ */
494
+ export function makeSmartDecision(
495
+ projectRoot: string,
496
+ projectType: ProjectType,
497
+ availableTools: TestingTool[],
498
+ ): SmartDecision {
499
+ const installedAutoTools = availableTools.filter((t) => t.installed);
500
+
501
+ // No tools available = ask human
502
+ if (installedAutoTools.length === 0) {
503
+ return {
504
+ shouldAutoTest: false,
505
+ confidence: 0.1,
506
+ reason: "No testing tools installed. Ask human for setup.",
507
+ suggestedAction: "ask_human",
508
+ };
509
+ }
510
+
511
+ // Check if test files exist
512
+ const testFilesExist = hasTestFiles(projectRoot, projectType);
513
+ const testDir = getTestDir(projectRoot, projectType);
514
+ const testFileCount = countTestFiles(projectRoot, testDir);
515
+
516
+ // Check for recent UI changes (heuristic)
517
+ const hasRecentUIChanges = checkRecentUIChanges(projectRoot);
518
+
519
+ // Native apps (Tauri, iOS, Android) need more careful consideration
520
+ if (["tauri", "ios", "android"].includes(projectType)) {
521
+ if (!installedAutoTools.some((t) => t.name.includes("WebdriverIO"))) {
522
+ return {
523
+ shouldAutoTest: false,
524
+ confidence: 0.4,
525
+ reason:
526
+ "Native app detected. Setup requires additional tools (WebdriverIO for Tauri). Ask human.",
527
+ suggestedAction: "ask_human",
528
+ };
529
+ }
530
+
531
+ // Has hardware testing setup
532
+ return {
533
+ shouldAutoTest: true,
534
+ confidence: 0.7,
535
+ reason:
536
+ "Native app with WebdriverIO available. Will auto-run if tests exist.",
537
+ suggestedAction: testFilesExist ? "auto_run" : "generate_stubs",
538
+ };
539
+ }
540
+
541
+ // Web apps are easier to test automatically
542
+ if (["nextjs", "react_vite", "generic"].includes(projectType)) {
543
+ const hasPlaywright = installedAutoTools.some((t) =>
544
+ t.name.includes("Playwright"),
545
+ );
546
+
547
+ if (!hasPlaywright) {
548
+ return {
549
+ shouldAutoTest: false,
550
+ confidence: 0.3,
551
+ reason:
552
+ "Web app detected but Playwright not installed. Ask human to install.",
553
+ suggestedAction: "ask_human",
554
+ };
555
+ }
556
+
557
+ // Good setup for auto-testing
558
+ if (testFilesExist) {
559
+ return {
560
+ shouldAutoTest: true,
561
+ confidence: 0.9,
562
+ reason: `Found ${testFileCount} test file(s). Auto-running tests.`,
563
+ suggestedAction: "auto_run",
564
+ };
565
+ }
566
+
567
+ // No tests but has UI code = generate stubs
568
+ if (hasRecentUIChanges) {
569
+ return {
570
+ shouldAutoTest: false,
571
+ confidence: 0.6,
572
+ reason: "UI changes detected but no tests found. Generate stubs?",
573
+ suggestedAction: "generate_stubs",
574
+ };
575
+ }
576
+
577
+ return {
578
+ shouldAutoTest: false,
579
+ confidence: 0.5,
580
+ reason: "No test files found. Consider generating some?",
581
+ suggestedAction: "skip",
582
+ };
583
+ }
584
+
585
+ // Backend frameworks
586
+ if (["django", "laravel"].includes(projectType)) {
587
+ const hasFrameworkTest =
588
+ installedAutoTools.some(
589
+ (t) => t.name.includes("Django") || t.name.includes("Laravel Dusk"),
590
+ ) || installedAutoTools.some((t) => t.name.includes("Playwright"));
591
+
592
+ if (!hasFrameworkTest) {
593
+ return {
594
+ shouldAutoTest: false,
595
+ confidence: 0.3,
596
+ reason:
597
+ "Backend framework detected. Install framework-specific test tools.",
598
+ suggestedAction: "ask_human",
599
+ };
600
+ }
601
+
602
+ if (testFilesExist) {
603
+ return {
604
+ shouldAutoTest: true,
605
+ confidence: 0.8,
606
+ reason: `Found ${testFileCount} test file(s). Auto-running tests.`,
607
+ suggestedAction: "auto_run",
608
+ };
609
+ }
610
+
611
+ return {
612
+ shouldAutoTest: false,
613
+ confidence: 0.5,
614
+ reason:
615
+ "Backend project detected. Auto-tests skipped (manual review needed).",
616
+ suggestedAction: "skip",
617
+ };
618
+ }
619
+
620
+ // Default fallback
621
+ return {
622
+ shouldAutoTest: false,
623
+ confidence: 0.2,
624
+ reason: "Unknown project type. Manual review recommended.",
625
+ suggestedAction: "ask_human",
626
+ };
627
+ }
628
+
629
+ /**
630
+ * Generate tools presentation for human
631
+ */
632
+ export function generateToolsPresentation(
633
+ projectType: ProjectType,
634
+ tools: TestingTool[],
635
+ ): string {
636
+ const config = TESTING_TOOLS_MAP[projectType];
637
+ if (!config) return "";
638
+
639
+ const lines: string[] = [];
640
+ lines.push("");
641
+ lines.push("## 🔧 Available E2E Testing Tools");
642
+ lines.push("");
643
+
644
+ const autoTools = tools.filter((t) => t.installed);
645
+ const notInstalledTools = tools.filter((t) => !t.installed);
646
+
647
+ if (autoTools.length > 0) {
648
+ lines.push("### ✅ Ready to Use");
649
+ lines.push("");
650
+ for (const tool of autoTools) {
651
+ lines.push(`- **${tool.name}**: ${tool.description}`);
652
+ if (tool.command) {
653
+ lines.push(` Run: \`${tool.command}\``);
654
+ }
655
+ }
656
+ lines.push("");
657
+ }
658
+
659
+ if (notInstalledTools.length > 0) {
660
+ lines.push("### 📦 Available (not installed)");
661
+ lines.push("");
662
+ for (const tool of notInstalledTools) {
663
+ lines.push(`- **${tool.name}**: ${tool.description}`);
664
+ if (tool.installHint) {
665
+ lines.push(` Install: \`${tool.installHint}\``);
666
+ }
667
+ }
668
+ lines.push("");
669
+ }
670
+
671
+ return lines.join("\n");
672
+ }
673
+
674
+ // --- Helper Functions ---
675
+
676
+ function hasFile(root: string, file: string): boolean {
677
+ return existsSync(join(root, file));
678
+ }
679
+
680
+ function hasFileWithExt(root: string, ext: string): boolean {
681
+ try {
682
+ const entries = readdirSync(root, { withFileTypes: true });
683
+ return entries.some((e) => e.isFile() && e.name.endsWith(ext));
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+
689
+ function readPackageJson(root: string): {
690
+ dependencies?: Record<string, string>;
691
+ devDependencies?: Record<string, string>;
692
+ } {
693
+ try {
694
+ const content = readFileSync(join(root, "package.json"), "utf-8");
695
+ return JSON.parse(content);
696
+ } catch {
697
+ return {};
698
+ }
699
+ }
700
+
701
+ function isCommandAvailable(cmd: string): boolean {
702
+ try {
703
+ const { execSync } = require("child_process");
704
+ execSync(`which ${cmd}`, { stdio: "ignore" });
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
710
+
711
+ function hasTestFiles(root: string, projectType: ProjectType): boolean {
712
+ const testDir = getTestDir(root, projectType);
713
+ return existsSync(testDir);
714
+ }
715
+
716
+ function getTestDir(root: string, projectType: ProjectType): string {
717
+ switch (projectType) {
718
+ case "tauri":
719
+ return join(root, "hardware-e2e", "specs");
720
+ case "ios":
721
+ return join(root, "ios", "MyAppTests");
722
+ case "android":
723
+ return join(root, "app", "src", "androidTest");
724
+ case "flutter":
725
+ return join(root, "integration_test");
726
+ case "nextjs":
727
+ case "react_vite":
728
+ case "generic":
729
+ return join(root, "tests");
730
+ case "django":
731
+ return join(root, "tests");
732
+ case "laravel":
733
+ return join(root, "tests", "Browser");
734
+ default:
735
+ return join(root, "tests");
736
+ }
737
+ }
738
+
739
+ function countTestFiles(_root: string, testDir: string): number {
740
+ if (!existsSync(testDir)) return 0;
741
+
742
+ try {
743
+ const { execSync } = require("child_process");
744
+ const output = execSync(
745
+ `find "${testDir}" -type f \\( -name "*.spec.ts" -o -name "*.test.ts" -o -name "*_test.dart" -o -name "*_test.py" \\) 2>/dev/null | wc -l`,
746
+ { encoding: "utf-8" },
747
+ );
748
+ return parseInt(output.trim(), 10) || 0;
749
+ } catch {
750
+ return 0;
751
+ }
752
+ }
753
+
754
+ function checkRecentUIChanges(root: string): boolean {
755
+ // Heuristic: check for recent changes in UI-related directories
756
+ const uiDirs = [
757
+ "src/components",
758
+ "app/components",
759
+ "components",
760
+ "pages",
761
+ "app/page",
762
+ "src/views",
763
+ ];
764
+
765
+ try {
766
+ const { execSync } = require("child_process");
767
+ // Check git for recent UI changes (last 24h)
768
+ for (const dir of uiDirs) {
769
+ if (existsSync(join(root, dir))) {
770
+ const output = execSync(
771
+ `cd "${root}" && git log --oneline --since="24 hours ago" -- "${dir}" 2>/dev/null | head -1`,
772
+ { encoding: "utf-8" },
773
+ );
774
+ if (output.trim()) return true;
775
+ }
776
+ }
777
+ } catch {
778
+ // Git not available or other error
779
+ }
780
+
781
+ return false;
782
+ }
783
+
784
+ /**
785
+ * Get E2E tools config for a project
786
+ */
787
+ export function getE2EToolsConfig(projectRoot: string): E2EToolsConfig {
788
+ const projectType = detectProjectType(projectRoot);
789
+ const allTools = detectAvailableTools(projectRoot, projectType);
790
+ const installedTools = allTools.filter((t) => t.installed);
791
+ const needsManualSetup = installedTools.length === 0;
792
+
793
+ return {
794
+ projectRoot,
795
+ projectType,
796
+ detectedTools: allTools,
797
+ recommendedTools: installedTools,
798
+ needsManualSetup,
799
+ setupInstructions: generateToolsPresentation(projectType, allTools),
800
+ };
801
+ }