@skyramp/mcp 0.1.5 → 0.1.6

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 (32) hide show
  1. package/build/index.js +6 -5
  2. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -7
  3. package/build/prompts/personas.js +2 -1
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
  5. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
  6. package/build/prompts/test-recommendation/analysisOutputPrompt.js +72 -14
  7. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
  8. package/build/prompts/test-recommendation/recommendationSections.js +4 -2
  9. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +20 -4
  10. package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -8
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -6
  12. package/build/prompts/testbot/testbot-prompts.js +7 -5
  13. package/build/prompts/testbot/testbot-prompts.test.js +2 -2
  14. package/build/resources/analysisResources.js +1 -0
  15. package/build/services/ScenarioGenerationService.js +2 -1
  16. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
  17. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +123 -1
  18. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -9
  19. package/build/tools/generate-tests/generateContractRestTool.js +19 -19
  20. package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
  21. package/build/tools/generate-tests/generateUIRestTool.js +23 -8
  22. package/build/tools/test-management/analyzeChangesTool.js +218 -2
  23. package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
  24. package/build/utils/featureFlags.js +7 -0
  25. package/build/utils/featureFlags.test.js +81 -0
  26. package/build/utils/httpDefaults.js +17 -0
  27. package/build/utils/httpDefaults.test.js +21 -0
  28. package/build/utils/scenarioDrafting.js +37 -15
  29. package/build/utils/scenarioDrafting.test.js +66 -0
  30. package/build/utils/telemetry.js +2 -1
  31. package/build/utils/utils.js +23 -0
  32. package/package.json +1 -1
package/build/index.js CHANGED
@@ -35,6 +35,7 @@ import { registerAnalysisResources } from "./resources/analysisResources.js";
35
35
  import { registerProgressResource } from "./resources/progressResource.js";
36
36
  import { AnalyticsService } from "./services/AnalyticsService.js";
37
37
  import { registerInitTriggerOnMCPInitialized } from "./utils/initAgent.js";
38
+ import { isTestbotEnabled } from "./utils/featureFlags.js";
38
39
  import { registerPlaywrightTools, registerTraceRecordingPrompt, getPlaywrightTraceService, } from "./playwright/index.js";
39
40
  const oneClickEnabled = process.env.SKYRAMP_FEATURE_ONE_CLICK === "1";
40
41
  const oneClickInstructions = oneClickEnabled
@@ -95,8 +96,8 @@ After \`skyramp_analyze_changes\`, inspect enriched data via MCP Resources (use
95
96
  Before calling ANY test generation tool, you MUST follow this flow:
96
97
 
97
98
  1. **Read** the .skyramp/workspace.yml file to get the configured defaults.
98
- 2. **Extract** the \`language\`, \`framework\`, \`outputDir\`, \`api.baseUrl\`, \`api.authHeader\`, and \`api.authType\` from the services section.
99
- 3. **Use those values** as defaults for the test generation tool call. Do NOT ask the user for these values if they are already configured in the workspace file.
99
+ 2. **Extract** the \`language\`, \`framework\`, \`testDirectory\`, \`api.baseUrl\`, \`api.authHeader\`, and \`api.authType\` from the matching service in the services section.
100
+ 3. **Use those values** as defaults for the test generation tool call. Pass the service \`testDirectory\` as the generation tool \`outputDir\`. Do NOT ask the user for these values if they are already configured in the workspace file.
100
101
  4. **CRITICAL — endpointURL**: The \`endpointURL\` parameter MUST be the full URL to the specific endpoint being tested, NOT just the base URL. Construct it by combining \`api.baseUrl\` with the endpoint path. Example: if \`api.baseUrl\` is \`http://localhost:8000\` and the endpoint is \`/api/v1/products\`, pass \`endpointURL: "http://localhost:8000/api/v1/products"\`. NEVER pass just the base URL (e.g. \`http://localhost:8000\`) as \`endpointURL\`.
101
102
  5. **CRITICAL — scenario generation**: When calling \`skyramp_batch_scenario_test_generation\`, ALWAYS pass:
102
103
  - \`baseURL\`: The full base URL from \`api.baseUrl\` (e.g., \`http://localhost:3000\`). This determines the scheme, host, and port in the generated trace. Without it, the trace defaults to https:443 which is almost always wrong for local development.
@@ -107,7 +108,7 @@ Before calling ANY test generation tool, you MUST follow this flow:
107
108
  6. **CRITICAL — integration test from scenario**: When calling \`skyramp_integration_test_generation\` with a \`scenarioFile\`:
108
109
  - If workspace has \`api.authType\` set: omit auth params entirely — passing auth here alongside workspace \`authType\` causes "${AUTH_CONFLICT_ERROR_MSG}".
109
110
  - If workspace has no \`api.authType\`: pass \`authHeader\` only (no \`authScheme\`).
110
- 7. **If the workspace file does not exist**, or the needed values (language, framework, outputDir) are missing from the workspace config, ASK the user which language and framework they want before calling the tool.
111
+ 7. **If the workspace file does not exist**, or the needed values (language, framework, testDirectory) are missing from the workspace config, ASK the user which language, framework, and outputDir they want before calling the tool.
111
112
  8. The user can always override workspace defaults by explicitly specifying values in their request.
112
113
  `,
113
114
  });
@@ -118,7 +119,7 @@ const prompts = [
118
119
  registerRecommendTestsPrompt,
119
120
  registerTraceRecordingPrompt,
120
121
  ];
