@skyramp/mcp 0.1.2 → 0.1.4

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 (28) hide show
  1. package/build/prompts/test-maintenance/driftAnalysisSections.js +2 -2
  2. package/build/prompts/test-recommendation/analysisOutputPrompt.js +26 -21
  3. package/build/prompts/test-recommendation/recommendationSections.js +42 -10
  4. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +2 -5
  5. package/build/prompts/test-recommendation/test-recommendation-prompt.js +114 -157
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +250 -18
  7. package/build/prompts/testbot/testbot-prompts.js +17 -9
  8. package/build/services/ScenarioGenerationService.js +2 -1
  9. package/build/services/TestDiscoveryService.js +22 -7
  10. package/build/services/TestDiscoveryService.test.js +44 -0
  11. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +3 -4
  12. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +9 -0
  13. package/build/tools/submitReportTool.js +4 -3
  14. package/build/tools/submitReportTool.test.js +16 -2
  15. package/build/tools/test-management/analyzeChangesTool.js +264 -140
  16. package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
  17. package/build/tools/test-management/analyzeTestHealthTool.js +5 -0
  18. package/build/types/RepositoryAnalysis.js +8 -0
  19. package/build/types/TestRecommendation.js +2 -0
  20. package/build/utils/branchDiff.js +24 -8
  21. package/build/utils/featureFlags.js +25 -0
  22. package/build/utils/httpDefaults.js +12 -0
  23. package/build/utils/repoScanner.js +16 -2
  24. package/build/utils/routeParsers.js +79 -79
  25. package/build/utils/routeParsers.test.js +192 -66
  26. package/build/utils/scenarioDrafting.js +116 -497
  27. package/build/utils/scenarioDrafting.test.js +260 -480
  28. 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 pre-allocates E2E and UI sections for full-stack repos", () => {
462
+ it("full_repo mode includes E2E/UI guidance for full-stack repos via Budget Plan", () => {
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 and UI sections must be present even though scenarioDrafting only produces backend types
480
- expect(prompt).toContain("### E2E");
481
- expect(prompt).toContain("### UI");
479
+ // E2E/UI split is now driven by LLM's Budget Plan, not hardcoded pre-allocation.
480
+ // The prompt must still reference the tools and provide guidance.
482
481
  expect(prompt).toContain("skyramp_e2e_test_generation");
483
482
  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: all GENERATE slots are UI placeholders (no backend)", () => {
692
+ it("UI-only PR: provides UI guidance with tool workflow (LLM derives scenarios)", () => {
693
693
  const analysis = minimalAnalysis({
694
694
  businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
695
695
  branchDiffContext: {
@@ -702,15 +702,14 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
702
702
  },
703
703
  });
704
704
  const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
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");
705
+ // UI-only PR: guidance tells LLM to derive UI tests from changed files
706
+ expect(prompt).toContain("UI-only PR");
709
707
  expect(prompt).toContain("skyramp_ui_test_generation");
710
- // Each slot targets a distinct changed component/flow
708
+ expect(prompt).toContain("skyramp_export_zip");
709
+ // Each item must be distinct
711
710
  expect(prompt).toContain("distinct changed component or user flow");
712
711
  });
713
- it("mixed PR: last GENERATE slot is UI, preceding slots are backend scenarios", () => {
712
+ it("mixed PR: all GENERATE slots are backend; UI/E2E added per Budget Plan", () => {
714
713
  const scenarios = [
715
714
  makeScenario("orders-create"),
716
715
  makeScenario("orders-update"),
@@ -728,14 +727,12 @@ describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
728
727
  },
729
728
  });
730
729
  const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
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)
730
+ // Backend scenarios fill GENERATE slots (no hardcoded UI placeholder)
736
731
  expect(prompt).toContain("#1 — GENERATE** | integration");
737
- // Scenario name from the pre-ranked list (orders-create or orders-update)
738
732
  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");
739
736
  });
740
737
  it("backend-only PR: all GENERATE slots are backend scenarios (no E2E injection)", () => {
741
738
  const scenarios = [makeScenario("items-create"), makeScenario("items-get"), makeScenario("items-delete")];
@@ -842,12 +839,21 @@ describe("buildRecommendationPrompt — Mandatory Reasoning Protocol", () => {
842
839
  expect(protocol).toContain("requestBody");
843
840
  expect(protocol).toContain("endpointURL");
844
841
  expect(protocol).toContain("authHeader");
845
- expect(protocol).toContain("FK path params");
842
+ expect(protocol).toContain("Foreign Key path params");
846
843
  });
847
844
  it("reasoning protocol instructs to read source file when value cannot be sourced", () => {
848
845
  const protocol = buildReasoningProtocol();
849
846
  expect(protocol).toContain("read the relevant source file");
850
847
  });
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
+ });
851
857
  });
