@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.
@@ -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
- Combines API endpoint scanning, branch diff computation, and test discovery into a single state file consumed by \`skyramp_analyze_test_health\` and \`skyramp_actions\`.
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
- changedResources = resolved.length > 0 ? resolved : ["unknown"];
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. schema, model, or
565
- // migration changes near route files). Use ["unknown"] so external
566
- // tests get name-only entries enough for the LLM to infer coverage
567
- // from filenames without flooding context with full extraction of
568
- // hundreds of irrelevant test files.
569
- changedResources = ["unknown"];
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 testScanPath = params.testsRepoDir ?? params.repositoryPath;
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 and health assessment instructions for existing tests — second step of the unified Test Health Analysis Flow.
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
- const absoluteRepoPath = path.resolve(repositoryPath);
57
- const scannedEndpoints = stateData.repositoryAnalysis?.skeletonEndpoints || [];
58
- const routerMountContext = stateData.repositoryAnalysis?.routerMountContext;
59
- const candidateRouteFiles = stateData.repositoryAnalysis?.candidateRouteFiles;
60
- const diffFilePath = stateData.repositoryAnalysis?.diffFilePath;
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.26";
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;
@@ -1,3 +1,3 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.26";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.27";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.4",
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.26",
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
- });