121
- if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
122
+ if (isTestbotEnabled()) {
122
123
  prompts.push(registerTestbotPrompt);
123
124
  registerTestbotResource(server);
124
125
  logger.info("TestBot prompt enabled via SKYRAMP_FEATURE_TESTBOT");
@@ -169,7 +170,7 @@ const infrastructureTools = [
169
170
  registerTraceTool,
170
171
  registerTraceStopTool,
171
172
  ];
172
- if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
173
+ if (isTestbotEnabled()) {
173
174
  infrastructureTools.push(registerSubmitReportTool);
174
175
  logger.info("TestBot tools enabled via SKYRAMP_FEATURE_TESTBOT");
175
176
  }
@@ -77,11 +77,15 @@ Create one service entry per deployable unit. You MUST include:
77
77
  - \`framework\` — \`playwright\` | \`pytest\` | \`robot\` | \`junit\`
78
78
  Detect from: pytest.ini/playwright.config/jest.config/junit in pom.xml
79
79
  MUST match the language: python → pytest or robot | typescript/javascript → playwright | java → junit
80
- - \`testDirectory\` — path relative to repo root where generated tests will be placed. **MUST match the test framework's configured test directory**:
81
- - **Playwright**: Read \`playwright.config.ts\` (or \`.js\`/\`.mjs\`) and extract the \`testDir\` value. If no \`testDir\` is specified, common defaults: "tests/", "test/".
82
- - **pytest**: Read \`pytest.ini\`, \`pyproject.toml [tool.pytest.ini_options]\`, or \`setup.cfg [tool:pytest]\` for \`testpaths\`. Common defaults: "tests/", "test/".
83
- - **JUnit**: Usually "src/test/java" check \`pom.xml\` or \`build.gradle\` for custom test source directories.
84
- ⚠️ **CRITICAL**: If the framework config specifies a test directory, you MUST use that exact path
80
+ - testDirectory — stable path relative to repo root where generated tests for this service will be placed.
81
+ - For each service, use the test directory configured by that service's test framework when one is discoverable:
82
+ - Playwright: Read playwright.config.ts (or .js/.mjs) and extract the testDir value.
83
+ - pytest: Read pytest.ini, pyproject.toml [tool.pytest.ini_options], or setup.cfg [tool:pytest] for testpaths.
84
+ - JUnit: Usually src/test/java check pom.xml or build.gradle for custom test source directories.
85
+ - If no framework-configured test directory is available, use the Skyramp deterministic fallback:
86
+ - Single generated-test service: set testDirectory to tests/.
87
+ - Multiple generated-test services: set testDirectory to tests/<serviceName>, where <serviceName> is the exact serviceName with path separators and whitespace replaced by -.
88
+ Framework config precedence: If framework config specifies a test directory, use that exact path. Use the Skyramp deterministic fallback only when no framework-configured test directory is available.
85
89
 
86
90
  **API fields:**
87
91
  - \`api.schemaPath\` — path or URL to OpenAPI/Protobuf/GraphQL schema
@@ -154,12 +158,12 @@ Create one service entry per deployable unit. You MUST include:
154
158
 
155
159
  Before calling \`skyramp_init_workspace\`, confirm all of the following:
156
160
  - ALWAYS SCAN REPO AND FIND SERVICES. A REPO SHOULD HAVE AT LEAST ONE SERVICE.
157
- - **CRITICAL**: ALL services are included — backend AND frontend. The workspace config is a complete registry of the entire repo, not just the service relevant to your current task. A fullstack or monorepo MUST have multiple services — if you found only one, re-scan every top-level directory before proceeding.
161
+ - CRITICAL: ALL services are included — backend AND frontend. The workspace config is a complete registry of the entire repo, not just the service relevant to your current task. A fullstack or monorepo MUST have multiple services — if you found only one, re-scan every top-level directory before proceeding.
158
162
  - Services NOT in docker-compose.yml (e.g. a frontend run with pnpm/npm locally) MUST still be included with runtime "local".
159
163
  - Every service has \`api.baseUrl\` set to a valid, discoverable URL — localhost for local services, or the actual deployment URL for cloud/external services. Never fabricate a URL.
160
164
  - Every service with \`authType: apiKey\` has \`authHeader\` explicitly set to the actual custom header name (e.g. \`"X-API-Key"\`, \`"X-Admin-Key"\`). If you cannot find the header name in the source code, env vars, or README, do NOT use \`authType: apiKey\` — use \`authType: none\` and add a YAML comment explaining auth is unresolved.
161
165
  - \`framework\` matches \`language\` (python → pytest/robot | typescript/javascript → playwright | java → junit)
162
- - \`testDirectory\` matches the framework's config file (Playwright: \`testDir\` in playwright.config.ts | pytest: \`testpaths\` in pytest.ini/pyproject.toml | JUnit: test source dir in pom.xml/build.gradle). If no config file is found, use the common defaults: "tests/", "test/".
166
+ - \`testDirectory\` follows the stable resolution rules above: framework config file when present (Playwright: \`testDir\` in playwright.config.ts | pytest: \`testpaths\` in pytest.ini/pyproject.toml | JUnit: test source dir in pom.xml/build.gradle); otherwise the deterministic default (\`tests/\` for a single service, \`tests/<serviceName>\` for multiple services).
163
167
  - \`serverStartCommand\` matches \`runtime\`
164
168
  - For services in docker-compose.yml: runtime MUST be "docker" and command MUST be a docker command (e.g. "docker compose up -d <service-name>").
165
169
  - NEVER use application-level commands (uvicorn, npm, node, python, java, etc.) with runtime "docker".
@@ -1,3 +1,4 @@
1
+ import { isTestbotEnabled } from "../utils/featureFlags.js";
1
2
  /**
2
3
  * Skyramp personas injected into tool descriptions and prompts.
3
4
  *
@@ -19,5 +20,5 @@ export const SKYRAMP_QA_PERSONA = `You are acting as a Skyramp QA Automation Eng
19
20
  * avoid duplicating it in every tool description.
20
21
  */
21
22
  export function getPersonaPrefix() {
22
- return process.env.SKYRAMP_FEATURE_TESTBOT ? '' : `${SKYRAMP_QA_PERSONA}\n\n`;
23
+ return isTestbotEnabled() ? '' : `${SKYRAMP_QA_PERSONA}\n\n`;
23
24
  }
@@ -74,8 +74,9 @@ ${candidateFilesSection}`;
74
74
  if (inlineMode) {
75
75
  // Testbot inline mode: all maintenance logic lives here so the testbot
76
76
  // prompt only orchestrates steps without duplicating rules.
77
+ // No persona statement here — the outer testbot prompt already establishes
78
+ // the agent's context; a nested identity statement causes role confusion.
77
79
  return `<drift_analysis_rules>
78
- You are acting as a Skyramp Integration Architect.
79
80
  For this maintenance step: assess each existing test against the diff returned by \`skyramp_analyze_changes\` and apply the correct action (IGNORE, UPDATE, REGENERATE, or DELETE) directly — no separate analysis step.
80
81
 
81
82
  ${buildActionDecisionMatrix()}
@@ -1,4 +1,32 @@
1
1
  import { buildDriftAnalysisPrompt } from "./drift-analysis-prompt.js";
2
+ describe("buildDriftAnalysisPrompt - inline mode (no stateFile)", () => {
3
+ function inlinePrompt() {
4
+ return buildDriftAnalysisPrompt({
5
+ existingTests: [],
6
+ scannedEndpoints: [],
7
+ repositoryPath: "/repo",
8
+ // stateFile omitted → inline mode
9
+ });
10
+ }
11
+ it("wraps inline rules in drift_analysis_rules XML tags", () => {
12
+ const prompt = inlinePrompt();
13
+ expect(prompt).toContain("<drift_analysis_rules>");
14
+ expect(prompt).toContain("</drift_analysis_rules>");
15
+ });
16
+ it("does not contain the persona statement", () => {
17
+ const prompt = inlinePrompt();
18
+ expect(prompt).not.toContain("You are acting as a Skyramp Integration Architect");
19
+ });
20
+ it("does not contain the standalone Test Health Analysis header", () => {
21
+ const prompt = inlinePrompt();
22
+ expect(prompt).not.toContain("# Test Health Analysis");
23
+ });
24
+ it("does not contain the skyramp_actions CTA (that belongs to standalone mode)", () => {
25
+ const prompt = inlinePrompt();
26
+ // Inline mode final step directs applying changes directly, not calling skyramp_actions
27
+ expect(prompt).not.toContain("call `skyramp_actions`");
28
+ });
29
+ });
2
30
  describe("buildDriftAnalysisPrompt - scanned endpoints rendering", () => {
3
31
  // Reproduces the [object Object] bug: skeletonEndpoints from analyzeChangesTool
4
32
  // stores methods as objects { method: string, ... }, not plain strings.
@@ -12,12 +12,22 @@ const FRONTEND_EXT = /\.(tsx?|jsx?|vue|svelte|css|scss|less|html|svg)$/i;
12
12
  * Returned as an empty string when no router context is available.
13
13
  */
14
14
  function buildPathResolutionTableStep(p) {
15
- if (!p.routerMountContext.length || p.wsSchemaPath)
16
- return "";
17
- return `### Step 1.5: Build path resolution table
18
- The **Routing entry-point files** section above lists the files to read.
19
-
20
- **Read each of those files** and trace every router mount call to understand nesting the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
15
+ // Case A: spec was fetched successfully — instruct LLM to validate paths against it
16
+ if (p.wsSchemaPath && p.specFetchSucceeded) {
17
+ return `### Step 1.5: Validate all endpoint paths against the OpenAPI spec
18
+ Fetch \`${p.wsSchemaPath}\` and extract all keys from \`spec.paths\`.
19
+ **Before placing any path in a tool call**, confirm it exists in that list.
20
+ If a path is NOT in the spec **and it did not come from the PR diff**, find the correct spelling by matching resource name do NOT use it unverified.
21
+ Paths the PR explicitly added or modified may not yet appear in the spec (spec lag) — treat those as valid.
22
+ `;
23
+ }
24
+ // Case B: no spec (or spec unreachable) but router mount context available
25
+ if (p.routerMountContext.length) {
26
+ const hasInlined = (p.routerFileContents?.length ?? 0) > 0;
27
+ return `### Step 1.5: Build path resolution table
28
+ ${hasInlined
29
+ ? "The **Routing entry-point files** section above contains the inlined file contents — use them directly to trace every router mount call"
30
+ : "The **Routing entry-point files** section above lists the files to read.\n\n**Read each of those files** and trace every router mount call"} to understand nesting — the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
21
31
  - Python (FastAPI/Flask): \`parent.include_router(child, prefix="...")\`, \`app.register_blueprint(...)\`
22
32
  - JS/TS (Express/Fastify/Hapi): \`app.use('/path', childRouter)\`, \`router.use('/path', sub)\`
23
33
  - NestJS: \`@Module({ imports: [FeatureModule] })\` — trace the module import chain; each \`@Controller('prefix')\` contributes a segment
@@ -33,6 +43,20 @@ Chain all segments from the app root down through every intermediate mount to ea
33
43
 
34
44
  **This table is authoritative.** Before placing any URL in a tool call, look up the source file. If the pre-built catalog shows a different path, use the table value.
35
45
 
46
+ `;
47
+ }
48
+ // Case C: no spec AND no router context — source-verify fallback
49
+ // Note: also fires when a spec was configured (wsSchemaPath set) but could not be
50
+ // fetched at analysis time (specFetchSucceeded = false). When that happens the LLM
51
+ // should know a spec was expected so it can be extra-skeptical about path correctness.
52
+ const specFailedNote = p.wsSchemaPath && !p.specFetchSucceeded
53
+ ? `\n> ⚠️ A spec was configured (\`${p.wsSchemaPath}\`) but could not be loaded at analysis time — treat all paths as unverified until confirmed against source.`
54
+ : "";
55
+ return `### Step 1.5: Verify endpoint paths from source files
56
+ The endpoint catalog below was produced by static regex analysis and is **unverified**.
57
+ Before using any path in a tool call, read the route definition file identified in the "Source" column and confirm the path string exactly.
58
+ Pay special attention to mount prefixes — a router at \`/api/v1\` + route \`/version\` → path is \`/api/v1/version\`, not \`/api/server-version\`.
59
+ ${specFailedNote}
36
60
  `;
37
61
  }
