@skyramp/mcp 0.2.4 → 0.2.5-rc.1
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/playwright/traceRecordingPrompt.js +2 -2
- package/build/prompts/test-maintenance/actionsInstructions.js +60 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +18 -101
- package/build/prompts/test-maintenance/driftAnalysisSections.js +210 -171
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +4 -3
- package/build/prompts/test-recommendation/recommendationSections.js +6 -6
- package/build/prompts/test-recommendation/scopeAssessment.js +3 -1
- package/build/prompts/test-recommendation/scopeAssessment.test.js +13 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +20 -17
- package/build/prompts/testbot/testbot-prompts.test.js +21 -17
- package/build/services/TestDiscoveryService.js +11 -43
- package/build/tools/submitReportTool.js +9 -12
- package/build/tools/submitReportTool.test.js +4 -5
- package/build/tools/test-management/actionsTool.js +160 -240
- package/build/tools/test-management/analyzeChangesTool.js +43 -18
- package/build/tools/test-management/analyzeTestHealthTool.js +17 -29
- package/build/utils/docker.test.js +1 -1
- package/build/utils/versions.js +1 -1
- package/package.json +2 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -116
|
@@ -358,15 +358,9 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
358
358
|
idempotentHint: false,
|
|
359
359
|
openWorldHint: true, // may fetch PR comments from GitHub
|
|
360
360
|
},
|
|
361
|
-
description: `Scan repository API endpoints and discover existing tests — first step of the unified Test Health Analysis Flow.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
**Output:** stateFile path + ranked test recommendations + enrichment instructions for calling \`skyramp_recommend_tests\`.`,
|
|
366
|
-
// TODO: Define outputSchema here instead of embedding structured output format in the
|
|
367
|
-
// description string — per Archit's review comment. outputSchema reduces token usage
|
|
368
|
-
// by letting the MCP client understand the response shape structurally rather than
|
|
369
|
-
// through natural language in the description.
|
|
361
|
+
description: `Scan repository API endpoints and discover existing tests — first step of the unified Test Health Analysis Flow. Returns a stateFile path and ranked test recommendations. Pass stateFile to skyramp_analyze_test_health and skyramp_actions.`,
|
|
362
|
+
// TODO: Replace description-embedded output format with outputSchema — structural
|
|
363
|
+
// output schema reduces token usage vs natural language in description.
|
|
370
364
|
inputSchema: analyzeChangesInputSchema,
|
|
371
365
|
}, async (params, extra) => {
|
|
372
366
|
let errorResult;
|
|
@@ -538,6 +532,14 @@ Combines API endpoint scanning, branch diff computation, and test discovery into
|
|
|
538
532
|
}
|
|
539
533
|
await sendProgress(50, 100, "Discovering existing tests...");
|
|
540
534
|
// ── Step 3: Discover existing tests ──
|
|
535
|
+
// Always scan from repositoryPath so tests in any subdirectory (e.g.
|
|
536
|
+
// apps/api/v2/src/modules/) are found regardless of workspace.yml
|
|
537
|
+
// testDirectory. In PR mode, partitionByRelevance already filters the
|
|
538
|
+
// results to files relevant to the changed endpoints — no flooding.
|
|
539
|
+
// testDirectory only controls where generation tools write new files.
|
|
540
|
+
// testsRepoDir is a cross-repo path override — honour it when set.
|
|
541
|
+
// Otherwise always scan the full repo root.
|
|
542
|
+
const testDir = params.testsRepoDir ?? undefined;
|
|
541
543
|
// Compute changedResources from classified endpoints for test discovery filtering.
|
|
542
544
|
// undefined → full-repo mode (no diff context)
|
|
543
545
|
// [] → PR mode, no endpoints found → skip external tests
|
|
@@ -552,21 +554,44 @@ Combines API endpoint scanning, branch diff computation, and test discovery into
|
|
|
552
554
|
...classifiedEndpoints.newEndpoints,
|
|
553
555
|
...classifiedEndpoints.removedEndpoints,
|
|
554
556
|
];
|
|
557
|
+
const FRAMEWORK_SUFFIX_RE = /\.(service|controller|transformer|handler|middleware|resolver|repository|module|guard|interceptor|pipe|filter|decorator|input|output|dto|schema)$/i;
|
|
555
558
|
if (allClassified.length > 0) {
|
|
556
559
|
// Scanned endpoints always have full paths — extractResourceFromPath
|
|
557
560
|
// never returns "unknown" for properly resolved paths.
|
|
561
|
+
// Exception: NestJS versioned controllers register relative paths (e.g. "GET /")
|
|
562
|
+
// which resolve to "unknown". Fall through to file-path extraction in that case.
|
|
558
563
|
const resolved = allClassified
|
|
559
564
|
.map((ep) => extractResourceFromPath(ep.path))
|
|
560
565
|
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
561
|
-
|
|
566
|
+
if (resolved.length > 0) {
|
|
567
|
+
changedResources = resolved;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// All endpoints resolved to "unknown" (e.g. NestJS relative paths) —
|
|
571
|
+
// extract resource names from changed file paths instead.
|
|
572
|
+
const candidateFiles = classifiedEndpoints.unmatchedFiles.length > 0
|
|
573
|
+
? classifiedEndpoints.unmatchedFiles
|
|
574
|
+
: classifiedEndpoints.changedFiles ?? [];
|
|
575
|
+
const fromFiles = candidateFiles
|
|
576
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
577
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
578
|
+
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
579
|
+
}
|
|
562
580
|
}
|
|
563
581
|
else if (classifiedEndpoints.unmatchedFiles.length > 0) {
|
|
564
|
-
// Changed files don't map to any endpoint (e.g.
|
|
565
|
-
// migration
|
|
566
|
-
// tests
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
|
|
582
|
+
// Changed files don't map to any endpoint (e.g. DTO, service, output
|
|
583
|
+
// formatter, migration). Extract resource names from the file paths so
|
|
584
|
+
// external tests whose names overlap with these resources are surfaced
|
|
585
|
+
// for drift assessment.
|
|
586
|
+
// Strip the file extension and common framework suffixes (.service,
|
|
587
|
+
// .controller, .input, .output, etc.) before extracting so that
|
|
588
|
+
// "event-types.service.ts" yields "event-types" rather than
|
|
589
|
+
// "event-types.service", which would fail relevance scoring against
|
|
590
|
+
// test files that contain "event" and "types" but not "service".
|
|
591
|
+
const fromFiles = classifiedEndpoints.unmatchedFiles
|
|
592
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
593
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
594
|
+
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
570
595
|
}
|
|
571
596
|
else {
|
|
572
597
|
changedResources = [];
|
|
@@ -577,8 +602,7 @@ Combines API endpoint scanning, branch diff computation, and test discovery into
|
|
|
577
602
|
try {
|
|
578
603
|
const testDiscoveryService = new TestDiscoveryService();
|
|
579
604
|
setTestsRepoDir(params.testsRepoDir);
|
|
580
|
-
const
|
|
581
|
-
const discoveryResult = await testDiscoveryService.discoverTests(testScanPath, { changedResources });
|
|
605
|
+
const discoveryResult = await testDiscoveryService.discoverTests(testDir ?? params.repositoryPath, { changedResources });
|
|
582
606
|
existingTests = discoveryResult.tests.map((test) => ({
|
|
583
607
|
testFile: test.testFile,
|
|
584
608
|
testType: test.testType,
|
|
@@ -1188,6 +1212,7 @@ Combines API endpoint scanning, branch diff computation, and test discovery into
|
|
|
1188
1212
|
sessionId,
|
|
1189
1213
|
routerMountContext,
|
|
1190
1214
|
candidateRouteFiles,
|
|
1215
|
+
relevantExternalTestPaths,
|
|
1191
1216
|
},
|
|
1192
1217
|
};
|
|
1193
1218
|
// Clean up old state files (>24 hours) before creating new one
|
|
@@ -15,16 +15,7 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
15
15
|
idempotentHint: true,
|
|
16
16
|
openWorldHint: false,
|
|
17
17
|
},
|
|
18
|
-
description: `Generate drift
|
|
19
|
-
|
|
20
|
-
**PREREQUISITE:** Call \`skyramp_analyze_changes\` first to get a stateFile.
|
|
21
|
-
|
|
22
|
-
This tool reads existing tests, the branch diff, and scanned endpoints from the stateFile,
|
|
23
|
-
then returns a structured prompt for the LLM to assess each test for drift and health.
|
|
24
|
-
|
|
25
|
-
The LLM follows the returned prompt to assign drift details and actions (UPDATE / REGENERATE / VERIFY / DELETE / IGNORE) for each test, then calls \`skyramp_actions\`.
|
|
26
|
-
|
|
27
|
-
(Optional) Execute tests using \`skyramp_execute_test\` with \`stateFile\` parameter before \`skyramp_actions\` to validate tests live.`,
|
|
18
|
+
description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow. Optionally execute tests with skyramp_execute_test before calling skyramp_actions to capture beforeStatus.`,
|
|
28
19
|
inputSchema: {
|
|
29
20
|
stateFile: z
|
|
30
21
|
.string()
|
|
@@ -45,19 +36,24 @@ The LLM follows the returned prompt to assign drift details and actions (UPDATE
|
|
|
45
36
|
if (!stateData) {
|
|
46
37
|
return toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
47
38
|
}
|
|
48
|
-
// Only Skyramp tests are candidates for drift analysis and maintenance actions.
|
|
49
|
-
// External (user-written) tests are used only for recommendation deduplication.
|
|
50
|
-
// Default source to Skyramp for backwards compat with state files created before the source field existed.
|
|
51
|
-
const existingTests = (stateData.existingTests || []).filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External);
|
|
52
|
-
logger.info(`Loaded ${existingTests.length} existing Skyramp tests from state file (excluded external)`);
|
|
53
39
|
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
54
40
|
return toolError(`repositoryPath not found in state file metadata. The state file was likely created by an older version — re-run skyramp_analyze_changes to regenerate it.`);
|
|
55
41
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
// Skyramp tests: full drift analysis + all actions permitted.
|
|
43
|
+
// Relevant external tests (user-written, relevant to this PR's endpoints): drift analysis
|
|
44
|
+
// + UPDATE only — REGENERATE and DELETE are report-only (enforced in skyramp_actions).
|
|
45
|
+
// Other external tests: excluded entirely (deduplication only, not analysed).
|
|
46
|
+
// relevantExternalTestPaths are stored relative to repositoryPath in the state file.
|
|
47
|
+
// Re-absolutize here so has() comparisons against t.testFile (absolute) work correctly.
|
|
48
|
+
const relevantExternalPaths = new Set((stateData.repositoryAnalysis?.relevantExternalTestPaths ?? []).map((p) => path.isAbsolute(p) ? p : path.resolve(repositoryPath, p)));
|
|
49
|
+
const existingTests = (stateData.existingTests || []).filter((t) => {
|
|
50
|
+
if ((t.source ?? TestSource.Skyramp) !== TestSource.External)
|
|
51
|
+
return true;
|
|
52
|
+
return relevantExternalPaths.has(t.testFile);
|
|
53
|
+
});
|
|
54
|
+
const skyrampCount = existingTests.filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External).length;
|
|
55
|
+
const externalCount = existingTests.length - skyrampCount;
|
|
56
|
+
logger.info(`Loaded ${skyrampCount} Skyramp + ${externalCount} relevant external tests from state file`);
|
|
61
57
|
// Sweep stale diff files on this natural follow-up call so they don't accumulate.
|
|
62
58
|
// Pass [] for stateTypes so only skyramp-diff-*.diff files are deleted — state files
|
|
63
59
|
// (skyramp-analysis-*, skyramp-recommendation-*) must not be removed here because the
|
|
@@ -68,15 +64,7 @@ The LLM follows the returned prompt to assign drift details and actions (UPDATE
|
|
|
68
64
|
catch (error) {
|
|
69
65
|
logger.warning(`Failed to cleanup old diff files: ${error.message}`);
|
|
70
66
|
}
|
|
71
|
-
const promptText = buildDriftAnalysisPrompt({
|
|
72
|
-
existingTests,
|
|
73
|
-
scannedEndpoints,
|
|
74
|
-
repositoryPath: absoluteRepoPath,
|
|
75
|
-
stateFile: stateManager.getStatePath(),
|
|
76
|
-
routerMountContext,
|
|
77
|
-
candidateRouteFiles,
|
|
78
|
-
diffFilePath,
|
|
79
|
-
});
|
|
67
|
+
const promptText = buildDriftAnalysisPrompt(stateManager.getStatePath(), existingTests.map((t) => ({ testFile: t.testFile, source: t.source })));
|
|
80
68
|
return {
|
|
81
69
|
structuredContent: { prompt: promptText },
|
|
82
70
|
content: [{ type: "text", text: "Drift analysis prompt generated. Follow the prompt field to assess each test." }],
|
|
@@ -54,7 +54,7 @@ describe("dockerImageExistsLocally", () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
describe("pullDockerImage", () => {
|
|
57
|
-
const IMAGE = "skyramp/executor:v1.3.
|
|
57
|
+
const IMAGE = "skyramp/executor:v1.3.27";
|
|
58
58
|
beforeEach(() => jest.clearAllMocks());
|
|
59
59
|
describe("on amd64 host", () => {
|
|
60
60
|
const originalArch = process.arch;
|
package/build/utils/versions.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-rc.1",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./build/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
57
57
|
"@playwright/test": "^1.55.0",
|
|
58
|
-
"@skyramp/skyramp": "1.3.
|
|
58
|
+
"@skyramp/skyramp": "1.3.27",
|
|
59
59
|
"dockerode": "^5.0.0",
|
|
60
60
|
"fast-glob": "^3.3.3",
|
|
61
61
|
"js-yaml": "^4.1.1",
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { buildDriftAnalysisPrompt } from "./drift-analysis-prompt.js";
|
|
2
|
-
import { buildDriftOutputChecklist } from "./driftAnalysisSections.js";
|
|
3
|
-
describe("buildDriftOutputChecklist — final-step recommendations guidance", () => {
|
|
4
|
-
const STATE_FILE = "/tmp/skyramp-analysis-abc123.json";
|
|
5
|
-
it("non-inline mode includes recommendations and updateInstructions in final step", () => {
|
|
6
|
-
const checklist = buildDriftOutputChecklist(3, 0, false, STATE_FILE);
|
|
7
|
-
// Must instruct the LLM to pass recommendations to skyramp_actions
|
|
8
|
-
expect(checklist).toContain("recommendations");
|
|
9
|
-
// Must mention updateInstructions so the LLM knows to populate it
|
|
10
|
-
expect(checklist).toContain("updateInstructions");
|
|
11
|
-
// Must reference the stateFile path
|
|
12
|
-
expect(checklist).toContain(STATE_FILE);
|
|
13
|
-
// Must call skyramp_actions as the final action
|
|
14
|
-
expect(checklist).toContain("skyramp_actions");
|
|
15
|
-
});
|
|
16
|
-
it("non-inline mode does not contain JSON shape — schema is authoritative", () => {
|
|
17
|
-
const checklist = buildDriftOutputChecklist(3, 0, false, STATE_FILE);
|
|
18
|
-
// The JSON shape was moved to inputSchema — prompt must not duplicate it
|
|
19
|
-
expect(checklist).not.toContain('"testFile":');
|
|
20
|
-
expect(checklist).not.toContain('"action":');
|
|
21
|
-
});
|
|
22
|
-
it("inline mode does not reference skyramp_actions or stateFile", () => {
|
|
23
|
-
const checklist = buildDriftOutputChecklist(3, 0, true, STATE_FILE);
|
|
24
|
-
// Inline mode applies changes directly — no skyramp_actions call
|
|
25
|
-
expect(checklist).not.toContain("skyramp_actions");
|
|
26
|
-
expect(checklist).not.toContain(STATE_FILE);
|
|
27
|
-
});
|
|
28
|
-
it("full prompt (non-inline) includes recommendations guidance", () => {
|
|
29
|
-
const prompt = buildDriftAnalysisPrompt({
|
|
30
|
-
existingTests: [],
|
|
31
|
-
scannedEndpoints: [],
|
|
32
|
-
repositoryPath: "/repo",
|
|
33
|
-
stateFile: STATE_FILE,
|
|
34
|
-
});
|
|
35
|
-
expect(prompt).toContain("recommendations");
|
|
36
|
-
expect(prompt).toContain("updateInstructions");
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe("buildDriftAnalysisPrompt - inline mode", () => {
|
|
40
|
-
beforeEach(() => { process.env.SKYRAMP_FEATURE_TESTBOT = "1"; });
|
|
41
|
-
afterEach(() => { delete process.env.SKYRAMP_FEATURE_TESTBOT; });
|
|
42
|
-
function inlinePrompt() {
|
|
43
|
-
return buildDriftAnalysisPrompt({
|
|
44
|
-
existingTests: [],
|
|
45
|
-
scannedEndpoints: [],
|
|
46
|
-
repositoryPath: "/repo",
|
|
47
|
-
// stateFile omitted → inline mode
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
it("wraps inline rules in drift_analysis_rules XML tags", () => {
|
|
51
|
-
const prompt = inlinePrompt();
|
|
52
|
-
expect(prompt).toContain("<drift_analysis_rules>");
|
|
53
|
-
expect(prompt).toContain("</drift_analysis_rules>");
|
|
54
|
-
});
|
|
55
|
-
it("does not contain the persona statement", () => {
|
|
56
|
-
const prompt = inlinePrompt();
|
|
57
|
-
expect(prompt).not.toContain("You are acting as a Skyramp Integration Architect");
|
|
58
|
-
});
|
|
59
|
-
it("does not contain the standalone Test Health Analysis header", () => {
|
|
60
|
-
const prompt = inlinePrompt();
|
|
61
|
-
expect(prompt).not.toContain("# Test Health Analysis");
|
|
62
|
-
});
|
|
63
|
-
it("does not contain the skyramp_actions CTA (that belongs to standalone mode)", () => {
|
|
64
|
-
const prompt = inlinePrompt();
|
|
65
|
-
// Inline mode final step directs applying changes directly, not calling skyramp_actions
|
|
66
|
-
expect(prompt).not.toContain("call `skyramp_actions`");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
describe("buildDriftAnalysisPrompt - scanned endpoints rendering", () => {
|
|
70
|
-
// Reproduces the [object Object] bug: skeletonEndpoints from analyzeChangesTool
|
|
71
|
-
// stores methods as objects { method: string, ... }, not plain strings.
|
|
72
|
-
const skeletonMethodObjects = [
|
|
73
|
-
{
|
|
74
|
-
path: "/api/v1/",
|
|
75
|
-
methods: [{ method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "main.py", interactions: [] }],
|
|
76
|
-
resourceGroup: "v1",
|
|
77
|
-
pathParams: [],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
path: "/api/v1/orders",
|
|
81
|
-
methods: [
|
|
82
|
-
{ method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
|
|
83
|
-
{ method: "POST", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
|
|
84
|
-
],
|
|
85
|
-
resourceGroup: "orders",
|
|
86
|
-
pathParams: [],
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
it("renders HTTP methods as strings, not [object Object]", () => {
|
|
90
|
-
const prompt = buildDriftAnalysisPrompt({
|
|
91
|
-
existingTests: [],
|
|
92
|
-
scannedEndpoints: skeletonMethodObjects,
|
|
93
|
-
repositoryPath: "/repo",
|
|
94
|
-
stateFile: "/tmp/state.json",
|
|
95
|
-
});
|
|
96
|
-
expect(prompt).not.toContain("[object Object]");
|
|
97
|
-
expect(prompt).toContain("GET /api/v1/");
|
|
98
|
-
expect(prompt).toContain("GET|POST /api/v1/orders");
|
|
99
|
-
// CTA should appear exactly once (not duplicated)
|
|
100
|
-
const ctaCount = (prompt.match(/call `skyramp_actions`/g) || []).length;
|
|
101
|
-
expect(ctaCount).toBe(1);
|
|
102
|
-
});
|
|
103
|
-
it("also works with plain string methods (ScannedEndpoint format)", () => {
|
|
104
|
-
const stringMethods = [
|
|
105
|
-
{ path: "/api/v1/products", methods: ["GET", "POST"], sourceFile: "products.py" },
|
|
106
|
-
];
|
|
107
|
-
const prompt = buildDriftAnalysisPrompt({
|
|
108
|
-
existingTests: [],
|
|
109
|
-
scannedEndpoints: stringMethods,
|
|
110
|
-
repositoryPath: "/repo",
|
|
111
|
-
stateFile: "/tmp/state.json",
|
|
112
|
-
});
|
|
113
|
-
expect(prompt).not.toContain("[object Object]");
|
|
114
|
-
expect(prompt).toContain("GET|POST /api/v1/products");
|
|
115
|
-
});
|
|
116
|
-
});
|