@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.
- package/build/index.js +6 -5
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -7
- package/build/prompts/personas.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +72 -14
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
- package/build/prompts/test-recommendation/recommendationSections.js +4 -2
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +20 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -8
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -6
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +2 -2
- package/build/resources/analysisResources.js +1 -0
- package/build/services/ScenarioGenerationService.js +2 -1
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +123 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -9
- package/build/tools/generate-tests/generateContractRestTool.js +19 -19
- package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
- package/build/tools/generate-tests/generateUIRestTool.js +23 -8
- package/build/tools/test-management/analyzeChangesTool.js +218 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
- package/build/utils/featureFlags.js +7 -0
- package/build/utils/featureFlags.test.js +81 -0
- package/build/utils/httpDefaults.js +17 -0
- package/build/utils/httpDefaults.test.js +21 -0
- package/build/utils/scenarioDrafting.js +37 -15
- package/build/utils/scenarioDrafting.test.js +66 -0
- package/build/utils/telemetry.js +2 -1
- package/build/utils/utils.js +23 -0
- 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\`, \`
|
|
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,
|
|
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 (
|
|
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 (
|
|
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
|
-
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
-
|
|
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\`
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
179
|
-
//
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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 —
|
|
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\` =
|
|
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: "<
|
|
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: "<
|
|
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: "<
|
|
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: "<
|
|
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`." :
|
|
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 = `**
|
|
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
|
-
|
|
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 "
|
|
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("
|
|
938
|
-
expect(prompt).toMatch(/
|
|
939
|
-
expect(prompt).toMatch(/
|
|
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(/
|
|
987
|
-
expect(prompt).toMatch(/
|
|
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("
|
|
1024
|
+
expect(prompt).toContain("Likely changed in this PR");
|
|
1025
1025
|
});
|
|
1026
1026
|
});
|
|
1027
1027
|
// ---------------------------------------------------------------------------
|