@skyramp/mcp 0.2.8-rc.1 → 0.2.9

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 (41) hide show
  1. package/build/index.js +0 -7
  2. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  3. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  4. package/build/prompts/sut-setup/shared.d.ts +20 -0
  5. package/build/prompts/sut-setup/shared.js +69 -7
  6. package/build/prompts/test-maintenance/actionsInstructions.js +5 -1
  7. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +15 -2
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.js +58 -3
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +7 -22
  10. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +14 -0
  11. package/build/prompts/test-maintenance/driftAnalysisShared.js +62 -0
  12. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +38 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +228 -0
  14. package/build/prompts/test-recommendation/scopeAssessment.js +12 -2
  15. package/build/prompts/testbot/testbot-prompts.js +3 -2
  16. package/build/services/TestDiscoveryService.d.ts +12 -9
  17. package/build/services/TestDiscoveryService.js +125 -51
  18. package/build/services/TestDiscoveryService.test.js +235 -15
  19. package/build/services/TestExecutionService.d.ts +1 -1
  20. package/build/services/TestExecutionService.js +7 -7
  21. package/build/services/TestExecutionService.test.js +4 -1
  22. package/build/tools/submitReportTool.d.ts +5 -5
  23. package/build/tools/submitReportTool.js +11 -4
  24. package/build/tools/submitReportTool.test.js +2 -0
  25. package/build/tools/test-management/actionsTool.js +54 -36
  26. package/build/tools/test-management/analyzeChangesTool.js +11 -17
  27. package/build/tools/test-management/analyzeTestHealthTool.js +182 -8
  28. package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +1 -0
  29. package/build/tools/test-management/analyzeTestHealthTool.test.js +468 -0
  30. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  31. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  32. package/build/types/TestAnalysis.d.ts +3 -0
  33. package/build/types/TestbotReport.d.ts +1 -1
  34. package/build/utils/AnalysisStateManager.d.ts +3 -21
  35. package/build/utils/docker.test.js +1 -1
  36. package/build/utils/versions.d.ts +3 -3
  37. package/build/utils/versions.js +1 -1
  38. package/build/workspace/workspace.d.ts +21 -21
  39. package/build/workspace/workspace.js +7 -4
  40. package/build/workspace/workspace.test.js +65 -6
  41. package/package.json +2 -2
