@skyramp/mcp 0.0.65 → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/build/playwright/traceRecordingPrompt.js +30 -36
  2. package/build/prompts/architectPersona.js +19 -0
  3. package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
  5. package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
  6. package/build/prompts/test-recommendation/analysisOutputPrompt.js +42 -50
  7. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +125 -0
  8. package/build/prompts/test-recommendation/recommendationSections.js +121 -4
  9. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +151 -9
  10. package/build/prompts/test-recommendation/test-recommendation-prompt.js +416 -61
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +455 -63
  12. package/build/prompts/testbot/testbot-prompts.js +111 -100
  13. package/build/prompts/testbot/testbot-prompts.test.js +142 -0
  14. package/build/resources/analysisResources.js +13 -5
  15. package/build/services/ScenarioGenerationService.js +2 -2
  16. package/build/services/ScenarioGenerationService.test.js +35 -0
  17. package/build/services/TestExecutionService.js +1 -1
  18. package/build/tools/code-refactor/modularizationTool.js +2 -2
  19. package/build/tools/executeSkyrampTestTool.js +4 -3
  20. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +51 -21
  21. package/build/tools/generate-tests/generateContractRestTool.js +26 -4
  22. package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
  23. package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
  24. package/build/tools/generate-tests/generateUIRestTool.js +69 -4
  25. package/build/tools/submitReportTool.js +27 -13
  26. package/build/tools/test-management/analyzeChangesTool.js +32 -10
  27. package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
  28. package/build/types/RepositoryAnalysis.js +25 -3
  29. package/build/types/TestRecommendation.js +5 -4
  30. package/build/types/TestTypes.js +44 -9
  31. package/build/utils/AnalysisStateManager.js +43 -9
  32. package/build/utils/AnalysisStateManager.test.js +35 -0
  33. package/build/utils/routeParsers.js +35 -0
  34. package/build/utils/routeParsers.test.js +66 -1
  35. package/build/utils/scenarioDrafting.js +207 -360
  36. package/build/utils/scenarioDrafting.test.js +191 -256
  37. package/build/utils/trace-parser.js +24 -6
  38. package/build/utils/trace-parser.test.js +140 -0
  39. package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
  40. package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
  41. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
  42. package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
  43. package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
  44. package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
  45. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  46. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
  47. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
  48. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
  49. package/package.json +2 -2
  50. package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