852
858
  // ---------------------------------------------------------------------------
853
859
  // Tests — Context Fetching Guidance
@@ -886,6 +892,145 @@ describe("buildRecommendationPrompt — Tool Contract Framing", () => {
886
892
  });
887
893
  });
888
894
  // ---------------------------------------------------------------------------
895
+ // Tests — Multi-method endpoint partitioning
896
+ // ---------------------------------------------------------------------------
897
+ describe("buildRecommendationPrompt — multi-method endpoint partitioning", () => {
898
+ it("classifies all methods of a changed endpoint as changed", () => {
899
+ // When classifyEndpointsByChangedFiles identifies a file as changed,
900
+ // all methods from that endpoint's scanned catalog entry are included
901
+ // with concrete methods (no MULTI sentinels).
902
+ const analysis = minimalAnalysis({
903
+ apiEndpoints: {
904
+ totalCount: 2,
905
+ baseUrl: "http://localhost:3000",
906
+ endpoints: [
907
+ {
908
+ path: "/api/products",
909
+ resourceGroup: "products",
910
+ pathParams: [],
911
+ methods: [
912
+ { method: "GET", description: "List products", queryParams: [], authRequired: false, sourceFile: "app/api/products/route.ts", interactions: [] },
913
+ { method: "POST", description: "Create product", queryParams: [], authRequired: false, sourceFile: "app/api/products/route.ts", interactions: [] },
914
+ ],
915
+ },
916
+ {
917
+ path: "/api/items",
918
+ resourceGroup: "items",
919
+ pathParams: [],
920
+ methods: [
921
+ { method: "GET", description: "List items", queryParams: [], authRequired: false, sourceFile: "routes/items.ts", interactions: [] },
922
+ ],
923
+ },
924
+ ],
925
+ },
926
+ branchDiffContext: {
927
+ baseBranch: "main",
928
+ currentBranch: "feature/products",
929
+ changedFiles: ["app/api/products/route.ts"],
930
+ newEndpoints: [{
931
+ path: "/api/products",
932
+ methods: [
933
+ { method: "GET", sourceFile: "app/api/products/route.ts", interactionCount: 0 },
934
+ { method: "POST", sourceFile: "app/api/products/route.ts", interactionCount: 0 },
935
+ ],
936
+ }],
937
+ modifiedEndpoints: [],
938
+ affectedServices: [],
939
+ },
940
+ });
941
+ const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
942
+ // Both GET and POST for /api/products should be in "Changed in this PR"
943
+ expect(prompt).toContain("Changed in this PR");
944
+ expect(prompt).toMatch(/Changed in this PR:[\s\S]*GET \/api\/products/);
945
+ expect(prompt).toMatch(/Changed in this PR:[\s\S]*POST \/api\/products/);
946
+ // /api/items should NOT be in changed section
947
+ expect(prompt).toMatch(/Other endpoints[\s\S]*GET \/api\/items/);
948
+ });
949
+ it("handles mix of new and modified endpoints with concrete methods", () => {
950
+ const analysis = minimalAnalysis({
951
+ apiEndpoints: {
952
+ totalCount: 2,
953
+ baseUrl: "http://localhost:3000",
954
+ endpoints: [
955
+ {
956
+ path: "/api/products",
957
+ resourceGroup: "products",
958
+ pathParams: [],
959
+ methods: [
960
+ { method: "GET", description: "List", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
961
+ { method: "POST", description: "Create", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
962
+ ],
963
+ },
964
+ {
965
+ path: "/api/orders",
966
+ resourceGroup: "orders",
967
+ pathParams: [],
968
+ methods: [
969
+ { method: "POST", description: "Create order", queryParams: [], authRequired: false, sourceFile: "routes.ts", interactions: [] },
970
+ ],
971
+ },
972
+ ],
973
+ },
974
+ branchDiffContext: {
975
+ baseBranch: "main",
976
+ currentBranch: "feature/mix",
977
+ changedFiles: ["routes.ts"],
978
+ newEndpoints: [
979
+ { path: "/api/products", methods: [
980
+ { method: "GET", sourceFile: "routes.ts", interactionCount: 0 },
981
+ { method: "POST", sourceFile: "routes.ts", interactionCount: 0 },
982
+ ] },
983
+ ],
984
+ modifiedEndpoints: [
985
+ { path: "/api/orders", methods: [{ method: "POST", sourceFile: "routes.ts", changeType: "modified" }] },
986
+ ],
987
+ affectedServices: [],
988
+ },
989
+ });
990
+ const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
991
+ // Both products and orders should be in changed section
992
+ expect(prompt).toMatch(/Changed in this PR:[\s\S]*GET \/api\/products/);
993
+ expect(prompt).toMatch(/Changed in this PR:[\s\S]*POST \/api\/orders/);
994
+ });
995
+ });
996
+ // ---------------------------------------------------------------------------
997
+ // Tests — Removed endpoint [removed] marker in prompt (Fix 3 verification)
998
+ // ---------------------------------------------------------------------------
999
+ describe("buildRecommendationPrompt — removed endpoint listing", () => {
1000
+ it("appends [removed] marker for removed endpoints not in current catalog", () => {
1001
+ const analysis = minimalAnalysis({
1002
+ apiEndpoints: {
1003
+ totalCount: 1,
1004
+ baseUrl: "http://localhost:3000",
1005
+ endpoints: [{
1006
+ path: "/api/items",
1007
+ resourceGroup: "items",
1008
+ pathParams: [],
1009
+ methods: [{
1010
+ method: "GET", description: "List items", queryParams: [],
1011
+ authRequired: false, sourceFile: "routes.ts", interactions: [],
1012
+ }],
1013
+ }],
1014
+ },
1015
+ branchDiffContext: {
1016
+ baseBranch: "main",
1017
+ currentBranch: "feature/remove",
1018
+ changedFiles: ["routes.ts"],
1019
+ newEndpoints: [],
1020
+ modifiedEndpoints: [],
1021
+ removedEndpoints: [{
1022
+ path: "/api/legacy",
1023
+ methods: [{ method: "DELETE", sourceFile: "routes.ts", changeType: "removed" }],
1024
+ }],
1025
+ affectedServices: [],
1026
+ },
1027
+ });
1028
+ const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 10);
1029
+ expect(prompt).toContain("DELETE /api/legacy [removed]");
1030
+ expect(prompt).toContain("Changed in this PR");
1031
+ });
1032
+ });
1033
+ // ---------------------------------------------------------------------------
889
1034
  // Tests — Long-context best practices: XML tags structure
890
1035
  // ---------------------------------------------------------------------------
891
1036
  describe("buildRecommendationPrompt — XML tag structure (long-context best practice)", () => {
@@ -1373,3 +1518,90 @@ describe("externalDedupKey", () => {
1373
1518
  expect(externalDedupKey(scenario)).toBe("POST::orders::contract");
1374
1519
  });
1375
1520
  });
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,7 +4,7 @@ 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 } from "../../utils/featureFlags.js";
7
+ import { isContractConsumerModeEnabled, resolveServiceDetailsRef } from "../../utils/featureFlags.js";
8
8
  import { readWorkspaceConfigRaw } from "../../utils/workspaceAuth.js";