38
62
  // Inline note added to any step where the LLM reads Java source files. Java Spring
@@ -125,6 +149,33 @@ No diff was available — read the changed source files listed above directly to
125
149
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
126
150
  For each endpoint found: note the HTTP method, full path, and source file.
127
151
  Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.`;
152
+ // Step 2.3: Caller-tracing instruction — only emitted when the PR touches backend code
153
+ // files that contain no route annotations (utilities, helpers, services). Tells the LLM
154
+ // to search for callers of the changed functions to find the actual HTTP surface
155
+ // rather than falling back to the proximity-scanned CRUD endpoints. (Bug 5 fix)
156
+ //
157
+ // We filter out:
158
+ // - Frontend component files (.jsx/.tsx/.vue/.svelte) — UI changes have no callers
159
+ // in the HTTP graph; emitting this block for them produces irrelevant instructions.
160
+ // - Non-code files (docs, config, assets, lockfiles) — they have no "changed symbols"
161
+ // to trace and listing them as bullets is misleading.
162
+ const BACKEND_CODE_EXT = /\.(ts|js|mjs|cjs|py|java|kt|rb|go|cs|php|rs|scala|swift|c|cpp|h|hpp)$/i;
163
+ const traceableUnmatched = (p.unmatchedFiles ?? []).filter(f => BACKEND_CODE_EXT.test(f));
164
+ const callerTracingStep = isDiffScope && !isUIOnly && traceableUnmatched.length > 0
165
+ ? `
166
+ ### Step 2.3: Trace callers of changed non-route files
167
+ The following changed files contain **no HTTP endpoint registrations** (no route annotations, controller mappings, or handler decorators). Their changes will only be tested if you find and target the HTTP endpoints that *call* them:
168
+
169
+ ${traceableUnmatched.map(f => `- \`${f}\``).join("\n")}
170
+
171
+ For each file above:
172
+ 1. **Find the changed symbols** — read the diff (or the file) to identify which functions, methods, or classes were modified.
173
+ 2. **Search for callers** — look for import statements and call sites of those symbols across service, handler, and controller files. Use fully qualified names (e.g. \`DataUtils.addFileData\`, not just \`addFileData\`) to avoid false matches in large monorepos.
174
+ 3. **Trace to HTTP registration** — from each caller, follow up to the route/controller registration (Spring \`@PostMapping\`, Express \`router.post\`, FastAPI \`@router.post\`, etc.) to identify the endpoint(s) that invoke the changed logic.
175
+ 4. **Augment the endpoint list** from Step 2 with these execution-path endpoints.
176
+ 5. If an execution or processing endpoint is found (path ending in \`/execute\`, \`/run\`, \`/trigger\`, \`/process\`, \`/invoke\`, or similar), it **MUST** be included in the test candidates. Do not produce coverage consisting solely of CRUD endpoints when an execution-path endpoint was found — CRUD tests may still be included but must not be the only coverage.
177
+ `
178
+ : "";
128
179
  const criticalPatternStep = `### Step 2.5: Identify critical patterns for test categorization
129
180
  Look for these patterns in model/schema/handler files to inform test recommendations:
130
181
  - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations
@@ -168,22 +219,29 @@ Call \`skyramp_recommend_tests\` with:
168
219
  ### Step 1: Read the changed files and diff
169
220
  ${changedFiles}${diffFileRef}
170
221
  ${buildPathResolutionTableStep(p)}${step2}
171
-
222
+ ${callerTracingStep}
172
223
  ${criticalPatternStep}
173
224
 
174
225
  ${step3Content}`;
175
226
  }
