@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.
Files changed (67) hide show
  1. package/.env +24 -0
  2. package/.env.example +24 -0
  3. package/Jenkinsfile +210 -0
  4. package/MCP-SERVER-GUIDE.md +405 -0
  5. package/README.MD +450 -0
  6. package/dist/config/app.config.d.ts +65 -0
  7. package/dist/config/app.config.d.ts.map +1 -0
  8. package/dist/config/app.config.js +94 -0
  9. package/dist/config/app.config.js.map +1 -0
  10. package/dist/config/llm.config.d.ts +63 -0
  11. package/dist/config/llm.config.d.ts.map +1 -0
  12. package/dist/config/llm.config.js +158 -0
  13. package/dist/config/llm.config.js.map +1 -0
  14. package/dist/config/mcp.config.d.ts +175 -0
  15. package/dist/config/mcp.config.d.ts.map +1 -0
  16. package/dist/config/mcp.config.js +215 -0
  17. package/dist/config/mcp.config.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +175 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/llm/llamaClient.d.ts +14 -0
  23. package/dist/llm/llamaClient.d.ts.map +1 -0
  24. package/dist/llm/llamaClient.js +136 -0
  25. package/dist/llm/llamaClient.js.map +1 -0
  26. package/dist/mcp/mcpClient.d.ts +132 -0
  27. package/dist/mcp/mcpClient.d.ts.map +1 -0
  28. package/dist/mcp/mcpClient.js +784 -0
  29. package/dist/mcp/mcpClient.js.map +1 -0
  30. package/dist/models/testSpec.d.ts +78 -0
  31. package/dist/models/testSpec.d.ts.map +1 -0
  32. package/dist/models/testSpec.js +3 -0
  33. package/dist/models/testSpec.js.map +1 -0
  34. package/dist/orchestrator/aiTestRunner.d.ts +18 -0
  35. package/dist/orchestrator/aiTestRunner.d.ts.map +1 -0
  36. package/dist/orchestrator/aiTestRunner.js +247 -0
  37. package/dist/orchestrator/aiTestRunner.js.map +1 -0
  38. package/dist/utils/logger.d.ts +4 -0
  39. package/dist/utils/logger.d.ts.map +1 -0
  40. package/dist/utils/logger.js +49 -0
  41. package/dist/utils/logger.js.map +1 -0
  42. package/dist/utils/promptBuilder.d.ts +62 -0
  43. package/dist/utils/promptBuilder.d.ts.map +1 -0
  44. package/dist/utils/promptBuilder.js +333 -0
  45. package/dist/utils/promptBuilder.js.map +1 -0
  46. package/knowledge/app-knowledge.txt +100 -0
  47. package/logs/combined.log +486 -0
  48. package/logs/error.log +50 -0
  49. package/package.json +62 -0
  50. package/reports/screenshots/screenshot_1764535110518.png +0 -0
  51. package/reports/test-report.json +106 -0
  52. package/scripts/check-mcp-server.sh +100 -0
  53. package/scripts/extract-pom-knowledge.js +222 -0
  54. package/scripts/pre-test-setup.js +262 -0
  55. package/scripts/start-mcp-server.sh +76 -0
  56. package/src/config/app.config.ts +175 -0
  57. package/src/config/llm.config.ts +220 -0
  58. package/src/config/mcp.config.ts +291 -0
  59. package/src/index.ts +161 -0
  60. package/src/llm/llamaClient.ts +159 -0
  61. package/src/mcp/mcpClient.ts +878 -0
  62. package/src/models/testSpec.ts +85 -0
  63. package/src/orchestrator/aiTestRunner.ts +286 -0
  64. package/src/utils/logger.ts +59 -0
  65. package/src/utils/promptBuilder.ts +384 -0
  66. package/tests/nlp-specs/login-flow.yaml +31 -0
  67. package/tsconfig.json +31 -0