9
9
  // Cached at module-load — the flag is process-wide and cannot change per call.
10
10
  const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
@@ -100,16 +100,23 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
100
100
  Keep advancing until you have created exactly ${maxGenerate} new test files OR exhausted all candidates.
101
101
  - 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
102
  - 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
- - **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.
104
- **Skip only if one of these conditions is met:**
103
+ - **Backend tests come first (MANDATORY):** All pre-ranked GENERATE items are backend tests (contract/integration). You MUST generate them before spending budget on UI tests. UI/E2E tests fill the Budget Plan's UI% allocation AFTER backend GENERATE items are complete they do NOT replace backend tests.
104
+ - **Backfill priority (MANDATORY):** When filling budget slots beyond the pre-ranked GENERATE items, follow this order strictly:
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
112
  - **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
106
- - **(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:
113
+ - **(b) Budget Plan allocates 0% UI/E2E** (backend-only PR with no frontend files changed).
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:
107
115
  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).
108
116
  2. If no production file imports, re-exports, or renders it, the component has no DOM node in the running app → unintegrated.
109
117
  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.
110
118
  **Never** apply the unintegrated heuristic to framework route/entrypoint files themselves — those are always reachable by convention.
111
119
  **Never** generate tests for unrelated pages as a substitute for an unintegrated component.
