@skyramp/mcp 0.0.49 → 0.0.51
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/prompts/startTraceCollectionPrompts.js +4 -1
- package/build/prompts/testGenerationPrompt.js +4 -1
- package/build/prompts/testHealthPrompt.js +4 -1
- package/build/services/DriftAnalysisService.js +31 -10
- package/build/services/TestExecutionService.js +39 -1
- package/build/services/TestGenerationService.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +5 -0
- package/build/tools/generate-tests/generateE2ERestTool.js +4 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +5 -2
- package/build/tools/generate-tests/generateLoadRestTool.js +4 -1
- package/build/tools/generate-tests/generateScenarioRestTool.js +8 -1
- package/build/tools/test-maintenance/actionsTool.js +56 -45
- package/build/tools/test-maintenance/analyzeTestDriftTool.js +5 -4
- package/build/tools/test-maintenance/calculateHealthScoresTool.js +12 -33
- package/build/tools/test-maintenance/discoverTestsTool.js +3 -3
- package/build/tools/test-maintenance/executeBatchTestsTool.js +5 -4
- package/build/tools/test-maintenance/stateCleanupTool.js +13 -8
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +5 -1
- package/build/tools/test-recommendation/mapTestsTool.js +29 -19
- package/build/tools/test-recommendation/recommendTestsTool.js +27 -30
- package/build/tools/trace/startTraceCollectionTool.js +73 -18
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/TestTypes.js +7 -1
- package/build/utils/AnalysisStateManager.js +73 -62
- package/package.json +7 -2
|
@@ -3,45 +3,68 @@ import * as path from "path";
|
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import { logger } from "./logger.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* File prefix mapping for each state type
|
|
7
|
+
*/
|
|
8
|
+
const STATE_FILE_PREFIXES = {
|
|
9
|
+
analysis: "skyramp-analysis", // For test maintenance workflow
|
|
10
|
+
recommendation: "skyramp-recommendation", // For test recommendation workflow
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Generic State Manager for persisting workflow data
|
|
7
14
|
* Reduces token usage by storing intermediate results in filesystem
|
|
15
|
+
*
|
|
16
|
+
* Supports two state types:
|
|
17
|
+
* - analysis: Test maintenance workflow (discovery, drift, execution, health)
|
|
18
|
+
* - recommendation: Test recommendation workflow (analyze repo, map tests, recommendations)
|
|
8
19
|
*/
|
|
9
|
-
export class
|
|
20
|
+
export class StateManager {
|
|
10
21
|
stateFile;
|
|
11
22
|
sessionId;
|
|
23
|
+
stateType;
|
|
12
24
|
/**
|
|
13
25
|
* Create a new state manager
|
|
26
|
+
* @param stateType Type of state (analysis, recommendation)
|
|
14
27
|
* @param sessionId Unique session identifier (defaults to timestamp)
|
|
15
28
|
* @param stateDir Directory to store state files (defaults to /tmp)
|
|
16
29
|
*/
|
|
17
|
-
constructor(sessionId, stateDir) {
|
|
30
|
+
constructor(stateType = "analysis", sessionId, stateDir) {
|
|
31
|
+
this.stateType = stateType;
|
|
18
32
|
this.sessionId = sessionId || Date.now().toString();
|
|
19
33
|
const baseDir = stateDir || os.tmpdir();
|
|
20
|
-
|
|
34
|
+
const prefix = STATE_FILE_PREFIXES[stateType];
|
|
35
|
+
this.stateFile = path.join(baseDir, `${prefix}-${this.sessionId}.json`);
|
|
21
36
|
}
|
|
22
37
|
/**
|
|
23
38
|
* Create state manager from existing state file path
|
|
24
39
|
*/
|
|
25
40
|
static fromStatePath(stateFilePath) {
|
|
26
41
|
const basename = path.basename(stateFilePath, ".json");
|
|
27
|
-
const sessionId = basename.replace("skyramp-analysis-", "");
|
|
28
42
|
const stateDir = path.dirname(stateFilePath);
|
|
29
|
-
|
|
43
|
+
// Determine state type from filename
|
|
44
|
+
let stateType = "analysis";
|
|
45
|
+
let sessionId = basename;
|
|
46
|
+
for (const [type, prefix] of Object.entries(STATE_FILE_PREFIXES)) {
|
|
47
|
+
if (basename.startsWith(prefix)) {
|
|
48
|
+
stateType = type;
|
|
49
|
+
sessionId = basename.replace(`${prefix}-`, "");
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return new StateManager(stateType, sessionId, stateDir);
|
|
30
54
|
}
|
|
31
55
|
/**
|
|
32
|
-
* Read
|
|
33
|
-
* @returns Test analysis results, or empty array if file doesn't exist
|
|
56
|
+
* Read data from state file (excludes metadata)
|
|
34
57
|
*/
|
|
35
|
-
async
|
|
58
|
+
async readData() {
|
|
36
59
|
if (!this.exists()) {
|
|
37
60
|
logger.debug(`State file does not exist: ${this.stateFile}`);
|
|
38
|
-
return
|
|
61
|
+
return null;
|
|
39
62
|
}
|
|
40
63
|
try {
|
|
41
64
|
const content = await fs.promises.readFile(this.stateFile, "utf-8");
|
|
42
|
-
const
|
|
43
|
-
logger.debug(`Read
|
|
44
|
-
return
|
|
65
|
+
const { metadata, ...data } = JSON.parse(content);
|
|
66
|
+
logger.debug(`Read data from state file: ${this.stateFile}`);
|
|
67
|
+
return data;
|
|
45
68
|
}
|
|
46
69
|
catch (error) {
|
|
47
70
|
logger.error(`Failed to read state file: ${error.message}`);
|
|
@@ -65,11 +88,10 @@ export class AnalysisStateManager {
|
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
/**
|
|
68
|
-
* Write
|
|
69
|
-
*
|
|
70
|
-
* @param options Additional metadata options
|
|
91
|
+
* Write data to state file
|
|
92
|
+
* Data fields are spread at root level alongside metadata
|
|
71
93
|
*/
|
|
72
|
-
async
|
|
94
|
+
async writeData(data, options) {
|
|
73
95
|
try {
|
|
74
96
|
// Read existing metadata if file exists
|
|
75
97
|
let existingMetadata;
|
|
@@ -78,9 +100,10 @@ export class AnalysisStateManager {
|
|
|
78
100
|
existingMetadata = existing?.metadata;
|
|
79
101
|
}
|
|
80
102
|
const state = {
|
|
81
|
-
|
|
103
|
+
...data,
|
|
82
104
|
metadata: {
|
|
83
105
|
sessionId: this.sessionId,
|
|
106
|
+
stateType: this.stateType,
|
|
84
107
|
repositoryPath: options?.repositoryPath || existingMetadata?.repositoryPath,
|
|
85
108
|
createdAt: existingMetadata?.createdAt || new Date().toISOString(),
|
|
86
109
|
updatedAt: new Date().toISOString(),
|
|
@@ -88,49 +111,13 @@ export class AnalysisStateManager {
|
|
|
88
111
|
},
|
|
89
112
|
};
|
|
90
113
|
await fs.promises.writeFile(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
91
|
-
logger.debug(`Wrote
|
|
114
|
+
logger.debug(`Wrote data to state file: ${this.stateFile}`);
|
|
92
115
|
}
|
|
93
116
|
catch (error) {
|
|
94
117
|
logger.error(`Failed to write state file: ${error.message}`);
|
|
95
118
|
throw new Error(`Failed to write state file: ${error.message}`);
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
|
-
// /**
|
|
99
|
-
// * Read state with filtering
|
|
100
|
-
// * @param filter Function to filter tests
|
|
101
|
-
// */
|
|
102
|
-
// async readStateFiltered(
|
|
103
|
-
// filter: (test: TestAnalysisResult) => boolean,
|
|
104
|
-
// ): Promise<TestAnalysisResult[]> {
|
|
105
|
-
// const allTests = await this.readState();
|
|
106
|
-
// return allTests.filter(filter);
|
|
107
|
-
// }
|
|
108
|
-
// /**
|
|
109
|
-
// * Read state with pagination
|
|
110
|
-
// * @param page Page number (0-indexed)
|
|
111
|
-
// * @param pageSize Number of tests per page
|
|
112
|
-
// */
|
|
113
|
-
// async readStatePaginated(
|
|
114
|
-
// page: number,
|
|
115
|
-
// pageSize: number = 100,
|
|
116
|
-
// ): Promise<{
|
|
117
|
-
// tests: TestAnalysisResult[];
|
|
118
|
-
// hasMore: boolean;
|
|
119
|
-
// total: number;
|
|
120
|
-
// page: number;
|
|
121
|
-
// pageSize: number;
|
|
122
|
-
// }> {
|
|
123
|
-
// const allTests = await this.readState();
|
|
124
|
-
// const start = page * pageSize;
|
|
125
|
-
// const end = start + pageSize;
|
|
126
|
-
// return {
|
|
127
|
-
// tests: allTests.slice(start, end),
|
|
128
|
-
// hasMore: end < allTests.length,
|
|
129
|
-
// total: allTests.length,
|
|
130
|
-
// page,
|
|
131
|
-
// pageSize,
|
|
132
|
-
// };
|
|
133
|
-
// }
|
|
134
121
|
/**
|
|
135
122
|
* Get the state file path
|
|
136
123
|
*/
|
|
@@ -143,6 +130,12 @@ export class AnalysisStateManager {
|
|
|
143
130
|
getSessionId() {
|
|
144
131
|
return this.sessionId;
|
|
145
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the state type
|
|
135
|
+
*/
|
|
136
|
+
getStateType() {
|
|
137
|
+
return this.stateType;
|
|
138
|
+
}
|
|
146
139
|
/**
|
|
147
140
|
* Check if state file exists
|
|
148
141
|
*/
|
|
@@ -184,12 +177,17 @@ export class AnalysisStateManager {
|
|
|
184
177
|
* Cleanup old state files
|
|
185
178
|
* @param maxAgeHours Maximum age in hours
|
|
186
179
|
* @param stateDir Directory to clean (defaults to /tmp)
|
|
180
|
+
* @param stateTypes Which state types to clean (defaults to all)
|
|
187
181
|
* @returns Number of files deleted
|
|
188
182
|
*/
|
|
189
|
-
static async cleanupOldStateFiles(maxAgeHours = 24, stateDir) {
|
|
183
|
+
static async cleanupOldStateFiles(maxAgeHours = 24, stateDir, stateTypes) {
|
|
190
184
|
const baseDir = stateDir || os.tmpdir();
|
|
191
185
|
const files = await fs.promises.readdir(baseDir);
|
|
192
|
-
|
|
186
|
+
// Get prefixes to clean
|
|
187
|
+
const prefixesToClean = stateTypes
|
|
188
|
+
? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
|
|
189
|
+
: Object.values(STATE_FILE_PREFIXES);
|
|
190
|
+
const stateFiles = files.filter((f) => prefixesToClean.some((prefix) => f.startsWith(prefix)));
|
|
193
191
|
let deletedCount = 0;
|
|
194
192
|
const now = Date.now();
|
|
195
193
|
const maxAge = maxAgeHours * 60 * 60 * 1000;
|
|
@@ -216,19 +214,32 @@ export class AnalysisStateManager {
|
|
|
216
214
|
/**
|
|
217
215
|
* List all state files in directory
|
|
218
216
|
* @param stateDir Directory to search (defaults to /tmp)
|
|
217
|
+
* @param stateTypes Which state types to list (defaults to all)
|
|
219
218
|
*/
|
|
220
|
-
static async listStateFiles(stateDir) {
|
|
219
|
+
static async listStateFiles(stateDir, stateTypes) {
|
|
221
220
|
const baseDir = stateDir || os.tmpdir();
|
|
222
221
|
const files = await fs.promises.readdir(baseDir);
|
|
223
|
-
|
|
222
|
+
// Get prefixes to list
|
|
223
|
+
const prefixesToList = stateTypes
|
|
224
|
+
? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
|
|
225
|
+
: Object.values(STATE_FILE_PREFIXES);
|
|
226
|
+
const stateFiles = files.filter((f) => prefixesToList.some((prefix) => f.startsWith(prefix)));
|
|
224
227
|
const fileInfos = await Promise.all(stateFiles.map(async (file) => {
|
|
225
228
|
const filePath = path.join(baseDir, file);
|
|
226
229
|
const stats = await fs.promises.stat(filePath);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
// Determine state type and session ID
|
|
231
|
+
let stateType = "analysis";
|
|
232
|
+
let sessionId = file.replace(".json", "");
|
|
233
|
+
for (const [type, prefix] of Object.entries(STATE_FILE_PREFIXES)) {
|
|
234
|
+
if (file.startsWith(prefix)) {
|
|
235
|
+
stateType = type;
|
|
236
|
+
sessionId = file.replace(`${prefix}-`, "").replace(".json", "");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
230
240
|
return {
|
|
231
241
|
sessionId,
|
|
242
|
+
stateType,
|
|
232
243
|
path: filePath,
|
|
233
244
|
size: stats.size,
|
|
234
245
|
createdAt: stats.birthtime,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"start": "node build/index.js",
|
|
15
15
|
"test:startup": "time node build/index.js --help",
|
|
16
16
|
"pretty": "npx prettier --write .",
|
|
17
|
+
"lint": "eslint src --ext .ts",
|
|
18
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
17
19
|
"test": "npx jest"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
46
|
-
"@skyramp/skyramp": "^1.3.
|
|
48
|
+
"@skyramp/skyramp": "^1.3.3",
|
|
47
49
|
"@playwright/test": "^1.55.0",
|
|
48
50
|
"dockerode": "^4.0.6",
|
|
49
51
|
"fast-glob": "^3.3.3",
|
|
@@ -55,6 +57,9 @@
|
|
|
55
57
|
"@types/jest": "^29.5.14",
|
|
56
58
|
"@types/mocha": "^10.0.10",
|
|
57
59
|
"@types/node": "^22.15.19",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
62
|
+
"eslint": "^9.0.0",
|
|
58
63
|
"jest": "^29.7.0",
|
|
59
64
|
"ts-jest": "^29.3.4",
|
|
60
65
|
"typescript": "^5.8.3"
|