@@ -14,44 +14,38 @@ export function registerTraceRecordingPrompt(server) {
14
14
  role: "user",
15
15
  content: {
16
16
  type: "text",
17
- text: `## Skyramp Trace Recording & UI Test Generation
18
-
19
- You have access to Playwright browser tools that let you interact with web applications.
20
- Use these tools to record a trace of browser interactions, then generate a Skyramp UI test from that trace.
21
-
22
- ### Flow
23
-
24
- 1. **Navigate**: ALWAYS call \`browser_navigate\` with the target URL as the very first step, even if the browser seems to already be on that page. This ensures a clean state.
25
- 2. **Understand the page**: Call \`browser_snapshot\` to see the current page state (ARIA tree).
26
- 3. **Interact**: Use \`browser_click\`, \`browser_type\`, \`browser_select_option\`, etc. to perform the user interactions described in the prompt.
27
- 4. **Repeat steps 2-3** until all interactions are complete. Assertions are automatically added at strategic points during export.
28
- 5. **Export the trace**: Call \`skyramp_export_zip\` with an output path (e.g. \`skyramp_export.zip\`). This produces a zip containing the JSONL trace and HAR network recording. Assertions are auto-injected based on API calls detected in the HAR.
29
- 6. **Generate the test**: Call \`skyramp_ui_test_generation\` with \`playwrightInput\` set to the absolute path of the zip file from step 5.
30
-
31
- ### Tips
32
- - **To type into a field**: Just use \`browser_type\` it automatically clears the field and types the new value. Do NOT press Ctrl+A or any keyboard shortcuts before typing.
33
- - If a \`browser_click\` or \`browser_type\` fails because the element reference is stale (page updated), call \`browser_snapshot\` to refresh the page state and retry.
34
- - Use \`browser_snapshot\` liberally — it helps you understand what elements are available.
35
- - The trace automatically deduplicates retries: if you navigate back to the start URL and redo steps, only the last complete attempt is exported.
36
- - After generating the test, the tool will suggest running \`skyramp_modularization\` for code quality.
37
- - **Dropdown/Select components**: For custom dropdowns (Radix, MUI, etc.) that show as \`combobox\` in the snapshot, do NOT use \`browser_select_option\` it only works on native \`<select>\` elements. Instead: (1) click the combobox to open the dropdown, (2) call \`browser_snapshot\` to see the options in a \`listbox\`, (3) click the desired \`option\`. This three-step pattern is required for all custom dropdown components.
38
- - **Always take a snapshot after each interaction** that changes the page (click, form submit, navigation) to see the updated state before proceeding.
39
-
40
- ### Critical rules for clicking
41
- - **NEVER click container/wrapper divs** (e.g. elements with "container" in their test-id). Always click the actual interactive element inside: a \`button\`, \`link\`, or \`input\`.
42
- - When the snapshot shows a container with a button inside, click the **button**, not the container. For example, if you see \`div "add-order-products-container" > button "Add"\`, click the button "Add", not the container.
43
- - To submit forms, click the submit \`button\` (e.g. "Add Order", "Submit"), never the form container.
44
- - After selecting a product from a dropdown, click the "Add" button to confirm, not the surrounding container.
17
+ text: `## Skyramp UI Test Recording
18
+
19
+ You are a Skyramp Integration Architect. Your role is to record browser interactions with zero hallucination: every action must be grounded in what \`browser_snapshot\` returns. If an element is not visible in the snapshot, do not interact with it.
20
+
21
+ ### Required workflow
22
+
23
+ Before starting, output a \`<thinking>\` block that maps each step of the user's intent to the specific browser interactions required. Do not call any tool until this mapping is complete.
24
+
25
+ Then execute in strict order:
26
+
27
+ 1. **Navigate**: Call \`browser_navigate\` with the target URL. Always do this first, even if the browser appears to be on the correct page.
28
+ 2. **Snapshot**: Call \`browser_snapshot\` to get the current ARIA tree and element refs.
29
+ 3. **Interact**: Call the appropriate tool (\`browser_click\`, \`browser_type\`, \`browser_hover\`, etc.) using refs from the snapshot.
30
+ 4. **Repeat steps 2–3** for each user action until all steps are complete.
31
+ 5. **Export**: Call \`skyramp_export_zip\` with \`outputPath\` set to the absolute zip path (same directory and base name as the test file, replacing \`.spec.ts\` with \`.zip\`). Do NOT ask the user first — call it automatically.
32
+ 6. **Generate**: Call \`skyramp_ui_test_generation\` with \`playwrightInput\` set to the absolute zip path from step 5.
33
+
34
+ ### Cross-tool rules
35
+
36
+ - **After every action that changes the page**, call \`browser_snapshot\` before the next interaction refs become stale after navigation, clicks that trigger page updates, and form submissions.
37
+ - **Iframe content** appears inline in the snapshot — interact with those elements using their refs normally.
38
+ - **Trace deduplication**: if you retry from the start URL, only the last complete attempt is exported.
39
+ - **After generating the test**, run \`skyramp_modularization\` for code quality.
45
40
 
46
41
  ### Assertions
47
- If the user requests assertions, you MUST call \`browser_assert\` at the appropriate points. Always provide the \`expected\` value.
48
- - \`type: "text"\` — verify element contains expected text (e.g., product name appears after creation)
49
- - \`type: "value"\` — verify input field has expected value (e.g., price field shows "29.99")
50
-
51
- ### Important
52
- - Do NOT ask the user before calling \`skyramp_export_zip\` call it automatically as the final step.
53
- - Do NOT write JSONL or HAR files manuallythe export tool handles everything.
54
- - Do NOT reuse existing zip files from previous sessions — always record fresh.
42
+ Call \`browser_assert\` when the user requests verification. Always provide the \`expected\` value.
43
+ - \`type: "text"\` — verify an element contains expected text
44
+ - \`type: "value"\` — verify an input field has an expected value
45
+
46
+ ### Constraints
47
+ - Do NOT write JSONL or HAR files manually — \`skyramp_export_zip\` handles everything.
48
+ - Do NOT reuse zip files from previous sessionsalways record fresh.
55
49
  `,
56
50
  },
57
51
  },
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Skyramp Integration Architect persona injected into generation tool descriptions.
3
+ *
4
+ * In TestBot environments (ENABLE_SKYRAMP_TESTBOT=true), the persona is injected
5
+ * once as a system prompt via `claude --system-prompt` rather than repeating it in
6
+ * every tool description. In that case this string is omitted from the tool description
7
+ * to avoid wasting context tokens.
8
+ *
9
+ * In IDE/MCP-direct environments, it is included in each tool description so the
10
+ * model has the role context available without a separate system prompt.
11
+ */
12
+ export const SKYRAMP_ARCHITECT_PERSONA = `You are acting as a Skyramp Integration Architect. Your responsibility is to map the user's test intent to the Skyramp generation spec with precision. No guessing — derive all parameters from the codebase, workspace config, and provided context only.`;
13
+ /**
14
+ * Returns the persona prefix for use in tool descriptions.
15
+ * Returns an empty string when running inside TestBot (persona is injected via system prompt instead).
16
+ */
17
+ export function getPersonaPrefix() {
18
+ return process.env.ENABLE_SKYRAMP_TESTBOT ? '' : `${SKYRAMP_ARCHITECT_PERSONA}\n\n`;
19
+ }
@@ -30,7 +30,16 @@ No existing Skyramp tests found in repository.
30
30
  `;
31
31
  const scannedSection = scannedEndpoints.length > 0
32
32
  ? `## Scanned Endpoints (${scannedEndpoints.length})
