@skyramp/mcp 0.2.9 → 0.2.10-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.
Files changed (76) hide show
  1. package/build/index.js +1 -1
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
  4. package/build/prompts/personas.d.ts +2 -2
  5. package/build/prompts/personas.js +2 -2
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
  7. package/build/prompts/sut-setup/shared.js +1 -3
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
  9. package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
  10. package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
  11. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
  14. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
  15. package/build/prompts/test-recommendation/recommendationShared.js +1 -1
  16. package/build/prompts/testbot/testbot-prompts.js +8 -8
  17. package/build/resources/testbotResource.js +1 -1
  18. package/build/services/TestDiscoveryService.d.ts +0 -1
  19. package/build/services/TestDiscoveryService.js +13 -33
  20. package/build/services/TestDiscoveryService.test.js +136 -1
  21. package/build/services/TestExecutionService.d.ts +1 -1
  22. package/build/tools/executeSkyrampTestTool.js +40 -71
  23. package/build/tools/submitReportTool.d.ts +20 -20
  24. package/build/tools/submitReportTool.js +113 -31
  25. package/build/tools/submitReportTool.test.js +411 -94
  26. package/build/tools/test-management/actionsTool.js +135 -53
  27. package/build/tools/test-management/actionsTool.test.d.ts +1 -0
  28. package/build/tools/test-management/actionsTool.test.js +297 -0
  29. package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
  30. package/build/tools/test-management/analyzeChangesTool.js +63 -152
  31. package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
  32. package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
  33. package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
  34. package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
  35. package/build/types/FrontendIntegration.d.ts +25 -0
  36. package/build/types/FrontendIntegration.js +19 -0
  37. package/build/types/RepositoryAnalysis.d.ts +8 -8
  38. package/build/types/TestAnalysis.d.ts +16 -50
  39. package/build/types/TestAnalysis.js +0 -28
  40. package/build/types/TestTypes.d.ts +3 -1
  41. package/build/types/TestTypes.js +3 -1
  42. package/build/utils/AnalysisStateManager.d.ts +13 -1
  43. package/build/utils/docker.test.js +1 -1
  44. package/build/utils/featureFlags.d.ts +1 -1
  45. package/build/utils/featureFlags.js +1 -1
  46. package/build/utils/frontendIntegration.d.ts +9 -0
  47. package/build/utils/frontendIntegration.js +237 -0
  48. package/build/utils/frontendIntegration.test.d.ts +1 -0
  49. package/build/utils/frontendIntegration.test.js +229 -0
  50. package/build/utils/pr-comment-parser.d.ts +3 -3
  51. package/build/utils/pr-comment-parser.js +6 -6
  52. package/build/utils/pr-comment-parser.test.js +3 -3
  53. package/build/utils/repoScanner.d.ts +7 -0
  54. package/build/utils/repoScanner.js +14 -6
  55. package/build/utils/repoScanner.test.js +9 -9
  56. package/build/utils/routeParsers.d.ts +32 -0
  57. package/build/utils/routeParsers.js +205 -2
  58. package/build/utils/routeParsers.test.js +279 -29
  59. package/build/utils/sourceRouteExtractor.js +174 -5
  60. package/build/utils/sourceRouteExtractor.test.js +173 -0
  61. package/build/utils/utils.d.ts +11 -0
  62. package/build/utils/utils.js +19 -0
  63. package/build/utils/utils.test.js +23 -1
  64. package/build/utils/versions.d.ts +3 -3
  65. package/build/utils/versions.js +1 -1
  66. package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
  67. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  68. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
  69. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
  70. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
  71. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  74. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  75. package/node_modules/playwright/package.json +1 -1
  76. package/package.json +3 -3
@@ -2,10 +2,11 @@ import { z } from "zod";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { StateManager, } from "../../utils/AnalysisStateManager.js";
4
4
  import { TestSource, DriftAction, RecommendationPriority, EstimatedWork } from "../../types/TestAnalysis.js";
5
+ import { TestType } from "../../types/TestTypes.js";
5
6
  import * as fs from "fs";
6
7
  import * as path from "path";
7
8
  import { AnalyticsService } from "../../services/AnalyticsService.js";
8
- import { toolError } from "../../utils/utils.js";
9
+ import { toolError, testFileMatches } from "../../utils/utils.js";
9
10
  import { buildRenameStrategy, buildFileRenameStrategy, buildUpdateStrategy, buildRegenerateStrategy, buildUpdateFileInstruction, buildRegenerateFileInstruction, } from "../../prompts/test-maintenance/actionsInstructions.js";
