@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.
- package/build/playwright/traceRecordingPrompt.js +30 -36
- package/build/prompts/architectPersona.js +19 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +42 -50
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +125 -0
- package/build/prompts/test-recommendation/recommendationSections.js +121 -4
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +151 -9
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +416 -61
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +455 -63
- package/build/prompts/testbot/testbot-prompts.js +111 -100
- package/build/prompts/testbot/testbot-prompts.test.js +142 -0
- package/build/resources/analysisResources.js +13 -5
- package/build/services/ScenarioGenerationService.js +2 -2
- package/build/services/ScenarioGenerationService.test.js +35 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/executeSkyrampTestTool.js +4 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +51 -21
- package/build/tools/generate-tests/generateContractRestTool.js +26 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
- package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
- package/build/tools/generate-tests/generateUIRestTool.js +69 -4
- package/build/tools/submitReportTool.js +27 -13
- package/build/tools/test-management/analyzeChangesTool.js +32 -10
- package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
- package/build/types/RepositoryAnalysis.js +25 -3
- package/build/types/TestRecommendation.js +5 -4
- package/build/types/TestTypes.js +44 -9
- package/build/utils/AnalysisStateManager.js +43 -9
- package/build/utils/AnalysisStateManager.test.js +35 -0
- package/build/utils/routeParsers.js +35 -0
- package/build/utils/routeParsers.test.js +66 -1
- package/build/utils/scenarioDrafting.js +207 -360
- package/build/utils/scenarioDrafting.test.js +191 -256
- package/build/utils/trace-parser.js +24 -6
- package/build/utils/trace-parser.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
- package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
- package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
- package/package.json +2 -2
- 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
|
|
18
|
-
|
|
19
|
-
You
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
- After
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
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
|
-
|
|
48
|
-
- \`type: "text"\` — verify element contains expected text
|
|
49
|
-
- \`type: "value"\` — verify input field has expected value
|
|
50
|
-
|
|
51
|
-
###
|
|
52
|
-
- Do NOT
|
|
53
|
-
- Do NOT
|
|
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 sessions — always 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) =>
|
|
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
|
|
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 ===
|
|
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:
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
**Resolve nested
|
|
20
|
-
|
|
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
|
|
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 ===
|
|
85
|
-
|
|
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
|
-
${
|
|
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
|
+
});
|