33
- ${scannedEndpoints.map((ep) => `- ${Array.isArray(ep.methods) ? ep.methods.join("|") : ep.method} ${ep.path}`).join("\n")}
33
+ ${scannedEndpoints.map((ep) => {
34
+ let methods;
35
+ if (Array.isArray(ep.methods)) {
36
+ methods = ep.methods.map((m) => (typeof m === "string" ? m : m.method)).join("|");
37
+ }
38
+ else {
39
+ methods = ep.method;
40
+ }
41
+ return `- ${methods} ${ep.path}`;
42
+ }).join("\n")}
34
43
  `
35
44
  : "";
36
45
  // In inline mode (testbot), skip the context header — existing tests and diff
@@ -70,9 +79,5 @@ ${buildUpdateExecutionRules()}
70
79
 
71
80
  ${buildAddRecommendationGuidelines()}
72
81
 
73
- ${buildDriftOutputChecklist(existingTests.length, newEndpointCount, inlineMode)}
74
-
75
- After completing the assessment above, call \`skyramp_actions\` with \`stateFile: "${stateFile}"\`
76
-
77
- **CRITICAL**: Do NOT create any .json or .md files. Only call skyramp_actions when done.`;
82
+ ${buildDriftOutputChecklist(existingTests.length, newEndpointCount, inlineMode, stateFile)}`;
78
83
  }
@@ -0,0 +1,49 @@
1
+ import { buildDriftAnalysisPrompt } from "./drift-analysis-prompt.js";
2
+ describe("buildDriftAnalysisPrompt - scanned endpoints rendering", () => {
3
+ // Reproduces the [object Object] bug: skeletonEndpoints from analyzeChangesTool
4
+ // stores methods as objects { method: string, ... }, not plain strings.
5
+ const skeletonMethodObjects = [
6
+ {
7
+ path: "/api/v1/",
8
+ methods: [{ method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "main.py", interactions: [] }],
9
+ resourceGroup: "v1",
10
+ pathParams: [],
11
+ },
12
+ {
13
+ path: "/api/v1/orders",
14
+ methods: [
15
+ { method: "GET", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
16
+ { method: "POST", description: "", queryParams: [], authRequired: true, sourceFile: "orders.py", interactions: [] },
17
+ ],
18
+ resourceGroup: "orders",
19
+ pathParams: [],
20
+ },
21
+ ];
22
+ it("renders HTTP methods as strings, not [object Object]", () => {
23
+ const prompt = buildDriftAnalysisPrompt({
24
+ existingTests: [],
25
+ scannedEndpoints: skeletonMethodObjects,
26
+ repositoryPath: "/repo",
27
+ stateFile: "/tmp/state.json",
28
+ });
29
+ expect(prompt).not.toContain("[object Object]");
30
+ expect(prompt).toContain("GET /api/v1/");
31
+ expect(prompt).toContain("GET|POST /api/v1/orders");
32
+ // CTA should appear exactly once (not duplicated)
33
+ const ctaCount = (prompt.match(/call `skyramp_actions`/g) || []).length;
34
+ expect(ctaCount).toBe(1);
35
+ });
36
+ it("also works with plain string methods (ScannedEndpoint format)", () => {
37
+ const stringMethods = [
38
+ { path: "/api/v1/products", methods: ["GET", "POST"], sourceFile: "products.py" },
39
+ ];
40
+ const prompt = buildDriftAnalysisPrompt({
41
+ existingTests: [],
42
+ scannedEndpoints: stringMethods,
43
+ repositoryPath: "/repo",
44
+ stateFile: "/tmp/state.json",
45
+ });
46
+ expect(prompt).not.toContain("[object Object]");
47
+ expect(prompt).toContain("GET|POST /api/v1/products");
48
+ });
49
+ });
@@ -163,12 +163,14 @@ Apply to **new test functions you are adding** and **existing functions that cov
163
163
 
164
164
  ${ENHANCE_ASSERTIONS_FOR_INTEGRATION_AND_CONTRACTPROVIDER}`;
