@skyramp/mcp 0.0.43 → 0.0.45
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/build/index.js +15 -0
- package/build/prompts/code-reuse.js +1 -1
- package/build/prompts/driftAnalysisPrompt.js +159 -0
- package/build/prompts/modularization/ui-test-modularization.js +2 -0
- package/build/prompts/testGenerationPrompt.js +2 -2
- package/build/prompts/testHealthPrompt.js +82 -0
- package/build/services/DriftAnalysisService.js +924 -0
- package/build/services/ModularizationService.js +16 -1
- package/build/services/TestDiscoveryService.js +237 -0
- package/build/services/TestExecutionService.js +311 -0
- package/build/services/TestGenerationService.js +16 -2
- package/build/services/TestHealthService.js +653 -0
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +5 -3
- package/build/tools/code-refactor/modularizationTool.js +8 -2
- package/build/tools/executeSkyrampTestTool.js +12 -122
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/test-maintenance/actionsTool.js +202 -0
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +188 -0
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +248 -0
- package/build/tools/test-maintenance/discoverTestsTool.js +135 -0
- package/build/tools/test-maintenance/executeBatchTestsTool.js +188 -0
- package/build/tools/test-maintenance/stateCleanupTool.js +145 -0
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +16 -1
- package/build/tools/test-recommendation/recommendTestsTool.js +6 -2
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/TestAnalysis.js +1 -0
- package/build/types/TestDriftAnalysis.js +1 -0
- package/build/types/TestExecution.js +6 -0
- package/build/types/TestHealth.js +4 -0
- package/build/utils/AnalysisStateManager.js +238 -0
- package/build/utils/utils.test.js +25 -9
- package/package.json +6 -3
package/build/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { registerStartTraceCollectionPrompt } from "./prompts/startTraceCollectionPrompts.js";
|
|
5
|
+
import { registerTestHealthPrompt } from "./prompts/testHealthPrompt.js";
|
|
5
6
|
import { registerTraceTool } from "./tools/trace/startTraceCollectionTool.js";
|
|
6
7
|
import { registerTraceStopTool } from "./tools/trace/stopTraceCollectionTool.js";
|
|
7
8
|
import { registerExecuteSkyrampTestTool } from "./tools/executeSkyrampTestTool.js";
|
|
@@ -23,9 +24,16 @@ import { registerRecommendTestsTool } from "./tools/test-recommendation/recommen
|
|
|
23
24
|
import { registerModularizationTool } from "./tools/code-refactor/modularizationTool.js";
|
|
24
25
|
import { registerCodeReuseTool } from "./tools/code-refactor/codeReuseTool.js";
|
|
25
26
|
import { registerScenarioTestTool } from "./tools/generate-tests/generateScenarioRestTool.js";
|
|
27
|
+
import { registerDiscoverTestsTool } from "./tools/test-maintenance/discoverTestsTool.js";
|
|
28
|
+
import { registerAnalyzeTestDriftTool } from "./tools/test-maintenance/analyzeTestDriftTool.js";
|
|
29
|
+
import { registerExecuteBatchTestsTool } from "./tools/test-maintenance/executeBatchTestsTool.js";
|
|
30
|
+
import { registerCalculateHealthScoresTool } from "./tools/test-maintenance/calculateHealthScoresTool.js";
|
|
31
|
+
import { registerActionsTool } from "./tools/test-maintenance/actionsTool.js";
|
|
32
|
+
import { registerStateCleanupTool } from "./tools/test-maintenance/stateCleanupTool.js";
|
|
26
33
|
const server = new McpServer({
|
|
27
34
|
name: "Skyramp MCP Server",
|
|
28
35
|
version: "1.0.0",
|
|
36
|
+
}, {
|
|
29
37
|
capabilities: {
|
|
30
38
|
tools: {
|
|
31
39
|
listChanged: true,
|
|
@@ -40,6 +48,7 @@ logger.info("Starting prompt registration process");
|
|
|
40
48
|
const prompts = [
|
|
41
49
|
registerTestGenerationPrompt,
|
|
42
50
|
registerStartTraceCollectionPrompt,
|
|
51
|
+
registerTestHealthPrompt,
|
|
43
52
|
];
|
|
44
53
|
prompts.forEach((registerPrompt) => registerPrompt(server));
|
|
45
54
|
logger.info("All prompts registered successfully");
|
|
@@ -69,6 +78,12 @@ if (enableInternalTools) {
|
|
|
69
78
|
registerAnalyzeRepositoryTool(server);
|
|
70
79
|
registerMapTestsTool(server);
|
|
71
80
|
registerRecommendTestsTool(server);
|
|
81
|
+
registerDiscoverTestsTool(server);
|
|
82
|
+
registerAnalyzeTestDriftTool(server);
|
|
83
|
+
registerExecuteBatchTestsTool(server);
|
|
84
|
+
registerCalculateHealthScoresTool(server);
|
|
85
|
+
registerActionsTool(server);
|
|
86
|
+
registerStateCleanupTool(server);
|
|
72
87
|
}
|
|
73
88
|
else {
|
|
74
89
|
logger.info("Internal tools disabled. Set SKYRAMP_ENABLE_INTERNAL_TOOLS=true to enable.");
|
|
@@ -144,7 +144,7 @@ NOT A HELPER FUNCTION (do not extract):
|
|
|
144
144
|
4. Found no other test files? → SKIP to Step 6, NO utils file
|
|
145
145
|
5. Other test files identical to current? → SKIP to Step 6, NO utils file
|
|
146
146
|
|
|
147
|
-
**MANDATORY**: After code reuse is complete, proceed to modularization by calling skyramp_modularization tool
|
|
147
|
+
**MANDATORY**: After code reuse is complete, proceed to modularization by calling skyramp_modularization tool ONLY for UI, E2E, INTEGRATION or LOAD test types generated from traces.
|
|
148
148
|
|
|
149
149
|
SUMMARIZE THE CODE REUSE PROCESS AND THE RESULTS.
|
|
150
150
|
`;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build optimized drift analysis prompt - compares TEST expectations vs CURRENT CODE
|
|
3
|
+
*/
|
|
4
|
+
export function buildDriftAnalysisPrompt(testType, testFile, fileChanges, apiSchemaChanges, uiComponentChanges) {
|
|
5
|
+
const isUiTest = testType === "ui" || testType === "e2e";
|
|
6
|
+
return `# Test Drift Analysis
|
|
7
|
+
|
|
8
|
+
You are analyzing drift for a **${testType.toUpperCase()}** test to find breaking changes.
|
|
9
|
+
|
|
10
|
+
## Test File
|
|
11
|
+
${testFile}
|
|
12
|
+
|
|
13
|
+
## Changed Files (${fileChanges.length})
|
|
14
|
+
${fileChanges.length > 0
|
|
15
|
+
? fileChanges
|
|
16
|
+
.map((c) => `- ${c.file} (+${c.linesAdded}/-${c.linesRemoved})`)
|
|
17
|
+
.join("\n")
|
|
18
|
+
: "No changes detected"}
|
|
19
|
+
|
|
20
|
+
${buildSchemaChangesSection(apiSchemaChanges, uiComponentChanges)}
|
|
21
|
+
|
|
22
|
+
## Your Task
|
|
23
|
+
|
|
24
|
+
${isUiTest ? buildUiAnalysisInstructions() : buildApiAnalysisInstructions()}
|
|
25
|
+
|
|
26
|
+
## Scoring Guide
|
|
27
|
+
- **85-100**: Critical breaking changes (type mismatch, missing endpoint/selector, auth change)
|
|
28
|
+
- **60-84**: High impact (path changed, required field added, status code change)
|
|
29
|
+
- **40-59**: Medium impact (optional field removed, component renamed)
|
|
30
|
+
- **20-39**: Low impact (new optional fields, minor refactoring)
|
|
31
|
+
- **0-19**: Minimal/no impact
|
|
32
|
+
|
|
33
|
+
## Output Format (JSON)
|
|
34
|
+
\`\`\`json
|
|
35
|
+
{
|
|
36
|
+
"driftScore": 0-100,
|
|
37
|
+
"reasoning": "Concise explanation of key mismatches",
|
|
38
|
+
"breakingChanges": ["specific mismatch 1", "specific mismatch 2"],
|
|
39
|
+
"apiDependencyChanges": [{"file": "", "linesAdded": 0, "linesRemoved": 0, "updates": ""}],
|
|
40
|
+
"uiDependencyChanges": [{"file": "", "linesAdded": 0, "linesRemoved": 0, "updates": ""}],
|
|
41
|
+
"fieldTypeChanges": [{"file": "", "field": "", "testExpects": "", "codeHas": ""}],
|
|
42
|
+
"endpointChanges": [{"type": "removed|modified", "path": "", "method": "", "issue": ""}],
|
|
43
|
+
"selectorChanges": [{"file": "", "testExpects": "", "codeHas": ""}]
|
|
44
|
+
}
|
|
45
|
+
\`\`\``;
|
|
46
|
+
}
|
|
47
|
+
function buildSchemaChangesSection(apiSchemaChanges, uiComponentChanges) {
|
|
48
|
+
let section = "";
|
|
49
|
+
if (apiSchemaChanges) {
|
|
50
|
+
const removed = apiSchemaChanges.endpointsRemoved;
|
|
51
|
+
const modified = apiSchemaChanges.endpointsModified;
|
|
52
|
+
const authChanged = apiSchemaChanges.authenticationChanged;
|
|
53
|
+
if (removed.length > 0 || modified.length > 0 || authChanged) {
|
|
54
|
+
section += "\n## API Schema Changes\n";
|
|
55
|
+
if (removed.length > 0) {
|
|
56
|
+
section += `- **Removed**: ${removed
|
|
57
|
+
.map((e) => `${e.method} ${e.path}`)
|
|
58
|
+
.join(", ")}\n`;
|
|
59
|
+
}
|
|
60
|
+
if (modified.length > 0) {
|
|
61
|
+
section += `- **Modified**: ${modified
|
|
62
|
+
.map((e) => `${e.method} ${e.path}`)
|
|
63
|
+
.join(", ")}\n`;
|
|
64
|
+
}
|
|
65
|
+
if (authChanged) {
|
|
66
|
+
section += "- **Authentication**: Changed\n";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (uiComponentChanges) {
|
|
71
|
+
const hasChanges = uiComponentChanges.componentFiles.length > 0 ||
|
|
72
|
+
uiComponentChanges.routeFiles.length > 0 ||
|
|
73
|
+
uiComponentChanges.hasSelectorsChanges ||
|
|
74
|
+
uiComponentChanges.hasStylingChanges;
|
|
75
|
+
if (hasChanges) {
|
|
76
|
+
section += "\n## UI Component Changes\n";
|
|
77
|
+
if (uiComponentChanges.componentFiles.length > 0) {
|
|
78
|
+
section += `- **Components**: ${uiComponentChanges.componentFiles.length} files\n`;
|
|
79
|
+
}
|
|
80
|
+
if (uiComponentChanges.routeFiles.length > 0) {
|
|
81
|
+
section += `- **Routes**: ${uiComponentChanges.routeFiles.length} files\n`;
|
|
82
|
+
}
|
|
83
|
+
if (uiComponentChanges.hasSelectorsChanges) {
|
|
84
|
+
section += "- **Selectors**: Likely changed\n";
|
|
85
|
+
}
|
|
86
|
+
if (uiComponentChanges.hasStylingChanges) {
|
|
87
|
+
section += "- **Styles**: Changed\n";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return section;
|
|
92
|
+
}
|
|
93
|
+
function buildUiAnalysisInstructions() {
|
|
94
|
+
return `### Analysis Steps
|
|
95
|
+
|
|
96
|
+
1. **Extract Test Selectors**
|
|
97
|
+
- Find: \`getByTestId()\`, \`querySelector()\`, \`getElementById()\`, \`getByRole()\`
|
|
98
|
+
- Record: selector type and exact value
|
|
99
|
+
|
|
100
|
+
2. **Analyze Git Diffs**
|
|
101
|
+
- Look for changes in: \`data-testid\`, \`className\`, \`id\`, \`role\`
|
|
102
|
+
- Check: Lines with "-" (removed) vs "+" (added)
|
|
103
|
+
- Pattern: \`- data-testid="old-value"\` → \`+ data-testid="new-value"\`
|
|
104
|
+
|
|
105
|
+
3. **Compare & Detect Breaks**
|
|
106
|
+
- ❌ **CRITICAL**: Test selector not found in current code
|
|
107
|
+
- ❌ **CRITICAL**: Selector value changed (\`submit-button\` → \`submit-btn\`)
|
|
108
|
+
- ❌ **CRITICAL**: Element type changed (\`<button>\` → \`<div>\`)
|
|
109
|
+
- ❌ **HIGH**: Route changed (\`/products\` → \`/items\`)
|
|
110
|
+
- ❌ **HIGH**: Component renamed/removed
|
|
111
|
+
|
|
112
|
+
4. **Categorize Changes**
|
|
113
|
+
- **uiDependencyChanges**: Components, pages, templates (.tsx, .jsx, .vue)
|
|
114
|
+
- **apiDependencyChanges**: API calls, services (if E2E test)
|
|
115
|
+
|
|
116
|
+
### Key Patterns
|
|
117
|
+
- Selector mismatch → Score 85+
|
|
118
|
+
- Route change → Score 75+
|
|
119
|
+
- Styling only → Score 20-39`;
|
|
120
|
+
}
|
|
121
|
+
function buildApiAnalysisInstructions() {
|
|
122
|
+
return `### Analysis Steps
|
|
123
|
+
|
|
124
|
+
1. **Extract Test Request Data**
|
|
125
|
+
- Find: Request bodies, payloads, field names
|
|
126
|
+
- Infer types from values:
|
|
127
|
+
* \`"text"\` = string
|
|
128
|
+
* \`123\` = integer
|
|
129
|
+
* \`12.5\` = decimal/float
|
|
130
|
+
* \`true\`/\`false\` = boolean
|
|
131
|
+
* \`[]\` = array
|
|
132
|
+
* \`null\` = optional
|
|
133
|
+
|
|
134
|
+
2. **Analyze Git Diffs**
|
|
135
|
+
- Look for field type changes: \`- field: type_A\` → \`+ field: type_B\`
|
|
136
|
+
- Look for endpoint changes: \`- @app.post("/old")\` → \`+ @app.post("/new")\`
|
|
137
|
+
- Look for auth changes: \`- def endpoint()\` → \`+ @require_auth def endpoint()\`
|
|
138
|
+
- Check status codes: \`- return 200\` → \`+ return 201\`
|
|
139
|
+
|
|
140
|
+
3. **Compare & Detect Breaks**
|
|
141
|
+
- ❌ **CRITICAL**: Field type changed (\`int\` → \`string\`)
|
|
142
|
+
- ❌ **CRITICAL**: Test value type ≠ code field type
|
|
143
|
+
- ❌ **CRITICAL**: Endpoint removed or path changed
|
|
144
|
+
- ❌ **HIGH**: Required field added (test doesn't send it)
|
|
145
|
+
- ❌ **HIGH**: Auth added/removed
|
|
146
|
+
- ❌ **HIGH**: Status code changed
|
|
147
|
+
- ⚠️ **MEDIUM**: Response field removed (test expects it)
|
|
148
|
+
|
|
149
|
+
4. **Categorize Changes**
|
|
150
|
+
- **apiDependencyChanges**: Routes, controllers, models, schemas
|
|
151
|
+
- **uiDependencyChanges**: Only if E2E test with UI components
|
|
152
|
+
|
|
153
|
+
### Key Patterns
|
|
154
|
+
- Type mismatch in diff → Score 85+
|
|
155
|
+
- Endpoint removed → Score 85+
|
|
156
|
+
- Auth change → Score 75+
|
|
157
|
+
- New required field → Score 75+
|
|
158
|
+
- Response structure change → Score 60-74`;
|
|
159
|
+
}
|
|
@@ -137,6 +137,8 @@ Use the EXACT same values from the original test when calling helpers.
|
|
|
137
137
|
## STEP 5: VERIFY - CRITICAL CHECKS
|
|
138
138
|
|
|
139
139
|
**BUGS TO AVOID:**
|
|
140
|
+
- [ ] **NO TYPE ANNOTATIONS FOR \`page\` IN FUNCTION SIGNATURES** - Parameters must be untyped (e.g., \`page\`, not \`page: Page\`)
|
|
141
|
+
- [ ] **NO RETURN TYPES** - Do not add \`: Promise<void>\` or any return type annotations
|
|
140
142
|
- [ ] **ALL field mappings verified** - EVERY \`.fill()\` uses the correct parameter. Check field gets quantity parameter, NOT hardcoded
|
|
141
143
|
- [ ] **No duplicate helpers** - Consolidate similar helpers (e.g., one createOrder vs createSimpleOrder + createOrderWithItems)
|
|
142
144
|
- [ ] **No nested helpers** - Helpers don't call other helpers unnecessarily
|
|
@@ -15,8 +15,8 @@ export function registerTestGenerationPrompt(mcpServer) {
|
|
|
15
15
|
- Provide clear execution instructions for generated tests
|
|
16
16
|
- Answer questions about Skyramp capabilities and testing concepts
|
|
17
17
|
- ALWAYS SHOW STEPS TO GENERATE TEST USING MCP TOOLS AND NEVER SHOW THE CLI COMMANDS.
|
|
18
|
-
- CRITICAL: UI, INTEGRATION, E2E TESTS MUST BE MODULARIZED USING skyramp_modularization TOOL.
|
|
19
|
-
- **CRITICAL: skyramp_reuse_code TOOL MUST BE CALLED IF DURING THE TEST GENERATION THE CODE REUSE FLAG IS SET TO TRUE EXPLICITLY BY THE USER.**
|
|
18
|
+
- **CRITICAL: ONLY UI, INTEGRATION, E2E, LOAD TESTS GENERATED FROM TRACES MUST BE MODULARIZED USING skyramp_modularization TOOL. ADD A TASK TO MODULARIZE THE TEST USING skyramp_modularization TOOL AFTER GENERATING THESE(UI, INTEGRATION, E2E, LOAD) TESTS. DO NOT MODULARIZE THESE TESTS IF THEY ARE NOT GENERATED FROM TRACES.**
|
|
19
|
+
- **CRITICAL: skyramp_reuse_code TOOL MUST BE CALLED IF DURING THE TEST GENERATION THE CODE REUSE FLAG IS SET TO TRUE EXPLICITLY BY THE USER AND THE TEST IS GENERATED FROM TRACES.**
|
|
20
20
|
|
|
21
21
|
**MANDATORY RULES**:
|
|
22
22
|
1. **Priority Scores Must Remain Unchanged**: When a test type is missing required inputs (e.g., Playwright recordings, traces), **DO NOT**:
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the test health analysis prompt
|
|
3
|
+
*
|
|
4
|
+
* This prompt guides the AI to perform EXPLICIT TOOL CHAINING:
|
|
5
|
+
* 1. Call skyramp_discover_tests (If user provides a list of tests, skip this step)
|
|
6
|
+
* 2. Call skyramp_analyze_test_drift (with stateFile from discovery or list of tests)
|
|
7
|
+
* 3. (Optional) Call skyramp_execute_tests_batch (with stateFile)
|
|
8
|
+
* 4. Call skyramp_calculate_health_scores (with stateFile)
|
|
9
|
+
* 5. Call skyramp_actions (with stateFile) to perform test updates
|
|
10
|
+
*/
|
|
11
|
+
export function registerTestHealthPrompt(server) {
|
|
12
|
+
server.prompt("skyramp_test_health_analysis", "Skyramp Test Health Analysis", {}, () => {
|
|
13
|
+
const messages = [
|
|
14
|
+
{
|
|
15
|
+
role: "user",
|
|
16
|
+
content: {
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `Check health of test suite
|
|
19
|
+
|
|
20
|
+
Analyze the health of my test suite.
|
|
21
|
+
|
|
22
|
+
Repository: /path/to/repository`,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
role: "assistant",
|
|
27
|
+
content: {
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `I'll analyze your test suite health through explicit tool chaining with state files for maximum efficiency.
|
|
30
|
+
|
|
31
|
+
**CRITICAL: YOU MUST DISPLAY BELOW MESSAGE BEFORE ANY TOOL CALL:**
|
|
32
|
+
** This tool is currently in Early Preview stage. Please verify the results. **
|
|
33
|
+
|
|
34
|
+
**The Analysis Process (State File Mode - 98% Token Reduction):**
|
|
35
|
+
|
|
36
|
+
1. **Discovery (Optional)** - Call \`skyramp_discover_tests\` → Returns stateFile
|
|
37
|
+
2. **Drift Analysis** - Call \`skyramp_analyze_test_drift\` with stateFile → Returns updated stateFile
|
|
38
|
+
3. **Execution** - Call \`skyramp_execute_tests_batch\` with stateFile (Optional, only if user requests) → Returns updated stateFile
|
|
39
|
+
4. **Health Scoring** - Call \`skyramp_calculate_health_scores\` with stateFile → Returns updated stateFile with health data
|
|
40
|
+
5. **Perform Actions** - Call \`skyramp_actions\` with stateFile → Performs test updates
|
|
41
|
+
|
|
42
|
+
**State File Benefits:**
|
|
43
|
+
- Reduces token usage by 98%+ (from ~2.6MB to ~25KB for 100 tests)
|
|
44
|
+
- Faster processing
|
|
45
|
+
- Intermediate results saved to /tmp for debugging
|
|
46
|
+
|
|
47
|
+
Before I start, should I execute your tests (Step 3)?
|
|
48
|
+
|
|
49
|
+
**YES** (~5-10 min, requires Docker):
|
|
50
|
+
- Most comprehensive assessment
|
|
51
|
+
- Identifies actually failing tests
|
|
52
|
+
|
|
53
|
+
**NO** (~2-3 min):
|
|
54
|
+
- Faster, drift-based analysis only
|
|
55
|
+
|
|
56
|
+
Please respond "yes" or "no".`,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
role: "user",
|
|
61
|
+
content: {
|
|
62
|
+
type: "text",
|
|
63
|
+
text: "no",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content: {
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Perfect! I'll run the analysis without execution. Starting now:
|
|
71
|
+
|
|
72
|
+
**Step 1: Discovering tests...**
|
|
73
|
+
|
|
74
|
+
I'll call \`skyramp_discover_tests\` to find all Skyramp tests in your repository.`,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
return {
|
|
79
|
+
messages,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|