176
227
  export function buildAnalysisOutputText(p) {
177
228
  const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
178
- // Router mounting context is unique to this prompt (not in recommendationPrompt).
179
- // Branch diff, endpoint catalog, auth config, and OpenAPI spec are omitted here
180
- // because they are already present in the recommendation prompt that is
181
- // concatenated in the same tool response.
182
- const routerSection = !p.wsSchemaPath && p.routerMountContext.length
229
+ // Router mounting context is unique to this prompt; shown whenever mount context
230
+ // is available, regardless of whether a spec is configured.
231
+ const routerSection = p.routerMountContext.length
183
232
  ? `
184
233
  ## Routing entry-point files
185
- Read these in Step 1.5 to trace the full router/module hierarchy:
186
- ${p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
234
+ ${p.routerFileContents?.length
235
+ ? p.routerFileContents.map(({ file, content }) => `### \`${file}\`\n\`\`\`\n${content}\n\`\`\``)
236
+ .join("\n\n") + (p.routerMountContext.length > (p.routerFileContents?.length ?? 0)
237
+ ? `\n\nAdditional files (too large to inline — read manually if needed):\n` +
238
+ p.routerMountContext
239
+ .filter(f => !(p.routerFileContents ?? []).some(r => r.file === f))
240
+ .map(f => `- \`${f}\``)
241
+ .join("\n")
242
+ : "")
243
+ : `Read these in Step 1.5 to trace the full router/module hierarchy:\n` +
244
+ p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
187
245
  : "";
188
246
  const enrichment = buildEnrichmentInstructions(p);
189
247
  return `# Repository Analysis
@@ -0,0 +1,154 @@
1
+ jest.mock("@skyramp/skyramp", () => ({
2
+ WorkspaceConfigManager: { create: jest.fn() },
3
+ }));
4
+ import { buildAnalysisOutputText } from "./analysisOutputPrompt.js";
5
+ import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
6
+ // ---------------------------------------------------------------------------
7
+ // Minimal fixture factory
8
+ // ---------------------------------------------------------------------------
9
+ function baseParams(overrides = {}) {
10
+ return {
11
+ sessionId: "test-session-id",
12
+ repositoryPath: "/repo",
13
+ analysisScope: AnalysisScope.CurrentBranchDiff,
14
+ scannedEndpoints: [],
15
+ wsBaseUrl: "http://localhost:3000",
16
+ wsAuthHeader: "Authorization",
17
+ wsAuthType: "",
18
+ wsSchemaPath: "",
19
+ routerMountContext: [],
20
+ parsedDiff: {
21
+ changedFiles: [],
22
+ newEndpoints: [],
23
+ modifiedEndpoints: [],
24
+ },
25
+ ...overrides,
26
+ };
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Step 2.3 caller-tracing block
30
+ // ---------------------------------------------------------------------------
31
+ describe("buildAnalysisOutputText — unmatchedFiles / Step 2.3 caller-tracing", () => {
32
+ it("includes Step 2.3 block when unmatchedFiles is non-empty and scope is CurrentBranchDiff", () => {
33
+ const params = baseParams({
34
+ unmatchedFiles: [
35
+ "server/src/main/java/helpers/DataUtils.java",
36
+ "server/src/main/java/helpers/MustacheHelper.java",
37
+ ],
38
+ });
39
+ const output = buildAnalysisOutputText(params);
40
+ expect(output).toContain("### Step 2.3: Trace callers of changed non-route files");
41
+ expect(output).toContain("DataUtils.java");
42
+ expect(output).toContain("MustacheHelper.java");
43
+ expect(output).toContain("/execute");
44
+ });
45
+ it("lists each unmatched file as a bullet in the Step 2.3 block", () => {
46
+ const params = baseParams({
47
+ unmatchedFiles: ["src/services/OrderService.ts", "src/utils/pricingHelper.ts"],
48
+ });
49
+ const output = buildAnalysisOutputText(params);
50
+ expect(output).toContain("- `src/services/OrderService.ts`");
51
+ expect(output).toContain("- `src/utils/pricingHelper.ts`");
52
+ });
53
+ it("omits Step 2.3 block when unmatchedFiles is empty", () => {
54
+ const params = baseParams({ unmatchedFiles: [] });
55
+ const output = buildAnalysisOutputText(params);
56
+ expect(output).not.toContain("Step 2.3");
57
+ expect(output).not.toContain("Trace callers of changed non-route files");
58
+ });
59
+ it("omits Step 2.3 block when unmatchedFiles is undefined", () => {
60
+ const params = baseParams({ unmatchedFiles: undefined });
61
+ const output = buildAnalysisOutputText(params);
62
+ expect(output).not.toContain("Step 2.3");
63
+ });
64
+ it("omits Step 2.3 block when scope is full_repo even if unmatchedFiles is non-empty", () => {
65
+ const params = baseParams({
66
+ analysisScope: AnalysisScope.FullRepo,
67
+ unmatchedFiles: ["src/services/SomeService.ts"],
68
+ });
69
+ const output = buildAnalysisOutputText(params);
70
+ expect(output).not.toContain("Step 2.3");
71
+ });
72
+ it("Step 2.3 appears before Step 2.5 in the output", () => {
73
+ const params = baseParams({
74
+ unmatchedFiles: ["src/utils/helper.ts"],
75
+ });
76
+ const output = buildAnalysisOutputText(params);
77
+ const pos23 = output.indexOf("Step 2.3");
78
+ const pos25 = output.indexOf("Step 2.5");
79
+ expect(pos23).toBeGreaterThan(-1);
80
+ expect(pos25).toBeGreaterThan(-1);
81
+ expect(pos23).toBeLessThan(pos25);
82
+ });
83
+ it("Step 2.5 critical-patterns block is always present regardless of unmatchedFiles", () => {
84
+ const withUnmatched = buildAnalysisOutputText(baseParams({ unmatchedFiles: ["src/utils/foo.ts"] }));
85
+ const withoutUnmatched = buildAnalysisOutputText(baseParams({ unmatchedFiles: [] }));
86
+ expect(withUnmatched).toContain("Step 2.5: Identify critical patterns");
87
+ expect(withoutUnmatched).toContain("Step 2.5: Identify critical patterns");
88
+ });
89
+ it("omits Step 2.3 block when unmatchedFiles contains only frontend component files (UI-only PR)", () => {
90
+ // Frontend files (.tsx, .jsx, .vue, .svelte) end up in unmatchedFiles because they
91
+ // have no route annotations, but they have no HTTP callers to trace — emitting
92
+ // Step 2.3 for them would produce irrelevant instructions. (Copilot review fix)
93
+ const params = baseParams({
94
+ unmatchedFiles: [
95
+ "src/components/Button.tsx",
96
+ "src/pages/Dashboard.jsx",
97
+ "src/views/UserProfile.vue",
98
+ "src/routes/Settings.svelte",
99
+ ],
100
+ });
101
+ const output = buildAnalysisOutputText(params);
102
+ expect(output).not.toContain("Step 2.3");
103
+ expect(output).not.toContain("Trace callers of changed non-route files");
104
+ });
105
+ it("omits Step 2.3 block when unmatchedFiles contains only non-code files (docs/config)", () => {
106
+ // README.md, package.json, etc. have no changed symbols to trace — listing them
107
+ // in Step 2.3 is misleading. (Copilot review fix)
108
+ const params = baseParams({
109
+ unmatchedFiles: [
110
+ "README.md",
111
+ "package.json",
112
+ "docker-compose.yml",
113
+ ".github/workflows/ci.yml",
114
+ ],
115
+ });
116
+ const output = buildAnalysisOutputText(params);
117
+ expect(output).not.toContain("Step 2.3");
118
+ expect(output).not.toContain("Trace callers of changed non-route files");
119
+ });
120
+ it("emits Step 2.3 for backend code files but excludes frontend/non-code siblings", () => {
121
+ // Mixed PR: one Java helper + one React component + one config file.
122
+ // Only the Java file should appear in the Step 2.3 bullets.
123
+ const params = baseParams({
124
+ unmatchedFiles: [
125
+ "server/helpers/DataUtils.java",
126
+ "client/components/ActionButton.tsx",
127
+ "package.json",
128
+ ],
129
+ });
130
+ const output = buildAnalysisOutputText(params);
131
+ expect(output).toContain("Step 2.3");
132
+ expect(output).toContain("DataUtils.java");
133
+ expect(output).not.toContain("ActionButton.tsx");
134
+ expect(output).not.toContain("package.json");
135
+ });
136
+ it("omits Step 2.3 when unmatchedFiles contains .ts/.js frontend files but isUIOnly is true", () => {
137
+ // Angular services, React hooks, Vue composables — all .ts/.js — pass the
138
+ // BACKEND_CODE_EXT filter but belong to a UI-only PR. The !isUIOnly guard
139
+ // prevents Step 2.3 from emitting contradictory caller-tracing instructions
140
+ // alongside the UI-only Step 2 guidance. (Copilot review fix)
141
+ const params = baseParams({
142
+ // parsedDiff.changedFiles drives isUIOnly detection; all frontend-ext → isUIOnly=true
143
+ parsedDiff: {
144
+ changedFiles: ["src/services/auth.service.ts", "src/hooks/useAuth.ts"],
145
+ newEndpoints: [],
146
+ modifiedEndpoints: [],
147
+ },
148
+ unmatchedFiles: ["src/services/auth.service.ts", "src/hooks/useAuth.ts"],
149
+ });
150
+ const output = buildAnalysisOutputText(params);
151
+ expect(output).not.toContain("Step 2.3");
152
+ expect(output).not.toContain("Trace callers of changed non-route files");
153
+ });
154
+ });
@@ -1,7 +1,9 @@
1
1
  import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
2
+ import { resolveServiceDetailsRef } from "../../utils/utils.js";
2
3
  import { WorkspaceAuthType, getAuthScheme, isAuthorizationHeaderName, AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
3
- // Cached at module-load — the flag is process-wide and cannot change per call.
4
+ // Cached at module-load — flags are process-wide and cannot change per call.
4
5
  const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
6
+ const SERVICE_REFS = resolveServiceDetailsRef();
5
7
  export const MAX_TESTS_TO_GENERATE = 3;
6
8
  export const MAX_RECOMMENDATIONS = 20;
7
9
  export const MAX_CRITICAL_TESTS = 3;
@@ -356,7 +358,7 @@ Only provider-side contract tests are supported. Pass \`providerMode: true\` for
356
358
  3. Interact using \`browser_click\`, \`browser_type\`, \`browser_fill_form\`, etc.
357
359
  4. \`browser_snapshot\` after each interaction that changes the page
358
360
  5. \`skyramp_export_zip\` with an **absolute** output path: \`<repositoryPath>/.skyramp/<test_name>_trace.zip\`
359
- 6. \`skyramp_ui_test_generation\` with \`playwrightInput\` = the **absolute** path of the exported zip, and \`outputDir\` = the **frontend** service's \`testDirectory\` from workspace.yml (e.g. \`frontend/tests\`). Do NOT use the backend service's testDirectory — UI tests must go in the frontend service's test directory.
361
+ 6. \`skyramp_ui_test_generation\` with \`playwrightInput\` = the **absolute** path of the exported zip, and \`outputDir\` = ${SERVICE_REFS.frontendTestDirRef} (e.g. \`frontend/tests\`). Do NOT use the backend service's testDirectory — UI tests must go in the frontend service's test directory.
360
362
 
361
363
  Tips: For custom dropdowns (Radix, MUI): click combobox → snapshot → click option (NOT \`browser_select_option\`).
362
364
 
@@ -4,6 +4,7 @@ import { logger } from "../../utils/logger.js";
4
4
  import { buildRecommendationPrompt } from "./test-recommendation-prompt.js";
5
5
  import { ScenarioSource, AnalysisScope } from "../../types/RepositoryAnalysis.js";
6
6
  import { SCENARIO_CATEGORIES } from "../../types/TestRecommendation.js";
7
+ import { inferExpectedStatus } from "../../utils/httpDefaults.js";
7
8
  export function mergeEnrichedScenarios(serverScenarios, raw) {
8
9
  const rejectionNotes = [];
9
10
  let parsed;
@@ -55,10 +56,7 @@ export function mergeEnrichedScenarios(serverScenarios, raw) {
55
56
  queryParams: st.queryParams,
56
57
  responseBody: st.responseBody,
57
58
  // Default status code by method if omitted to avoid `statusCode: undefined` in tool calls
58
- expectedStatusCode: st.expectedStatusCode ??
59
- (String(st.method ?? "").toUpperCase() === "POST" ? 201
60
- : String(st.method ?? "").toUpperCase() === "DELETE" ? 204
61
- : 200),
59
+ expectedStatusCode: st.expectedStatusCode ?? inferExpectedStatus(String(st.method ?? "GET")),
62
60
  expectedResponseFields: st.expectedResponseFields,
63
61
  bodyMustInclude: st.bodyMustInclude,
64
62
  chainsFrom: st.chainsFrom,
@@ -153,11 +151,29 @@ export function registerRecommendTestsPrompt(server) {
153
151
  }
154
152
  }
155
153
  if (!fullAnalysis) {
154
+ if (sessionId) {
155
+ logger.warning(`Session not found in memory (sessionId=${sessionId}) — server may have restarted; falling back to state file`);
156
+ }
156
157
  fullAnalysis = state.repositoryAnalysis.fullAnalysis;
157
158
  }
158
159
  if (!fullAnalysis) {
159
160
  throw new Error(`Analysis data for session not found in memory or on disk. Re-run skyramp_analyze_changes.`);
160
161
  }
162
+ // Hydrate testLocations from the disk-persisted field when fullAnalysis came from disk
163
+ // (after a server restart, fullAnalysis is loaded from state.repositoryAnalysis.fullAnalysis
164
+ // but testLocations was persisted separately under state.repositoryAnalysis.testLocations)
165
+ if (fullAnalysis.existingTests &&
166
+ !fullAnalysis.existingTests.testLocations &&
167
+ state.repositoryAnalysis.testLocations) {
168
+ fullAnalysis = {
169
+ ...fullAnalysis,
170
+ existingTests: {
171
+ ...fullAnalysis.existingTests,
172
+ testLocations: state.repositoryAnalysis.testLocations,
173
+ },
174
+ };
175
+ logger.debug("Hydrated existingTests.testLocations from disk-persisted state", { sessionId });
176
+ }
161
177
  // Normalize legacy state files: before AnalysisScope enum normalization, state stored
162
178
  // the user-facing param value "branch_diff". Map it explicitly so diff-mode detection
163
179
  // works correctly on state created before this deployment (2-hour TTL window).
@@ -6,6 +6,9 @@ import { extractResourceFromPath } from "../../utils/routeParsers.js";
6
6
  import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildTestPatternGuidelines, buildTestQualityCriteria, buildFewShotExamples, buildVerificationChecklist, buildGenerationRules, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, } from "./recommendationSections.js";
7
7
  import { CATEGORY_PRIORITY, TEST_CATEGORIES } from "../../types/TestRecommendation.js";
8
8
  import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
9
+ import { resolveServiceDetailsRef } from "../../utils/utils.js";
10
+ // Cached at module-load — flag is process-wide and cannot change per call.
11
+ const SERVICE_REFS = resolveServiceDetailsRef();
9
12
  function formatTestLocations(locs) {
10
13
  const entries = Object.entries(locs || {});
11
14
  if (entries.length === 0)
@@ -448,7 +451,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
448
451
  ? (`**#${rank} — GENERATE** | ui | workflow | new\n` +
449
452
  `Scenario: ui-test-from-trace-${rank} (rename from the actual changed component/flow)\n` +
450
453
  `Validates: UI interactions for a changed frontend component or flow.\n\n` +
451
- `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend service testDirectory from workspace.yml e.g. frontend/tests>" })\``)
454
+ `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`)
452
455
  : (`**#${rank} — GENERATE** | ui | workflow | new\n` +
453
456
  `Scenario: ui-test-for-changed-component-${rank} (rename from the actual changed component/flow)\n` +
454
457
  `Validates: UI interactions for changed frontend component/flow ${rank}.\n\n` +
@@ -457,7 +460,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
457
460
  ` 2. Interact with the changed component (read the diff to identify which component changed and what interactions it supports)\n` +
458
461
  ` 3. \`browser_snapshot()\` after each key interaction\n` +
459
462
  ` 4. \`skyramp_export_zip({ outputPath: "${zipPath}" })\` — absolute path\n` +
460
- ` 5. \`skyramp_ui_test_generation({ playwrightInput: "${zipPath}", outputDir: "<frontend service testDirectory from workspace.yml e.g. frontend/tests>" })\`\n\n` +
463
+ ` 5. \`skyramp_ui_test_generation({ playwrightInput: "${zipPath}", outputDir: "<frontend_output_dir>" })\` set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}\n\n` +
461
464
  `Each item must target a distinct changed component or user flow.`);