@@ -0,0 +1,228 @@
1
+ /**
2
+ * UI Drift Analysis prompt sections for Component 2.
3
+ *
4
+ * Mirrors the driftAnalysisSections.ts pattern but for UI-specific drift detection.
5
+ * These sections are included whenever `uiContext.changedFrontendFiles` is non-empty —
6
+ * both browser tests (Playwright/Cypress/Selenium) and component/widget tests
7
+ * (RTL/Flutter widget/vue-test-utils/angular-testbed) go through the same gate.
8
+ * Component tests always use source-only analysis regardless of blueprintCaptured.
9
+ */
10
+ import { buildUiAssignAction } from "./driftAnalysisShared.js";
11
+ /**
12
+ * Build the UI Action Decision Tree section.
13
+ * This is the main entry point for UI drift analysis.
14
+ */
15
+ export function buildUiActionDecisionTree() {
16
+ return `<ui_decision_rules>
17
+ For each UI test listed in the \`<ui_analysis_context>\` above, assess whether the test is still valid given the diff (and blueprint diff when blueprints were captured). The diff may have changed:
18
+ - Element selectors (CSS class changes, component restructuring)
19
+ - Page structure (new containers, removed sections)
20
+ - Route paths (URL changes)
21
+ - Component behavior (state changes, visibility conditions)
22
+
23
+ **Scope gate:** Read each test file to determine its type, then apply the correct gate:
24
+ - **Browser test** (has \`page.goto()\`, \`cy.visit()\`, \`driver.get()\`): check that the test navigates to a page the changed frontend file serves. Infer from the changed file's directory path (e.g. \`modules/bookings/\` → bookings page) or explicit route-path diff signals.
25
+ - **Component/widget test** (has \`render(<…>)\`, \`mount()\`, \`shallowMount()\`): check if it directly renders/mounts a changed component. If none of the rendered components match a changed file, the test is unaffected.
26
+ If neither condition applies, assign **IGNORE** and skip remaining checks.
27
+
28
+
29
+ **Severity order (highest first): DELETE > REGENERATE > UPDATE > VERIFY > IGNORE**
30
+
31
+ **Pre-scan the diff for UI-relevant changes:**
32
+ - Component file changes: \`+ import { NewComponent }\`, \`- <OldComponent />\` / \`+ <NewComponent />\`
33
+ - CSS/Tailwind changes: \`+ className="..."\`, \`+ tw="..."\`, \`+ styles.\`
34
+ - Route changes: \`- path: "/old"\` / \`+ path: "/new"\`
35
+ - Selector changes: \`+ data-testid="..."\`, \`- data-testid="..."\`, \`+ aria-label="..."\`
36
+ - State/visibility changes: \`+ isOpen\`, \`+ isVisible\`, \`+ showModal\`
37
+
38
+ Build a detection list first: for each matched diff line, write one line: \`{pattern type} — "{diff line}" — affects {route/component}\`.
39
+
40
+ **Action definitions:**
41
+ - **DELETE**: The test targets a page/route that no longer exists, or all selectors the test uses have been removed and there's no migration path.
42
+ - **REGENERATE**: The interaction model changed so fundamentally that the test flow itself is broken — not just selector values, but the sequence of actions (e.g. a form replaced by a modal, buttons replaced by a dropdown, a multi-step flow collapsed into one). There is no rename mapping; re-recording from scratch is the only viable approach.
43
+ - **UPDATE**: Specific selectors need updating, assertions need adjustment, or new elements were added to a page the test already exercises. Always an in-place edit to the existing file — do not generate a new spec to cover the same page or component.
44
+ - **VERIFY**: The test may be impacted but you cannot confirm breakage without reading more context (e.g. selectors are hidden in a page object file you have not read, or the component is imported indirectly).
45
+ - **IGNORE**: The changed code cannot reach this test's routes or rendered components — the test covers unchanged functionality. If you cannot point to a diff line that affects this test's routes, selectors, or rendered components, the action is IGNORE or VERIFY, not UPDATE.
46
+
47
+ **Rules:**
48
+ - DELETE when all routes the test covers were removed or all critical selectors are gone.
49
+ - REGENERATE when the interaction model changed fundamentally — the test's sequence of actions is broken with no migration path (e.g. a click-to-update button replaced by an auto-updating stepper; a remove button moved into a dropdown). The number of broken selectors alone does not determine REGENERATE; what matters is whether the flow can still be expressed with targeted edits.
50
+ - UPDATE when specific selectors changed (renamed classes, moved data-testid) but the test flow remains valid, or when new elements were added to a page the test already covers — append the assertion to the existing file.
51
+ - VERIFY when a page object or indirect import hides the affected selectors — read the imported file before escalating to UPDATE.
52
+ - IGNORE when the diff touches unrelated frontend code (different routes, different components).
53
+
54
+ **Additional rule for component/widget tests** ([rtl], [flutter-widget], [vue-test-utils], [angular-testbed]):
55
+ - Rule #7: Component deleted — source file removed with no re-export → **DELETE** (report-only for external tests). These tests have no navigation; the rendered component is their only target.
56
+ - Skip the route-existence check (step 1.1) for component/widget tests — they have no \`page.goto()\` or equivalent navigation calls.
57
+ - Primary matching signal: read the test to check if it directly renders a changed component — if so, it is impacted.
58
+ </ui_decision_rules>`;
59
+ }
60
+ /**
61
+ * Pre-scan step: agent greps changed files' exported symbols across the test
62
+ * directory to catch tests that server-side discovery missed — aliased imports,
63
+ * indirect renders, or any test the blueprint diff doesn't directly reference.
64
+ */
65
+ export function buildUiCheckExportedSymbols(changedFrontendFiles) {
66
+ const files = (changedFrontendFiles ?? []).join(", ") || "the changed frontend files";
67
+ return `The server pre-discovered the tests listed above using exported symbols and UI signals (testids, aria-labels, i18n keys, CSS classes) from the diff. Before assessing, do a quick complementary grep to catch any tests the server may have missed — aliased imports, indirect renders, or tests using symbols not present in the diff:
68
+
69
+ 1. For each changed frontend file (${files}):
70
+ a. Read the file and extract exported symbols — component names, function names, hook names, class names.
71
+ b. Grep the test directory for each symbol name (case-insensitive).
72
+ c. For any matching test file not already in the \`<ui_analysis_context>\` list — read it and apply the decision tree.
73
+
74
+ This catches tests that import a changed symbol under an alias, exercise a shared hook, or render the changed component indirectly via a parent.`;
75
+ }
76
+ /**
77
+ * Check if routes targeted by the UI test still exist.
78
+ */
79
+ export function buildUiCheckRouteExistence() {
80
+ return `Does the route/page this UI test navigates to still exist?
81
+
82
+ **Skip this check for component/widget tests** ([rtl], [flutter-widget], [vue-test-utils], [angular-testbed]) — they render components directly with no navigation calls. Proceed to selector checks.
83
+
84
+ Diff signals to look for (browser tests only):
85
+ - Route removed: \`- path: "/old-path"\`, \`- @app.route("/old-path")\`, \`- router.get("/old-path")\`
86
+ - Route renamed: paired \`-\` and \`+\` on route definitions with different paths
87
+ - Redirect added: \`+ redirect: "/old-path" → "/new-path"\`, \`+ res.redirect(301, "/new-path")\`
88
+
89
+ Actions:
90
+ - ALL routes the test uses were removed → **DELETE**
91
+ - SOME routes renamed with no redirect → **UPDATE** (update navigation calls to new URLs)
92
+ - Routes renamed with redirect kept → **UPDATE** (verify redirect behavior, test should still work)
93
+ - Routes unchanged → proceed to selector checks`;
94
+ }
95
+ /**
96
+ * Check if selectors used by the UI test still exist in the blueprints.
97
+ */
98
+ export function buildUiCheckSelectors() {
99
+ return `Do the selectors/locators this UI test uses still match the current UI?
100
+
101
+ Read the test (and any imported page object/fixture files) to extract the selectors/locators it uses — \`getByTestId(...)\`, \`locator(...)\`, \`.find('...')\`, \`data-testid\`, \`aria-label\`, CSS class names.
102
+
103
+ Diff signals to look for:
104
+ - CSS class changes: \`- className="old-class"\` / \`+ className="new-class"\`
105
+ - Tailwind changes: \`- tw="p-4"\` / \`+ tw="p-6"\`
106
+ - Test ID changes: \`- data-testid="old-id"\` / \`+ data-testid="new-id"\`
107
+ - ARIA changes: \`- aria-label="Old"\` / \`+ aria-label="New"\`
108
+ - Component restructuring: \`+ <NewWrapper>\` around existing content, \`- <OldWrapper>\`
109
+
110
+ **When blueprints are available** (\`blueprintCaptured=true\`): compare extracted selectors against \`browser_blueprint_diff\` for the affected page — elements absent from the new blueprint signal breakage.
111
+
112
+ **When blueprints are not available** (\`blueprintCaptured=false\`): use diff signals only. If a selector the test uses matches a value renamed or removed in the diff, assign UPDATE. If you cannot confirm breakage from the diff alone (e.g. selectors live in a page object you have not read), assign VERIFY rather than guessing.
113
+
114
+ Actions:
115
+ - Selectors confirmed unchanged (diff or blueprint) → **IGNORE** (no drift)
116
+ - Selectors renamed (class, testid, aria-label changed) → **UPDATE** (patch selector values)
117
+ - Selectors removed with no confirmed replacement → **UPDATE** (find replacement; use blueprint if available, diff otherwise)
118
+ - Major restructuring (most selectors gone) → **REGENERATE** (page structure fundamentally changed)
119
+ - All selectors gone, no replacement elements → **DELETE** (page no longer testable)
120
+ - Cannot confirm without reading page object or additional files → **VERIFY**
121
+ - Hardcoded visible string wrapped in a translation key (e.g. a literal moved to \`$t('...')\`) and the default-locale rendering is unchanged → **UPDATE**: the assertion now silently depends on the active locale. Add explicit locale context to the test fixture or assert via the translation key so the test tracks the translation rather than a hardcoded copy.`;
122
+ }
123
+ /**
124
+ * Check if page objects/fixtures referenced by the test still exist.
125
+ */
126
+ export function buildUiCheckPageObjects() {
127
+ return `Do the page objects or fixture files imported by this UI test still exist, and are their underlying selectors still valid?
128
+
129
+ **Always follow imports to find page objects — regardless of whether blueprints were captured.**
130
+ Page objects can live anywhere in the repo (not just \`page-objects/\` or \`fixtures/\`). Do not assume a fixed directory. The only reliable way to find them is to follow the spec file's \`import\` statements.
131
+
132
+ **Protocol for every browser test (Playwright/Cypress/Selenium):**
133
+ 1. Read the spec file and collect all local \`import\` statements (skip \`@playwright/test\`, \`vitest\`, etc. — only follow relative or repo-local paths).
134
+ 2. For each imported file, read it and extract the selectors it encapsulates — \`locator()\`, \`getByTestId()\`, \`getByRole()\`, \`getByLabel()\`, \`data-test-*\`, \`aria-label\`, CSS class strings.
135
+ 3. Compare those selectors against the diff: if a selector value the page object uses was renamed or removed in a \`-\` diff line → the page object needs **UPDATE**.
136
+ 4. **Update the page object file, not the spec file** — the page object is the selector source of truth. In the recommendation: set \`testFile\` to the **spec file** (so it shows correctly in the maintenance report) and set \`pomFile\` to the **page object path** (so \`skyramp_actions\` edits the right file). If the edit is rejected at action time, the agent or developer can address it separately.
137
+ 5. If the imported file is missing or unreadable → **VERIFY** (cannot confirm without the file).
138
+
139
+ A clean spec with no raw selector calls is not a reason to assign VERIFY — it is expected when page objects are used. Assign the action based on the POM's selectors.
140
+
141
+ Diff signals to look for in the spec's imports:
142
+ - Import deleted: \`- import { OldPage } from './pages/OldPage'\` → page object removed
143
+ - Import path changed: paired \`-\`/\`+\` on the same import → page object moved or renamed
144
+ - New import added: \`+ import { NewFixture } from '../fixtures/new'\` → new dependency
145
+
146
+ Actions:
147
+ - Page object's selectors match a renamed/removed diff value → **UPDATE** (patch the page object)
148
+ - Page object import path changed in the diff → **UPDATE** (fix the import in the spec)
149
+ - Page object file deleted, no replacement found → **UPDATE** (inline selectors or find alternative)
150
+ - Selectors in page object unchanged and diff does not affect them → **IGNORE**
151
+ - **All POM selectors are gone with no migration path in the diff** (the component's interaction model changed fundamentally — no ` + ` lines showing replacement selectors) → **REGENERATE**. Do not stay at VERIFY after reading the POM and confirming this — escalate once you have confirmed the failure.`;
152
+ }
153
+ /**
154
+ * Check for component behavior changes that affect assertions.
155
+ */
156
+ export function buildUiCheckBehavioralChanges() {
157
+ return `Have component behaviors changed in ways that would break test assertions?
158
+
159
+ Diff signals to look for:
160
+ - State changes: \`+ isLoading\`, \`+ isOpen\`, \`+ showModal\`, \`+ disabled\`
161
+ - Visibility conditions: \`+ if (condition) return null\`, \`+ {condition && <Component />}\`
162
+ - Interaction changes: \`+ onClick={() => setOpen(true)}\`, \`+ require('auth')\`
163
+ - Async behavior: \`+ setTimeout\`, \`+ debounce\`, \`+ async/await\` in handlers
164
+
165
+ Actions:
166
+ - New loading states → **UPDATE** (add waitFor or assertion for loading state)
167
+ - New conditional rendering → **UPDATE** (update assertions to match new conditions)
168
+ - Auth behavior changed → **UPDATE** (adjust login flow in test)
169
+ - Async delays added → **UPDATE** (add appropriate waits/timeouts)
170
+ - Shared component changed (used across multiple pages, dialogs, or layouts): if the test covers only one consumer and you have not read all other consumers, assign **VERIFY** — the blast radius across other consumers is indeterminate. Only assign UPDATE if you have confirmed the change is isolated to this test's consumer.`;
171
+ }
172
+ /**
173
+ * Assign the final UI drift action.
174
+ */
175
+ export function buildUiCheckAssignAction() {
176
+ return buildUiAssignAction(`**Component/widget test matching (source-only):**
177
+ - Read the test to check if it directly renders a changed component — if so, it is impacted.
178
+ - If the test only imports but does not directly render the component, assign VERIFY (component-import signal — cannot confirm impact without reading the parent).
179
+ - If neither signal fires, assign **IGNORE**.`);
180
+ }
181
+ /**
182
+ * Build the UI drift output checklist.
183
+ */
184
+ export function buildUiDriftOutputChecklist(stateFile, tests) {
185
+ const finalStep = `Call \`skyramp_actions\` with \`stateFile: "${stateFile ?? "<stateFile>"}"\`.`;
186
+ const hasTests = (tests?.length ?? 0) > 0;
187
+ const testList = hasTests
188
+ ? tests.map(t => `- ${t.testFile}${t.source === "external" ? " [external]" : ""}`).join("\n")
189
+ : "- (none)";
190
+ const noTestsNote = !hasTests
191
+ ? `\nNo existing tests were found. Call \`skyramp_actions\` with \`recommendations: []\`.\n`
192
+ : "";
193
+ const existingTestSection = `**Tests (${tests?.length ?? 0} total) — assess ALL of the following:**
194
+ ${testList}
195
+ ${noTestsNote}
196
+
197
+ **Execution policy:**
198
+ - Skyramp-generated tests: UPDATE/REGENERATE/DELETE applied automatically by \`skyramp_actions\`. Run via \`skyramp_execute_test\`.
199
+ - External tests (Playwright, Cypress, RTL, etc.): \`skyramp_actions\` applies UPDATE edits to \`testFile\` — which can be the test itself, a snapshot file, a page object, or a fixture. Application source files are automatically rejected. After a successful UPDATE, call \`skyramp_execute_test\` to confirm the patched test passes (the test environment is already running). Record the result as the after-state in the report. For DELETE and REGENERATE on external tests, see the execution policy in the decision rules above.
200
+
201
+ For each UI test above, output one structured entry in \`recommendations[]\`:
202
+ - **IGNORE**: \`{testFile} — IGNORE: {specific reason the diff cannot reach this test — name the route or component that differs}\` — do not pass to \`skyramp_actions\`
203
+ - **VERIFY**: \`{testFile} — VERIFY: {uncertain element, e.g. "selectors hidden in page object not yet read"}\`
204
+ \`\`\`
205
+ Test: {testFile}
206
+ Action: UPDATE
207
+ Rationale: UPDATE because {quoted diff line}; {selector/route/behavior change}
208
+ UpdateInstructions: {what must change — specific selectors to update, routes to patch, assertions to add}
209
+ \`\`\`
210
+ \`\`\`
211
+ Test: {testFile}
212
+ Action: REGENERATE
213
+ Rationale: REGENERATE because {quoted diff line}; page structure fundamentally changed
214
+ \`\`\`
215
+ \`\`\`
216
+ Test: {testFile}
217
+ Action: DELETE
218
+ Rationale: DELETE because {quoted diff line}; all routes removed or no valid selectors remain
219
+ \`\`\`
220
+
221
+ Be concise — one line per VERIFY/IGNORE entry.`;
222
+ const sections = [existingTestSection, finalStep];
223
+ return `<ui_output_format>
224
+ Complete ALL of the following:
225
+
226
+ ${sections.join("\n\n")}
227
+ </ui_output_format>`;
228
+ }
@@ -11,7 +11,12 @@ const FLUTTER_DART_EXT = /\.dart$/i;
11
11
  const LIKELY_FRONTEND_EXT = /\.(tsx|jsx)$/i;
