@skyramp/mcp 0.1.8 → 0.2.0-rc.2

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 (122) hide show
  1. package/build/index.js +4 -2
  2. package/build/playwright/registerPlaywrightTools.js +12 -0
  3. package/build/playwright/traceRecordingPrompt.js +15 -0
  4. package/build/prompts/code-reuse.js +106 -7
  5. package/build/prompts/pom-aware-code-reuse.js +106 -7
  6. package/build/prompts/startTraceCollectionPrompts.js +37 -15
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.js +26 -31
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +40 -1
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +90 -86
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +286 -163
  11. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -45
  12. package/build/prompts/test-recommendation/diffExecutionPlan.js +246 -117
  13. package/build/prompts/test-recommendation/promptPlan.js +290 -0
  14. package/build/prompts/test-recommendation/promptPlan.test.js +336 -0
  15. package/build/prompts/test-recommendation/recommendationSections.js +4 -3
  16. package/build/prompts/test-recommendation/recommendationShared.js +23 -1
  17. package/build/prompts/test-recommendation/scopeAssessment.js +65 -14
  18. package/build/prompts/test-recommendation/scopeAssessment.test.js +93 -2
  19. package/build/prompts/test-recommendation/test-recommendation-prompt.js +36 -12
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +316 -1
  21. package/build/prompts/testbot/testbot-prompts.js +73 -13
  22. package/build/prompts/testbot/testbot-prompts.test.js +114 -1
  23. package/build/resources/testbotResource.js +1 -1
  24. package/build/services/ScenarioGenerationService.integration.test.js +158 -0
  25. package/build/services/ScenarioGenerationService.js +47 -4
  26. package/build/services/ScenarioGenerationService.test.js +158 -22
  27. package/build/services/TestExecutionService.js +73 -15
  28. package/build/services/TestExecutionService.test.js +105 -0
  29. package/build/services/TestGenerationService.js +11 -1
  30. package/build/tools/executeSkyrampTestTool.js +1 -10
  31. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +16 -4
  32. package/build/tools/generate-tests/generateIntegrationRestTool.js +2 -0
  33. package/build/tools/generate-tests/generateUIRestTool.js +2 -0
  34. package/build/tools/test-management/actionsTool.js +152 -63
  35. package/build/tools/test-management/analyzeChangesTool.js +178 -64
  36. package/build/tools/test-management/analyzeChangesTool.test.js +103 -16
  37. package/build/tools/test-management/analyzeTestHealthTool.js +30 -81
  38. package/build/tools/test-management/index.js +1 -0
  39. package/build/tools/test-management/uiAnalyzeChangesTool.js +149 -0
  40. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +100 -0
  41. package/build/tools/trace/resolveSaveStoragePath.js +16 -0
  42. package/build/tools/trace/resolveSaveStoragePath.test.js +17 -0
  43. package/build/tools/trace/resolveSessionPaths.js +39 -0
  44. package/build/tools/trace/resolveSessionPaths.test.js +103 -0
  45. package/build/tools/trace/sessionState.js +14 -0
  46. package/build/tools/trace/sessionState.test.js +17 -0
  47. package/build/tools/trace/startTraceCollectionTool.js +84 -14
  48. package/build/tools/trace/stopTraceCollectionTool.js +9 -2
  49. package/build/types/TestAnalysis.js +50 -0
  50. package/build/types/TestRecommendation.js +6 -58
  51. package/build/types/TestTypes.js +1 -1
  52. package/build/utils/AnalysisStateManager.js +22 -11
  53. package/build/utils/branchDiff.js +11 -2
  54. package/build/utils/docker.test.js +1 -1
  55. package/build/utils/gitStaging.js +52 -3
  56. package/build/utils/gitStaging.test.js +19 -1
  57. package/build/utils/repoScanner.js +18 -10
  58. package/build/utils/repoScanner.test.js +92 -0
  59. package/build/utils/routeParsers.js +180 -25
  60. package/build/utils/routeParsers.test.js +180 -1
  61. package/build/utils/scenarioDrafting.js +220 -17
  62. package/build/utils/scenarioDrafting.test.js +182 -9
  63. package/build/utils/sourceRouteExtractor.js +806 -0
  64. package/build/utils/sourceRouteExtractor.test.js +565 -0
  65. package/build/utils/uiPageEnumerator.js +319 -0
  66. package/build/utils/uiPageEnumerator.test.js +422 -0
  67. package/build/utils/utils.js +27 -0
  68. package/build/utils/versions.js +1 -1
  69. package/build/utils/workspaceAuth.js +33 -4
  70. package/node_modules/playwright/ThirdPartyNotices.txt +6 -6
  71. package/node_modules/playwright/lib/dom-analyzer/analyze.js +111 -0
  72. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1210 -0
  73. package/node_modules/playwright/lib/dom-analyzer/blueprint.test.js +396 -0
  74. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.js +57 -0
  75. package/node_modules/playwright/lib/dom-analyzer/blueprintCache.test.js +57 -0
  76. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.js +254 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprintDiff.test.js +304 -0
  78. package/node_modules/playwright/lib/dom-analyzer/crawler.js +384 -0
  79. package/node_modules/playwright/lib/dom-analyzer/curatedWidgets.js +73 -0
  80. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +43 -0
  81. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +85 -0
  82. package/node_modules/playwright/lib/dom-analyzer/fingerprint.js +90 -0
  83. package/node_modules/playwright/lib/dom-analyzer/fingerprint.test.js +231 -0
  84. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.fixtures.js +145 -0
  85. package/node_modules/playwright/lib/dom-analyzer/fingerprintAblation.test.js +41 -0
  86. package/node_modules/playwright/lib/dom-analyzer/graph.js +36 -0
  87. package/node_modules/playwright/lib/dom-analyzer/liveFingerprints.js +43 -0
  88. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.js +72 -0
  89. package/node_modules/playwright/lib/dom-analyzer/logicalNameResolver.test.js +182 -0
  90. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.js +150 -0
  91. package/node_modules/playwright/lib/dom-analyzer/possibleAssertions.test.js +470 -0
  92. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.js +169 -0
  93. package/node_modules/playwright/lib/dom-analyzer/sectionGrouper.test.js +269 -0
  94. package/node_modules/playwright/lib/dom-analyzer/serialization.js +75 -0
  95. package/node_modules/playwright/lib/dom-analyzer/slug.js +30 -0
  96. package/node_modules/playwright/lib/dom-analyzer/slug.test.js +84 -0
  97. package/node_modules/playwright/lib/dom-analyzer/widgetContract.js +127 -0
  98. package/node_modules/playwright/lib/dom-analyzer/widgetContract.test.js +212 -0
  99. package/node_modules/playwright/lib/mcp/browser/browserContextFactory.js +3 -1
  100. package/node_modules/playwright/lib/mcp/browser/config.js +1 -1
  101. package/node_modules/playwright/lib/mcp/browser/context.js +17 -1
  102. package/node_modules/playwright/lib/mcp/browser/tab.js +38 -0
  103. package/node_modules/playwright/lib/mcp/browser/tools/domAnalyzer.js +261 -0
  104. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -3
  105. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +146 -0
  106. package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.test.js +140 -0
  107. package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +226 -0
  108. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +2 -2
  109. package/node_modules/playwright/lib/mcp/browser/tools/widgetContract.js +168 -0
  110. package/node_modules/playwright/lib/mcp/browser/tools.js +6 -0
  111. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +52 -12
  112. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +64 -13
  113. package/node_modules/playwright/package.json +1 -1
  114. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.3.tgz +0 -0
  115. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.4.tgz +0 -0
  116. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.5.tgz +0 -0
  117. package/node_modules/playwright/skyramp-playwright-1.58.2-skyramp.8.9.6.tgz +0 -0
  118. package/package.json +3 -3
  119. package/build/services/TestHealthService.js +0 -694
  120. package/build/services/TestHealthService.test.js +0 -241
  121. package/build/types/TestDriftAnalysis.js +0 -1
  122. package/build/types/TestHealth.js +0 -4
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { StateManager, } from "../../utils/AnalysisStateManager.js";
4
- import { TestSource } from "../../types/TestAnalysis.js";
4
+ import { TestSource, DriftAction, RecommendationPriority, EstimatedWork } from "../../types/TestAnalysis.js";
5
5
  import { TestType } from "../../types/TestTypes.js";