462
465
  }).join("\n\n")
463
466
  : "";
@@ -469,7 +472,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
469
472
  ? (`**#${uiRank} — GENERATE** | ui | workflow | new\n` +
470
473
  `Scenario: ui-test-for-changed-components (rename from the actual changed component/flow)\n` +
471
474
  `Validates: UI interactions for the changed frontend components in this PR.\n\n` +
472
- `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend service testDirectory from workspace.yml e.g. frontend/tests>" })\``)
475
+ `**Tool**: \`skyramp_ui_test_generation({ playwrightInput: "<discovered_trace_file_path>", outputDir: "<frontend_output_dir>" })\` set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}`)
473
476
  : (`**#${uiRank} — GENERATE** | ui | workflow | new\n` +
474
477
  `Scenario: ui-test-for-changed-components (rename from the actual changed component/flow)\n` +
475
478
  `Validates: UI interactions for the changed frontend components in this PR.\n\n` +
@@ -478,7 +481,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
478
481
  ` 2. Interact with the changed component (read the diff to identify which component changed and what interactions it supports)\n` +
479
482
  ` 3. \`browser_snapshot()\` after each key interaction\n` +
480
483
  ` 4. \`skyramp_export_zip({ outputPath: "<repositoryPath>/.skyramp/ui_mixed_pr_trace.zip" })\` — absolute path\n` +
481
- ` 5. \`skyramp_ui_test_generation({ playwrightInput: "<repositoryPath>/.skyramp/ui_mixed_pr_trace.zip", outputDir: "<frontend service testDirectory from workspace.yml e.g. frontend/tests>" })\`\n\n` +
484
+ ` 5. \`skyramp_ui_test_generation({ playwrightInput: "<repositoryPath>/.skyramp/ui_mixed_pr_trace.zip", outputDir: "<frontend_output_dir>" })\` set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}\n\n` +
482
485
  `Derive scenario name and steps from the actual changed frontend files.`)
