@mseep/ai-tech-app-agent 1.0.0
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/.env +24 -0
- package/.env.example +24 -0
- package/Jenkinsfile +210 -0
- package/MCP-SERVER-GUIDE.md +405 -0
- package/README.MD +450 -0
- package/dist/config/app.config.d.ts +65 -0
- package/dist/config/app.config.d.ts.map +1 -0
- package/dist/config/app.config.js +94 -0
- package/dist/config/app.config.js.map +1 -0
- package/dist/config/llm.config.d.ts +63 -0
- package/dist/config/llm.config.d.ts.map +1 -0
- package/dist/config/llm.config.js +158 -0
- package/dist/config/llm.config.js.map +1 -0
- package/dist/config/mcp.config.d.ts +175 -0
- package/dist/config/mcp.config.d.ts.map +1 -0
- package/dist/config/mcp.config.js +215 -0
- package/dist/config/mcp.config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/llamaClient.d.ts +14 -0
- package/dist/llm/llamaClient.d.ts.map +1 -0
- package/dist/llm/llamaClient.js +136 -0
- package/dist/llm/llamaClient.js.map +1 -0
- package/dist/mcp/mcpClient.d.ts +132 -0
- package/dist/mcp/mcpClient.d.ts.map +1 -0
- package/dist/mcp/mcpClient.js +784 -0
- package/dist/mcp/mcpClient.js.map +1 -0
- package/dist/models/testSpec.d.ts +78 -0
- package/dist/models/testSpec.d.ts.map +1 -0
- package/dist/models/testSpec.js +3 -0
- package/dist/models/testSpec.js.map +1 -0
- package/dist/orchestrator/aiTestRunner.d.ts +18 -0
- package/dist/orchestrator/aiTestRunner.d.ts.map +1 -0
- package/dist/orchestrator/aiTestRunner.js +247 -0
- package/dist/orchestrator/aiTestRunner.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/promptBuilder.d.ts +62 -0
- package/dist/utils/promptBuilder.d.ts.map +1 -0
- package/dist/utils/promptBuilder.js +333 -0
- package/dist/utils/promptBuilder.js.map +1 -0
- package/knowledge/app-knowledge.txt +100 -0
- package/logs/combined.log +486 -0
- package/logs/error.log +50 -0
- package/package.json +62 -0
- package/reports/screenshots/screenshot_1764535110518.png +0 -0
- package/reports/test-report.json +106 -0
- package/scripts/check-mcp-server.sh +100 -0
- package/scripts/extract-pom-knowledge.js +222 -0
- package/scripts/pre-test-setup.js +262 -0
- package/scripts/start-mcp-server.sh +76 -0
- package/src/config/app.config.ts +175 -0
- package/src/config/llm.config.ts +220 -0
- package/src/config/mcp.config.ts +291 -0
- package/src/index.ts +161 -0
- package/src/llm/llamaClient.ts +159 -0
- package/src/mcp/mcpClient.ts +878 -0
- package/src/models/testSpec.ts +85 -0
- package/src/orchestrator/aiTestRunner.ts +286 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/promptBuilder.ts +384 -0
- package/tests/nlp-specs/login-flow.yaml +31 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
export interface LLMConfig {
|
|
6
|
+
provider: 'ollama' | 'openai' | 'anthropic' | 'custom';
|
|
7
|
+
|
|
8
|
+
// Connection settings
|
|
9
|
+
connection: {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
timeout: number;
|
|
12
|
+
retryAttempts: number;
|
|
13
|
+
retryDelay: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Model settings
|
|
17
|
+
model: {
|
|
18
|
+
name: string;
|
|
19
|
+
temperature: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
topP: number;
|
|
22
|
+
topK: number;
|
|
23
|
+
frequencyPenalty: number;
|
|
24
|
+
presencePenalty: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Prompt configuration
|
|
28
|
+
prompt: {
|
|
29
|
+
systemMessage: string;
|
|
30
|
+
includeAppContext: boolean;
|
|
31
|
+
includeUIContext: boolean;
|
|
32
|
+
maxUIElements: number;
|
|
33
|
+
maxContextTokens: number;
|
|
34
|
+
includeReasoningInResponse: boolean;
|
|
35
|
+
strictJsonMode: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Performance
|
|
39
|
+
performance: {
|
|
40
|
+
cachingEnabled: boolean;
|
|
41
|
+
cacheTTL: number;
|
|
42
|
+
batchRequests: boolean;
|
|
43
|
+
maxBatchSize: number;
|
|
44
|
+
streamResponse: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Fallback
|
|
48
|
+
fallback: {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
fallbackModel?: string;
|
|
51
|
+
maxFallbackAttempts: number;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const llmConfig: LLMConfig = {
|
|
56
|
+
provider: (process.env.LLM_PROVIDER as 'ollama' | 'openai' | 'anthropic' | 'custom') || 'ollama',
|
|
57
|
+
|
|
58
|
+
connection: {
|
|
59
|
+
baseUrl: process.env.OLLAMA_BASE_URL || process.env.LLM_BASE_URL || 'http://localhost:11434',
|
|
60
|
+
timeout: parseInt(process.env.LLM_TIMEOUT || '60000'),
|
|
61
|
+
retryAttempts: parseInt(process.env.LLM_RETRY_ATTEMPTS || '3'),
|
|
62
|
+
retryDelay: parseInt(process.env.LLM_RETRY_DELAY || '2000'),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
model: {
|
|
66
|
+
name: process.env.LLM_MODEL || 'llama3.2:3b',
|
|
67
|
+
temperature: parseFloat(process.env.LLM_TEMPERATURE || '0.1'),
|
|
68
|
+
maxTokens: parseInt(process.env.LLM_MAX_TOKENS || '2000'),
|
|
69
|
+
topP: parseFloat(process.env.LLM_TOP_P || '0.9'),
|
|
70
|
+
topK: parseInt(process.env.LLM_TOP_K || '40'),
|
|
71
|
+
frequencyPenalty: parseFloat(process.env.LLM_FREQUENCY_PENALTY || '0.0'),
|
|
72
|
+
presencePenalty: parseFloat(process.env.LLM_PRESENCE_PENALTY || '0.0'),
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
prompt: {
|
|
76
|
+
systemMessage: process.env.LLM_SYSTEM_MESSAGE ||
|
|
77
|
+
'You are an expert mobile test automation agent specialized in Appium automation.',
|
|
78
|
+
includeAppContext: process.env.LLM_INCLUDE_APP_CONTEXT !== 'false',
|
|
79
|
+
includeUIContext: process.env.LLM_INCLUDE_UI_CONTEXT !== 'false',
|
|
80
|
+
maxUIElements: parseInt(process.env.LLM_MAX_UI_ELEMENTS || '30'),
|
|
81
|
+
maxContextTokens: parseInt(process.env.LLM_MAX_CONTEXT_TOKENS || '4000'),
|
|
82
|
+
includeReasoningInResponse: process.env.LLM_INCLUDE_REASONING !== 'false',
|
|
83
|
+
strictJsonMode: process.env.LLM_STRICT_JSON_MODE !== 'false',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
performance: {
|
|
87
|
+
cachingEnabled: process.env.LLM_CACHING_ENABLED === 'true',
|
|
88
|
+
cacheTTL: parseInt(process.env.LLM_CACHE_TTL || '3600'),
|
|
89
|
+
batchRequests: process.env.LLM_BATCH_REQUESTS === 'true',
|
|
90
|
+
maxBatchSize: parseInt(process.env.LLM_MAX_BATCH_SIZE || '5'),
|
|
91
|
+
streamResponse: process.env.LLM_STREAM_RESPONSE === 'true',
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
fallback: {
|
|
95
|
+
enabled: process.env.LLM_FALLBACK_ENABLED === 'true',
|
|
96
|
+
fallbackModel: process.env.LLM_FALLBACK_MODEL,
|
|
97
|
+
maxFallbackAttempts: parseInt(process.env.LLM_FALLBACK_ATTEMPTS || '1'),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Predefined prompt templates
|
|
102
|
+
export const PromptTemplates = {
|
|
103
|
+
actionPlanning: `You are an expert mobile test automation agent. Your task is to generate precise Appium actions to accomplish a test step.
|
|
104
|
+
|
|
105
|
+
**Your Capabilities:**
|
|
106
|
+
- Analyze mobile app UI hierarchies (XML format)
|
|
107
|
+
- Identify the best element selectors (text, id, accessibility-id, xpath, class)
|
|
108
|
+
- Generate reliable action sequences (tap, type, scroll, swipe, wait, assert)
|
|
109
|
+
- Handle both Android and iOS platforms
|
|
110
|
+
- Adapt to different screen sizes and device configurations
|
|
111
|
+
|
|
112
|
+
**Priority for Element Selection:**
|
|
113
|
+
1. Text content (exact or partial match)
|
|
114
|
+
2. Accessibility ID or Content Description
|
|
115
|
+
3. Resource ID (Android) or Name (iOS)
|
|
116
|
+
4. Class name with additional attributes
|
|
117
|
+
5. XPath (only when necessary)
|
|
118
|
+
6. Coordinates (last resort)
|
|
119
|
+
|
|
120
|
+
**Action Types Available:**
|
|
121
|
+
- tap: Click/tap on an element
|
|
122
|
+
- type: Enter text into an input field
|
|
123
|
+
- scroll: Scroll in a direction (up, down, left, right)
|
|
124
|
+
- swipe: Swipe gesture in a direction
|
|
125
|
+
- wait: Wait for a duration or element
|
|
126
|
+
- assert: Verify element state (exists, visible, text, enabled)
|
|
127
|
+
- screenshot: Capture screen state`,
|
|
128
|
+
|
|
129
|
+
contextAnalysis: `Analyze the current UI context and identify key interactive elements that are relevant for accomplishing the given test step.
|
|
130
|
+
|
|
131
|
+
**Focus on:**
|
|
132
|
+
- Interactive elements (buttons, inputs, links)
|
|
133
|
+
- Navigation elements (tabs, back buttons)
|
|
134
|
+
- Text labels and headers for context
|
|
135
|
+
- Error messages or alerts
|
|
136
|
+
- Permission dialogs
|
|
137
|
+
- Loading indicators`,
|
|
138
|
+
|
|
139
|
+
errorRecovery: `The previous action attempt failed. Analyze the current UI state and suggest alternative approaches.
|
|
140
|
+
|
|
141
|
+
**Consider:**
|
|
142
|
+
- Is the target element visible and enabled?
|
|
143
|
+
- Are there any blocking dialogs or overlays?
|
|
144
|
+
- Is the app in an unexpected state?
|
|
145
|
+
- Are there alternative selectors for the same element?
|
|
146
|
+
- Should we wait for animations or transitions to complete?`,
|
|
147
|
+
|
|
148
|
+
assertionGeneration: `Generate appropriate assertions to verify the expected outcome of the test step.
|
|
149
|
+
|
|
150
|
+
**Common Assertions:**
|
|
151
|
+
- Element exists on screen
|
|
152
|
+
- Element is visible and enabled
|
|
153
|
+
- Text content matches expected value
|
|
154
|
+
- Specific screen or activity is displayed
|
|
155
|
+
- No error messages are present`,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Helper function to get appropriate template
|
|
159
|
+
export function getPromptTemplate(type: keyof typeof PromptTemplates): string {
|
|
160
|
+
return PromptTemplates[type];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validation
|
|
164
|
+
export function validateLLMConfig(): void {
|
|
165
|
+
const errors: string[] = [];
|
|
166
|
+
|
|
167
|
+
// Check base URL
|
|
168
|
+
if (!llmConfig.connection.baseUrl) {
|
|
169
|
+
errors.push('LLM_BASE_URL or OLLAMA_BASE_URL is required');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate model name
|
|
173
|
+
if (!llmConfig.model.name) {
|
|
174
|
+
errors.push('LLM_MODEL is required');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate temperature range
|
|
178
|
+
if (llmConfig.model.temperature < 0 || llmConfig.model.temperature > 2) {
|
|
179
|
+
errors.push('LLM_TEMPERATURE must be between 0 and 2');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate max tokens
|
|
183
|
+
if (llmConfig.model.maxTokens < 100 || llmConfig.model.maxTokens > 100000) {
|
|
184
|
+
errors.push('LLM_MAX_TOKENS must be between 100 and 100000');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate timeout
|
|
188
|
+
if (llmConfig.connection.timeout < 5000) {
|
|
189
|
+
errors.push('LLM_TIMEOUT must be at least 5000ms');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (errors.length > 0) {
|
|
193
|
+
throw new Error(`LLM configuration validation failed:\n${errors.join('\n')}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Get configuration for specific provider
|
|
198
|
+
export function getProviderConfig(provider: string) {
|
|
199
|
+
const configs = {
|
|
200
|
+
ollama: {
|
|
201
|
+
endpoint: '/api/generate',
|
|
202
|
+
healthEndpoint: '/api/tags',
|
|
203
|
+
format: 'ollama',
|
|
204
|
+
},
|
|
205
|
+
openai: {
|
|
206
|
+
endpoint: '/v1/chat/completions',
|
|
207
|
+
healthEndpoint: '/v1/models',
|
|
208
|
+
format: 'openai',
|
|
209
|
+
},
|
|
210
|
+
anthropic: {
|
|
211
|
+
endpoint: '/v1/messages',
|
|
212
|
+
healthEndpoint: '/v1/models',
|
|
213
|
+
format: 'anthropic',
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return configs[provider as keyof typeof configs] || configs.ollama;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default llmConfig;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
export interface MCPConfig {
|
|
6
|
+
server: {
|
|
7
|
+
type: 'mcp-appium' | 'mobile-mcp' | 'custom';
|
|
8
|
+
host: string;
|
|
9
|
+
port: number;
|
|
10
|
+
protocol: 'http' | 'https' | 'stdio';
|
|
11
|
+
timeout: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
appium: {
|
|
15
|
+
serverUrl: string;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
capabilities: {
|
|
18
|
+
platformName?: string;
|
|
19
|
+
platformVersion?: string;
|
|
20
|
+
deviceName?: string;
|
|
21
|
+
automationName?: string;
|
|
22
|
+
app?: string;
|
|
23
|
+
appPackage?: string;
|
|
24
|
+
appActivity?: string;
|
|
25
|
+
bundleId?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
tools: {
|
|
30
|
+
enabledTools: string[];
|
|
31
|
+
customTools?: Record<string, any>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
uiContext: {
|
|
35
|
+
capturePageSource: boolean;
|
|
36
|
+
parseXML: boolean;
|
|
37
|
+
includeInvisibleElements: boolean;
|
|
38
|
+
maxElementDepth: number;
|
|
39
|
+
elementFilters: {
|
|
40
|
+
excludeClasses: string[];
|
|
41
|
+
includeOnlyInteractive: boolean;
|
|
42
|
+
minTextLength: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
actions: {
|
|
47
|
+
tapStrategy: 'center' | 'visible-center' | 'coordinates';
|
|
48
|
+
typeStrategy: 'native' | 'adb' | 'auto';
|
|
49
|
+
scrollStrategy: 'uiautomator' | 'gesture' | 'auto';
|
|
50
|
+
swipeStrategy: 'touch-action' | 'gesture' | 'auto';
|
|
51
|
+
waitStrategy: 'implicit' | 'explicit' | 'smart';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
errorHandling: {
|
|
55
|
+
retryOnStaleElement: boolean;
|
|
56
|
+
retryOnNoSuchElement: boolean;
|
|
57
|
+
retryAttempts: number;
|
|
58
|
+
retryDelay: number;
|
|
59
|
+
captureScreenshotOnError: boolean;
|
|
60
|
+
capturePageSourceOnError: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
performance: {
|
|
64
|
+
cacheUIContext: boolean;
|
|
65
|
+
cacheDuration: number;
|
|
66
|
+
parallelActions: boolean;
|
|
67
|
+
throttleActions: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mcpConfig: MCPConfig = {
|
|
72
|
+
server: {
|
|
73
|
+
type: (process.env.MCP_SERVER_TYPE as 'mcp-appium' | 'mobile-mcp' | 'custom') || 'mcp-appium',
|
|
74
|
+
host: process.env.MCP_SERVER_HOST || 'localhost',
|
|
75
|
+
port: parseInt(process.env.MCP_SERVER_PORT || '3000'),
|
|
76
|
+
protocol: (process.env.MCP_PROTOCOL as 'http' | 'https' | 'stdio') || 'http',
|
|
77
|
+
timeout: parseInt(process.env.MCP_TIMEOUT || '30000'),
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
appium: {
|
|
81
|
+
serverUrl: process.env.APPIUM_SERVER_URL || 'http://localhost:4723',
|
|
82
|
+
sessionId: process.env.APPIUM_SESSION_ID,
|
|
83
|
+
capabilities: {
|
|
84
|
+
platformName: process.env.PLATFORM_NAME,
|
|
85
|
+
platformVersion: process.env.PLATFORM_VERSION,
|
|
86
|
+
deviceName: process.env.DEVICE_NAME,
|
|
87
|
+
automationName: process.env.AUTOMATION_NAME,
|
|
88
|
+
app: process.env.APP_PATH,
|
|
89
|
+
appPackage: process.env.APP_PACKAGE,
|
|
90
|
+
appActivity: process.env.APP_ACTIVITY,
|
|
91
|
+
bundleId: process.env.BUNDLE_ID,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
tools: {
|
|
96
|
+
enabledTools: (process.env.MCP_ENABLED_TOOLS ||
|
|
97
|
+
'find_element,tap,type,scroll,swipe,get_page_source,take_screenshot').split(','),
|
|
98
|
+
customTools: {},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
uiContext: {
|
|
102
|
+
capturePageSource: process.env.MCP_CAPTURE_PAGE_SOURCE !== 'false',
|
|
103
|
+
parseXML: process.env.MCP_PARSE_XML !== 'false',
|
|
104
|
+
includeInvisibleElements: process.env.MCP_INCLUDE_INVISIBLE === 'true',
|
|
105
|
+
maxElementDepth: parseInt(process.env.MCP_MAX_ELEMENT_DEPTH || '10'),
|
|
106
|
+
elementFilters: {
|
|
107
|
+
excludeClasses: (process.env.MCP_EXCLUDE_CLASSES ||
|
|
108
|
+
'android.view.ViewGroup,android.widget.LinearLayout,UIView').split(','),
|
|
109
|
+
includeOnlyInteractive: process.env.MCP_ONLY_INTERACTIVE === 'true',
|
|
110
|
+
minTextLength: parseInt(process.env.MCP_MIN_TEXT_LENGTH || '1'),
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
actions: {
|
|
115
|
+
tapStrategy: (process.env.MCP_TAP_STRATEGY as 'center' | 'visible-center' | 'coordinates') || 'center',
|
|
116
|
+
typeStrategy: (process.env.MCP_TYPE_STRATEGY as 'native' | 'adb' | 'auto') || 'native',
|
|
117
|
+
scrollStrategy: (process.env.MCP_SCROLL_STRATEGY as 'uiautomator' | 'gesture' | 'auto') || 'auto',
|
|
118
|
+
swipeStrategy: (process.env.MCP_SWIPE_STRATEGY as 'touch-action' | 'gesture' | 'auto') || 'auto',
|
|
119
|
+
waitStrategy: (process.env.MCP_WAIT_STRATEGY as 'implicit' | 'explicit' | 'smart') || 'smart',
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
errorHandling: {
|
|
123
|
+
retryOnStaleElement: process.env.MCP_RETRY_STALE !== 'false',
|
|
124
|
+
retryOnNoSuchElement: process.env.MCP_RETRY_NO_ELEMENT === 'true',
|
|
125
|
+
retryAttempts: parseInt(process.env.MCP_ERROR_RETRY_ATTEMPTS || '2'),
|
|
126
|
+
retryDelay: parseInt(process.env.MCP_ERROR_RETRY_DELAY || '1000'),
|
|
127
|
+
captureScreenshotOnError: process.env.MCP_SCREENSHOT_ON_ERROR !== 'false',
|
|
128
|
+
capturePageSourceOnError: process.env.MCP_PAGE_SOURCE_ON_ERROR === 'true',
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
performance: {
|
|
132
|
+
cacheUIContext: process.env.MCP_CACHE_UI_CONTEXT === 'true',
|
|
133
|
+
cacheDuration: parseInt(process.env.MCP_CACHE_DURATION || '5000'),
|
|
134
|
+
parallelActions: process.env.MCP_PARALLEL_ACTIONS === 'true',
|
|
135
|
+
throttleActions: parseInt(process.env.MCP_THROTTLE_ACTIONS || '0'),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// MCP Tool definitions
|
|
140
|
+
export const MCPTools = {
|
|
141
|
+
find_element: {
|
|
142
|
+
name: 'find_element',
|
|
143
|
+
description: 'Find an element on the screen using various selector strategies',
|
|
144
|
+
parameters: {
|
|
145
|
+
strategy: ['id', 'xpath', 'accessibility-id', 'text', 'class'],
|
|
146
|
+
value: 'string',
|
|
147
|
+
multiple: 'boolean',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
tap: {
|
|
151
|
+
name: 'tap',
|
|
152
|
+
description: 'Tap/click on an element',
|
|
153
|
+
parameters: {
|
|
154
|
+
selector: 'object',
|
|
155
|
+
x: 'number (optional)',
|
|
156
|
+
y: 'number (optional)',
|
|
157
|
+
duration: 'number (optional)',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
type: {
|
|
161
|
+
name: 'type',
|
|
162
|
+
description: 'Type text into an element',
|
|
163
|
+
parameters: {
|
|
164
|
+
selector: 'object',
|
|
165
|
+
text: 'string',
|
|
166
|
+
clear: 'boolean (optional)',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
scroll: {
|
|
170
|
+
name: 'scroll',
|
|
171
|
+
description: 'Scroll the screen',
|
|
172
|
+
parameters: {
|
|
173
|
+
direction: ['up', 'down', 'left', 'right'],
|
|
174
|
+
percentage: 'number (optional)',
|
|
175
|
+
element: 'object (optional)',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
swipe: {
|
|
179
|
+
name: 'swipe',
|
|
180
|
+
description: 'Perform swipe gesture',
|
|
181
|
+
parameters: {
|
|
182
|
+
direction: ['up', 'down', 'left', 'right'],
|
|
183
|
+
startX: 'number (optional)',
|
|
184
|
+
startY: 'number (optional)',
|
|
185
|
+
endX: 'number (optional)',
|
|
186
|
+
endY: 'number (optional)',
|
|
187
|
+
duration: 'number (optional)',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
get_page_source: {
|
|
191
|
+
name: 'get_page_source',
|
|
192
|
+
description: 'Get the current page source (XML hierarchy)',
|
|
193
|
+
parameters: {},
|
|
194
|
+
},
|
|
195
|
+
take_screenshot: {
|
|
196
|
+
name: 'take_screenshot',
|
|
197
|
+
description: 'Capture a screenshot',
|
|
198
|
+
parameters: {
|
|
199
|
+
path: 'string (optional)',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
wait: {
|
|
203
|
+
name: 'wait',
|
|
204
|
+
description: 'Wait for element or duration',
|
|
205
|
+
parameters: {
|
|
206
|
+
selector: 'object (optional)',
|
|
207
|
+
condition: ['exists', 'visible', 'enabled', 'clickable'],
|
|
208
|
+
timeout: 'number (optional)',
|
|
209
|
+
duration: 'number (optional)',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
get_current_activity: {
|
|
213
|
+
name: 'get_current_activity',
|
|
214
|
+
description: 'Get current activity (Android) or bundle ID (iOS)',
|
|
215
|
+
parameters: {},
|
|
216
|
+
},
|
|
217
|
+
press_key: {
|
|
218
|
+
name: 'press_key',
|
|
219
|
+
description: 'Press device key',
|
|
220
|
+
parameters: {
|
|
221
|
+
key: ['home', 'back', 'enter', 'menu', 'volume_up', 'volume_down'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Selector strategies priority
|
|
227
|
+
export const SelectorPriority = [
|
|
228
|
+
'accessibility-id',
|
|
229
|
+
'id',
|
|
230
|
+
'text',
|
|
231
|
+
'class',
|
|
232
|
+
'xpath',
|
|
233
|
+
'coordinates',
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
// Platform-specific configurations
|
|
237
|
+
export const PlatformConfig = {
|
|
238
|
+
android: {
|
|
239
|
+
automationName: 'UiAutomator2',
|
|
240
|
+
defaultTimeout: 30000,
|
|
241
|
+
keyboardStrategy: 'adb',
|
|
242
|
+
preferredSelectors: ['resource-id', 'content-desc', 'text'],
|
|
243
|
+
},
|
|
244
|
+
ios: {
|
|
245
|
+
automationName: 'XCUITest',
|
|
246
|
+
defaultTimeout: 30000,
|
|
247
|
+
keyboardStrategy: 'native',
|
|
248
|
+
preferredSelectors: ['accessibility-id', 'name', 'label'],
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Validation
|
|
253
|
+
export function validateMCPConfig(): void {
|
|
254
|
+
const errors: string[] = [];
|
|
255
|
+
|
|
256
|
+
// Check Appium server URL
|
|
257
|
+
if (!mcpConfig.appium.serverUrl) {
|
|
258
|
+
errors.push('APPIUM_SERVER_URL is required');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Validate server port
|
|
262
|
+
if (mcpConfig.server.port < 1 || mcpConfig.server.port > 65535) {
|
|
263
|
+
errors.push('MCP_SERVER_PORT must be between 1 and 65535');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Validate timeout
|
|
267
|
+
if (mcpConfig.server.timeout < 1000) {
|
|
268
|
+
errors.push('MCP_TIMEOUT must be at least 1000ms');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Validate element depth
|
|
272
|
+
if (mcpConfig.uiContext.maxElementDepth < 1 || mcpConfig.uiContext.maxElementDepth > 20) {
|
|
273
|
+
errors.push('MCP_MAX_ELEMENT_DEPTH must be between 1 and 20');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (errors.length > 0) {
|
|
277
|
+
throw new Error(`MCP configuration validation failed:\n${errors.join('\n')}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Helper to get platform-specific config
|
|
282
|
+
export function getPlatformConfig(platform: 'android' | 'ios') {
|
|
283
|
+
return PlatformConfig[platform];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Helper to check if tool is enabled
|
|
287
|
+
export function isToolEnabled(toolName: string): boolean {
|
|
288
|
+
return mcpConfig.tools.enabledTools.includes(toolName);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default mcpConfig;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
import logger from './utils/logger';
|
|
8
|
+
import AITestRunner from './orchestrator/aiTestRunner';
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('ai-test-runner')
|
|
16
|
+
.description('AI-powered mobile test automation using MCP and LLM')
|
|
17
|
+
.version('1.0.0');
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('run')
|
|
21
|
+
.description('Run AI-powered test specs')
|
|
22
|
+
.argument('<spec-path>', 'Path to test spec file or directory')
|
|
23
|
+
.option('-s, --session-id <id>', 'Existing Appium session ID')
|
|
24
|
+
.option('-k, --knowledge <path>', 'Path to app knowledge file')
|
|
25
|
+
.option('-o, --output <path>', 'Output path for test report', './reports/test-report.json')
|
|
26
|
+
.action(async (specPath: string, options: any) => {
|
|
27
|
+
try {
|
|
28
|
+
logger.info('Starting AI Test Runner...');
|
|
29
|
+
logger.info(`Spec path: ${specPath}`);
|
|
30
|
+
|
|
31
|
+
const runner = new AITestRunner();
|
|
32
|
+
|
|
33
|
+
// Load app knowledge if provided
|
|
34
|
+
if (options.knowledge) {
|
|
35
|
+
await runner.loadAppKnowledge(options.knowledge);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const results = [];
|
|
39
|
+
|
|
40
|
+
// Check if spec path is a file or directory
|
|
41
|
+
const stats = fs.statSync(specPath);
|
|
42
|
+
|
|
43
|
+
if (stats.isDirectory()) {
|
|
44
|
+
// Run all specs in directory
|
|
45
|
+
const files = fs.readdirSync(specPath)
|
|
46
|
+
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json'))
|
|
47
|
+
.map(f => path.join(specPath, f));
|
|
48
|
+
|
|
49
|
+
logger.info(`Found ${files.length} test spec(s) in directory`);
|
|
50
|
+
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const result = await runner.runTestSpec(file, options.sessionId);
|
|
53
|
+
results.push(result);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
// Run single spec
|
|
57
|
+
const result = await runner.runTestSpec(specPath, options.sessionId);
|
|
58
|
+
results.push(result);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Generate report
|
|
62
|
+
await runner.generateReport(results, options.output);
|
|
63
|
+
|
|
64
|
+
// Print summary
|
|
65
|
+
console.log('\n' + '='.repeat(60));
|
|
66
|
+
console.log('TEST EXECUTION SUMMARY');
|
|
67
|
+
console.log('='.repeat(60));
|
|
68
|
+
|
|
69
|
+
const passed = results.filter(r => r.status === 'passed').length;
|
|
70
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
71
|
+
const total = results.length;
|
|
72
|
+
|
|
73
|
+
console.log(`Total Tests: ${total}`);
|
|
74
|
+
console.log(`Passed: ${passed}`);
|
|
75
|
+
console.log(`Failed: ${failed}`);
|
|
76
|
+
console.log(`Success Rate: ${((passed / total) * 100).toFixed(2)}%`);
|
|
77
|
+
console.log('='.repeat(60) + '\n');
|
|
78
|
+
|
|
79
|
+
// Exit with appropriate code
|
|
80
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
81
|
+
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
logger.error(`Test execution failed: ${error.message}`);
|
|
84
|
+
console.error(`Error: ${error.message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('validate')
|
|
91
|
+
.description('Validate test spec file(s)')
|
|
92
|
+
.argument('<spec-path>', 'Path to test spec file or directory')
|
|
93
|
+
.action((specPath: string) => {
|
|
94
|
+
try {
|
|
95
|
+
const stats = fs.statSync(specPath);
|
|
96
|
+
const files = stats.isDirectory()
|
|
97
|
+
? fs.readdirSync(specPath)
|
|
98
|
+
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json'))
|
|
99
|
+
.map(f => path.join(specPath, f))
|
|
100
|
+
: [specPath];
|
|
101
|
+
|
|
102
|
+
let valid = 0;
|
|
103
|
+
let invalid = 0;
|
|
104
|
+
|
|
105
|
+
files.forEach(file => {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
108
|
+
const ext = path.extname(file).toLowerCase();
|
|
109
|
+
|
|
110
|
+
if (ext === '.json') {
|
|
111
|
+
JSON.parse(content);
|
|
112
|
+
} else {
|
|
113
|
+
// For YAML, we'd need to validate structure
|
|
114
|
+
// This is a basic check
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`✓ ${file} - Valid`);
|
|
118
|
+
valid++;
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
console.error(`✗ ${file} - Invalid: ${error.message}`);
|
|
121
|
+
invalid++;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log(`\nValidation complete: ${valid} valid, ${invalid} invalid`);
|
|
126
|
+
process.exit(invalid > 0 ? 1 : 0);
|
|
127
|
+
|
|
128
|
+
} catch (error: any) {
|
|
129
|
+
console.error(`Validation failed: ${error.message}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
program
|
|
135
|
+
.command('health')
|
|
136
|
+
.description('Check health of LLM and MCP components')
|
|
137
|
+
.action(async () => {
|
|
138
|
+
try {
|
|
139
|
+
const runner = new AITestRunner();
|
|
140
|
+
|
|
141
|
+
console.log('Checking LLM health...');
|
|
142
|
+
const llamaClient = new (await import('./llm/llamaClient')).default();
|
|
143
|
+
const llmHealthy = await llamaClient.healthCheck();
|
|
144
|
+
|
|
145
|
+
if (llmHealthy) {
|
|
146
|
+
console.log('✓ LLM is healthy');
|
|
147
|
+
} else {
|
|
148
|
+
console.error('✗ LLM is not available');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add MCP health check here if needed
|
|
152
|
+
|
|
153
|
+
process.exit(llmHealthy ? 0 : 1);
|
|
154
|
+
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
console.error(`Health check failed: ${error.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
program.parse(process.argv);
|