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,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
|
+
}
|