483
486
  : "";
484
487
  const generateBlocks = generateItems.map((item, i) => {
@@ -571,7 +574,7 @@ function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, auth
571
574
  const uiGuidance = !isUIOnlyPR ? `
572
575
  **UI/E2E tests (add per your Budget Plan):** If your Budget Plan requires UI/E2E items beyond what is already in your GENERATE list, append an [ADDITIONAL] entry for each. If a UI test already occupies a GENERATE slot above, that slot satisfies your UI/E2E generate count — do NOT add it again to ADDITIONAL. Tool workflow for each new item:
573
576
  - **E2E**: ${hasTraces ? "Use discovered trace/recording files with `skyramp_e2e_test_generation`." : "Add to additionalRecommendations with a note that both a backend API trace (`skyramp_start_trace_collection` / `skyramp_stop_trace_collection`) and a browser Playwright recording must be collected in a live environment first. Do NOT attempt `skyramp_e2e_test_generation` without both traces present."}
574
- - **UI**: ${hasTraces ? "Use an existing Playwright `.zip` trace with `skyramp_ui_test_generation`." : "Record a trace using `browser_navigate` + `browser_snapshot` + `skyramp_export_zip`, then call `skyramp_ui_test_generation({ playwrightInput: \"<zip_path>\", outputDir: \"<frontend testDirectory from workspace.yml>\" })`."}
577
+ - **UI**: ${hasTraces ? "Use an existing Playwright `.zip` trace with `skyramp_ui_test_generation`." : `Record a trace using \`browser_navigate\` + \`browser_snapshot\` + \`skyramp_export_zip\`, then call \`skyramp_ui_test_generation({ playwrightInput: "<zip_path>", outputDir: "<frontend_output_dir>" })\` set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}.`}
575
578
  Derive scenario names and steps from the actual changed frontend files. If your Budget Plan calls for 0% UI/E2E, omit this entirely.` : "";
576
579
  const supplementNote = `\n**If your Budget Plan total exceeds the pre-ranked items listed above:** draft additional tests from source-code enrichment (Step 1). For each new or changed endpoint, identify boundary or variation scenarios — formula parameters, search/filter constraints, required field validation. Only after exhausting PR-specific scenarios, add generic patterns (auth boundary → 401, non-existent ID → 404). Do NOT supplement with tests whose endpoint + test type match a GENERATE item.`;
577
580
  // ── PR / branch-diff mode: execution plan ────────────────────────────────
@@ -753,7 +756,7 @@ Output should be concise and immediately actionable.`
753
756
  changedLines.push(` ${m.method} ${ep.path} [removed]`);
754
757
  }
755
758
  }
756
- endpointLines = `**Changed in this PR:**\n${changedLines.join("\n") || " none"}\n\n**Other endpoints (reference only — do not prioritize for testing):**\n${otherLines.join("\n") || " none"}`;
759
+ endpointLines = `**Likely changed in this PR (from static file→endpoint mapping — verify against diff in Step 2):**\n${changedLines.join("\n") || " none"}\n\n**Other endpoints (reference only):**\n${otherLines.join("\n") || " none"}`;
757
760
  }
