@skyramp/mcp 0.1.4 → 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-maintenance/driftAnalysisSections.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +74 -16
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
- package/build/prompts/test-recommendation/recommendationSections.js +13 -43
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +19 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +158 -70
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +24 -117
- package/build/prompts/testbot/testbot-prompts.js +12 -18
- package/build/prompts/testbot/testbot-prompts.test.js +2 -2
- package/build/resources/analysisResources.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +127 -4
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -18
- 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 +222 -11
- package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
- package/build/types/TestRecommendation.js +0 -2
- package/build/utils/featureFlags.js +4 -22
- package/build/utils/featureFlags.test.js +81 -0
- package/build/utils/httpDefaults.js +6 -1
- package/build/utils/httpDefaults.test.js +21 -0
- package/build/utils/scenarioDrafting.js +511 -100
- package/build/utils/scenarioDrafting.test.js +545 -259
- package/build/utils/telemetry.js +2 -1
- package/build/utils/utils.js +23 -0
- package/package.json +1 -1
|
@@ -459,7 +459,7 @@ describe("buildRecommendationPrompt — maxGenerateOverride", () => {
|
|
|
459
459
|
expect(prompt).toContain("Test type mix — MANDATORY");
|
|
460
460
|
expect(prompt).toContain("Present up to 6 recommendations.");
|
|
461
461
|
});
|
|
462
|
-
it("full_repo mode
|
|
462
|
+
it("full_repo mode pre-allocates E2E and UI sections for full-stack repos", () => {
|
|
463
463
|
const fullStackAnalysis = minimalAnalysis({
|
|
464
464
|
projectClassification: {
|
|
465
465
|
projectType: "full-stack",
|
|
@@ -476,11 +476,11 @@ describe("buildRecommendationPrompt — maxGenerateOverride", () => {
|
|
|
476
476
|
},
|
|
477
477
|
});
|
|
478
478
|
const prompt = buildRecommendationPrompt(fullStackAnalysis, AnalysisScope.FullRepo, 10);
|
|
479
|
-
// E2E
|
|
480
|
-
|
|
479
|
+
// E2E and UI sections must be present even though scenarioDrafting only produces backend types
|
|
480
|
+
expect(prompt).toContain("### E2E");
|
|
481
|
+
expect(prompt).toContain("### UI");
|
|
481
482
|
expect(prompt).toContain("skyramp_e2e_test_generation");
|
|
482
483
|
expect(prompt).toContain("skyramp_ui_test_generation");
|
|
483
|
-
expect(prompt).toContain("Budget Plan");
|
|
484
484
|
// Backend sections should still be present
|
|
485
485
|
expect(prompt).toContain("### Integration");
|
|
486
486
|
});
|
|
@@ -689,7 +689,7 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
689
689
|
function makeScenario(name) {
|
|
690
690
|
return minimalScenario({ scenarioName: name, category: "new_endpoint" });
|
|
691
691
|
}
|
|
692
|
-
it("UI-only PR:
|
|
692
|
+
it("UI-only PR: all GENERATE slots are UI placeholders (no backend)", () => {
|
|
693
693
|
const analysis = minimalAnalysis({
|
|
694
694
|
businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
|
|
695
695
|
branchDiffContext: {
|
|
@@ -702,14 +702,15 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
702
702
|
},
|
|
703
703
|
});
|
|
704
704
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
705
|
-
//
|
|
706
|
-
expect(prompt).toContain("
|
|
705
|
+
// The GENERATE slots are the UI placeholder blocks — check their specific scenario names
|
|
706
|
+
expect(prompt).toContain("#1 — GENERATE** | ui | workflow | new");
|
|
707
|
+
expect(prompt).toContain("ui-test-for-changed-component-1");
|
|
708
|
+
expect(prompt).toContain("ui_test_1_trace.zip");
|
|
707
709
|
expect(prompt).toContain("skyramp_ui_test_generation");
|
|
708
|
-
|
|
709
|
-
// Each item must be distinct
|
|
710
|
+
// Each slot targets a distinct changed component/flow
|
|
710
711
|
expect(prompt).toContain("distinct changed component or user flow");
|
|
711
712
|
});
|
|
712
|
-
it("mixed PR:
|
|
713
|
+
it("mixed PR: last GENERATE slot is UI, preceding slots are backend scenarios", () => {
|
|
713
714
|
const scenarios = [
|
|
714
715
|
makeScenario("orders-create"),
|
|
715
716
|
makeScenario("orders-update"),
|
|
@@ -727,12 +728,14 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|
|
|
727
728
|
},
|
|
728
729
|
});
|
|
729
730
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
730
|
-
//
|
|
731
|
+
// Last GENERATE slot is UI (not E2E)
|
|
732
|
+
expect(prompt).toContain("— GENERATE** | ui");
|
|
733
|
+
expect(prompt).toContain("skyramp_ui_test_generation");
|
|
734
|
+
expect(prompt).not.toContain("— GENERATE** | e2e");
|
|
735
|
+
// At least one backend scenario in GENERATE (#1 or #2)
|
|
731
736
|
expect(prompt).toContain("#1 — GENERATE** | integration");
|
|
737
|
+
// Scenario name from the pre-ranked list (orders-create or orders-update)
|
|
732
738
|
expect(prompt).toContain("orders-create");
|
|
733
|
-
// UI/E2E guidance is present for the LLM to add per its Budget Plan
|
|
734
|
-
expect(prompt).toContain("UI/E2E tests (add per your Budget Plan)");
|
|
735
|
-
expect(prompt).toContain("skyramp_ui_test_generation");
|
|
736
739
|
});
|
|
737
740
|
it("backend-only PR: all GENERATE slots are backend scenarios (no E2E injection)", () => {
|
|
738
741
|
const scenarios = [makeScenario("items-create"), makeScenario("items-get"), makeScenario("items-delete")];
|
|
@@ -839,21 +842,12 @@ describe("buildRecommendationPrompt — Mandatory Reasoning Protocol", () => {
|
|
|
839
842
|
expect(protocol).toContain("requestBody");
|
|
840
843
|
expect(protocol).toContain("endpointURL");
|
|
841
844
|
expect(protocol).toContain("authHeader");
|
|
842
|
-
expect(protocol).toContain("
|
|
845
|
+
expect(protocol).toContain("FK path params");
|
|
843
846
|
});
|
|
844
847
|
it("reasoning protocol instructs to read source file when value cannot be sourced", () => {
|
|
845
848
|
const protocol = buildReasoningProtocol();
|
|
846
849
|
expect(protocol).toContain("read the relevant source file");
|
|
847
850
|
});
|
|
848
|
-
it("reasoning protocol includes Coverage Reasoning Block for all 3 PR types", () => {
|
|
849
|
-
const protocol = buildReasoningProtocol();
|
|
850
|
-
expect(protocol).toContain("Coverage Reasoning Block");
|
|
851
|
-
expect(protocol).toContain("backend-only PRs");
|
|
852
|
-
expect(protocol).toContain("frontend-only PRs");
|
|
853
|
-
expect(protocol).toContain("mixed (frontend + backend) PRs");
|
|
854
|
-
expect(protocol).toContain("All HTTP methods affected");
|
|
855
|
-
expect(protocol).toContain("Testable surfaces:");
|
|
856
|
-
});
|
|
857
851
|
});
|
|
858
852
|
// ---------------------------------------------------------------------------
|
|
859
853
|
// Tests — Context Fetching Guidance
|
|
@@ -940,9 +934,9 @@ describe("buildRecommendationPrompt — multi-method endpoint partitioning", ()
|
|
|
940
934
|
});
|
|
941
935
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
942
936
|
// Both GET and POST for /api/products should be in "Changed in this PR"
|
|
943
|
-
expect(prompt).toContain("
|
|
944
|
-
expect(prompt).toMatch(/
|
|
945
|
-
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/);
|
|
946
940
|
// /api/items should NOT be in changed section
|
|
947
941
|
expect(prompt).toMatch(/Other endpoints[\s\S]*GET \/api\/items/);
|
|
948
942
|
});
|
|
@@ -989,8 +983,8 @@ describe("buildRecommendationPrompt — multi-method endpoint partitioning", ()
|
|
|
989
983
|
});
|
|
990
984
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
991
985
|
// Both products and orders should be in changed section
|
|
992
|
-
expect(prompt).toMatch(/
|
|
993
|
-
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/);
|
|
994
988
|
});
|
|
995
989
|
});
|
|
996
990
|
// ---------------------------------------------------------------------------
|
|
@@ -1027,7 +1021,7 @@ describe("buildRecommendationPrompt — removed endpoint listing", () => {
|
|
|
1027
1021
|
});
|
|
1028
1022
|
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
|
|
1029
1023
|
expect(prompt).toContain("DELETE /api/legacy [removed]");
|
|
1030
|
-
expect(prompt).toContain("
|
|
1024
|
+
expect(prompt).toContain("Likely changed in this PR");
|
|
1031
1025
|
});
|
|
1032
1026
|
});
|
|
1033
1027
|
// ---------------------------------------------------------------------------
|
|
@@ -1518,90 +1512,3 @@ describe("externalDedupKey", () => {
|
|
|
1518
1512
|
expect(externalDedupKey(scenario)).toBe("POST::orders::contract");
|
|
1519
1513
|
});
|
|
1520
1514
|
});
|
|
1521
|
-
// ---------------------------------------------------------------------------
|
|
1522
|
-
// Tests — UI-only PR classification fix
|
|
1523
|
-
// ---------------------------------------------------------------------------
|
|
1524
|
-
describe("buildRecommendationPrompt — isUIOnlyPR classification", () => {
|
|
1525
|
-
it("does not classify as UI-only when backend service files changed but no endpoints detected", () => {
|
|
1526
|
-
const analysis = minimalAnalysis({
|
|
1527
|
-
branchDiffContext: {
|
|
1528
|
-
baseBranch: "main",
|
|
1529
|
-
currentBranch: "feature/field-rbac",
|
|
1530
|
-
changedFiles: [
|
|
1531
|
-
"api/src/services/items.ts",
|
|
1532
|
-
"api/src/services/permissions.ts",
|
|
1533
|
-
"api/src/middleware/validate-access.ts",
|
|
1534
|
-
"app/src/components/fields.vue",
|
|
1535
|
-
],
|
|
1536
|
-
newEndpoints: [],
|
|
1537
|
-
modifiedEndpoints: [],
|
|
1538
|
-
affectedServices: ["items", "permissions"],
|
|
1539
|
-
},
|
|
1540
|
-
});
|
|
1541
|
-
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 6);
|
|
1542
|
-
expect(prompt).toContain("Endpoint Discovery Required");
|
|
1543
|
-
expect(prompt).not.toContain("UI-only PR");
|
|
1544
|
-
expect(prompt).not.toContain("frontend-only PR — set **100% UI/E2E**");
|
|
1545
|
-
});
|
|
1546
|
-
it("correctly classifies as UI-only when only frontend files changed", () => {
|
|
1547
|
-
const analysis = minimalAnalysis({
|
|
1548
|
-
branchDiffContext: {
|
|
1549
|
-
baseBranch: "main",
|
|
1550
|
-
currentBranch: "feature/ui-tweak",
|
|
1551
|
-
changedFiles: [
|
|
1552
|
-
"app/src/components/fields.vue",
|
|
1553
|
-
"app/src/views/settings.vue",
|
|
1554
|
-
],
|
|
1555
|
-
newEndpoints: [],
|
|
1556
|
-
modifiedEndpoints: [],
|
|
1557
|
-
affectedServices: [],
|
|
1558
|
-
},
|
|
1559
|
-
});
|
|
1560
|
-
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 6);
|
|
1561
|
-
expect(prompt).toContain("UI-only PR");
|
|
1562
|
-
expect(prompt).not.toContain("Endpoint Discovery Required");
|
|
1563
|
-
});
|
|
1564
|
-
it("does not classify as UI-only when endpoints are directly detected", () => {
|
|
1565
|
-
const analysis = minimalAnalysis({
|
|
1566
|
-
branchDiffContext: {
|
|
1567
|
-
baseBranch: "main",
|
|
1568
|
-
currentBranch: "feature/new-route",
|
|
1569
|
-
changedFiles: [
|
|
1570
|
-
"api/src/routes/items.ts",
|
|
1571
|
-
"app/src/components/fields.vue",
|
|
1572
|
-
],
|
|
1573
|
-
newEndpoints: [{
|
|
1574
|
-
path: "/api/items",
|
|
1575
|
-
methods: [{ method: "POST", sourceFile: "api/src/routes/items.ts", interactionCount: 1 }],
|
|
1576
|
-
}],
|
|
1577
|
-
modifiedEndpoints: [],
|
|
1578
|
-
affectedServices: ["items"],
|
|
1579
|
-
},
|
|
1580
|
-
});
|
|
1581
|
-
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 6);
|
|
1582
|
-
expect(prompt).not.toContain("UI-only PR");
|
|
1583
|
-
expect(prompt).toContain("Mixed PR");
|
|
1584
|
-
});
|
|
1585
|
-
it("backend-only PR: no UI-only or Mixed classification in mode preamble", () => {
|
|
1586
|
-
const analysis = minimalAnalysis({
|
|
1587
|
-
branchDiffContext: {
|
|
1588
|
-
baseBranch: "main",
|
|
1589
|
-
currentBranch: "feature/add-endpoint",
|
|
1590
|
-
changedFiles: [
|
|
1591
|
-
"api/src/routes/orders.ts",
|
|
1592
|
-
"api/src/services/orders.ts",
|
|
1593
|
-
],
|
|
1594
|
-
newEndpoints: [{
|
|
1595
|
-
path: "/api/orders",
|
|
1596
|
-
methods: [{ method: "POST", sourceFile: "api/src/routes/orders.ts", interactionCount: 2 }],
|
|
1597
|
-
}],
|
|
1598
|
-
modifiedEndpoints: [],
|
|
1599
|
-
affectedServices: ["orders"],
|
|
1600
|
-
},
|
|
1601
|
-
});
|
|
1602
|
-
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 6);
|
|
1603
|
-
// Mode preamble should NOT label this as UI-only or Mixed
|
|
1604
|
-
expect(prompt).not.toContain("**UI-only PR**");
|
|
1605
|
-
expect(prompt).not.toContain("**Mixed PR**");
|
|
1606
|
-
});
|
|
1607
|
-
});
|
|
@@ -4,10 +4,12 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
4
4
|
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PARAM_UUID_GUIDANCE, AUTH_CONFLICT_ERROR_MSG, } from "../test-recommendation/recommendationSections.js";
|
|
5
5
|
import { buildDriftAnalysisPrompt } from "../test-maintenance/drift-analysis-prompt.js";
|
|
6
6
|
import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
|
|
7
|
-
import { isContractConsumerModeEnabled
|
|
7
|
+
import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
|
|
8
|
+
import { resolveServiceDetailsRef } from "../../utils/utils.js";
|
|
8
9
|
import { readWorkspaceConfigRaw } from "../../utils/workspaceAuth.js";
|
|
9
|
-
// Cached at module-load —
|
|
10
|
+
// Cached at module-load — flags are process-wide and cannot change per call.
|
|
10
11
|
const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
|
|
12
|
+
const SERVICE_REFS = resolveServiceDetailsRef();
|
|
11
13
|
// Mode-aware bullet block that appears inside the "How to generate each type"
|
|
12
14
|
// section. When consumer mode is disabled, only provider-mode guidance is
|
|
13
15
|
// surfaced so the agent never recommends or invokes consumer contract tests.
|
|
@@ -100,23 +102,16 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
100
102
|
Keep advancing until you have created exactly ${maxGenerate} new test files OR exhausted all candidates.
|
|
101
103
|
- Example: If enrichment reveals that sending \`discount_value\` without \`discount_type\` silently orphans the value (a concrete bug), complete all planned GENERATE items first, then generate this discovered scenario as an extra test and report it in \`newTestsCreated\`.
|
|
102
104
|
- Total generated: Follow the "Budget: N generate" line in the Execution Plan. Process every GENERATE-tagged item in order. Backfill from ADDITIONAL candidates (highest-ranked first) until \`newTestsCreated\` reaches ${maxGenerate} or all candidates are exhausted.
|
|
103
|
-
- **
|
|
104
|
-
|
|
105
|
-
1. PR-endpoint edge cases — error paths (404, 422), auth boundary (401/403), validation for endpoints changed in this PR
|
|
106
|
-
2. Same-resource alternative flows — different HTTP methods or state variations on the same resource
|
|
107
|
-
3. Cross-resource workflows involving a PR endpoint
|
|
108
|
-
4. UI/E2E tests per your Budget Plan's UI% allocation
|
|
109
|
-
5. Unrelated endpoint coverage — NEVER backfill with tests for endpoints or pages not touched by this PR unless ALL options 1–4 are exhausted
|
|
110
|
-
- **UI test generation** (only when Budget Plan allocates UI% > 0): Use \`browser_navigate\` to the app's base URL — if the app responds, record a trace and generate the test.
|
|
111
|
-
**Skip UI only if one of these conditions is met:**
|
|
105
|
+
- **UI test priority**: If the diff contains frontend/UI changes (e.g. \`.tsx\`, \`.jsx\`, \`.vue\`, \`.svelte\` files), you MUST attempt to generate at least one UI test. Use \`browser_navigate\` to the app's base URL — if the app responds, record a trace and generate the test.
|
|
106
|
+
**Skip only if one of these conditions is met:**
|
|
112
107
|
- **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
|
|
113
|
-
- **(b)
|
|
114
|
-
- **(c) Unintegrated non-route component** — the changed file is a leaf component (not a framework route/entrypoint) that has no integration point in the running app. To confirm:
|
|
108
|
+
- **(b) Unintegrated non-route component** — the changed file is a leaf component (not a framework route/entrypoint) that has no integration point in the running app. To confirm:
|
|
115
109
|
1. Grep for the component's exported name AND its module path/filename across all production source files (excluding \`*.test.*\`, \`*.spec.*\`, \`*.stories.*\`, \`__tests__/\` directories — only production code imports count).
|
|
116
110
|
2. If no production file imports, re-exports, or renders it, the component has no DOM node in the running app → unintegrated.
|
|
117
111
|
3. **Exception**: if the same PR also adds a route/page file (e.g. under Next.js \`pages/\` or \`app/\`) that imports the component, the route IS the integration point — test through it.
|
|
118
112
|
**Never** apply the unintegrated heuristic to framework route/entrypoint files themselves — those are always reachable by convention.
|
|
119
113
|
**Never** generate tests for unrelated pages as a substitute for an unintegrated component.
|
|
114
|
+
This rule takes priority over generating additional backend-only tests.
|
|
120
115
|
- **Always generate a test for critical bugs, even if it will fail.** When a GENERATE-tagged item targets a page or endpoint with a known bug, do NOT skip it because you expect the test to fail — a failing test that documents a bug is more valuable than a text-only description. This applies within the existing GENERATE budget; do not add extra tests beyond the plan.
|
|
121
116
|
- For UI rendering bugs: navigate to the broken page and add a \`browser_assert\` that verifies the page rendered its expected content (e.g. assert the page heading is visible). The assertion will fail on the broken page, which is the correct outcome — it documents the bug as a failing test.
|
|
122
117
|
- The assertion MUST target the broken page itself, not a different page that works. If \`/orders/{id}/edit\` crashes, assert on \`/orders/{id}/edit\` (e.g. "Edit Order" heading visible), NOT on \`/orders\`.
|
|
@@ -128,8 +123,8 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
128
123
|
- Critical-category tests are already ranked first by the pre-computed scores — follow the plan order.
|
|
129
124
|
|
|
130
125
|
**Auth — determine ONCE, apply to EVERY tool call:**
|
|
131
|
-
1. Read auth params from the Execution Plan returned by \`skyramp_analyze_changes\` — they are pre-resolved from ${
|
|
132
|
-
2. If workspace shows \`authType: none\` or \`authHeader: ""\` → proceed with no auth (\`authHeader: ""\`). If tests fail due to 401/403, add to \`issuesFound\`: "Auth may be required — update \`api.authType\` in ${
|
|
126
|
+
1. Read auth params from the Execution Plan returned by \`skyramp_analyze_changes\` — they are pre-resolved from ${SERVICE_REFS.authSourceRef}. **Use these as-is; do not infer or override.**
|
|
127
|
+
2. If workspace shows \`authType: none\` or \`authHeader: ""\` → proceed with no auth (\`authHeader: ""\`). If tests fail due to 401/403, add to \`issuesFound\`: "Auth may be required — update \`api.authType\` in ${SERVICE_REFS.authSourceRef}."
|
|
133
128
|
3. **Auth params by header type — quick reference:**
|
|
134
129
|
|
|
135
130
|
| \`authHeader\` | \`authType\` examples | \`skyramp_batch_scenario_*\` / \`skyramp_contract_*\` | \`skyramp_integration_test_generation\` (scenarioFile) |
|
|
@@ -140,7 +135,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
140
135
|
| none / \`""\` | \`none\` | \`authHeader: ""\` only when endpoint confirmed unauthenticated | \`authHeader: ""\` |
|
|
141
136
|
|
|
142
137
|
**Omit \`authToken\` entirely** — \`SKYRAMP_PLACEHOLDER_TOKEN\` is auto-inserted at execution time.
|
|
143
|
-
The \`authScheme\` for \`Authorization\` headers is pre-resolved in the Execution Plan — use it exactly (e.g. \`"Bearer"\`, \`"Token"\`, or a custom scheme from ${
|
|
138
|
+
The \`authScheme\` for \`Authorization\` headers is pre-resolved in the Execution Plan — use it exactly (e.g. \`"Bearer"\`, \`"Token"\`, or a custom scheme from ${SERVICE_REFS.authSourceRef}).
|
|
144
139
|
|
|
145
140
|
Passing auth alongside workspace \`authType\` on \`skyramp_integration_test_generation\` causes "${AUTH_CONFLICT_ERROR_MSG}" — follow the table.
|
|
146
141
|
4. Only pass \`authHeader: ""\` if you can confirm the endpoint is truly unauthenticated.
|
|
@@ -148,7 +143,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
148
143
|
**How to generate each type (for ADD):**
|
|
149
144
|
- **Integration**: call \`skyramp_batch_scenario_test_generation\` with ALL steps in a single call (pass the \`steps\` array with method, path, requestBody, statusCode for each step). Then call \`skyramp_integration_test_generation\` with the returned scenario file.
|
|
150
145
|
**Use the pre-built scenario JSON from the Execution Plan** — pass the steps array directly. Do NOT read source code models to construct request bodies if the plan already provides them.
|
|
151
|
-
Scenario JSON and test files go in ${
|
|
146
|
+
Scenario JSON and test files go in ${SERVICE_REFS.testDirRef}. Do NOT create a new \`tests/\` directory at the repo root — use that path. If no \`testDirectory\` is configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).
|
|
152
147
|
**Pipeline for speed**: Call ALL \`skyramp_batch_scenario_test_generation\` calls in one batch. When they return, call ALL \`skyramp_integration_test_generation\` calls in the next batch. Do NOT serialize per-scenario (batch→integration→batch→integration) — batch ALL scenarios first, then generate ALL integration tests.
|
|
153
148
|
- **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`, and \`requestData\` for POST/PUT/PATCH.
|
|
154
149
|
Pass \`apiSchema\` if an OpenAPI spec exists.
|
|
@@ -156,7 +151,6 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
156
151
|
- ${PATH_PARAM_UUID_GUIDANCE}
|
|
157
152
|
- **UI**: First check for existing Playwright trace \`.zip\` files in the repo (Testbot scans recursively up to 5 directory levels — the per-service output directories, \`frontend/\`, \`public/\`, \`.skyramp/\`, or any subdirectory).
|
|
158
153
|
If a relevant trace exists (covers the UI changes in this PR), use it directly with \`skyramp_ui_test_generation\` and \`modularizeCode: false\`.
|
|
159
|
-
**Output directory**: When calling \`skyramp_ui_test_generation\`, set \`outputDir\` to ${resolveServiceDetailsRef().frontendTestDirRef} — NOT \`.skyramp/\` (that directory is only for trace \`.zip\` files and workspace config).
|
|
160
154
|
If NO relevant trace exists, **you MUST write out your full trace plan as text BEFORE calling \`browser_navigate\`**. Do not touch the browser until the plan is written.
|
|
161
155
|
|
|
162
156
|
**Browser authentication (check BEFORE navigating)**: If \`<ui-credentials>\` appears in your context above, the app requires login. Parse the credentials — each line is \`username:password\`. Type the values verbatim (they are not encoded or escaped). Before navigating to ANY feature URL:
|
|
@@ -211,12 +211,12 @@ describe("drift analysis inline embedding", () => {
|
|
|
211
211
|
expect(prompt).toContain("<drift_analysis_rules>");
|
|
212
212
|
expect(prompt).toContain("</drift_analysis_rules>");
|
|
213
213
|
});
|
|
214
|
-
it("
|
|
214
|
+
it("does not include a persona statement inside the inline XML block", () => {
|
|
215
215
|
const prompt = basePrompt();
|
|
216
216
|
const start = prompt.indexOf("<drift_analysis_rules>");
|
|
217
217
|
const end = prompt.indexOf("</drift_analysis_rules>");
|
|
218
218
|
const block = prompt.slice(start, end);
|
|
219
|
-
expect(block).toContain("You are acting as a Skyramp Integration Architect");
|
|
219
|
+
expect(block).not.toContain("You are acting as a Skyramp Integration Architect");
|
|
220
220
|
});
|
|
221
221
|
it("drift_analysis_rules block appears inside Task 1, before Task 2", () => {
|
|
222
222
|
const prompt = basePrompt();
|
|
@@ -29,6 +29,7 @@ export function registerAnalysisResources(server) {
|
|
|
29
29
|
return memData;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
logger.warning(`Session not found in memory (sessionId=${sessionId}) — server may have restarted; falling back to state file`);
|
|
32
33
|
// Fall back to state file for backward compatibility.
|
|
33
34
|
// Try both "analysis" and "recommendation" prefixes since the default changed.
|
|
34
35
|
const registeredPath = getSessionFilePath(sessionId);
|
|
@@ -4,6 +4,7 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
4
4
|
import { getContractProviderAssertionsPrompt } from "../../prompts/enhance-assertions/contractProviderAssertionsPrompt.js";
|
|
5
5
|
import { getIntegrationAssertionsPrompt } from "../../prompts/enhance-assertions/integrationAssertionsPrompt.js";
|
|
6
6
|
import { getUIAssertionsPrompt } from "../../prompts/enhance-assertions/uiAssertionsPrompt.js";
|
|
7
|
+
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
7
8
|
const TOOL_NAME = "skyramp_enhance_assertions";
|
|
8
9
|
const TESTBOT_UI_CHECKS = `
|
|
9
10
|
### Additional Testbot-Specific Checks
|
|
@@ -37,7 +38,7 @@ export function registerEnhanceAssertionsTool(server) {
|
|
|
37
38
|
let instructions;
|
|
38
39
|
if (testType === TestType.UI) {
|
|
39
40
|
instructions = getUIAssertionsPrompt(testFile, enhanceCtx);
|
|
40
|
-
if (
|
|
41
|
+
if (isTestbotEnabled()) {
|
|
41
42
|
instructions += TESTBOT_UI_CHECKS;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -5,6 +5,7 @@ import fs from "fs";
|
|
|
5
5
|
import { baseSchema, AUTH_PLACEHOLDER_TOKEN, HttpMethod } from "../../types/TestTypes.js";
|
|
6
6
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
7
7
|
import { getWorkspaceAuthConfig, WorkspaceAuthType, getDefaultAuthHeader, isAuthorizationHeaderName, getAuthScheme } from "../../utils/workspaceAuth.js";
|
|
8
|
+
import yaml from "js-yaml";
|
|
8
9
|
import { logger } from "../../utils/logger.js";
|
|
9
10
|
function isJsonValue(v) {
|
|
10
11
|
if (v === undefined || v === null)
|
|
@@ -57,7 +58,8 @@ export const stepSchema = z.object({
|
|
|
57
58
|
.describe("JSON string of the expected response body"),
|
|
58
59
|
statusCode: z
|
|
59
60
|
.number()
|
|
60
|
-
.
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Expected HTTP status code. Defaults: POST→201, DELETE→204, GET/PUT/PATCH→200."),
|
|
61
63
|
responseHeaders: z
|
|
62
64
|
.record(z.array(z.string()))
|
|
63
65
|
.optional()
|
|
@@ -140,7 +142,7 @@ export function registerBatchScenarioTestTool(server) {
|
|
|
140
142
|
This tool accepts AI-parsed structured parameters from a natural language scenario description. Analyze the scenario and provide structured parameters for all steps.
|
|
141
143
|
1. **Dynamic context**: If \`skyramp_analyze_changes\` has already run and returned a \`sessionId\`, fetch endpoint detail before building each step: \`skyramp://analysis/{sessionId}/endpoints/{path}/{method}\`. This gives you exact request body fields, types, and required vs optional — use it instead of guessing from field names.
|
|
142
144
|
2. **Endpoints**: Confirm each step's method + path exists as a real endpoint (from OpenAPI spec, source code routes, or skyramp_analyze_changes output). Do NOT invent paths.
|
|
143
|
-
3. **
|
|
145
|
+
3. **Status codes**: Confirm expected status code per step (defaults: POST→201, DELETE→204, GET/PUT/PATCH→200 — note if non-standard).
|
|
144
146
|
4. **Request bodies**: Identify each field and its source (schema / prior step response / user input). For GET/DELETE steps, confirm filters go in queryParams — NEVER in requestBody.
|
|
145
147
|
5. **Chaining**: For steps that use an ID from a prior step, use the same concrete value in the path (e.g. \`/api/v1/orders/1\`) that will appear in the prior step's response body (e.g. \`{"id": 1}\`). The backend auto-detects chaining by matching values across step responses.
|
|
146
148
|
6. **Echo-back fields**: Identify which request body fields will be returned unchanged in the response — these will need exact-value assertions in the generated test.
|
|
@@ -183,6 +185,126 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
|
|
|
183
185
|
logger.warning("Could not resolve auth from workspace config");
|
|
184
186
|
}
|
|
185
187
|
}
|
|
188
|
+
// Separate try/catch so auth errors don't silently swallow schema population.
|
|
189
|
+
// Walk up from outputDir with a sync existsSync check — one stat per level,
|
|
190
|
+
// one read+parse only when found — instead of an async readWorkspaceConfigRaw
|
|
191
|
+
// call (WorkspaceConfigManager instantiation + 2 async I/O ops) per level.
|
|
192
|
+
if (!params.apiSchema) {
|
|
193
|
+
try {
|
|
194
|
+
const WS_SUBPATH = path.join(".skyramp", "workspace.yml");
|
|
195
|
+
// Assumption: outputDir lives somewhere inside the repo tree so the walk-up
|
|
196
|
+
// eventually reaches .skyramp/workspace.yml. If outputDir is outside the repo
|
|
197
|
+
// (e.g. /tmp/skyramp-test), the loop exits at the filesystem root without finding
|
|
198
|
+
// the config — the surrounding try/catch handles this gracefully (non-critical).
|
|
199
|
+
let searchDir = path.resolve(params.outputDir);
|
|
200
|
+
let wsConfigPath = null;
|
|
201
|
+
while (searchDir !== path.dirname(searchDir)) {
|
|
202
|
+
const candidate = path.join(searchDir, WS_SUBPATH);
|
|
203
|
+
if (fs.existsSync(candidate)) {
|
|
204
|
+
wsConfigPath = candidate;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
searchDir = path.dirname(searchDir);
|
|
208
|
+
}
|
|
209
|
+
if (wsConfigPath) {
|
|
210
|
+
const wsRaw = yaml.load(fs.readFileSync(wsConfigPath, "utf-8"));
|
|
211
|
+
// Best-effort: picks the first service. Multi-service workspaces may have
|
|
212
|
+
// the wrong schema if outputDir belongs to a later service — acceptable
|
|
213
|
+
// limitation for now; the user can always pass apiSchema explicitly.
|
|
214
|
+
const rawSchemaPath = wsRaw?.services?.[0]?.api?.schemaPath;
|
|
215
|
+
if (rawSchemaPath && typeof rawSchemaPath === "string") {
|
|
216
|
+
const isUrl = rawSchemaPath.startsWith("http://") || rawSchemaPath.startsWith("https://");
|
|
217
|
+
// searchDir is the directory where workspace.yml was found — use it as
|
|
218
|
+
// the resolution base so relative paths like "../openapi.yml" resolve
|
|
219
|
+
// correctly regardless of outputDir depth.
|
|
220
|
+
const schemaPath = isUrl
|
|
221
|
+
? rawSchemaPath
|
|
222
|
+
: path.resolve(searchDir, rawSchemaPath);
|
|
223
|
+
params = { ...params, apiSchema: schemaPath };
|
|
224
|
+
logger.info("Auto-populated apiSchema from workspace config", { schemaPath });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// non-critical
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// ── Change 10b: Reject GraphQL endpoint steps ──
|
|
233
|
+
// Skyramp supports REST testing only; /graphql* requires introspection not implemented.
|
|
234
|
+
{
|
|
235
|
+
const graphqlSteps = params.steps.filter((s) => {
|
|
236
|
+
if (typeof s.path !== "string")
|
|
237
|
+
return false;
|
|
238
|
+
const normalized = s.path.replace(/\/+$/, "").toLowerCase();
|
|
239
|
+
return normalized.split("/").some((seg) => seg === "graphql");
|
|
240
|
+
});
|
|
241
|
+
if (graphqlSteps.length > 0) {
|
|
242
|
+
return {
|
|
243
|
+
isError: true,
|
|
244
|
+
content: [{ type: "text", text: `GraphQL endpoints are not supported by Skyramp's test generation.\n` +
|
|
245
|
+
`Affected steps: ${graphqlSteps.map((s) => `${s.method} ${s.path}`).join(", ")}\n\n` +
|
|
246
|
+
`Skyramp supports REST API testing only. Remove GraphQL steps and use ` +
|
|
247
|
+
`the REST endpoints for the same resource instead.`,
|
|
248
|
+
}],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── Change 3: Soft spec path validation ──
|
|
253
|
+
// Warn when step paths are missing from the spec — proceed with generation regardless.
|
|
254
|
+
// Hard rejection was removed: specs frequently lag code (new endpoints, undocumented
|
|
255
|
+
// internal routes, stale auto-gen) so a missing path != a phantom path.
|
|
256
|
+
let specValidationWarning = "";
|
|
257
|
+
if (params.apiSchema) {
|
|
258
|
+
try {
|
|
259
|
+
const isUrl = params.apiSchema.startsWith("http://") || params.apiSchema.startsWith("https://");
|
|
260
|
+
let specText;
|
|
261
|
+
if (isUrl) {
|
|
262
|
+
const specRes = await fetch(params.apiSchema, { signal: AbortSignal.timeout(10_000) });
|
|
263
|
+
if (!specRes.ok) {
|
|
264
|
+
throw new Error(`HTTP ${specRes.status} ${specRes.statusText} fetching spec at ${params.apiSchema}`);
|
|
265
|
+
}
|
|
266
|
+
specText = await specRes.text();
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Note: relative apiSchema paths resolve against outputDir, not the workspace root.
|
|
270
|
+
// In practice apiSchema is always absolute (Gap Fix 2 / schema guidance), so this is safe.
|
|
271
|
+
specText = fs.readFileSync(path.resolve(params.outputDir, params.apiSchema), "utf-8");
|
|
272
|
+
}
|
|
273
|
+
// js-yaml handles both JSON and YAML specs
|
|
274
|
+
const specLoaded = yaml.load(specText);
|
|
275
|
+
const specPaths = new Set(Object.keys((specLoaded && typeof specLoaded === "object" ? specLoaded.paths : null) ?? {}));
|
|
276
|
+
if (specPaths.size === 0) {
|
|
277
|
+
logger.warning("Spec loaded but contains no paths — skipping path check", {
|
|
278
|
+
apiSchema: params.apiSchema,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const unverifiedSteps = params.steps.filter((s) => {
|
|
283
|
+
if (typeof s.path !== "string")
|
|
284
|
+
return false;
|
|
285
|
+
const norm = s.path.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, (m) => `{${m.slice(1)}}`);
|
|
286
|
+
return !specPaths.has(s.path) && !specPaths.has(norm);
|
|
287
|
+
});
|
|
288
|
+
if (unverifiedSteps.length > 0) {
|
|
289
|
+
specValidationWarning =
|
|
290
|
+
`\n\n⚠️ **Spec warning** — the following paths were not found in \`${params.apiSchema}\` ` +
|
|
291
|
+
`(spec may be stale or incomplete — verify paths against source before running tests):\n` +
|
|
292
|
+
unverifiedSteps.map((s) => ` ${s.method} ${s.path}`).join("\n") +
|
|
293
|
+
`\n\nKnown spec paths (first 20): ${[...specPaths].slice(0, 20).join(", ")}`;
|
|
294
|
+
logger.warning("Step paths not found in spec — proceeding (spec may lag code)", {
|
|
295
|
+
unverifiedPaths: unverifiedSteps.map((s) => s.path),
|
|
296
|
+
apiSchema: params.apiSchema,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
logger.warning("Spec check skipped — could not load apiSchema", {
|
|
303
|
+
apiSchema: params.apiSchema,
|
|
304
|
+
error: err instanceof Error ? err.message : String(err),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
186
308
|
const service = new ScenarioGenerationService();
|
|
187
309
|
const steps = params.steps;
|
|
188
310
|
const scenarioSlug = params.scenarioName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "scenario";
|
|
@@ -258,9 +380,10 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
|
|
|
258
380
|
type: "text",
|
|
259
381
|
text: `**Batch Scenario Generated — ${stepCount} steps**\n\n`
|
|
260
382
|
+ `**Scenario:** ${params.scenarioName}\n`
|
|
261
|
-
+ `**Steps:**\n${steps.map((s, i) => ` ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? ""}`).join("\n")}\n\n`
|
|
383
|
+
+ `**Steps:**\n${steps.map((s, i) => ` ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? "default"}`).join("\n")}\n\n`
|
|
262
384
|
+ `**File:** ${filePath}\n\n`
|
|
263
|
-
+ `**Next:** Call \`skyramp_integration_test_generation\` with \`scenarioFile: "${filePath}"
|
|
385
|
+
+ `**Next:** Call \`skyramp_integration_test_generation\` with \`scenarioFile: "${filePath}"\``
|
|
386
|
+
+ specValidationWarning,
|
|
264
387
|
},
|
|
265
388
|
],
|
|
266
389
|
isError: false,
|