112
- This rule takes priority over generating additional backend-only tests.
113
120
  - **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.
114
121
  - 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.
115
122
  - 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\`.
@@ -121,8 +128,8 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
121
128
  - Critical-category tests are already ranked first by the pre-computed scores — follow the plan order.
122
129
 
123
130
  **Auth — determine ONCE, apply to EVERY tool call:**
124
- 1. Read auth params from the Execution Plan returned by \`skyramp_analyze_changes\` — they are resolved directly from workspace.yml. **Use these as-is; do not infer or override.**
125
- 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 workspace.yml."
131
+ 1. Read auth params from the Execution Plan returned by \`skyramp_analyze_changes\` — they are pre-resolved from ${resolveServiceDetailsRef().authSourceRef}. **Use these as-is; do not infer or override.**
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 ${resolveServiceDetailsRef().authSourceRef}."
126
133
  3. **Auth params by header type — quick reference:**
127
134
 
128
135
  | \`authHeader\` | \`authType\` examples | \`skyramp_batch_scenario_*\` / \`skyramp_contract_*\` | \`skyramp_integration_test_generation\` (scenarioFile) |
@@ -133,7 +140,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
133
140
  | none / \`""\` | \`none\` | \`authHeader: ""\` only when endpoint confirmed unauthenticated | \`authHeader: ""\` |
134
141
 
135
142
  **Omit \`authToken\` entirely** — \`SKYRAMP_PLACEHOLDER_TOKEN\` is auto-inserted at execution time.
136
- The \`authScheme\` for \`Authorization\` headers is pre-resolved in the Execution Plan — use it exactly (e.g. \`"Bearer"\`, \`"Token"\`, or a custom scheme from \`api.authScheme\` in workspace.yml).
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 ${resolveServiceDetailsRef().authSourceRef}).
137
144
 
138
145
  Passing auth alongside workspace \`authType\` on \`skyramp_integration_test_generation\` causes "${AUTH_CONFLICT_ERROR_MSG}" — follow the table.
139
146
  4. Only pass \`authHeader: ""\` if you can confirm the endpoint is truly unauthenticated.
@@ -141,7 +148,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
141
148
  **How to generate each type (for ADD):**
142
149
  - **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.
143
150
  **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.
144
- Scenario JSON and test files go in the \`testDirectory\` from \`workspace.yml\` (visible in the service context block at the top of this prompt). Do NOT create a new \`tests/\` directory at the repo root — use the path the workspace config specifies. If no \`testDirectory\` is configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).
151
+ Scenario JSON and test files go in ${resolveServiceDetailsRef().testDirRef}. Do NOT create a new \`tests/\` directory at the repo root — use that path. If not configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).
145
152
  **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.
146
153
  - **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`, and \`requestData\` for POST/PUT/PATCH.
147
154
  Pass \`apiSchema\` if an OpenAPI spec exists.
@@ -149,6 +156,7 @@ ${CONTRACT_MODE_GUIDANCE}
149
156
  - ${PATH_PARAM_UUID_GUIDANCE}
150
157
  - **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).
151
158
  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).
152
160
  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.
153
161
 
154
162
  **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:
@@ -1,5 +1,6 @@
1
1
  import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
2
2
  import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
3
+ import { inferExpectedStatus } from "../utils/httpDefaults.js";
3
4
  import { logger } from "../utils/logger.js";
4
5
  import fs from "fs";
5
6
  import path from "path";
@@ -124,7 +125,7 @@ ${JSON.stringify(traceRequest, null, 2)}
124
125
  }
125
126
  const timestamp = new Date().toISOString();
126
127
  const method = params.method;
127
- const statusCode = params.statusCode ?? (method === "POST" ? 201 : method === "DELETE" ? 204 : 200);
128
+ const statusCode = params.statusCode ?? inferExpectedStatus(method);
128
129
  const requestBody = params.requestBody ||
129
130
  (method === "GET" || method === "DELETE" ? "" : "{}");
130
131
  const responseHeaders = params.responseHeaders
@@ -62,10 +62,11 @@ export class TestDiscoveryService {
62
62
  * Uses fast-glob for cross-platform file scanning, then classifies discovered files
63
63
  * as Skyramp-generated tests, external tests, or not-a-test during processing.
64
64
  *
65
- * When `options.changedResources` is provided (PR mode), external files are partitioned
66
- * by relevance: files whose path/name overlaps with the changed resource names get full
67
- * endpoint extraction; low-relevance files are returned as name-only entries (no reads).
68
- * This eliminates the old hard cap while keeping state file size bounded.
65
+ * External test handling depends on `options.changedResources`:
66
+ * - `string[]` with entries (PR mode, endpoints detected): partition by relevance.
67
+ * - `[]` empty array (PR mode, scanner found no endpoints): skip external tests entirely
68
+ * rather than flooding context with irrelevant files.
69
+ * - `undefined` (full-repo mode, no diff): cap at MAX_EXTERNAL_FULL_REPO.
69
70
  */
70
71
  async discoverTests(repositoryPath, options = {}) {
71
72
  logger.info(`Starting test discovery in: ${repositoryPath}`);
@@ -86,23 +87,37 @@ export class TestDiscoveryService {
86
87
  skyrampTests.forEach(t => { t.source = TestSource.Skyramp; });
87
88
  // Partition external tests into relevant (full extraction) and low-relevance (name-only).
88
89
  //
89
- // PR mode (changedResources provided):
90
+ // PR mode + endpoints detected (changedResources is non-empty array):
90
91
  // Files whose path/name token-overlaps with the changed resource names are "relevant".
91
92
  // Only they get full endpoint extraction. Low-relevance files get name-only entries.
92
93
  // No hard cap — the relevance filter naturally bounds the read set to PR scope.
94
+ // The sentinel ["unknown"] falls into this branch — most files score 0 (low-relevance)
95
+ // and get name-only entries, so external coverage is preserved without context flood.
93
96
  //
94
- // Full-repo mode (no changedResources):
97
+ // PR mode + truly no endpoints (changedResources is empty array []):
98
+ // Diff contained no endpoints at all (new, modified, or removed) — skip external
99
+ // tests entirely rather than flooding the prompt with hundreds of irrelevant files.
100
+ //
101
+ // Full-repo mode (changedResources is undefined):
95
102
  // No diff context — all external files treated as potentially relevant.
96
103
  // Cap at MAX_EXTERNAL_FULL_REPO to avoid reading hundreds of files.
97
104
  const { changedResources } = options;
98
105
  let relevantExternal;
99
106
  let otherExternal;
100
107
  if (changedResources?.length) {
108
+ // PR mode with detected endpoints — partition by relevance
101
109
  ({ relevant: relevantExternal, other: otherExternal } =
102
110
  this.partitionByRelevance(classified.external, changedResources));
103
111
  }
112
+ else if (changedResources !== undefined) {
113
+ // PR mode with an explicit empty endpoint list from diff parsing — don't flood
114
+ // context with irrelevant external tests. The LLM will work from Skyramp tests
115
+ // and scanned endpoints only.
116
+ relevantExternal = [];
117
+ otherExternal = [];
118
+ }
104
119
  else {
105
- // Full-repo mode: cap full-extraction set, remaining become name-only
120
+ // Full-repo mode (no diff context): cap full-extraction set, remaining become name-only
106
121
  relevantExternal = classified.external.slice(0, this.MAX_EXTERNAL_FULL_REPO);
107
122
  otherExternal = classified.external.slice(this.MAX_EXTERNAL_FULL_REPO);
108
123
  }
@@ -348,6 +348,50 @@ describe("TestDiscoveryService", () => {
348
348
  const withEndpoints = externalTests.filter(t => t.apiEndpoint !== "");
349
349
  expect(withEndpoints.length).toBe(12);
350
350
  });
351
+ it("returns zero external tests when changedResources is empty array (PR mode, no endpoints)", async () => {
352
+ // Simulate PR mode where a parsed diff produced no detected endpoints:
353
+ // newEndpoints=[], modifiedEndpoints=[], and removedEndpoints=[] → changedResources = []
354
+ writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
355
+ writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
356
+ const result = await service.discoverTests(tmpDir, { changedResources: [] });
357
+ const externalTests = result.tests.filter(t => t.source === TestSource.External);
358
+ // Empty changedResources = PR mode with no detected endpoints → zero external tests
359
+ expect(externalTests.length).toBe(0);
360
+ expect(result.relevantExternalTestPaths.length).toBe(0);
361
+ });
362
+ it("still returns external tests in full-repo mode (changedResources undefined)", async () => {
363
+ // Full-repo mode: changedResources not provided → should use capped full-repo behavior
364
+ writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
365
+ writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
366
+ const result = await service.discoverTests(tmpDir); // no options → undefined
367
+ const externalTests = result.tests.filter(t => t.source === TestSource.External);
368
+ expect(externalTests.length).toBe(2);
369
+ expect(result.relevantExternalTestPaths.length).toBe(2);
370
+ });
371
+ it("Skyramp tests are unaffected by empty changedResources", async () => {
372
+ writeFile("tests/test_orders_smoke.py", '# Generated by Skyramp\nskyramp generate smoke rest');
373
+ writeFile("test_external.py", 'import pytest\ndef test(): pass');
374
+ const result = await service.discoverTests(tmpDir, { changedResources: [] });
375
+ const skyrampTests = result.tests.filter(t => t.source === TestSource.Skyramp);
376
+ const externalTests = result.tests.filter(t => t.source === TestSource.External);
377
+ // Skyramp tests always discovered regardless of changedResources
378
+ expect(skyrampTests.length).toBe(1);
379
+ // External tests suppressed in PR-mode-no-endpoints
380
+ expect(externalTests.length).toBe(0);
381
+ });
382
+ it("returns external tests as name-only with ['unknown'] sentinel (unresolvable resources)", async () => {
383
+ // When diff endpoints exist but all paths resolve to "unknown" (e.g. decorator-relative
384
+ // paths like "/{order_id}"), changedResources = ["unknown"]. External tests should be
385
+ // discovered (not skipped) but scored as low-relevance since "unknown" won't match filenames.
386
+ writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
387
+ writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
388
+ const result = await service.discoverTests(tmpDir, { changedResources: ["unknown"] });
389
+ const externalTests = result.tests.filter(t => t.source === TestSource.External);
390
+ // External tests discovered (not skipped like empty array)
391
+ expect(externalTests.length).toBe(2);
392
+ // But all are low-relevance (name-only) since "unknown" doesn't match any filename tokens
393
+ expect(result.relevantExternalTestPaths.length).toBe(0);
394
+ });
351
395
  it("low-relevance files have empty apiEndpoint and empty framework in PR mode", async () => {
352
396
  writeFile("test_orders_api.py", 'import requests\nrequests.get("/api/orders")');
353
397
  writeFile("test_products_api.py", 'import requests\nrequests.get("/api/products")');
@@ -57,8 +57,7 @@ export const stepSchema = z.object({
57
57
  .describe("JSON string of the expected response body"),
58
58
  statusCode: z
59
59
  .number()
60
- .optional()
61
- .describe("Expected HTTP status code. Defaults: POST→201, DELETE→204, GET/PUT/PATCH→200."),
60
+ .describe("Expected HTTP status code — determine from the source code (e.g. 200, 201, 204)."),
62
61
  responseHeaders: z
63
62
  .record(z.array(z.string()))
64
63
  .optional()
@@ -141,7 +140,7 @@ export function registerBatchScenarioTestTool(server) {
141
140
  This tool accepts AI-parsed structured parameters from a natural language scenario description. Analyze the scenario and provide structured parameters for all steps.
142
141
  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.
143
142
  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.
144
- 3. **Status codes**: Confirm expected status code per step (defaults: POST→201, DELETE→204, GET/PUT/PATCH→200note if non-standard).
143
+ 3. **HTTP Status Codes**: Determine expected HTTP status code per step from the source codedo not assume conventions.
145
144
  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.
146
145
  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.
147
146
  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.
@@ -259,7 +258,7 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
259
258
  type: "text",
260
259
  text: `**Batch Scenario Generated — ${stepCount} steps**\n\n`
261
260
  + `**Scenario:** ${params.scenarioName}\n`
262
- + `**Steps:**\n${steps.map((s, i) => ` ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? "default"}`).join("\n")}\n\n`
261
+ + `**Steps:**\n${steps.map((s, i) => ` ${i + 1}. ${s.method} ${s.path} → ${s.statusCode ?? ""}`).join("\n")}\n\n`
263
262
  + `**File:** ${filePath}\n\n`
264
263
  + `**Next:** Call \`skyramp_integration_test_generation\` with \`scenarioFile: "${filePath}"\``,
265
264
  },