165
165
  }
166
- export function buildDriftOutputChecklist(existingTestCount, newEndpointCount, inlineMode = false) {
166
+ export function buildDriftOutputChecklist(existingTestCount, newEndpointCount, inlineMode = false, stateFile) {
167
167
  const finalStep = inlineMode
168
168
  ? `### Final step
169
169
  Apply all maintenance actions (UPDATE / REGENERATE / DELETE) directly by editing the test files. New test generation (ADD) is handled separately in the next step.`
170
170
  : `### Final step
171
- After completing all assessments above, call \`skyramp_actions\` with the stateFile to execute the recommended changes.`;
171
+ After completing all assessments above, call \`skyramp_actions\` with \`stateFile: "${stateFile}"\` to execute the recommended changes.
172
+
173
+ **CRITICAL**: Do NOT create any .json or .md files. Only call skyramp_actions when done.`;
172
174
  // In inline mode, existing test counts are unknown at prompt-build time —
173
175
  // they come from skyramp_analyze_changes at runtime. Skip the count headers.
174
176
  const existingTestSection = inlineMode
@@ -1,27 +1,32 @@
1
+ import { AnalysisScope } from "../../types/RepositoryAnalysis.js";
1
2
  function buildEnrichmentInstructions(p) {
2
- const isDiffScope = p.analysisScope === "current_branch_diff";
3
+ const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
3
4
  const useHealthFlow = p.nextTool === "skyramp_analyze_test_health";
4
5
  if (!isDiffScope) {
5
6
  const nextStep = useHealthFlow
6
7
  ? `### Step 3: Identify tests at risk of drift
7
8
  Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\``
8
- : `### Step 3: Call recommend tests
9
- Call \`skyramp_recommend_tests\` with \`sessionId: "${p.sessionId}"\``;
10
- return `## Your Task — Enrich & Recommend (full repo)
9
+ : `### Step 3: Present the catalog
10
+ The ranked test recommendation catalog is pre-built and shown below (after the separator line).
11
+
12
+ **Your only job is to present it.**
13
+
14
+ 1. Fill in every \`<…from source>\` placeholder using the field names, computed formulas, and auth details you found in Steps 1–2.
15
+ 2. Output the completed catalog **exactly as formatted — grouped by test type (### E2E / ### UI / ### Integration / ### Contract)**. Do NOT restructure, reorder, rename sections, or generate a new format.
16
+ 3. Do NOT call any Skyramp generation tools. The catalog shows ready-to-use tool calls that can be executed on demand.
17
+
18
+ **If** Steps 1–2 revealed additional scenarios the catalog does not cover (e.g. a computed formula or FK relationship that was missed), you may optionally call \`skyramp_recommend_tests\` with \`stateFile: "${p.stateFile ?? p.sessionId}"\` and \`enrichedScenarios\` to regenerate a more complete catalog — but only after presenting the current one.`;
19
+ return `## Your Task — Fill in and Present the Catalog (full repo)
11
20
 
12
21
  ### Step 1: Read key files
13
- Read \`package.json\` / \`requirements.txt\`, \`docker-compose.yml\`, route/controller files,
14
- and model/schema files (Zod schemas, Pydantic models, TypeScript interfaces, DTOs)
15
- to understand the tech stack, endpoint shapes, auth mechanisms, and request/response schemas.
16
-
17
- ### Step 2: Identify resource relationships and parameter locations
18
- Map how endpoints relate to each other which POST creates resources consumed by other endpoints?
19
- **Resolve nested/sub-router paths** from the Router Mounting section above.
20
- **CRITICAL Distinguish query params vs request body:** For each endpoint, determine whether
21
- parameters are sent as URL query params (typical for GET search/filter/list) or request body
22
- (typical for POST/PUT/PATCH). Look at FastAPI \`Query()\` annotations, Express \`req.query\` usage,
23
- Spring \`@RequestParam\`, Flask \`request.args\`, etc. Populate \`queryParams\` in interactions
24
- for GET endpoints that accept search/filter/pagination parameters.
22
+ Read route/controller files and model/schema files (Pydantic models, Zod schemas, DTOs)
23
+ to find: required request body fields, computed response fields and formulas, auth middleware type, storage backend, and how sub-routers are mounted (cross-check against Router Mounting section above).
24
+
25
+ ### Step 2: Map cross-resource relationships and resolve endpoint paths
26
+ (Distinct from Step 1 — Step 1 reads individual schemas; Step 2 maps how endpoints relate to each other.)
27
+ For each endpoint: which POST creates resources consumed by other endpoints?
28
+ **Resolve nested paths** from the Router Mounting section — a router mounted at \`/products/{product_id}/reviews\` means \`GET /\` in that file is actually \`GET /api/v1/products/{product_id}/reviews\`.
29
+ For GET list endpoints: identify query params (\`limit\`, \`offset\`, \`order\`, \`orderBy\`) from framework annotations (FastAPI \`Query()\`, Express \`req.query\`, etc.).
25
30
 
26
31
  ${nextStep}`;
27
32
  }
@@ -67,8 +72,20 @@ Draft multi-step scenarios simulating realistic user workflows:
67
72
  response data verification, actual field names for chaining.
68
73
  **Parameter placement:** GET search/filter endpoints MUST use \`queryParams\`, not \`requestBody\`.
69
74
 
75
+ **No duplicate scenarios.** Each scenario must cover a distinct code path (unique method + path + expected status). Do NOT draft two scenarios that differ only in request body values but hit the same code path (e.g. discount=10% vs discount=25% — both succeed with 200, same logic). A negative-case variant with a different expected status (e.g. discount=-10% → 422) IS a distinct scenario — use a single-step contract test for it (see below).
76
+
77
+ **For each new or modified endpoint, ensure at least one error-path scenario is drafted** — a single-step contract test that triggers a specific error (404 for a missing resource ID, 422 for an invalid field value) that the source code explicitly handles. One auth-boundary scenario (missing auth → 401/403) is enough across all endpoints — do not repeat it per endpoint.
78
+
79
+ **For every scenario you draft, fill \`bugCatchingTarget\`** with the specific formula, constraint, or failure mode the test is designed to expose. Examples:
80
+ - \`"discount formula: total_amount = subtotal * (1 - discount_value / 100) — wrong if addition is used instead of subtraction"\`
81
+ - \`"items not recalculated after PATCH — total_amount stays at old value if collection update is ignored"\`
82
+ - \`"missing 404 guard on resource ID — returns 500 instead of 404 for unknown IDs"\`
83
+ This field is used at test generation time to compute exact assertion values. Leave it empty only if no specific formula or constraint applies.
84
+
70
85
  ### Step 4: Call recommend tests
71
- Call \`skyramp_recommend_tests\` with \`sessionId: "${p.sessionId}"\``;
86
+ Call \`skyramp_recommend_tests\` with:
87
+ - \`stateFile: "${p.stateFile}"\`
88
+ - \`enrichedScenarios\`: (optional) JSON array of your Step 3 scenarios — see the tool's inputSchema for the exact shape. Your enriched scenarios override server-side ones with the same \`scenarioName\` and are prioritized in ranking. Omit if you drafted nothing in Step 3.`;
72
89
  return `## Your Task — Enrich & Recommend (PR-scoped)
73
90
 
74
91
  ### Step 1: Read the changed files
@@ -81,39 +98,19 @@ ${criticalPatternStep}
81
98
  ${step3Content}`;
82
99
  }
83
100
  export function buildAnalysisOutputText(p) {
84
- const isDiffScope = p.analysisScope === "current_branch_diff";
85
- const diffSection = p.parsedDiff
101
+ const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
102
+ // Router mounting context is unique to this prompt (not in recommendationPrompt).
103
+ // Branch diff, endpoint catalog, auth config, and OpenAPI spec are omitted here
104
+ // because they are already present in the recommendation prompt that is
105
+ // concatenated in the same tool response.
106
+ const routerSection = !p.wsSchemaPath && p.routerMountContext
86
107
  ? `
87
- ## Branch Diff Context
88
- **Branch**: \`${p.parsedDiff.currentBranch}\` → base: \`${p.parsedDiff.baseBranch}\`
89
- **Changed Files** (${p.parsedDiff.changedFiles.length}): ${p.parsedDiff.changedFiles.join(", ")}
90
- **New Endpoints** (${p.parsedDiff.newEndpoints.length}): ${p.parsedDiff.newEndpoints.map((e) => `${e.method} ${e.path} (${e.sourceFile})`).join(", ") || "none"}
91
- **Modified Endpoints** (${p.parsedDiff.modifiedEndpoints.length}): ${p.parsedDiff.modifiedEndpoints.map((e) => `${e.method} ${e.path} (${e.sourceFile})`).join(", ") || "none"}
92
- **Affected Services**: ${p.parsedDiff.affectedServices.join(", ") || "none"}
93
- `
94
- : "";
95
- const endpointCatalog = p.scannedEndpoints.length > 0
96
- ? `
97
- ## Pre-Scanned Endpoint Catalog (${p.scannedEndpoints.length} routes)
98
- ${p.scannedEndpoints.map((ep) => ` ${ep.methods.join("|")} ${ep.path} (${ep.sourceFile})`).join("\n")}
99
- `
100
- : "";
101
- const wsLine = p.wsBaseUrl
102
- ? `**Base URL**: \`${p.wsBaseUrl}\`${p.wsAuthHeader ? ` | **Auth header**: \`${p.wsAuthHeader}\`` : ""}${p.wsAuthType ? ` | **Auth type**: \`${p.wsAuthType}\`` : ""}`
103
- : "";
104
- const specSection = p.wsSchemaPath
105
- ? `
106
- ## OpenAPI Spec Available
107
- Spec at \`${p.wsSchemaPath}\`. **Read it** for authoritative paths and schemas.
108
- Pass \`apiSchema: "${p.wsSchemaPath}"\` to ALL test generation tool calls.`
109
- : p.routerMountContext
110
- ? `
111
108
  ## Router Mounting / Nesting
112
109
  \`\`\`
113
110
  ${p.routerMountContext}
114
111
  \`\`\`
115
112
  Use this to resolve full URL paths for nested endpoints.`
116
- : "";
113
+ : "";
117
114
  const enrichment = buildEnrichmentInstructions(p);
118
115
  return `# Repository Analysis
119
116
 
@@ -121,12 +118,7 @@ Use this to resolve full URL paths for nested endpoints.`
121
118
  **Repository**: \`${p.repositoryPath}\`
122
119
  **Analysis Scope**: \`${p.analysisScope}\`
123
120
  ${isDiffScope ? `**Diff endpoints**: ${(p.parsedDiff?.newEndpoints.length ?? 0) + (p.parsedDiff?.modifiedEndpoints.length ?? 0)}` : `**Pre-scanned endpoints**: ${p.scannedEndpoints.length}`}
124
- ${wsLine}
125
- ${p.wsSchemaPath ? `**OpenAPI Spec**: \`${p.wsSchemaPath}\` (spec-based flow)` : "**Flow**: Code-scanning (may miss nesting)"}
126
-
127
- ${diffSection}
128
- ${endpointCatalog}
129
- ${specSection}
121
+ ${routerSection}
130
122
  ${enrichment}
131
123
 
132
124
  **CRITICAL**: No .json/.md file creation. Prioritize cross-resource workflows.`;
@@ -0,0 +1,125 @@
1
+ jest.mock("@skyramp/skyramp", () => ({ Skyramp: class {
2
+ } }));
3
+ import { mergeEnrichedScenarios } from "./registerRecommendTestsPrompt.js";
4
+ import { ScenarioSource } from "../../types/RepositoryAnalysis.js";
5
+ import { TestType } from "../../types/TestTypes.js";
6
+ function makeScenario(overrides = {}) {
7
+ return {
8
+ scenarioName: "base-scenario",
9
+ description: "base",
10
+ category: "crud",
11
+ priority: "medium",
12
+ steps: [{ order: 1, method: "GET", path: "/api/items", description: "list", interactionType: "success", expectedStatusCode: 200 }],
13
+ chainingKeys: [],
14
+ requiresAuth: true,
15
+ estimatedComplexity: "simple",
16
+ source: ScenarioSource.CodeInferred,
17
+ testType: TestType.CONTRACT,
18
+ ...overrides,
19
+ };
20
+ }
21
+ const VALID_STEP = { order: 1, method: "post", path: "/api/orders", expectedStatusCode: 201 };
22
+ describe("mergeEnrichedScenarios — happy path", () => {
23
+ it("merges a valid agent scenario into server scenarios", () => {
24
+ const server = [makeScenario({ scenarioName: "existing" })];
25
+ const raw = JSON.stringify([{
26
+ scenarioName: "new-orders-flow",
27
+ category: "business_rule",
28
+ steps: [VALID_STEP],
29
+ }]);
30
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios(server, raw);
31
+ expect(rejectionNotes).toHaveLength(0);
32
+ expect(scenarios.find(s => s.scenarioName === "new-orders-flow")).toBeDefined();
33
+ expect(scenarios.find(s => s.scenarioName === "existing")).toBeDefined();
34
+ expect(scenarios).toHaveLength(2);
35
+ });
36
+ it("overrides a server scenario when agent provides same scenarioName", () => {
37
+ const server = [makeScenario({ scenarioName: "orders-flow", description: "server version" })];
38
+ const raw = JSON.stringify([{
39
+ scenarioName: "orders-flow",
40
+ category: "business_rule",
41
+ description: "agent version",
42
+ steps: [VALID_STEP],
43
+ }]);
44
+ const { scenarios } = mergeEnrichedScenarios(server, raw);
45
+ expect(scenarios).toHaveLength(1);
46
+ expect(scenarios[0].description).toBe("agent version");
47
+ expect(scenarios[0].source).toBe("agent-enriched");
48
+ });
49
+ it("normalizes method to uppercase", () => {
50
+ const raw = JSON.stringify([{
51
+ scenarioName: "uppercase-test",
52
+ category: "crud",
53
+ steps: [{ order: 1, method: "post", path: "/api/items", expectedStatusCode: 201 }],
54
+ }]);
55
+ const { scenarios } = mergeEnrichedScenarios([], raw);
56
+ expect(scenarios[0].steps[0].method).toBe("POST");
57
+ });
58
+ it("preserves bugCatchingTarget when provided", () => {
59
+ const raw = JSON.stringify([{
60
+ scenarioName: "formula-test",
61
+ category: "business_rule",
62
+ bugCatchingTarget: "total = price * qty",
63
+ steps: [VALID_STEP],
64
+ }]);
65
+ const { scenarios } = mergeEnrichedScenarios([], raw);
66
+ expect(scenarios[0].bugCatchingTarget).toBe("total = price * qty");
67
+ });
68
+ it("falls back to server scenarios on empty agent array", () => {
69
+ const server = [makeScenario({ scenarioName: "server-only" })];
70
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios(server, "[]");
71
+ // Empty array → no agent scenarios, return server ones unchanged
72
+ expect(scenarios).toEqual(server);
73
+ expect(rejectionNotes).toHaveLength(0);
74
+ });
75
+ });
76
+ describe("mergeEnrichedScenarios — rejection cases", () => {
77
+ it("rejects scenario with missing scenarioName", () => {
78
+ const raw = JSON.stringify([{ category: "crud", steps: [VALID_STEP] }]);
79
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios([], raw);
80
+ expect(scenarios).toHaveLength(0);
81
+ expect(rejectionNotes[0]).toMatch(/missing scenarioName/);
82
+ });
83
+ it("rejects scenario with missing steps array", () => {
84
+ const raw = JSON.stringify([{ scenarioName: "no-steps", category: "crud" }]);
85
+ const { rejectionNotes } = mergeEnrichedScenarios([], raw);
86
+ expect(rejectionNotes[0]).toMatch(/missing or empty steps/);
87
+ });
88
+ it("rejects scenario with empty steps array", () => {
89
+ const raw = JSON.stringify([{ scenarioName: "empty-steps", category: "crud", steps: [] }]);
90
+ const { rejectionNotes } = mergeEnrichedScenarios([], raw);
91
+ expect(rejectionNotes[0]).toMatch(/missing or empty steps/);
92
+ });
93
+ it("rejects scenario with missing category", () => {
94
+ const raw = JSON.stringify([{ scenarioName: "no-cat", steps: [VALID_STEP] }]);
95
+ const { rejectionNotes } = mergeEnrichedScenarios([], raw);
96
+ expect(rejectionNotes[0]).toMatch(/missing category/);
97
+ });
98
+ it("rejects scenario with unknown category", () => {
99
+ const raw = JSON.stringify([{ scenarioName: "bad-cat", category: "not_a_real_category", steps: [VALID_STEP] }]);
100
+ const { rejectionNotes } = mergeEnrichedScenarios([], raw);
101
+ expect(rejectionNotes[0]).toMatch(/unknown category/);
102
+ });
103
+ it("falls back to server scenarios on invalid JSON", () => {
104
+ const server = [makeScenario()];
105
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios(server, "{ bad json");
106
+ expect(scenarios).toEqual(server);
107
+ expect(rejectionNotes[0]).toMatch(/invalid JSON/);
108
+ });
109
+ it("falls back to server scenarios when JSON is not an array", () => {
110
+ const server = [makeScenario()];
111
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios(server, JSON.stringify({ not: "array" }));
112
+ expect(scenarios).toEqual(server);
113
+ expect(rejectionNotes[0]).toMatch(/expected a JSON array/);
114
+ });
115
+ it("accepts valid scenarios and rejects invalid ones in the same batch", () => {
116
+ const raw = JSON.stringify([
117
+ { scenarioName: "valid-one", category: "crud", steps: [VALID_STEP] },
118
+ { category: "crud", steps: [VALID_STEP] }, // missing scenarioName
119
+ ]);
120
+ const { scenarios, rejectionNotes } = mergeEnrichedScenarios([], raw);
121
+ expect(scenarios).toHaveLength(1);
122
+ expect(scenarios[0].scenarioName).toBe("valid-one");
123
+ expect(rejectionNotes).toHaveLength(1);
124
+ });
125
+ });