12
12
  // Ambiguous extensions need directory context to distinguish frontend from backend.
13
13
  const AMBIGUOUS_FRONTEND_PATTERN = /\.(tsx?|jsx?|css|scss|less|html?|erb|jsp|asp|jinja2?|twig)$/i;
14
- const API_DIR_PATTERN = /\/(api|routes?|controllers?|routers?|handlers?|endpoints?|server)\//;
14
+ // /api/ always signals backend — even .tsx files (e.g. Next.js pages/api/*.tsx handlers).
15
+ const STRICT_API_DIR_PATTERN = /\/(api)\//i;
16
+ // Framework route dirs that contain React page components in modern full-stack apps
17
+ // (Medusa Admin, Remix, React Router v6). These only block non-.tsx/.jsx files —
18
+ // a .tsx file in routes/ is a page component, not a route handler.
19
+ const FRAMEWORK_ROUTE_DIR_PATTERN = /\/(routes?|controllers?|routers?|handlers?|endpoints?|server)\//i;
15
20
  const FRONTEND_DIR_PATTERN = /(^|\/)(components?|pages?|views?|layouts?|app|src\/app|frontend|client|public|styles?|templates?)\//i;
16
21
  /**
17
22
  * Returns true if the repository root's `pubspec.yaml` declares a Flutter
@@ -120,7 +125,12 @@ export function isFrontendFile(filePath, { hasFlutterSdkDep = false } = {}) {
120
125
  return true;
121
126
  if (hasFlutterSdkDep && FLUTTER_DART_EXT.test(filePath))
122
127
  return true;
123
- if (API_DIR_PATTERN.test(filePath))
128
+ // /api/ always means backend regardless of extension (Next.js API routes use .tsx).
129
+ if (STRICT_API_DIR_PATTERN.test(filePath))
130
+ return false;
131
+ // routes/, controllers/ etc. block .ts/.js but not .tsx/.jsx — Medusa Admin and
132
+ // React Router place page components under routes/, not API handlers.
133
+ if (!LIKELY_FRONTEND_EXT.test(filePath) && FRAMEWORK_ROUTE_DIR_PATTERN.test(filePath))
124
134
  return false;
125
135
  if (LIKELY_FRONTEND_EXT.test(filePath))
126
136
  return true;
@@ -126,7 +126,7 @@ ${hasRelatedRepos ? `
126
126
 
127
127
  2. **Maintain existing tests:**
128
128
 
129
- a. Call \`skyramp_analyze_test_health\` with \`stateFile\` (from \`skyramp_analyze_changes\` output). **Do NOT read application source files** (routes, models, controllers) — all change information you need is in the \`skyramp_analyze_changes\` output and the diff.
129
+ a. Call \`skyramp_analyze_test_health\` with \`stateFile\` (from \`skyramp_analyze_changes\` output). Pass \`blueprintCaptured: true\` when \`browser_blueprint\` was called successfully earlier in this session — see the parameter description for when this applies. **Do NOT read application source files** (routes, models, controllers) — all change information you need is in the \`skyramp_analyze_changes\` output and the diff. Exception: the UI drift pre-scan (\`UI_SYMBOL_PRESCAN\`) may instruct you to read changed frontend files to extract exported symbols — follow those instructions when present.
130
130
 
131
131
  b. Write \`updateInstructions\` for each UPDATE or REGENERATE test before calling \`skyramp_actions\` — articulating the change first prevents file content from overriding diff-based reasoning.
132
132
 
@@ -572,13 +572,14 @@ In these cases:
572
572
  Otherwise: in \`newTestsCreated\`, you must have exactly ${maxGenerate} budget-counting new tests for the planned GENERATE items. Only new files (ADD) created for those planned GENERATE items count toward this ${maxGenerate} target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the ${maxGenerate} budget. If you have fewer than ${maxGenerate} budget-counting new tests, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer than ${maxGenerate} budget-counting new tests if all candidates failed after retry AND the fallback single-contract test also failed.
573
573
 
574
574
  Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
575
+
575
576
  ${hasRelatedRepos ? `
576
577
  - **MULTI-REPO attribution**: Set the \`repository\` field (\`owner/repo\`) on EVERY \`newTestsCreated\`, \`testResults\`, \`issuesFound\`, and \`additionalRecommendations\` item — including items about the PRIMARY repo — so each finding is unambiguously attributed. The primary repo's \`repository\` is \`${primaryRepo || "<the primary repo's owner/repo>"}\`; items derived from a related repo's diff (from the \`<related_repositories>\` analysis) carry that repo's \`repository\` value. In \`businessCaseAnalysis\`, include a short per-repo subsection and call out any cross-repo correlations you found.
577
578
  ` : ""}
578
579
  - **additionalRecommendations**: AT MOST ${maxRecommendations - maxGenerate} items.
579
580
  - For \`testType: "contract"\` entries: **\`primaryEndpoint\` is required** (e.g. \`"GET /api/v1/users/{user_id}"\`). The tool will reject the submission without it — do not omit it or you will be forced to resubmit.
580
581
  - For \`testType: "integration"\` or \`"e2e"\` entries: omit \`primaryEndpoint\` — use \`description\` to list the endpoints involved instead.
581
- - **testMaintenance**: Use \`[]\` **only** if no existing Skyramp tests were found in the repository. If existing tests were found (any score), include one entry per test. Set \`action\` to the exact drift action you chose from the Action Decision Matrix (\`UPDATE\`, \`REGENERATE\`, \`DELETE\`, \`VERIFY\`, or \`IGNORE\`). For UPDATE/REGENERATE/DELETE tests that were modified and executed, populate \`beforeDetails\` and \`afterDetails\` from what you observed in \`skyramp_execute_test\` output one line each, no newlines. For passing runs write the count and timing (e.g. \`"4 passed in 15.09s"\`). For failing runs write the test name and failure reason (e.g. \`"1 failed: test_foo - assert X == Y — shared-state pollution"\`). For VERIFY/IGNORE tests (not modified), omit \`beforeDetails\` and \`afterDetails\`. Do **not** add entries for tests that were not returned by the health analysis.
582
+ - **\`testMaintenance\`**: include one entry per test that received UPDATE, REGENERATE, DELETE, or VERIFY. Do NOT include IGNOREa test assessed and confirmed clean is not a maintenance action and adds noise to the report.
582
583
  - **testResults**: For UI/E2E test entries, include the \`videoPath\` field if \`skyramp_execute_test\` returned one.
583
584
 
584
585
  ---
@@ -9,14 +9,16 @@ export interface TestDiscoveryOptions {
9
9
  * Resource names derived from endpoints in changed files (e.g. ["orders", "products"]).
10
10
  * - Non-empty array: external files partitioned by relevance; only relevant files
11
11
  * get full endpoint extraction. May be the sentinel `["unknown"]` when endpoints
12
- * exist but resource names are unresolvable — treated as "endpoints present" to
13
- * avoid skipping external coverage. Also used for "add field" PRs where the changed
14
- * files have no endpoint definitions but the scanner found related endpoints nearby.
15
- * - Empty array `[]`: PR mode with no endpoints from diff or scanner — external
16
- * tests are skipped entirely to avoid flooding context.
12
+ * exist but resource names are unresolvable — most files score 0 (low-relevance)
13
+ * and are excluded; only the few that happen to match the sentinel are kept.
14
+ * - Empty array `[]`: PR mode with no endpoints from diff or scanner API/integration
15
+ * external tests are excluded entirely; UI tests may still be promoted via hasFrontendChanges.
17
16
  * - `undefined`: full-repo mode — external tests capped at MAX_EXTERNAL_FULL_REPO.
18
17
  */