6
6
  import * as fs from "fs";
7
7
  import * as path from "path";
8
8
  import { AnalyticsService } from "../../services/AnalyticsService.js";
9
+ import { toolError } from "../../utils/utils.js";
9
10
  /**
10
11
  * Compute a suggested new filename when an endpoint is renamed.
11
12
  */
@@ -59,25 +60,74 @@ function selectTestTypesForEndpoint(method) {
59
60
  return [TestType.CONTRACT, TestType.SMOKE];
60
61
  }
61
62
  }
63
+ const recommendationSchema = z.object({
64
+ testFile: z
65
+ .string()
66
+ .refine((p) => path.isAbsolute(p), { message: "testFile must be an absolute path" })
67
+ .describe("Absolute path to the test file — use the path as reported by skyramp_analyze_changes or skyramp_analyze_test_health"),
68
+ action: z
69
+ .nativeEnum(DriftAction)
70
+ .describe("Drift action assigned by the LLM health assessment"),
71
+ priority: z
72
+ .nativeEnum(RecommendationPriority)
73
+ .optional()
74
+ .describe("Update priority"),
75
+ rationale: z
76
+ .string()
77
+ .optional()
78
+ .describe("1-2 sentence explanation of why this action is needed"),
79
+ estimatedWork: z
80
+ .nativeEnum(EstimatedWork)
81
+ .optional()
82
+ .describe("Estimated effort to apply the update"),
83
+ updateInstructions: z
84
+ .string()
85
+ .optional()
86
+ .describe("Free-form summary of what this test must change — written for the downstream LLM that will edit the file. " +
87
+ "Specificity prevents incomplete or mismatched edits. Include diff-specific details: " +
88
+ "new response fields to assert, constraint details (types, ranges, defaults), " +
89
+ "auth changes, new request params, removed fields, or other drift-related changes. " +
90
+ "Example: 'Added stock_count: int (ge=0, default=0) to ProductBase. " +
91
+ "Test hits GET /products — assert stock_count is present and non-negative.'"),
92
+ renamedEndpoints: z
93
+ .array(z.object({
94
+ oldPath: z.string().describe("Previous endpoint path"),
95
+ newPath: z.string().describe("New endpoint path after rename"),
96
+ method: z.string().describe("HTTP method, e.g. GET"),
97
+ }))
98
+ .optional()
99
+ .describe("Renamed endpoints — supply this array when action is UPDATE and the endpoint path has changed. Omit if action is not UPDATE."),
100
+ });
62
101
  const actionsSchema = {
63
102
  stateFile: z
64
103
  .string()
65
- .describe("Path to state file from skyramp_analyze_test_health"),
104
+ .refine((p) => path.isAbsolute(p), { message: "stateFile must be an absolute path" })
105
+ .describe("Path to state file from skyramp_analyze_changes"),
106
+ recommendations: z
107
+ .array(recommendationSchema)
108
+ .optional()
109
+ .describe("LLM drift assessment — one entry per test assessed. Required for UPDATE instructions to be emitted; omitting results in no maintenance actions."),
66
110
  };