758
761
  else {
759
762
  endpointLines = allEndpoints
@@ -826,7 +829,7 @@ Framework: ${analysis.projectClassification.primaryFramework} (${analysis.projec
826
829
  Project type: ${analysis.projectClassification.projectType}
827
830
  Auth: ${authMethod} (header: ${authHeaderValue}${authTypeValue ? `, type: ${authTypeValue}` : ""})
828
831
  Base URL: ${analysis.apiEndpoints.baseUrl}
829
- Endpoints (${analysis.apiEndpoints.totalCount}):
832
+ Candidate endpoints from static scan — unverified, confirm paths against spec or source before use (${analysis.apiEndpoints.totalCount}):
830
833
  ${endpointLines}${testFingerprint}
831
834
  `.trim();
832
835
  // ── Branch diff ──
@@ -847,7 +850,7 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
847
850
 
848
851
  Focus on tests that validate these changes and how they interact with existing resources.
849
852
  For removed endpoints: verify they now return 404 or the appropriate deprecation status code.
850
- Allocate your test budget to endpoints listed under "Changed in this PR". Use other endpoints only as setup steps (e.g. creating a resource before testing its deletion).
853
+ Allocate your test budget to endpoints listed under "Likely changed in this PR". Use other endpoints only as setup steps (e.g. creating a resource before testing its deletion).
851
854
  `;
852
855
  }
853
856
  // ── Interactions ──
@@ -934,9 +934,9 @@ describe("buildRecommendationPrompt — multi-method endpoint partitioning", ()
934
934
  });
935
935
  const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
936
936
  // Both GET and POST for /api/products should be in "Changed in this PR"
937
- expect(prompt).toContain("Changed in this PR");
938
- expect(prompt).toMatch(/Changed in this PR:[\s\S]*GET \/api\/products/);
939
- expect(prompt).toMatch(/Changed in this PR:[\s\S]*POST \/api\/products/);
937
+ expect(prompt).toContain("Likely changed in this PR");
938
+ expect(prompt).toMatch(/Likely changed in this PR[\s\S]*GET \/api\/products/);
939
+ expect(prompt).toMatch(/Likely changed in this PR[\s\S]*POST \/api\/products/);
940
940
  // /api/items should NOT be in changed section
941
941
  expect(prompt).toMatch(/Other endpoints[\s\S]*GET \/api\/items/);
942
942
  });
@@ -983,8 +983,8 @@ describe("buildRecommendationPrompt — multi-method endpoint partitioning", ()
983
983
  });
984
984
  const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
985
985
  // Both products and orders should be in changed section
986
- expect(prompt).toMatch(/Changed in this PR:[\s\S]*GET \/api\/products/);
987
- expect(prompt).toMatch(/Changed in this PR:[\s\S]*POST \/api\/orders/);
986
+ expect(prompt).toMatch(/Likely changed in this PR[\s\S]*GET \/api\/products/);
987
+ expect(prompt).toMatch(/Likely changed in this PR[\s\S]*POST \/api\/orders/);
988
988
  });
989
989
  });
990
990
  // ---------------------------------------------------------------------------
@@ -1021,7 +1021,7 @@ describe("buildRecommendationPrompt — removed endpoint listing", () => {
1021
1021
  });
1022
1022
  const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1023
1023
  expect(prompt).toContain("DELETE /api/legacy [removed]");
1024
- expect(prompt).toContain("Changed in this PR");
1024
+ expect(prompt).toContain("Likely changed in this PR");
1025
1025
  });
1026
1026
  });
1027
1027
  // ---------------------------------------------------------------------------