10
11
  /**
11
12
  * Compute a suggested new filename when an endpoint is renamed.
@@ -45,26 +46,36 @@ export function computeRenamedTestFile(testFile, renames) {
45
46
  return newFilePath;
46
47
  }
47
48
  const recommendationSchema = z.object({
48
- testFile: z
49
+ testFilePath: z
49
50
  .string()
50
- .refine((p) => path.isAbsolute(p), { message: "testFile must be an absolute path" })
51
+ .refine((p) => path.isAbsolute(p), { message: "testFilePath must be an absolute path" })
51
52
  .describe("Absolute path to the test spec file — use the path as reported by skyramp_analyze_changes or skyramp_analyze_test_health. For POM-based tests, this is the spec file (shown in the report); set pomFile to the page object path to edit."),
52
53
  pomFile: z
53
54
  .string()
54
55
  .optional()
55
56
  .refine((p) => p === undefined || path.isAbsolute(p), { message: "pomFile must be an absolute path" })
56
- .describe("Absolute path to the page object (POM) file to edit, when the spec delegates selectors to a page object. When set, skyramp_actions edits this file instead of testFile. testFile (the spec) is still used for catalog validation and the maintenance report."),
57
+ .describe("Absolute path to the page object (POM) file to edit, when the spec delegates selectors to a page object. When set, skyramp_actions edits this file instead of testFilePath. testFilePath (the spec) is still used for catalog validation and the maintenance report."),
57
58
  action: z
58
59
  .nativeEnum(DriftAction)
59
- .describe("Drift action assigned by the LLM health assessment"),
60
+ .refine((a) => a !== DriftAction.Ignore, {
61
+ message: "Use ignoredTestFiles for IGNORE tests; omit IGNORE entries from recommendations.",
62
+ })
63
+ .describe("Drift action assigned by the LLM health assessment. Never IGNORE — omit the recommendation and use ignoredTestFiles instead."),
64
+ testType: z
65
+ .nativeEnum(TestType)
66
+ .describe("Type of this test — carried through to the maintenance report."),
67
+ endpoint: z
68
+ .string()
69
+ .describe("HTTP verb and path this test covers, e.g. 'GET /api/v1/products'."),
60
70
  priority: z
61
71
  .nativeEnum(RecommendationPriority)
62
72
  .optional()
63
73
  .describe("Update priority"),
64
74
  rationale: z
65
75
  .string()
66
- .optional()
67
- .describe("1-2 sentence explanation of why this action is needed"),
76
+ .trim()
77
+ .min(1)
78
+ .describe("1-2 sentence explanation of why this action is needed — becomes the maintenance report's description, so it must not be blank."),
68
79
  estimatedWork: z
69
80
  .nativeEnum(EstimatedWork)
70
81
  .optional()
@@ -100,6 +111,15 @@ const actionsSchema = {
100
111
  .array(recommendationSchema)
101
112
  .optional()
102
113
  .describe("Drift recommendations from skyramp_analyze_test_health. Omit IGNORE entries. Omitting this field results in no maintenance actions."),
114
+ ignoredTestFiles: z
115
+ .array(z.string().refine((p) => path.isAbsolute(p), {
116
+ message: "ignoredTestFiles entries must be absolute paths — use the exact path reported by skyramp_analyze_changes or skyramp_analyze_test_health. A bare filename is ambiguous when multiple discovered tests share that basename.",
117
+ }))
118
+ .optional()
119
+ .default([])
120
+ .describe("Absolute path of every existing test you assessed during drift analysis and decided needs no action — use the exact path reported by skyramp_analyze_changes or skyramp_analyze_test_health. " +
121
+ "Every test discovered by skyramp_analyze_changes must appear in either `recommendations` or here — " +
122
+ "otherwise this call fails, naming the test(s) you didn't account for."),
103
123
  };
104
124
  const TOOL_NAME = "skyramp_actions";
105
125
  export function registerActionsTool(server) {
@@ -110,7 +130,7 @@ export function registerActionsTool(server) {
110
130
  idempotentHint: false,
111
131
  openWorldHint: true,
112
132
  },
113
- description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow.`,
133
+ description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow. Call this once per run even when no tests need action — it is the only point where every assessed existing test is reconciled (via \`recommendations\` + \`ignoredTestFiles\`) before the run moves on.`,
114
134
  inputSchema: actionsSchema,
115
135
  }, async (args) => {
116
136
  let errorResult;
@@ -127,15 +147,19 @@ export function registerActionsTool(server) {
127
147
  }
128
148
  // Resolve repo root for path normalization and security checks.
129
149
  const repoRoot = repositoryPath ? path.resolve(repositoryPath) : "";
150
+ // Related repos are read-only context for the agent (it never has write access to
151
+ // their checkout) — only the primary repo's tests can actually be edited.
152
+ const relatedRepoKeys = await stateManager.listRelatedRepos();
153
+ const isRelatedRepo = !!args.repository && relatedRepoKeys.includes(args.repository);
130
154
  // Allowlist: Skyramp-generated tests + relevant external tests.
131
155
  // Using an allowlist (not a blocklist) catches hallucinated paths the LLM
132
156
  // may supply that are not in the scanned catalog at all.
133
157
  const testAnalysisResults = (stateData.existingTests || []);
134
158
  const skyrampTestFiles = new Set(testAnalysisResults
135
- .filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External)
159
+ .filter((t) => t.source !== TestSource.External)
136
160
  .map((t) => t.testFile));
137
161
  const externalTestFiles = new Set(testAnalysisResults
138
- .filter((t) => (t.source ?? TestSource.Skyramp) === TestSource.External)
162
+ .filter((t) => t.source === TestSource.External)
139
163
  .map((t) => t.testFile));
140
164
  // ── Build recommendations from LLM-supplied drift assessment ──
141
165
  // The LLM performs the drift assessment in context after skyramp_analyze_test_health
@@ -147,37 +171,54 @@ export function registerActionsTool(server) {
147
171
  // against repoRoot in case the LLM sends a relative path despite the schema.
148
172
  // Normalize via path.resolve to collapse any `..` segments before the
149
173
  // traversal guard — otherwise "/repo/../etc/passwd" would pass startsWith.
150
- const rawFile = path.isAbsolute(rec.testFile)
151
- ? rec.testFile
174
+ const rawFile = path.isAbsolute(rec.testFilePath)
175
+ ? rec.testFilePath
152
176
  : repoRoot
153
- ? path.resolve(repoRoot, rec.testFile)
154
- : rec.testFile;
177
+ ? path.resolve(repoRoot, rec.testFilePath)
178
+ : rec.testFilePath;
155
179
  const resolvedFile = path.resolve(rawFile);
156
180
  // Reject files outside the repo root (path-traversal guard).
157
181
  // Exception: files already in the scanned test catalog (externalTestFiles / skyrampTestFiles)
158
182
  // may legitimately live in a separate testsRepoDir outside repositoryPath — catalog
159
183
  // membership is a sufficient provenance check for those paths.
160
- const isInCatalog = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFile)
161
- || externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFile);
184
+ const isInCatalog = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFilePath)
185
+ || externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFilePath);
162
186
  if (repoRoot && !isInCatalog && !resolvedFile.startsWith(repoRoot + path.sep) && resolvedFile !== repoRoot) {
163
- logger.warning(`Skipping recommendation for path outside repo root: ${rec.testFile}`);
187
+ logger.warning(`Skipping recommendation for path outside repo root: ${rec.testFilePath}`);
164
188
  return;
165
189
  }
166
190
  // Guard: only files present in the scanned test catalog may receive any
167
191
  // recommendation. Hallucinated paths (not in either set) are rejected for
168
192
  // all actions, including VERIFY and IGNORE, to keep the report consistent
169
193
  // with what was actually discovered.
170
- const isSkyramp = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFile);
194
+ const isSkyramp = skyrampTestFiles.has(resolvedFile) || skyrampTestFiles.has(rec.testFilePath);
171
195
  // Catalog membership is the safety boundary — prevents editing non-test files.
172
- const isRelevantExternal = (externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFile));
196
+ const isRelevantExternal = (externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.testFilePath));
173
197
  const isInAnyKnownCatalog = isSkyramp || isRelevantExternal;
174
198
  if (!isInAnyKnownCatalog) {
175
- logger.warning(`Skipping ${rec.action} for unknown test (not in scanned catalog): ${rec.testFile}`);
199
+ logger.warning(`Skipping ${rec.action} for unknown test (not in scanned catalog): ${rec.testFilePath}`);
176
200
  return;
177
201
  }
178
202
  const isActionable = [DriftAction.Update, DriftAction.Regenerate, DriftAction.Delete].includes(rec.action);
203
+ // No write access to a related repo's checkout — convert any edit-requiring
204
+ // action to VERIFY so the finding still surfaces without attempting a write that
205
+ // would fail (or land in a checkout that shouldn't be touched at all).
206
+ if (isRelatedRepo && isActionable) {
207
+ recommendations.push({
208
+ testFilePath: resolvedFile,
209
+ action: DriftAction.Verify,
210
+ testType: rec.testType,
211
+ endpoint: rec.endpoint,
212
+ priority: rec.priority ?? RecommendationPriority.Medium,
213
+ rationale: `[related repo — no write access, needs manual review] ${rec.rationale}`,
214
+ estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
215
+ updateInstructions: "",
216
+ renamedEndpoints: [],
217
+ });
218
+ return;
219
+ }
179
220
  if (isActionable && !isSkyramp && !isRelevantExternal) {
180
- logger.warning(`Skipping ${rec.action} for irrelevant external test: ${rec.testFile}`);
221
+ logger.warning(`Skipping ${rec.action} for irrelevant external test: ${rec.testFilePath}`);
181
222
  return;
182
223
  }
183
224
  // REGENERATE and DELETE on external tests are report-only — convert to VERIFY so
@@ -185,17 +226,19 @@ export function registerActionsTool(server) {
185
226
  if (isRelevantExternal && !isSkyramp &&
186
227
  (rec.action === DriftAction.Regenerate || rec.action === DriftAction.Delete)) {
187
228
  recommendations.push({
188
- testFile: resolvedFile,
229
+ testFilePath: resolvedFile,
189
230
  action: DriftAction.Verify,
231
+ testType: rec.testType,
232
+ endpoint: rec.endpoint,
190
233
  priority: rec.priority ?? RecommendationPriority.Medium,
191
- rationale: `[external test — needs manual review] ${rec.rationale ?? ""}`.trimEnd(),
234
+ rationale: `[external test — needs manual review] ${rec.rationale}`,
192
235
  estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
193
236
  updateInstructions: "",
194
237
  renamedEndpoints: [],
195
238
  });
196
239
  return;
197
240
  }
198
- // Resolve and validate pomFile with the same path-traversal guard as testFile.
241
+ // Resolve and validate pomFile with the same path-traversal guard as testFilePath.
199
242
  let resolvedPomFile;
200
243
  if (rec.pomFile) {
201
244
  const rawPom = path.isAbsolute(rec.pomFile)
@@ -208,21 +251,60 @@ export function registerActionsTool(server) {
208
251
  }
209
252
  }
210
253
  recommendations.push({
211
- testFile: resolvedFile,
254
+ testFilePath: resolvedFile,
212
255
  pomFile: resolvedPomFile,
213
256
  action: rec.action,
257
+ testType: rec.testType,
258
+ endpoint: rec.endpoint,
214
259
  priority: rec.priority ?? RecommendationPriority.Medium,
215
- rationale: rec.rationale ?? "",
260
+ rationale: rec.rationale,
216
261
  estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
217
262
  updateInstructions: rec.updateInstructions ?? "",
218
263
  renamedEndpoints: rec.renamedEndpoints ?? [],
219
264
  });
220
265
  });
266
+ // Use the validated `recommendations` array (built above), not the raw
267
+ // `args.recommendations` — a hallucinated/rejected path (not in the catalog, or
268
+ // outside the repo root) never made it into `recommendations`, so it must not
269
+ // count as "accounted for" here either. Counting the raw input would let a typo'd
270
+ // testFilePath mask a real file that never received a valid recommendation.
271
+ const accountedForNames = [
272
+ ...recommendations.map((r) => r.testFilePath),
273
+ ...(args.ignoredTestFiles ?? []),
274
+ ];
275
+ const unaccountedFor = testAnalysisResults
276
+ .map((t) => t.testFile)
277
+ .filter((testFile) => !accountedForNames.some((name) => testFileMatches(testFile, name)));
278
+ if (unaccountedFor.length > 0) {
279
+ errorResult = toolError(`${unaccountedFor.length} existing test(s) assessed by skyramp_analyze_test_health are not accounted for: ${unaccountedFor.join(", ")}. ` +
280
+ "Add a recommendation for each one that needs UPDATE/REGENERATE/DELETE/VERIFY, or add its path to ignoredTestFiles if it needs no action.");
281
+ return errorResult;
282
+ }
283
+ // So skyramp_submit_report can read this back instead of re-asking the LLM.
284
+ // Must fail loud — edits below apply regardless, so a swallowed failure here
285
+ // would let edits ship with no testMaintenance in the final report.
286
+ try {
287
+ await stateManager.writeRepoData({
288
+ ...stateData,
289
+ ignoredTestFiles: args.ignoredTestFiles ?? [],
290
+ maintenanceVerdicts: recommendations.map((r) => ({
291
+ testFilePath: r.testFilePath,
292
+ action: r.action,
293
+ rationale: r.rationale,
294
+ testType: r.testType,
295
+ endpoint: r.endpoint,
296
+ })),
297
+ }, { repo: args.repository, repositoryPath, step: "actions" });
298
+ }
299
+ catch (error) {
300
+ errorResult = toolError(`Failed to persist maintenance reconciliation to state file: ${error.message}. Retry this call.`);
301
+ return errorResult;
302
+ }
221
303
  // ── Process UPDATE and REGENERATE recommendations ──
222
- // UPDATE dedup key: `${testFile}::${pomFile ?? ''}` — a spec may emit one
304
+ // UPDATE dedup key: `${testFilePath}::${pomFile ?? ''}` — a spec may emit one
223
305
  // UPDATE entry per page object (different pomFile values), so the same
224
- // testFile can appear multiple times with different edit targets.
225
- // REGENERATE dedup key: testFile only — REGENERATE covers the whole spec.
306
+ // testFilePath can appear multiple times with different edit targets.
307
+ // REGENERATE dedup key: testFilePath only — REGENERATE covers the whole spec.
226
308
  // Priority order: high > medium > low.
227
309
  const priorityRank = {
228
310
  [RecommendationPriority.High]: 2,
@@ -235,15 +317,15 @@ export function registerActionsTool(server) {
235
317
  const regenerateByFile = new Map();
236
318
  for (const rec of recommendations) {
237
319
  if (rec.action === DriftAction.Regenerate) {
238
- const existing = regenerateByFile.get(rec.testFile);
320
+ const existing = regenerateByFile.get(rec.testFilePath);
239
321
  if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
240
- regenerateByFile.set(rec.testFile, rec);
322
+ regenerateByFile.set(rec.testFilePath, rec);
241
323
  }
242
324
  }
243
325
  else if (rec.action === DriftAction.Update) {
244
326
  // Only add if no REGENERATE exists for this spec.
245
- if (!regenerateByFile.has(rec.testFile)) {
246
- const key = `${rec.testFile}::${rec.pomFile ?? ""}`;
327
+ if (!regenerateByFile.has(rec.testFilePath)) {
328
+ const key = `${rec.testFilePath}::${rec.pomFile ?? ""}`;
247
329
  const existing = updateByKey.get(key);
248
330
  if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
249
331
  updateByKey.set(key, rec);
@@ -266,13 +348,13 @@ export function registerActionsTool(server) {
266
348
  const testFileContentMap = new Map();
267
349
  // ── UPDATE: read file, emit targeted edit instructions ──
268
350
  for (const rec of updateRecommendations) {
269
- if (!rec.testFile) {
270
- logger.warning("Recommendation missing testFile", rec);
351
+ if (!rec.testFilePath) {
352
+ logger.warning("Recommendation missing testFilePath", rec);
271
353
  continue;
272
354
  }
273
355
  // When a POM file is specified, edit the POM (selector source of truth)
274
- // rather than the spec. testFile (the spec) remains in the report.
275
- const editTarget = rec.pomFile ?? rec.testFile;
356
+ // rather than the spec. testFilePath (the spec) remains in the report.
357
+ const editTarget = rec.pomFile ?? rec.testFilePath;
276
358
  testFilesToUpdate.push(editTarget);
277
359
  try {
278
360
  testFileContentMap.set(editTarget, fs.readFileSync(editTarget, "utf-8"));
@@ -283,12 +365,12 @@ export function registerActionsTool(server) {
283
365
  }
284
366
  const renames = rec.renamedEndpoints || [];
285
367
  const isRenameUpdate = renames.length > 0;
286
- // Always derive the rename suggestion from the spec (testFile), not the
287
- // POM (editTarget). fileRenames keys off rec.testFile so the spec is
368
+ // Always derive the rename suggestion from the spec (testFilePath), not the
369
+ // POM (editTarget). fileRenames keys off rec.testFilePath so the spec is
288
370
  // what gets renamed; computing from editTarget when pomFile is set
289
371
  // would produce a name derived from the page-object path instead.
290
372
  const suggestedNewFile = isRenameUpdate
291
- ? computeRenamedTestFile(rec.testFile, renames) ?? undefined
373
+ ? computeRenamedTestFile(rec.testFilePath, renames) ?? undefined
292
374
  : undefined;
293
375
  if (suggestedNewFile)
294
376
  rec._suggestedNewFile = suggestedNewFile;
@@ -305,22 +387,22 @@ export function registerActionsTool(server) {
305
387
  const testFilesToRegenerate = [];
306
388
  const regenerateContentMap = new Map();
307
389
  for (const rec of regenerateRecommendations) {
308
- if (!rec.testFile) {
309
- logger.warning("Recommendation missing testFile", rec);
390
+ if (!rec.testFilePath) {
391
+ logger.warning("Recommendation missing testFilePath", rec);
310
392
  continue;
311
393
  }
312
- testFilesToRegenerate.push(rec.testFile);
394
+ testFilesToRegenerate.push(rec.testFilePath);
313
395
  try {
314
- regenerateContentMap.set(rec.testFile, fs.readFileSync(rec.testFile, "utf-8"));
396
+ regenerateContentMap.set(rec.testFilePath, fs.readFileSync(rec.testFilePath, "utf-8"));
315
397
  }
316
398
  catch (error) {
317
- logger.warning(`Could not read file for REGENERATE context ${rec.testFile}: ${error.message}`);
399
+ logger.warning(`Could not read file for REGENERATE context ${rec.testFilePath}: ${error.message}`);
318
400
  }
319
401
  regenerateInstructions.push(buildRegenerateFileInstruction({
320
- testFile: rec.testFile,
402
+ testFile: rec.testFilePath,
321
403
  updateInstructions: rec.updateInstructions,
322
- outputDir: path.dirname(rec.testFile),
323
- outputFile: path.basename(rec.testFile),
404
+ outputDir: path.dirname(rec.testFilePath),
405
+ outputFile: path.basename(rec.testFilePath),
324
406
  }));
325
407
  }
326
408
  // ── Build response text ──
@@ -340,7 +422,7 @@ export function registerActionsTool(server) {
340
422
  }
341
423
  if (otherRecs.length > 0) {
342
424
  sections.push(`## Other Findings (${otherRecs.length})\n\n` +
343
- otherRecs.map((rec) => `- **${rec.testFile}** — Action: ${rec.action}, Priority: ${rec.priority}` +
425
+ otherRecs.map((rec) => `- **${rec.testFilePath}** — Action: ${rec.action}, Priority: ${rec.priority}` +
344
426
  (rec.rationale ? ` — ${rec.rationale}` : "")).join("\n"));
345
427
  }
346
428
  else if (updateRecommendations.length === 0 && regenerateRecommendations.length === 0) {
@@ -367,7 +449,7 @@ export function registerActionsTool(server) {
367
449
  const fileRenames = [];
368
450
  for (const rec of updateRecommendations) {
369
451
  if (rec._suggestedNewFile) {
370
- fileRenames.push({ from: rec.testFile, to: rec._suggestedNewFile });
452
+ fileRenames.push({ from: rec.testFilePath, to: rec._suggestedNewFile });
371
453
  }
372
454
  }
373
455
  if (fileRenames.length > 0) {
@@ -380,7 +462,7 @@ export function registerActionsTool(server) {
380
462
  const updateContext = updateRecommendations
381
463
  .filter((rec) => !!rec.updateInstructions)
382
464
  .map((rec) => {
383
- const editTarget = rec.pomFile ?? rec.testFile;
465
+ const editTarget = rec.pomFile ?? rec.testFilePath;
384
466
  const current_content = testFileContentMap.get(editTarget);
385
467
  return { file: editTarget, context: rec.updateInstructions, ...(current_content !== undefined && { current_content }) };
386
468
  });
@@ -392,8 +474,8 @@ export function registerActionsTool(server) {
392
474
  // endpoint URL, auth pattern, test type, and language to replicate.
393
475
  if (regenerateRecommendations.length > 0) {
394
476
  llmInstructionsObj.regenerate_context = regenerateRecommendations.map((rec) => {
395
- const existing_content = regenerateContentMap.get(rec.testFile);
396
- return { file: rec.testFile, rationale: rec.updateInstructions || rec.rationale, ...(existing_content !== undefined && { existing_content }) };
477
+ const existing_content = regenerateContentMap.get(rec.testFilePath);
478
+ return { file: rec.testFilePath, rationale: rec.updateInstructions || rec.rationale, ...(existing_content !== undefined && { existing_content }) };
397
479
  });
398
480
  llmInstructionsObj.regenerate_strategy = buildRegenerateStrategy();
399
481
  }
@@ -0,0 +1 @@
1
+ export {};