67
111
  const TOOL_NAME = "skyramp_actions";
68
112
  export function registerActionsTool(server) {
69
113
  server.registerTool(TOOL_NAME, {
114
+ annotations: {
115
+ destructiveHint: true,
116
+ readOnlyHint: false,
117
+ idempotentHint: false,
118
+ openWorldHint: true,
119
+ },
70
120
  description: `Execute test maintenance and generation actions — final step of the unified Test Health Analysis Flow.
71
121
 
72
- **PREREQUISITE:** Call \`skyramp_analyze_test_health\`.
122
+ **PREREQUISITE:** Call \`skyramp_analyze_changes\` (produces the stateFile), then \`skyramp_analyze_test_health\` (runs the drift assessment). This tool reads the stateFile from \`skyramp_analyze_changes\`.
73
123
 
74
- **CRITICAL:** This tool MUST be called automatically after the LLM completes the drift assessment. Do NOT wait for user confirmation.
124
+ Call this tool after completing the drift assessment. It executes maintenance actions automatically from the stateFile no user confirmation required.
75
125
 
76
126
  **EXECUTING ACTIONS:**
77
- - UPDATE: Apply changes to test files (path renames, field updates) using the write tool
78
- - REGENERATE: Provide summary for human review / tool re-invocation
79
- - VERIFY: Provide summary for human review
80
- - ADD: Auto-generate tests for new endpoints via LLM instructions
127
+ - UPDATE: Tests with drift emits targeted per-file edit instructions driven by updateInstructions and renamedEndpoints
128
+ - REGENERATE: Emits file-level summary; follow up by calling the appropriate generation tool (e.g. skyramp_integration_test_generation) with the same filename to overwrite
129
+ - VERIFY: Emits file-level summary for human review — no automated edits
130
+ - ADD: Auto-generates tests for new endpoints via LLM instructions
81
131
 
82
132
  **OUTPUT:**
83
133
  Comprehensive report with executed actions, summary, and instructions for ADD recommendations
@@ -93,43 +143,78 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
93
143
  const fullState = await stateManager.readFullState();
94
144
  const repositoryPath = fullState?.metadata.repositoryPath || "";
95
145
  if (!stateData) {
96
- errorResult = {
97
- content: [
98
- {
99
- type: "text",
100
- text: JSON.stringify({
101
- error: "State file is empty or invalid",
102
- stateFile: args.stateFile,
103
- }, null, 2),
104
- },
105
- ],
106
- isError: true,
107
- };
146
+ errorResult = toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
108
147
  return errorResult;
109
148
  }
110
149
  // External tests must not be candidates for UPDATE/REGENERATE/DELETE actions.
111
150
  // Default source to Skyramp for backwards compat with state files created before the source field existed.
112
151
  const testAnalysisResults = (stateData.existingTests || []).filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External);
113
152
  const newEndpoints = stateData.newEndpoints || [];
114
- // ── Build recommendations from existing tests ──
153
+ // Resolve repo root for path normalization and security checks.
154
+ const repoRoot = repositoryPath ? path.resolve(repositoryPath) : "";
155
+ // Set of non-external (Skyramp-generated) test file paths — the only files
156
+ // that may receive UPDATE/REGENERATE/DELETE actions. Using the allowlist rather
157
+ // than a blocklist catches both external tests AND hallucinated paths the LLM
158
+ // may supply that are not present in the scanned catalog at all.
159
+ const skyrampTestFiles = new Set(testAnalysisResults.map((t) => t.testFile));
160
+ // ── Build recommendations from LLM-supplied drift assessment ──
161
+ // The LLM performs the drift assessment in context after skyramp_analyze_test_health
162
+ // and passes results here directly — analyzeTestHealthTool never writes assessment
163
+ // data back to the state file.
115
164
  const recommendations = [];
116
- testAnalysisResults.forEach((test) => {
117
- if (test.healthScore !== undefined && test.recommendation) {
118
- recommendations.push({
119
- testFile: test.testFile,
120
- action: test.recommendation.action,
121
- priority: test.recommendation.priority,
122
- rationale: test.recommendation.rationale,
123
- estimatedWork: test.recommendation.estimatedWork,
124
- issues: test.issues || [],
125
- renamedEndpoints: test.recommendation.details?.renamedEndpoints || [],
126
- });
165
+ (args.recommendations ?? []).forEach((rec) => {
166
+ // Schema requires absolute paths; resolve any relative paths defensively
167
+ // against repoRoot in case the LLM sends a relative path despite the schema.
168
+ const resolvedFile = path.isAbsolute(rec.testFile)
169
+ ? rec.testFile
170
+ : repoRoot
171
+ ? path.resolve(repoRoot, rec.testFile)
172
+ : rec.testFile;
173
+ // Reject files outside the repo root (path-traversal guard).
174
+ if (repoRoot && !resolvedFile.startsWith(repoRoot + path.sep) && resolvedFile !== repoRoot) {
175
+ logger.warning(`Skipping recommendation for path outside repo root: ${rec.testFile}`);
176
+ return;
127
177
  }
178
+ // Guard: only Skyramp-generated tests may receive UPDATE/REGENERATE/DELETE.
179
+ // Using an allowlist (skyrampTestFiles) rather than a blocklist catches both
180
+ // external tests and hallucinated paths the LLM may supply that are not in
181
+ // the scanned catalog. IGNORE/VERIFY are informational and pass through.
182
+ const isActionable = [DriftAction.Update, DriftAction.Regenerate, DriftAction.Delete].includes(rec.action);
183
+ if (isActionable && !skyrampTestFiles.has(resolvedFile) && !skyrampTestFiles.has(rec.testFile)) {
184
+ logger.warning(`Skipping ${rec.action} for non-Skyramp or unknown test: ${rec.testFile}`);
185
+ return;
186
+ }
187
+ recommendations.push({
188
+ testFile: resolvedFile,
189
+ action: rec.action,
190
+ priority: rec.priority ?? RecommendationPriority.Medium,
191
+ rationale: rec.rationale ?? "",
192
+ estimatedWork: rec.estimatedWork ?? EstimatedWork.Small,
193
+ updateInstructions: rec.updateInstructions ?? "",
194
+ renamedEndpoints: rec.renamedEndpoints ?? [],
195
+ });
128
196
  });
129
197
  // ── Process UPDATE recommendations ──
130
- const updateRecommendations = (recommendations || []).filter((rec) => rec.action === "UPDATE");
131
- const updateInstructions = [];
198
+ // Deduplicate by testFile keep the highest-priority entry when the LLM
199
+ // repeats a file. Priority order: high > medium > low.
200
+ const priorityRank = {
201
+ [RecommendationPriority.High]: 2,
202
+ [RecommendationPriority.Medium]: 1,
203
+ [RecommendationPriority.Low]: 0,
204
+ };
205
+ const updateByFile = new Map();
206
+ for (const rec of recommendations) {
207
+ if (rec.action !== DriftAction.Update)
208
+ continue;
209
+ const existing = updateByFile.get(rec.testFile);
210
+ if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
211
+ updateByFile.set(rec.testFile, rec);
212
+ }
213
+ }
214
+ const updateRecommendations = Array.from(updateByFile.values());
215
+ const fileInstructions = [];
132
216
  const testFilesToUpdate = [];
217
+ const testFileContentMap = new Map();
133
218
  for (const rec of updateRecommendations) {
134
219
  if (!rec.testFile) {
135
220
  logger.warning("Recommendation missing testFile", rec);
@@ -138,11 +223,11 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
138
223
  testFilesToUpdate.push(rec.testFile);
139
224
  const testData = testAnalysisResults.find((t) => t.testFile === rec.testFile);
140
225
  const driftData = testData?.drift;
141
- const issues = rec.issues || [];
142
226
  const driftChanges = driftData?.changes || [];
143
227
  let testFileContent = "";
144
228
  try {
145
229
  testFileContent = fs.readFileSync(rec.testFile, "utf-8");
230
+ testFileContentMap.set(rec.testFile, testFileContent);
146
231
  }
147
232
  catch (error) {
148
233
  logger.error(`Failed to read test file ${rec.testFile}: ${error.message}`);
@@ -152,7 +237,7 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
152
237
  const isRenameUpdate = renames.length > 0;
153
238
  let instruction = `\n### ${rec.testFile}\n\n`;
154
239
  instruction += `**Priority:** ${rec.priority} | `;
155
- instruction += `**Estimated Effort:** ${rec.estimatedWork || "Small"}\n\n`;
240
+ instruction += `**Estimated Effort:** ${rec.estimatedWork || EstimatedWork.Small}\n\n`;
156
241
  instruction += `**Why Update Needed:** ${rec.rationale}\n\n`;
157
242
  if (isRenameUpdate) {
158
243
  instruction += `**Endpoint Rename Detected — Path Substitution Required:**\n\n`;
@@ -172,11 +257,21 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
172
257
  rec._suggestedNewFile = suggestedNewFile;
173
258
  }
174
259
  }
260
+ const recUpdateInstructions = rec.updateInstructions ?? "";
261
+ if (recUpdateInstructions) {
262
+ instruction += `**What to change:**\n\n${recUpdateInstructions}\n\n`;
263
+ instruction += `Match the assertion style already used in the file. `;
264
+ instruction += `Preserve all existing test logic — only add or adjust what is described above.\n\n`;
265
+ }
266
+ else if (!isRenameUpdate) {
267
+ instruction += `**Action:** Update this test file per the rationale above. `;
268
+ instruction += `Match the assertion style already used in the file. `;
269
+ instruction += `Preserve all existing test logic — only add or adjust the minimum required assertions.\n\n`;
270
+ }
175
271
  if (driftData) {
176
272
  instruction += `**Analysis:**\n`;
177
- instruction += `- Drift Score: ${driftData.driftScore ?? "N/A"}\n`;
178
273
  instruction += `- Changes Detected: ${driftData.changes?.length || 0}\n`;
179
- instruction += `- Affected Files: ${driftData.affectedFiles.files || 0}\n\n`;
274
+ instruction += `- Affected Files: ${driftData.affectedFiles.files?.length || 0}\n\n`;
180
275
  }
181
276
  if (driftChanges.length > 0) {
182
277
  instruction += `**Changes Detected:**\n`;
@@ -191,18 +286,8 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
191
286
  });
192
287
  instruction += `\n`;
193
288
  }