19
18
  changedResources?: string[];
19
+ /** True when the diff contains frontend files — promotes UI test files for framework
20
+ * detection regardless of changedResources. Orthogonal to API relevance scoring. */
21
+ hasFrontendChanges?: boolean;
20
22
  }
21
23
  export declare class TestDiscoveryService {
22
24
  private readonly EXCLUDED_DIRS;
@@ -45,9 +47,13 @@ export declare class TestDiscoveryService {
45
47
  * Example: "test_orders_api.py" vs ["orders"] → score 1.
46
48
  */
47
49
  private scoreRelevance;
50
+ private readonly MAX_CONTENT_PROMOTED;
48
51
  /**
49
52
  * Partition external test files into relevant (score > 0) and low-relevance (score = 0).
50
- * Signal: path/name token overlap with changedResources.
53
+ * Two-pass: filename token overlap first (primary); content-based endpoint path scan
54
+ * as a capped fallback for tests misnamed relative to the endpoint they exercise
55
+ * (e.g. test_checkout_flow.py testing /api/orders scores 0 by name but matches in content).
56
+ * Content matches are capped at MAX_CONTENT_PROMOTED to bound token impact.
51
57
  */
52
58
  private partitionByRelevance;
53
59
  /**
@@ -101,9 +107,6 @@ export declare class TestDiscoveryService {
101
107
  * Falls back to directory/filename heuristics since external tests lack Skyramp command lines.
102
108
  */
103
109
  private detectExternalTestType;
104
- /**
105
- * Detect test framework from external test file content by checking imports.
106
- */
107
110
  private detectExternalFramework;
108
111
  /**
109
112
  * Detect programming language from file extension
@@ -23,7 +23,8 @@ export class TestDiscoveryService {
23
23
  SUPPORTED_EXTENSIONS = [
24
24
  ".py", ".js", ".ts", ".tsx", ".jsx", ".java",
25
25
  ".go", ".rb", ".kt", ".kts", ".cs", ".rs",
26
- ".php", ".scala", ".swift", ".m",
26
+ ".php", ".scala", ".swift", ".m", ".dart",
27
+ ".snap", // Jest/Vitest snapshot files — may need updating when UI changes break them
27
28
  ];
28
29
  // Concurrency control for parallel operations
29
30
  MAX_CONCURRENT_OPERATIONS = 10;
@@ -47,11 +48,20 @@ export class TestDiscoveryService {
47
48
  // Skyramp-generated test suffixes — must be matched even outside test dirs
48
49
  /.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
49
50
  ];
50
- // Directory patterns that signal test directories — works with both / and \ separators
51
+ // Directory patterns that signal test-owned files — works with both / and \ separators.
52
+ // Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
53
+ // Playwright, Cypress, and other frameworks for page objects and support code.
51
54
  TEST_DIR_PATTERNS = [
52
55
  /[\\/]tests?[\\/]/,
53
56
  /[\\/]__tests__[\\/]/,
57
+ /[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
54
58
  /[\\/]spec[\\/]/,
59
+ /[\\/]e2e[\\/]/,
60
+ /[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
61
+ /[\\/]playwright[\\/]/, // Playwright fixtures and helpers
62
+ /[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
63
+ /[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
64
+ /[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
55
65
  ];
56
66
  /**
57
67
  * Discover all tests under testDir — both Skyramp-generated and external (user-written).
@@ -93,48 +103,57 @@ export class TestDiscoveryService {
93
103
  // Full-repo mode (changedResources is undefined):
94
104
  // No diff context — all external files treated as potentially relevant.
95
105
  // Cap at MAX_EXTERNAL_FULL_REPO to avoid reading hundreds of files.
96
- const { changedResources } = options;
106
+ const { changedResources, hasFrontendChanges } = options;
97
107
  let relevantExternal;
98
- let otherExternal;
108
+ // Includes .e2e.ts — Playwright/Cypress tests that navigate pages. These get
109
+ // testType="e2e" from path detection but are never named *.test.ts or *.spec.ts
110
+ // so without explicit promotion they'd be excluded from stateData.existingTests,
111
+ // making the server-side symbol grep unable to find them.
112
+ const UI_TEST_EXT = /\.(test|spec|e2e)\.(tsx|jsx|ts|js)$/;
113
+ const SEC_EXT = /\.(snap|ambr|txt|yml|yaml)$/;
114
+ // Step 1 — API relevance partitioning (driven by changedResources).
115
+ // Two-pass: filename token overlap first, then content-based endpoint path scan
116
+ // for tests that are misnamed relative to the endpoint they exercise.
99
117
  if (changedResources?.length) {
100
- // PR mode with detected endpoints — partition by relevance
101
- ({ relevant: relevantExternal, other: otherExternal } =
102
- this.partitionByRelevance(classified.external, changedResources));
118
+ ({ relevant: relevantExternal } =
119
+ this.partitionByRelevance(classified.external, changedResources, classified.contentCache));
103
120
  }
104
121
  else if (changedResources !== undefined) {
105
- // PR mode with no path-resolvable endpoints — no path-based scoring available.
106
122
  relevantExternal = [];
107
- otherExternal = [];
108
123
  }
109
124
  else {
110
- // Full-repo mode (no diff context): cap full-extraction set, remaining become name-only
111
125
  relevantExternal = classified.external.slice(0, this.MAX_EXTERNAL_FULL_REPO);
112
- otherExternal = classified.external.slice(this.MAX_EXTERNAL_FULL_REPO);
113
126
  }
114
- logger.info(`External tests: ${relevantExternal.length} relevant (full extraction), ${otherExternal.length} low-relevance (name-only)`);
115
- // Free cached content for files we will not extract — their content is not needed.
116
- for (const f of otherExternal) {
117
- classified.contentCache.delete(f);
127
+ // Step 2 UI test promotion (driven by hasFrontendChanges, independent of Step 1).
128
+ if (hasFrontendChanges) {
129
+ const alreadyRelevant = new Set(relevantExternal);
130
+ const promoted = classified.external.filter(f => UI_TEST_EXT.test(f) && !alreadyRelevant.has(f));
131
+ const promotedSet = new Set(promoted);
132
+ // Promote snapshot/fixture siblings of scored AND newly promoted UI tests.
133
+ // Include promoted files so siblings are found on UI-only PRs where
134
+ // relevantExternal started empty before Step 2.
135
+ const relevantTestBasenames = new Set([...relevantExternal, ...promoted].filter(f => UI_TEST_EXT.test(f)).map(f => path.basename(f)));
136
+ const promotedSecondary = classified.external.filter(f => !alreadyRelevant.has(f) &&
137
+ !promotedSet.has(f) &&
138
+ relevantTestBasenames.has(path.basename(f).replace(SEC_EXT, '')));
139
+ const allPromoted = [...promoted, ...promotedSecondary];
140
+ if (allPromoted.length > 0) {
141
+ logger.info(`Promoting ${promoted.length} UI test + ${promotedSecondary.length} secondary file(s) for content reading`);
142
+ relevantExternal = [...relevantExternal, ...allPromoted];
143
+ }
144
+ }
145
+ // Free content cache for external files not selected for full extraction.
146
+ const relevantSet = new Set(relevantExternal);
147
+ for (const f of classified.external) {
148
+ if (!relevantSet.has(f))
149
+ classified.contentCache.delete(f);
118
150
  }
119
- // Full metadata + endpoint extraction only for relevant external files
151
+ logger.info(`External tests: ${relevantExternal.length} relevant (full extraction)`);
120
152
  const relevantExternalTests = await this.processFilesInBatches(relevantExternal, true, classified.contentCache);
121
- // Minimal entries for low-relevance files — no content reads, no endpoint extraction
122
- const otherExternalTests = otherExternal.map(filePath => ({
123
- testFile: filePath,
124
- testType: this.detectExternalTestType(filePath, ""),
125
- language: this.detectLanguage(filePath),
126
- framework: "",
127
- apiEndpoint: "",
128
- source: TestSource.External,
129
- }));
130
- const externalTests = [...relevantExternalTests, ...otherExternalTests];
153
+ const externalTests = relevantExternalTests;
131
154
  logger.info(`Discovered ${skyrampTests.length} Skyramp tests, ${externalTests.length} external tests`);
132
155
  return {
133
156
  tests: [...skyrampTests, ...externalTests],
134
- // Expose the relevant file paths so callers can build read instructions for the LLM.
135
- // These are the files the agent should read in Step 0 for definitive coverage
136
- // verification — includes files where regex extraction may have failed (e.g. helper
137
- // abstractions) since those most need semantic inspection.
138
157
  relevantExternalTestPaths: relevantExternal,
139
158
  };
140
159
  }
@@ -168,22 +187,46 @@ export class TestDiscoveryService {
168
187
  return parts.length > 1 && parts.every(p => p.length >= 3 && expanded.has(p));
169
188
  }).length;
170
189
  }
190
+ // Max additional tests promoted via content-based scoring on top of filename matches.
191
+ // Content matches are lower-confidence than filename matches — cap to avoid token bloat.
192
+ MAX_CONTENT_PROMOTED = 5;
171
193
  /**
172
194
  * Partition external test files into relevant (score > 0) and low-relevance (score = 0).
173
- * Signal: path/name token overlap with changedResources.
195
+ * Two-pass: filename token overlap first (primary); content-based endpoint path scan
196
+ * as a capped fallback for tests misnamed relative to the endpoint they exercise
197
+ * (e.g. test_checkout_flow.py testing /api/orders scores 0 by name but matches in content).
198
+ * Content matches are capped at MAX_CONTENT_PROMOTED to bound token impact.
174
199
  */
175
- partitionByRelevance(files, changedResources) {
176
- const relevant = [];
177
- const other = [];
200
+ partitionByRelevance(files, changedResources, contentCache) {
201
+ const nameMatched = [];
202
+ const contentCandidates = [];
203
+ // Precompile content patterns once — avoids 20 resources × 150 files = 3K compilations.
204
+ const contentPatterns = contentCache
205
+ ? changedResources.map(r => {
206
+ // Escape all regex metacharacters, then restore [-/] as a character class
207
+ // for hyphens and slashes that appear in resource names like "order-items".
208
+ const escaped = r.toLowerCase()
209
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // escape regex metacharacters
210
+ .replace(/-|\//g, "[-/]"); // treat hyphens/slashes interchangeably
211
+ return new RegExp(`["'\`][^"'\`]*/${escaped}`, "i");
212
+ })
213
+ : [];
178
214
  for (const f of files) {
179
215
  if (this.scoreRelevance(f, changedResources) > 0) {
180
- relevant.push(f);
216
+ nameMatched.push(f);
181
217
  }
182
- else {
183
- other.push(f);
218
+ else if (contentPatterns.length > 0) {
219
+ const content = contentCache.get(f) ?? "";
220
+ if (content && contentPatterns.some(p => p.test(content))) {
221
+ contentCandidates.push(f);
222
+ }
184
223
  }
185
224
  }
186
- return { relevant, other };
225
+ const contentMatched = contentCandidates.slice(0, this.MAX_CONTENT_PROMOTED);
226
+ if (contentMatched.length > 0) {
227
+ logger.info(`Content-based relevance: promoting ${contentMatched.length} test(s) (capped at ${this.MAX_CONTENT_PROMOTED})`);
228
+ }
229
+ return { relevant: [...nameMatched, ...contentMatched] };
187
230
  }
188
231
  /**
189
232
  * Process test files in parallel batches with concurrency control
@@ -458,6 +501,10 @@ export class TestDiscoveryService {
458
501
  return "integration";
459
502
  if (/e2e|end[_-]to[_-]end/.test(lowerPath))
460
503
  return "e2e";
504
+ // Playwright/Cypress directory paths → e2e even without "e2e" in the filename.
505
+ // Ensures correct testType for tests where content wasn't read (framework stays "").
506
+ if (/[\\/]playwright[\\/]|[\\/]cypress[\\/]/.test(lowerPath))
507
+ return "e2e";
461
508
  if (/contract/.test(lowerPath))
462
509
  return "contract";
463
510
  if (/smoke/.test(lowerPath))
@@ -466,35 +513,57 @@ export class TestDiscoveryService {
466
513
  return "load";
467
514
  if (/[\\/]unit[\\/]|_unit/.test(lowerPath))
468
515
  return "unit";
469
- // Heuristic: files making HTTP calls are likely integration/API tests
470
516
  if (/(?:requests|fetch|axios|supertest|RestAssured)/i.test(content))
471
517
  return "integration";
472
518
  return "unknown";
473
519
  }
474
- /**
475
- * Detect test framework from external test file content by checking imports.
476
- */
477
520
  detectExternalFramework(content) {
478
- // Python
521
+ if (/from selenium\b|import selenium\b|from appium\b|import appium\b/.test(content))
522
+ return "selenium";
523
+ // pytest-playwright: check before the generic pytest return.
524
+ if (/from playwright\.(?:sync|async)_api\b|import playwright\./.test(content)) {
525
+ const usesPage = /\bpage\.(?:goto|click|locator|get_by|fill|type|press|wait_for)\b/.test(content);
526
+ const usesRequestOnly = !usesPage && /\brequest\.(?:get|post|put|patch|delete)\b/.test(content);
527
+ return usesRequestOnly ? "playwright" : "playwright-ui";
528
+ }
479
529
  if (/import pytest|from pytest/.test(content))
480
530
  return "pytest";
481
531
  if (/import unittest|from unittest/.test(content))
482
532
  return "unittest";
483
- // JavaScript/TypeScript
484
533
  if (/from ['"]vitest['"]|import.*vitest|require\(['"]vitest['"]\)|\bvi\./.test(content))
485
534
  return "vitest";
486
- if (/from ['"]@jest\/globals['"]|from ['"]jest['"]|import.*jest|require\(['"]jest['"]\)|jest\./.test(content) ||
487
- (/\b(?:test|it)\s*\(/.test(content) && /\bexpect\s*\(/.test(content))) {
488
- return "jest";
489
- }
535
+ // Component/UI test frameworks — checked BEFORE the broad jest heuristic below,
536
+ // because RTL and similar frameworks use it()/expect() which would otherwise
537
+ // match the jest fallback and hide the more specific framework signal.
538
+ if (/from ['"]@testing-library\/react['"]|from ['"]@testing-library\/react-native['"]/.test(content))
539
+ return "rtl";
540
+ if (/from ['"]@testing-library\/vue['"]|from ['"]@vue\/test-utils['"]/.test(content))
541
+ return "vue-test-utils";
542
+ if (/from ['"]@testing-library\/angular['"]/.test(content))
543
+ return "angular-testbed";
544
+ if (/from ['"]@testing-library\/svelte['"]/.test(content))
545
+ return "svelte-testing-library";
546
+ if (/from ['"]enzyme['"]|require\(['"]enzyme['"]\)/.test(content))
547
+ return "enzyme";
490
548
  if (/from ['"]mocha['"]|require\(['"]mocha['"]\)/.test(content))
491
549
  return "mocha";
492
550
  if (/supertest/.test(content))
493
551
  return "supertest";
494
- if (/from ['"]@playwright/.test(content))
495
- return "playwright";
552
+ if (/from ['"]@playwright/.test(content)) {
553
+ // Distinguish browser UI usage (page fixture) from REST API usage (request fixture only).
554
+ // Using Playwright for API testing is an edge case — the default classification is UI.
555
+ const usesPage = /\bpage\.(goto|click|locator|getBy|fill|type|press|waitFor)\b/.test(content);
556
+ const usesRequestOnly = !usesPage && /\brequest\.(get|post|put|patch|delete)\b/.test(content);
557
+ return usesRequestOnly ? "playwright" : "playwright-ui";
558
+ }
496
559
  if (/from ['"]cypress/.test(content))
497
- return "cypress";
560
+ return "cypress-ui";
561
+ if (/package:flutter_test\/flutter_test\.dart/.test(content))
562
+ return "flutter-widget";
563
+ if (/from ['"]@jest\/globals['"]|from ['"]jest['"]|import.*jest|require\(['"]jest['"]\)|jest\./.test(content) ||
564
+ (/\b(?:test|it)\s*\(/.test(content) && /\bexpect\s*\(/.test(content))) {
565
+ return "jest";
566
+ }
498
567
  // Java / Kotlin — check TestNG before @Test since TestNG also uses @Test
499
568
  if (/import org\.testng/.test(content))
500
569
  return "testng";
@@ -502,6 +571,10 @@ export class TestDiscoveryService {
502
571
  return "junit";
503
572
  if (/RestAssured/.test(content))
504
573
  return "rest-assured";
574
+ if (/import androidx\.test\b|import com\.android\.support\.test\b|EspressoIdlingResource|onView\(/.test(content))
575
+ return "espresso";
576
+ if (/import io\.appium\./.test(content))
577
+ return "appium";
505
578
  // Go
506
579
  if (/import.*"testing"|func\s+Test\w+\(t\s+\*testing\.T\)/.test(content))
507
580
  return "go-testing";
@@ -555,6 +628,7 @@ export class TestDiscoveryService {
555
628
  ".scala": "scala",
556
629
  ".swift": "swift",
557
630
  ".m": "objective-c",
631
+ ".dart": "dart",
558
632
  };
559
633
  return languageMap[ext] || "unknown";
560
634
  }