@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.
- package/build/index.js +1 -1
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
- package/build/prompts/personas.d.ts +2 -2
- package/build/prompts/personas.js +2 -2
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
- package/build/prompts/sut-setup/shared.js +1 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
- package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
- package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
- package/build/prompts/test-recommendation/recommendationShared.js +1 -1
- package/build/prompts/testbot/testbot-prompts.js +8 -8
- package/build/resources/testbotResource.js +1 -1
- package/build/services/TestDiscoveryService.d.ts +0 -1
- package/build/services/TestDiscoveryService.js +13 -33
- package/build/services/TestDiscoveryService.test.js +136 -1
- package/build/services/TestExecutionService.d.ts +1 -1
- package/build/tools/executeSkyrampTestTool.js +40 -71
- package/build/tools/submitReportTool.d.ts +20 -20
- package/build/tools/submitReportTool.js +113 -31
- package/build/tools/submitReportTool.test.js +411 -94
- package/build/tools/test-management/actionsTool.js +135 -53
- package/build/tools/test-management/actionsTool.test.d.ts +1 -0
- package/build/tools/test-management/actionsTool.test.js +297 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
- package/build/tools/test-management/analyzeChangesTool.js +63 -152
- package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
- package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
- package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
- package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
- package/build/types/FrontendIntegration.d.ts +25 -0
- package/build/types/FrontendIntegration.js +19 -0
- package/build/types/RepositoryAnalysis.d.ts +8 -8
- package/build/types/TestAnalysis.d.ts +16 -50
- package/build/types/TestAnalysis.js +0 -28
- package/build/types/TestTypes.d.ts +3 -1
- package/build/types/TestTypes.js +3 -1
- package/build/utils/AnalysisStateManager.d.ts +13 -1
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +1 -1
- package/build/utils/featureFlags.js +1 -1
- package/build/utils/frontendIntegration.d.ts +9 -0
- package/build/utils/frontendIntegration.js +237 -0
- package/build/utils/frontendIntegration.test.d.ts +1 -0
- package/build/utils/frontendIntegration.test.js +229 -0
- package/build/utils/pr-comment-parser.d.ts +3 -3
- package/build/utils/pr-comment-parser.js +6 -6
- package/build/utils/pr-comment-parser.test.js +3 -3
- package/build/utils/repoScanner.d.ts +7 -0
- package/build/utils/repoScanner.js +14 -6
- package/build/utils/repoScanner.test.js +9 -9
- package/build/utils/routeParsers.d.ts +32 -0
- package/build/utils/routeParsers.js +205 -2
- package/build/utils/routeParsers.test.js +279 -29
- package/build/utils/sourceRouteExtractor.js +174 -5
- package/build/utils/sourceRouteExtractor.test.js +173 -0
- package/build/utils/utils.d.ts +11 -0
- package/build/utils/utils.js +19 -0
- package/build/utils/utils.test.js +23 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- 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
|
-
|
|
49
|
+
testFilePath: z
|
|
49
50
|
.string()
|
|
50
|
-
.refine((p) => path.isAbsolute(p), { message: "
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
67
|
-
.
|
|
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) =>
|
|
159
|
+
.filter((t) => t.source !== TestSource.External)
|
|
136
160
|
.map((t) => t.testFile));
|
|
137
161
|
const externalTestFiles = new Set(testAnalysisResults
|
|
138
|
-
.filter((t) =>
|
|
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.
|
|
151
|
-
? rec.
|
|
174
|
+
const rawFile = path.isAbsolute(rec.testFilePath)
|
|
175
|
+
? rec.testFilePath
|
|
152
176
|
: repoRoot
|
|
153
|
-
? path.resolve(repoRoot, rec.
|
|
154
|
-
: rec.
|
|
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.
|
|
161
|
-
|| externalTestFiles.has(resolvedFile) || externalTestFiles.has(rec.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: `${
|
|
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
|
-
//
|
|
225
|
-
// REGENERATE dedup key:
|
|
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.
|
|
320
|
+
const existing = regenerateByFile.get(rec.testFilePath);
|
|
239
321
|
if (!existing || priorityRank[rec.priority] > priorityRank[existing.priority]) {
|
|
240
|
-
regenerateByFile.set(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.
|
|
246
|
-
const key = `${rec.
|
|
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.
|
|
270
|
-
logger.warning("Recommendation missing
|
|
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.
|
|
275
|
-
const editTarget = rec.pomFile ?? rec.
|
|
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 (
|
|
287
|
-
// POM (editTarget). fileRenames keys off rec.
|
|
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.
|
|
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.
|
|
309
|
-
logger.warning("Recommendation missing
|
|
390
|
+
if (!rec.testFilePath) {
|
|
391
|
+
logger.warning("Recommendation missing testFilePath", rec);
|
|
310
392
|
continue;
|
|
311
393
|
}
|
|
312
|
-
testFilesToRegenerate.push(rec.
|
|
394
|
+
testFilesToRegenerate.push(rec.testFilePath);
|
|
313
395
|
try {
|
|
314
|
-
regenerateContentMap.set(rec.
|
|
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.
|
|
399
|
+
logger.warning(`Could not read file for REGENERATE context ${rec.testFilePath}: ${error.message}`);
|
|
318
400
|
}
|
|
319
401
|
regenerateInstructions.push(buildRegenerateFileInstruction({
|
|
320
|
-
testFile: rec.
|
|
402
|
+
testFile: rec.testFilePath,
|
|
321
403
|
updateInstructions: rec.updateInstructions,
|
|
322
|
-
outputDir: path.dirname(rec.
|
|
323
|
-
outputFile: path.basename(rec.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
396
|
-
return { file: rec.
|
|
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 {};
|