194
- if (issues.length > 0) {
195
- instruction += `**Issues Found:**\n`;
196
- issues.forEach((issue) => {
197
- instruction += `**${issue.type}** (Severity: ${issue.severity}): ${issue.description}\n`;
198
- if (issue.details) {
199
- instruction += ` └─ ${issue.details}\n`;
200
- }
201
- });
202
- instruction += `\n`;
203
- }
204
- instruction += `**Test File Content:**\n\`\`\`\n${testFileContent}\n\`\`\`\n\n`;
205
- updateInstructions.push(instruction);
289
+ // File content is provided in LLM_INSTRUCTIONS.update_context.current_content omit here to avoid duplication.
290
+ fileInstructions.push(instruction);
206
291
  }
207
292
  // ── Build ADD section for new endpoints ──
208
293
  const wsBaseUrl = stateData.repositoryAnalysis?.wsBaseUrl || "";
@@ -261,7 +346,7 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
261
346
  responseText += `${idx + 1}. \`${file}\`\n`;
262
347
  });
263
348
  responseText += `\n---\n`;
264
- responseText += updateInstructions.join("\n---\n");
349
+ responseText += fileInstructions.join("\n---\n");
265
350
  }
266
351
  if (newEndpoints.length > 0) {
267
352
  responseText += `\n## New Endpoint Tests to Generate (${newEndpoints.length} endpoints)\n\n`;
@@ -271,7 +356,7 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
271
356
  responseText += `\nThe following tests will be generated automatically.\n`;
272
357
  }
