@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,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;
|