@@ -0,0 +1,85 @@
1
+ export interface TestStep {
2
+ step: string;
3
+ description?: string;
4
+ expectedOutcome?: string;
5
+ timeout?: number;
6
+ }
7
+
8
+ export interface TestSpec {
9
+ name: string;
10
+ description?: string;
11
+ platform: 'android' | 'ios' | 'both';
12
+ appPackage?: string;
13
+ appActivity?: string;
14
+ bundleId?: string;
15
+ device?: {
16
+ deviceName?: string;
17
+ platformVersion?: string;
18
+ };
19
+ setup?: TestStep[];
20
+ steps: TestStep[];
21
+ teardown?: TestStep[];
22
+ }
23
+
24
+ export interface ActionPlan {
25
+ actions: Action[];
26
+ reasoning?: string;
27
+ }
28
+
29
+ export interface Action {
30
+ type: 'tap' | 'type' | 'scroll' | 'swipe' | 'wait' | 'assert' | 'screenshot' | 'launch';
31
+ selector?: Selector;
32
+ value?: string;
33
+ direction?: 'up' | 'down' | 'left' | 'right';
34
+ duration?: number;
35
+ assertionType?: 'exists' | 'visible' | 'text' | 'enabled';
36
+ expectedValue?: string;
37
+ }
38
+
39
+ export interface Selector {
40
+ strategy: 'text' | 'id' | 'accessibility-id' | 'xpath' | 'class' | 'coordinates';
41
+ value: string;
42
+ index?: number;
43
+ }
44
+
45
+ export interface UIContext {
46
+ pageSource: string;
47
+ currentActivity?: string;
48
+ currentPackage?: string;
49
+ visibleElements?: UIElement[];
50
+ platform?: 'android' | 'ios';
51
+ screenSize?: {
52
+ width: number;
53
+ height: number;
54
+ };
55
+ }
56
+
57
+ export interface UIElement {
58
+ type: string;
59
+ text?: string;
60
+ resourceId?: string;
61
+ contentDesc?: string;
62
+ className?: string;
63
+ bounds?: string;
64
+ clickable?: boolean;
65
+ enabled?: boolean;
66
+ index?: number;
67
+ }
68
+
69
+ export interface TestResult {
70
+ specName: string;
71
+ status: 'passed' | 'failed' | 'skipped';
72
+ duration: number;
73
+ steps: StepResult[];
74
+ screenshots?: string[];
75
+ error?: string;
76
+ }
77
+
78
+ export interface StepResult {
79
+ step: string;
80
+ status: 'passed' | 'failed' | 'skipped';
81
+ duration: number;
82
+ actions: Action[];
83
+ error?: string;
84
+ screenshot?: string;
85
+ }
@@ -0,0 +1,286 @@
1
+ import fs from 'fs';
2
+ import yaml from 'js-yaml';
3
+ import path from 'path';
4
+ import logger from '../utils/logger';
5
+ import LlamaClient from '../llm/llamaClient';
6
+ import MCPClient from '../mcp/mcpClient';
7
+ import {
8
+ TestSpec,
9
+ TestResult,
10
+ StepResult,
11
+ TestStep,
12
+ Action,
13
+ } from '../models/testSpec';
14
+
15
+ export class AITestRunner {
16
+ private llamaClient: LlamaClient;
17
+ private mcpClient: MCPClient;
18
+ private appKnowledge: string = '';
19
+ private maxRetries: number;
20
+ private screenshotOnFailure: boolean;
21
+
22
+ constructor() {
23
+ this.llamaClient = new LlamaClient();
24
+ this.mcpClient = new MCPClient();
25
+ this.maxRetries = parseInt(process.env.MAX_RETRY_ATTEMPTS || '2');
26
+ this.screenshotOnFailure = process.env.SCREENSHOT_ON_FAILURE === 'true';
27
+ }
28
+
29
+ async loadAppKnowledge(knowledgePath?: string): Promise<void> {
30
+ if (!knowledgePath) {
31
+ logger.info('No app knowledge file provided, using defaults');
32
+ return;
33
+ }
34
+
35
+ try {
36
+ this.appKnowledge = fs.readFileSync(knowledgePath, 'utf-8');
37
+ logger.info(`Loaded app knowledge from: ${knowledgePath}`);
38
+ } catch (error: any) {
39
+ logger.warn(`Failed to load app knowledge: ${error.message}`);
40
+ }
41
+ }
42
+
43
+ async runTestSpec(specPath: string, sessionId?: string): Promise<TestResult> {
44
+ const startTime = Date.now();
45
+
46
+ try {
47
+ // Load test spec
48
+ const testSpec = this.loadTestSpec(specPath);
49
+ logger.info(`Running test spec: ${testSpec.name}`);
50
+
51
+ // Initialize MCP client
52
+ await this.mcpClient.initialize();
53
+ if (sessionId) {
54
+ this.mcpClient.setSessionId(sessionId);
55
+ }
56
+
57
+ // Health check LLM
58
+ const llmHealthy = await this.llamaClient.healthCheck();
59
+ if (!llmHealthy) {
60
+ throw new Error('LLM is not available');
61
+ }
62
+
63
+ // Execute setup steps
64
+ const setupResults: StepResult[] = [];
65
+ if (testSpec.setup) {
66
+ logger.info('Executing setup steps...');
67
+ for (const step of testSpec.setup) {
68
+ const result = await this.executeStep(step);
69
+ setupResults.push(result);
70
+ if (result.status === 'failed') {
71
+ throw new Error(`Setup failed at step: ${step.step}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Execute main test steps
77
+ const stepResults: StepResult[] = [];
78
+ for (const step of testSpec.steps) {
79
+ const result = await this.executeStep(step);
80
+ stepResults.push(result);
81
+
82
+ if (result.status === 'failed' && !process.env.CONTINUE_ON_FAILURE) {
83
+ logger.error(`Test failed at step: ${step.step}`);
84
+ break;
85
+ }
86
+ }
87
+
88
+ // Execute teardown steps
89
+ if (testSpec.teardown) {
90
+ logger.info('Executing teardown steps...');
91
+ for (const step of testSpec.teardown) {
92
+ await this.executeStep(step);
93
+ }
94
+ }
95
+
96
+ // Determine overall test status
97
+ const allSteps = [...setupResults, ...stepResults];
98
+ const testStatus = allSteps.every(s => s.status === 'passed')
99
+ ? 'passed'
100
+ : allSteps.some(s => s.status === 'failed')
101
+ ? 'failed'
102
+ : 'skipped';
103
+
104
+ const duration = Date.now() - startTime;
105
+
106
+ const result: TestResult = {
107
+ specName: testSpec.name,
108
+ status: testStatus,
109
+ duration,
110
+ steps: allSteps,
111
+ };
112
+
113
+ logger.info(`Test ${testSpec.name} completed with status: ${testStatus}`);
114
+ return result;
115
+
116
+ } catch (error: any) {
117
+ logger.error(`Test execution failed: ${error.message}`);
118
+
119
+ return {
120
+ specName: path.basename(specPath),
121
+ status: 'failed',
122
+ duration: Date.now() - startTime,
123
+ steps: [],
124
+ error: error.message,
125
+ };
126
+ } finally {
127
+ await this.mcpClient.cleanup();
128
+ }
129
+ }
130
+
131
+ private async executeStep(testStep: TestStep): Promise<StepResult> {
132
+ const startTime = Date.now();
133
+ let attempts = 0;
134
+ let lastError: string | undefined;
135
+
136
+ logger.info(`Executing step: "${testStep.step}"`);
137
+
138
+ while (attempts <= this.maxRetries) {
139
+ attempts++;
140
+
141
+ try {
142
+ // Get current UI context
143
+ const uiContext = await this.mcpClient.getUIContext();
144
+ const uiContextStr = this.formatUIContext(uiContext);
145
+
146
+ // Generate action plan from LLM
147
+ const actionPlan = await this.llamaClient.generateActionPlan(
148
+ testStep.step,
149
+ uiContextStr,
150
+ this.appKnowledge
151
+ );
152
+
153
+ if (actionPlan.actions.length === 0) {
154
+ throw new Error('LLM generated no actions for this step');
155
+ }
156
+
157
+ logger.info(`Generated ${actionPlan.actions.length} actions`);
158
+ if (actionPlan.reasoning) {
159
+ logger.debug(`LLM reasoning: ${actionPlan.reasoning}`);
160
+ }
161
+
162
+ // Execute each action
163
+ let allActionsSucceeded = true;
164
+ for (const action of actionPlan.actions) {
165
+ const success = await this.mcpClient.executeAction(action);
166
+ if (!success) {
167
+ allActionsSucceeded = false;
168
+ throw new Error(`Action failed: ${action.type}`);
169
+ }
170
+
171
+ // Small delay between actions for stability
172
+ await this.delay(500);
173
+ }
174
+
175
+ // Step succeeded
176
+ const duration = Date.now() - startTime;
177
+ return {
178
+ step: testStep.step,
179
+ status: 'passed',
180
+ duration,
181
+ actions: actionPlan.actions,
182
+ };
183
+
184
+ } catch (error: any) {
185
+ lastError = error.message;
186
+ logger.warn(`Step attempt ${attempts} failed: ${lastError}`);
187
+
188
+ if (attempts > this.maxRetries) {
189
+ // Final failure
190
+ if (this.screenshotOnFailure) {
191
+ await this.mcpClient.executeAction({ type: 'screenshot' });
192
+ }
193
+
194
+ const duration = Date.now() - startTime;
195
+ return {
196
+ step: testStep.step,
197
+ status: 'failed',
198
+ duration,
199
+ actions: [],
200
+ error: lastError,
201
+ };
202
+ }
203
+
204
+ // Wait before retry
205
+ await this.delay(2000);
206
+ }
207
+ }
208
+
209
+ // Should not reach here, but handling it
210
+ return {
211
+ step: testStep.step,
212
+ status: 'failed',
213
+ duration: Date.now() - startTime,
214
+ actions: [],
215
+ error: lastError || 'Unknown error',
216
+ };
217
+ }
218
+
219
+ private loadTestSpec(specPath: string): TestSpec {
220
+ try {
221
+ const fileContent = fs.readFileSync(specPath, 'utf-8');
222
+ const ext = path.extname(specPath).toLowerCase();
223
+
224
+ if (ext === '.yaml' || ext === '.yml') {
225
+ return yaml.load(fileContent) as TestSpec;
226
+ } else if (ext === '.json') {
227
+ return JSON.parse(fileContent) as TestSpec;
228
+ } else {
229
+ throw new Error(`Unsupported spec file format: ${ext}`);
230
+ }
231
+ } catch (error: any) {
232
+ logger.error(`Failed to load test spec: ${error.message}`);
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ private formatUIContext(uiContext: any): string {
238
+ // Format UI context for LLM consumption
239
+ // Keep it concise to save tokens
240
+ const elements = uiContext.visibleElements || [];
241
+
242
+ let formatted = `Current Activity: ${uiContext.currentActivity || 'Unknown'}\n\n`;
243
+ formatted += `Visible UI Elements (${elements.length}):\n`;
244
+
245
+ elements.slice(0, 30).forEach((el: any, idx: number) => {
246
+ formatted += `${idx + 1}. ${el.type}`;
247
+ if (el.text) formatted += ` text="${el.text}"`;
248
+ if (el.resourceId) formatted += ` id="${el.resourceId}"`;
249
+ if (el.contentDesc) formatted += ` desc="${el.contentDesc}"`;
250
+ if (el.clickable) formatted += ` [clickable]`;
251
+ formatted += '\n';
252
+ });
253
+
254
+ if (elements.length > 30) {
255
+ formatted += `... and ${elements.length - 30} more elements\n`;
256
+ }
257
+
258
+ return formatted;
259
+ }
260
+
261
+ private delay(ms: number): Promise<void> {
262
+ return new Promise(resolve => setTimeout(resolve, ms));
263
+ }
264
+
265
+ async generateReport(results: TestResult[], outputPath: string): Promise<void> {
266
+ try {
267
+ const report = {
268
+ timestamp: new Date().toISOString(),
269
+ summary: {
270
+ total: results.length,
271
+ passed: results.filter(r => r.status === 'passed').length,
272
+ failed: results.filter(r => r.status === 'failed').length,
273
+ skipped: results.filter(r => r.status === 'skipped').length,
274
+ },
275
+ results,
276
+ };
277
+
278
+ fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
279
+ logger.info(`Test report generated: ${outputPath}`);
280
+ } catch (error: any) {
281
+ logger.error(`Failed to generate report: ${error.message}`);
282
+ }
283
+ }
284
+ }
285
+
286
+ export default AITestRunner;
@@ -0,0 +1,59 @@
1
+ import winston from 'winston';
2
+ import path from 'path';
3
+ import dotenv from 'dotenv';
4
+ import fs from 'fs';
5
+
6
+ dotenv.config();
7
+
8
+ const logDir = process.env.LOG_DIR || './logs';
9
+
10
+ // Ensure log directory exists
11
+ if (!fs.existsSync(logDir)) {
12
+ fs.mkdirSync(logDir, { recursive: true });
13
+ }
14
+
15
+ const logger = winston.createLogger({
16
+ level: process.env.LOG_LEVEL || 'info',
17
+ format: winston.format.combine(
18
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
19
+ winston.format.errors({ stack: true }),
20
+ winston.format.splat(),
21
+ winston.format.json()
22
+ ),
23
+ defaultMeta: { service: 'ai-test-runner' },
24
+ transports: [
25
+ // Error log file
26
+ new winston.transports.File({
27
+ filename: path.join(logDir, 'error.log'),
28
+ level: 'error',
29
+ maxsize: 5242880, // 5MB
30
+ maxFiles: 5,
31
+ }),
32
+ // Combined log file
33
+ new winston.transports.File({
34
+ filename: path.join(logDir, 'combined.log'),
35
+ maxsize: 5242880,
36
+ maxFiles: 5,
37
+ }),
38
+ ],
39
+ });
40
+
41
+ // Also log to console in non-production environments
42
+ if (process.env.NODE_ENV !== 'production') {
43
+ logger.add(
44
+ new winston.transports.Console({
45
+ format: winston.format.combine(
46
+ winston.format.colorize(),
47
+ winston.format.printf(({ level, message, timestamp, ...meta }) => {
48
+ let metaStr = '';
49
+ if (Object.keys(meta).length > 0 && meta.service !== 'ai-test-runner') {
50
+ metaStr = ' ' + JSON.stringify(meta);
51
+ }
52
+ return `${timestamp} [${level}]: ${message}${metaStr}`;
53
+ })
54
+ ),
55
+ })
56
+ );
57
+ }
58
+
59
+ export default logger;