273
358
  if (updateRecommendations.length === 0 && newEndpoints.length === 0) {
274
- const otherRecs = recommendations.filter((rec) => rec.action !== "UPDATE");
359
+ const otherRecs = recommendations.filter((rec) => rec.action !== DriftAction.Update);
275
360
  if (otherRecs.length > 0) {
276
361
  responseText += `## Recommendations (${otherRecs.length})\n\n`;
277
362
  otherRecs.forEach((rec) => {
@@ -329,6 +414,20 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
329
414
  "After updating path content in each file, rename the file using 'mv' or equivalent. Use git mv if the repo tracks the file.";
330
415
  }
331
416
  }
417
+ // Update context: per-file guidance + current content for the downstream LLM.
418
+ // Including file content avoids re-reads on each Edit turn, reducing token usage.
419
+ const updateInstructionsFiles = [];
420
+ for (const rec of updateRecommendations) {
421
+ if (rec.updateInstructions) {
422
+ const current_content = testFileContentMap.get(rec.testFile);
423
+ updateInstructionsFiles.push({ file: rec.testFile, context: rec.updateInstructions, ...(current_content !== undefined && { current_content }) });
424
+ }
425
+ }
426
+ if (updateInstructionsFiles.length > 0) {
427
+ llmInstructionsObj.update_context = updateInstructionsFiles;
428
+ llmInstructionsObj.update_strategy =
429
+ "For each file in update_context, apply the changes described in context to the provided current_content. Write the result using the Edit tool. Do NOT re-read the file first. Match the assertion style already used in the file. Preserve all existing test logic. After applying all edits, call skyramp_enhance_assertions with each updated file path to strengthen the assertions.";
430
+ }
332
431
  const llmInstructions = `<!-- LLM_INSTRUCTIONS:\n${JSON.stringify(llmInstructionsObj, null, 2)}\n-->\n`;
333
432
  const contentBlocks = [
334
433
  {
@@ -369,17 +468,7 @@ Comprehensive report with executed actions, summary, and instructions for ADD re
369
468
  }
370
469
  catch (error) {
371
470
  logger.error(`Actions tool failed: ${error.message}`, error);
372
- errorResult = {
373
- content: [
374
- {
375
- type: "text",
376
- text: JSON.stringify({
377
- error: error.message,
378
- }, null, 2),
379
- },
380
- ],
381
- isError: true,
382
- };
471
+ errorResult = toolError(`Actions tool failed: ${error.message}`);
383
472
  return errorResult;
384
473
  }
385
474
  finally {