@skyramp/mcp 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/playwright/registerPlaywrightTools.js +10 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +106 -5
- package/build/prompts/test-recommendation/scopeAssessment.test.js +128 -1
- package/build/prompts/testbot/testbot-prompts.js +2 -2
- package/build/prompts/testbot/testbot-prompts.test.js +21 -0
- package/build/tools/test-management/analyzeChangesTool.js +8 -2
- package/build/tools/test-management/uiAnalyzeChangesTool.js +8 -2
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +47 -0
- package/build/utils/dartRouteExtractor.js +319 -0
- package/build/utils/dartRouteExtractor.test.js +307 -0
- package/build/utils/uiPageEnumerator.js +67 -0
- package/build/utils/uiPageEnumerator.test.js +222 -0
- package/node_modules/playwright/lib/mcp/skyramp/common/mouseActions.js +123 -0
- package/node_modules/playwright/lib/mcp/skyramp/index.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +359 -0
- package/node_modules/playwright/lib/mcp/skyramp/mouseActionTool.js +131 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +146 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +313 -5
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +32 -14
- package/package.json +1 -1
|
@@ -40,13 +40,23 @@ export async function registerPlaywrightTools(server, options) {
|
|
|
40
40
|
'browser_select_option',
|
|
41
41
|
'browser_hover',
|
|
42
42
|
'browser_drag',
|
|
43
|
+
'browser_mouse_move_xy',
|
|
44
|
+
'browser_mouse_click_xy',
|
|
45
|
+
'browser_mouse_drag_xy',
|
|
46
|
+
'browser_mouse_down',
|
|
47
|
+
'browser_mouse_up',
|
|
48
|
+
'browser_file_upload',
|
|
49
|
+
'browser_evaluate',
|
|
43
50
|
'browser_tabs',
|
|
44
51
|
'browser_navigate_back',
|
|
45
52
|
'browser_wait_for',
|
|
46
53
|
'browser_take_screenshot',
|
|
47
54
|
'browser_assert',
|
|
48
55
|
'browser_assert_api_request',
|
|
56
|
+
'browser_assert_table_cell',
|
|
49
57
|
'skyramp_export_zip',
|
|
58
|
+
'skyramp_load_trace',
|
|
59
|
+
'browser_mouse_action',
|
|
50
60
|
// DOM Analyzer tools (Phase C)
|
|
51
61
|
'browser_blueprint',
|
|
52
62
|
'browser_blueprint_diff',
|
|
@@ -1,24 +1,125 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { MAX_RECOMMENDATIONS, MAX_TESTS_TO_GENERATE } from "./recommendationSections.js";
|
|
2
4
|
// .vue and .svelte cannot be route handlers in any framework — always frontend.
|
|
3
5
|
const ALWAYS_FRONTEND_EXT = /\.(vue|svelte)$/i;
|
|
6
|
+
// .dart is always frontend in Flutter projects (no server-side Dart web framework
|
|
7
|
+
// in scope). Gated on hasFlutterPubspec so non-Flutter Dart files (rare, but
|
|
8
|
+
// possible — e.g. Dart-on-server backends) don't accidentally classify as UI.
|
|
9
|
+
const FLUTTER_DART_EXT = /\.dart$/i;
|
|
4
10
|
// .tsx/.jsx are usually UI components but can be Next.js API handlers (pages/api/*.tsx).
|
|
5
11
|
const LIKELY_FRONTEND_EXT = /\.(tsx|jsx)$/i;
|
|
6
12
|
// Ambiguous extensions need directory context to distinguish frontend from backend.
|
|
7
13
|
const AMBIGUOUS_FRONTEND_PATTERN = /\.(tsx?|jsx?|css|scss|less|html?|erb|jsp|asp|jinja2?|twig)$/i;
|
|
8
14
|
const API_DIR_PATTERN = /\/(api|routes?|controllers?|routers?|handlers?|endpoints?|server)\//;
|
|
9
15
|
const FRONTEND_DIR_PATTERN = /(^|\/)(components?|pages?|views?|layouts?|app|src\/app|frontend|client|public|styles?|templates?)\//i;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if the repository root's `pubspec.yaml` declares a Flutter
|
|
18
|
+
* SDK dependency (i.e., a `flutter:` key with `sdk: flutter` under
|
|
19
|
+
* `dependencies:`). This is the load-bearing signal that the project
|
|
20
|
+
* is a Flutter UI app or package, NOT a pure Dart server / CLI / library
|
|
21
|
+
* (those have a `pubspec.yaml` too but no `sdk: flutter` dep).
|
|
22
|
+
*
|
|
23
|
+
* Used as the gate for treating `.dart` files as frontend in
|
|
24
|
+
* `isFrontendFile`. Pure-Dart-backend repos correctly fall through to
|
|
25
|
+
* the existing classifier behaviour (no `.dart` recognition).
|
|
26
|
+
*
|
|
27
|
+
* Synchronous + best-effort: returns false on any read/parse error.
|
|
28
|
+
* Cheap because pubspec.yaml is small and we only call this once per
|
|
29
|
+
* tool invocation (computed at the budget-driving boundary, then passed
|
|
30
|
+
* through as an option).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Common subdir locations for `pubspec.yaml` in real Flutter repos:
|
|
34
|
+
* - root (single-app — birdle, customer SPA)
|
|
35
|
+
* - app/, mobile/, frontend/, web/ (mixed-stack repos)
|
|
36
|
+
* - apps/<name>/, packages/<name>/ (monorepos — depth-2 walked)
|
|
37
|
+
*
|
|
38
|
+
* We stop at the first pubspec we find that declares `sdk: flutter`. Pure-Dart
|
|
39
|
+
* subprojects (e.g. a CLI tool in `tools/`) are correctly skipped because their
|
|
40
|
+
* pubspec doesn't have the Flutter SDK line.
|
|
41
|
+
*
|
|
42
|
+
* Less-common shapes that are NOT auto-discovered: `services/mobile/`,
|
|
43
|
+
* `clients/<name>/`, etc. If a customer's repo uses one of those, add the
|
|
44
|
+
* parent dir here or tell them to symlink `pubspec.yaml` at the root.
|
|
45
|
+
*/
|
|
46
|
+
const PUBSPEC_SEARCH_DIRS = [
|
|
47
|
+
".",
|
|
48
|
+
"app",
|
|
49
|
+
"mobile",
|
|
50
|
+
"frontend",
|
|
51
|
+
"web",
|
|
52
|
+
"client",
|
|
53
|
+
];
|
|
54
|
+
const PUBSPEC_MONOREPO_PARENTS = ["apps", "packages"];
|
|
55
|
+
export function hasFlutterSdkDep(repositoryPath) {
|
|
56
|
+
for (const subdir of PUBSPEC_SEARCH_DIRS) {
|
|
57
|
+
if (checkPubspec(path.join(repositoryPath, subdir, "pubspec.yaml"))) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Monorepo shape: apps/<name>/pubspec.yaml, packages/<name>/pubspec.yaml.
|
|
62
|
+
// One level of fan-out — enough for typical Flutter monorepos, cheap to walk.
|
|
63
|
+
for (const parent of PUBSPEC_MONOREPO_PARENTS) {
|
|
64
|
+
const parentPath = path.join(repositoryPath, parent);
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(parentPath, { withFileTypes: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
if (checkPubspec(path.join(parentPath, entry.name, "pubspec.yaml"))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function checkPubspec(pubspecPath) {
|
|
83
|
+
try {
|
|
84
|
+
if (!fs.existsSync(pubspecPath))
|
|
85
|
+
return false;
|
|
86
|
+
const content = fs.readFileSync(pubspecPath, "utf8");
|
|
87
|
+
// Detect the `flutter:` SDK dep — proper YAML parsing would be overkill.
|
|
88
|
+
// The canonical pattern is:
|
|
89
|
+
// dependencies:
|
|
90
|
+
// flutter:
|
|
91
|
+
// sdk: flutter
|
|
92
|
+
// The `sdk: flutter` line under a `flutter:` key is unique to Flutter
|
|
93
|
+
// projects; pure-Dart projects use `sdk: dart` or no sdk: line at all.
|
|
94
|
+
return /^\s+sdk:\s*flutter\s*$/m.test(content);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
10
100
|
/**
|
|
11
101
|
* Returns true if the file path is a frontend file.
|
|
12
102
|
*
|
|
13
|
-
*
|
|
103
|
+
* Four tiers:
|
|
14
104
|
* 1. .vue / .svelte — always frontend; these file types cannot be route handlers.
|
|
15
|
-
* 2. .
|
|
16
|
-
*
|
|
105
|
+
* 2. .dart — always frontend IFF `hasFlutterSdkDep` is true (Flutter project).
|
|
106
|
+
* Sits in tier 1 because there's no server-side Dart web framework in scope
|
|
107
|
+
* when the Flutter SDK dep is present, so `API_DIR_PATTERN` doesn't apply.
|
|
108
|
+
* 3. .tsx / .jsx — frontend unless in an API/backend directory (e.g. pages/api/).
|
|
109
|
+
* 4. Ambiguous extensions (.ts, .js, .css, .html, …) — require a recognised
|
|
17
110
|
* frontend directory AND must not be in an API/backend directory.
|
|
111
|
+
*
|
|
112
|
+
* The `hasFlutterSdkDep` opt is passed in by the budget-driving callers
|
|
113
|
+
* (uiAnalyzeChangesTool, analyzeChangesTool) which have repository context.
|
|
114
|
+
* Pure-path callers (test-recommendation-prompt, analysisOutputPrompt,
|
|
115
|
+
* scopeAssessment's own buildScopeAssessmentSection) default to false —
|
|
116
|
+
* they don't drive the budget.
|
|
18
117
|
*/
|
|
19
|
-
export function isFrontendFile(filePath) {
|
|
118
|
+
export function isFrontendFile(filePath, { hasFlutterSdkDep = false } = {}) {
|
|
20
119
|
if (ALWAYS_FRONTEND_EXT.test(filePath))
|
|
21
120
|
return true;
|
|
121
|
+
if (hasFlutterSdkDep && FLUTTER_DART_EXT.test(filePath))
|
|
122
|
+
return true;
|
|
22
123
|
if (API_DIR_PATTERN.test(filePath))
|
|
23
124
|
return false;
|
|
24
125
|
if (LIKELY_FRONTEND_EXT.test(filePath))
|
|
@@ -113,7 +214,7 @@ Read the Changed Files list and endpoint changes above, then work through the fo
|
|
|
113
214
|
|
|
114
215
|
**Step A — Classify changed files:**
|
|
115
216
|
Count each type from the diff context (ignore generated test files, lock files, and build artifacts):
|
|
116
|
-
- **Frontend files**: .vue / .svelte anywhere (always UI components). .tsx / .jsx anywhere except in api/, routes/, routers/, controllers/, handlers/, endpoints/, or server/ directories. .ts / .js / .html / .css / .scss / .less / .erb / .jsp / .asp / .jinja2 / .twig only when in a frontend directory (components/, pages/, views/, layouts/, app/, frontend/, client/, styles/, templates/).
|
|
217
|
+
- **Frontend files**: .vue / .svelte anywhere (always UI components). .dart anywhere (always UI in a Flutter project — repo has a \`pubspec.yaml\` with \`sdk: flutter\`). .tsx / .jsx anywhere except in api/, routes/, routers/, controllers/, handlers/, endpoints/, or server/ directories. .ts / .js / .html / .css / .scss / .less / .erb / .jsp / .asp / .jinja2 / .twig only when in a frontend directory (components/, pages/, views/, layouts/, app/, frontend/, client/, styles/, templates/).
|
|
117
218
|
- **Backend files**: route handlers, controllers, services, models, API modules, middleware, config with business logic
|
|
118
219
|
- **Non-application** (exclude from test value): CSS-only, copy/string changes, README, CI config with no logic
|
|
119
220
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
jest.mock("@skyramp/skyramp", () => ({
|
|
2
2
|
WorkspaceConfigManager: { create: jest.fn() },
|
|
3
3
|
}));
|
|
4
|
-
import
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import * as os from "os";
|
|
7
|
+
import { hasFlutterSdkDep, isFrontendFile, isTestFile, buildScopeAssessmentSection } from "./scopeAssessment.js";
|
|
5
8
|
// ---------------------------------------------------------------------------
|
|
6
9
|
// isFrontendFile
|
|
7
10
|
// ---------------------------------------------------------------------------
|
|
@@ -56,6 +59,130 @@ describe("isFrontendFile", () => {
|
|
|
56
59
|
it("returns false for a plain TS API client in utils/ (no frontend dir)", () => {
|
|
57
60
|
expect(isFrontendFile("utils/apiClient.ts")).toBe(false);
|
|
58
61
|
});
|
|
62
|
+
// Flutter / Dart support — gated on hasFlutterSdkDep.
|
|
63
|
+
// Without the flag, .dart is unrecognised (legacy behaviour). With it,
|
|
64
|
+
// .dart is always-frontend (tier 2, before API_DIR_PATTERN).
|
|
65
|
+
describe(".dart files (Flutter)", () => {
|
|
66
|
+
it("returns false for .dart without hasFlutterSdkDep (default)", () => {
|
|
67
|
+
expect(isFrontendFile("lib/main.dart")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it("returns false for .dart with hasFlutterSdkDep: false", () => {
|
|
70
|
+
expect(isFrontendFile("lib/main.dart", { hasFlutterSdkDep: false })).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it("returns true for .dart with hasFlutterSdkDep: true", () => {
|
|
73
|
+
expect(isFrontendFile("lib/main.dart", { hasFlutterSdkDep: true })).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it("returns true for nested .dart files in a Flutter project", () => {
|
|
76
|
+
expect(isFrontendFile("lib/widgets/game_board.dart", { hasFlutterSdkDep: true })).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it("returns true for .dart even in a directory that would otherwise look API-like (Flutter has no server-side Dart in scope)", () => {
|
|
79
|
+
// .dart sits in tier 2 (above API_DIR_PATTERN) so it's frontend even
|
|
80
|
+
// under api/ — there's no server-side Dart web framework that would
|
|
81
|
+
// make this ambiguous in the Flutter case.
|
|
82
|
+
expect(isFrontendFile("lib/api/client.dart", { hasFlutterSdkDep: true })).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it("returns true for game.dart at repo root in a Flutter project", () => {
|
|
85
|
+
expect(isFrontendFile("game.dart", { hasFlutterSdkDep: true })).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// hasFlutterSdkDep
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
describe("hasFlutterSdkDep", () => {
|
|
93
|
+
let tmpDir;
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "flutter-pubspec-"));
|
|
96
|
+
});
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
it("returns false when pubspec.yaml is absent", () => {
|
|
101
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it("returns true when pubspec.yaml has the canonical Flutter SDK dep", () => {
|
|
104
|
+
fs.writeFileSync(path.join(tmpDir, "pubspec.yaml"), `name: birdle
|
|
105
|
+
description: A Flutter app.
|
|
106
|
+
dependencies:
|
|
107
|
+
flutter:
|
|
108
|
+
sdk: flutter
|
|
109
|
+
cupertino_icons: ^1.0.6
|
|
110
|
+
`);
|
|
111
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it("returns false for a pure Dart project (no flutter SDK dep)", () => {
|
|
114
|
+
// Pure Dart server (e.g. shelf, dart_frog) has pubspec.yaml but no flutter SDK
|
|
115
|
+
fs.writeFileSync(path.join(tmpDir, "pubspec.yaml"), `name: my_dart_server
|
|
116
|
+
description: A Dart server using shelf.
|
|
117
|
+
environment:
|
|
118
|
+
sdk: ">=3.0.0 <4.0.0"
|
|
119
|
+
dependencies:
|
|
120
|
+
shelf: ^1.4.0
|
|
121
|
+
shelf_router: ^1.1.4
|
|
122
|
+
`);
|
|
123
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it("returns false for a Dart CLI tool (no flutter SDK dep)", () => {
|
|
126
|
+
fs.writeFileSync(path.join(tmpDir, "pubspec.yaml"), `name: my_cli
|
|
127
|
+
description: A Dart CLI.
|
|
128
|
+
environment:
|
|
129
|
+
sdk: ^3.5.0
|
|
130
|
+
dependencies:
|
|
131
|
+
args: ^2.5.0
|
|
132
|
+
`);
|
|
133
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
it("returns false on a malformed pubspec.yaml (graceful failure)", () => {
|
|
136
|
+
fs.writeFileSync(path.join(tmpDir, "pubspec.yaml"), "::: not yaml ::: \x00");
|
|
137
|
+
// Our regex-based check still works on string content; this is more of
|
|
138
|
+
// a "doesn't throw" test. Returns false because the malformed content
|
|
139
|
+
// doesn't match the sdk: flutter pattern.
|
|
140
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
it("returns true even with extra whitespace and comments in pubspec.yaml", () => {
|
|
143
|
+
fs.writeFileSync(path.join(tmpDir, "pubspec.yaml"), `name: birdle
|
|
144
|
+
# This is the SDK dep
|
|
145
|
+
dependencies:
|
|
146
|
+
flutter:
|
|
147
|
+
sdk: flutter
|
|
148
|
+
`);
|
|
149
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
// Subdir search — common shapes in real Flutter repos.
|
|
152
|
+
it("returns true when pubspec.yaml lives in app/", () => {
|
|
153
|
+
fs.mkdirSync(path.join(tmpDir, "app"));
|
|
154
|
+
fs.writeFileSync(path.join(tmpDir, "app", "pubspec.yaml"), "name: app\ndependencies:\n flutter:\n sdk: flutter\n");
|
|
155
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it("returns true when pubspec.yaml lives in mobile/", () => {
|
|
158
|
+
fs.mkdirSync(path.join(tmpDir, "mobile"));
|
|
159
|
+
fs.writeFileSync(path.join(tmpDir, "mobile", "pubspec.yaml"), "name: app\ndependencies:\n flutter:\n sdk: flutter\n");
|
|
160
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it("returns true for monorepo with apps/<name>/pubspec.yaml", () => {
|
|
163
|
+
fs.mkdirSync(path.join(tmpDir, "apps", "customer-app"), { recursive: true });
|
|
164
|
+
fs.writeFileSync(path.join(tmpDir, "apps", "customer-app", "pubspec.yaml"), "name: customer_app\ndependencies:\n flutter:\n sdk: flutter\n");
|
|
165
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it("returns true for monorepo with packages/<name>/pubspec.yaml", () => {
|
|
168
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "ui"), { recursive: true });
|
|
169
|
+
fs.writeFileSync(path.join(tmpDir, "packages", "ui", "pubspec.yaml"), "name: ui\ndependencies:\n flutter:\n sdk: flutter\n");
|
|
170
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
it("returns false when monorepo subdirs only contain pure-Dart packages", () => {
|
|
173
|
+
// Mixed monorepo: a Dart CLI in packages/cli/, no Flutter app anywhere.
|
|
174
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "cli"), { recursive: true });
|
|
175
|
+
fs.writeFileSync(path.join(tmpDir, "packages", "cli", "pubspec.yaml"), "name: cli\ndependencies:\n args: ^2.5.0\n");
|
|
176
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
it("does not walk arbitrary subdirs (e.g. node_modules-style nested deps)", () => {
|
|
179
|
+
// A pubspec deep under an unsupported subdir should NOT be picked up.
|
|
180
|
+
// Without this guard, a transitive dependency's pubspec could falsely
|
|
181
|
+
// flag a non-Flutter repo as Flutter.
|
|
182
|
+
fs.mkdirSync(path.join(tmpDir, "vendor", "nested", "lib"), { recursive: true });
|
|
183
|
+
fs.writeFileSync(path.join(tmpDir, "vendor", "nested", "lib", "pubspec.yaml"), "name: nested\ndependencies:\n flutter:\n sdk: flutter\n");
|
|
184
|
+
expect(hasFlutterSdkDep(tmpDir)).toBe(false);
|
|
185
|
+
});
|
|
59
186
|
});
|
|
60
187
|
// ---------------------------------------------------------------------------
|
|
61
188
|
// isTestFile
|
|
@@ -127,7 +127,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
|
|
|
127
127
|
Keep advancing until you have created exactly ${maxGenerate} new test files OR exhausted all candidates.
|
|
128
128
|
- 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\`.
|
|
129
129
|
- 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.
|
|
130
|
-
- **UI test priority**: If the
|
|
130
|
+
- **UI test priority**: If the PR scope assessment shows any UI/E2E budget OR \`uiContext.changedFrontendFiles\` is non-empty (the deterministic server signal — populated for all supported frontend file types including \`.tsx\`/\`.jsx\`/\`.vue\`/\`.svelte\`/\`.dart\`), 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.
|
|
131
131
|
**Skip only if one of these conditions is met:**
|
|
132
132
|
- **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
|
|
133
133
|
- **(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:
|
|
@@ -273,7 +273,7 @@ If a test **generation** tool call fails:
|
|
|
273
273
|
1. **Retry once** with the same parameters.
|
|
274
274
|
2. If it fails again, **skip** that candidate and move to the next ranked candidate.
|
|
275
275
|
3. If all candidates in the GENERATE set fail, fall back to generating the **simplest possible test**: a single contract test for the highest-scored endpoint (GET → 200 or POST → 201).
|
|
276
|
-
**Exception — frontend-only PRs**: If the diff modifies ONLY frontend files (\`.tsx\`, \`.jsx\`, \`.vue\`, \`.svelte\`, \`.css\`, \`.html\`) AND browser recording was not possible, do NOT generate a backend fallback contract test — it is irrelevant to the PR. Instead move ALL GENERATE candidates to \`additionalRecommendations\` and proceed to Task 3.
|
|
276
|
+
**Exception — frontend-only PRs**: If the diff modifies ONLY frontend files (\`.tsx\`, \`.jsx\`, \`.vue\`, \`.svelte\`, \`.dart\`, \`.css\`, \`.html\`) AND browser recording was not possible, do NOT generate a backend fallback contract test — it is irrelevant to the PR. Instead move ALL GENERATE candidates to \`additionalRecommendations\` and proceed to Task 3.
|
|
277
277
|
4. Log skipped candidates in \`issuesFound\` with the error message.
|
|
278
278
|
|
|
279
279
|
If a test **execution** (\`skyramp_execute_test\`) fails for a newly generated test:
|
|
@@ -360,4 +360,25 @@ describe("testbot prompt blueprint-grounded recommendations (slice 4)", () => {
|
|
|
360
360
|
// Make sure we removed the old capturedBlueprints threading directive.
|
|
361
361
|
expect(prompt).not.toMatch(/capturedBlueprints/);
|
|
362
362
|
});
|
|
363
|
+
// Flutter support — both the generalised UI trigger wording and the
|
|
364
|
+
// canvas/empty-ARIA issuesFound rule should appear in the prompt.
|
|
365
|
+
it("UI test priority defers to the deterministic server signal (uiContext.changedFrontendFiles), not just hard-coded extensions", () => {
|
|
366
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
367
|
+
// The trigger must reference the server signal (changedFrontendFiles)
|
|
368
|
+
// — that's what makes it framework-agnostic. Hard-coded extension lists
|
|
369
|
+
// become illustrative, not gating.
|
|
370
|
+
expect(prompt).toMatch(/uiContext.*changedFrontendFiles|changedFrontendFiles.*uiContext/);
|
|
371
|
+
// .dart should appear in the supported-types example list
|
|
372
|
+
expect(prompt).toMatch(/\.dart/);
|
|
373
|
+
});
|
|
374
|
+
it("frontend-only PR exception lists .dart alongside other frontend extensions", () => {
|
|
375
|
+
const prompt = getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath);
|
|
376
|
+
// The frontend-only-PR exception block (Task 2 backend-fallback skip)
|
|
377
|
+
// must include .dart so a Flutter-only PR doesn't emit a backend
|
|
378
|
+
// contract test as a fallback.
|
|
379
|
+
const exceptionBlock = prompt.slice(prompt.indexOf("Exception — frontend-only PRs"), prompt.indexOf("Exception — frontend-only PRs") + 800);
|
|
380
|
+
expect(exceptionBlock).toContain(".dart");
|
|
381
|
+
expect(exceptionBlock).toContain(".tsx");
|
|
382
|
+
expect(exceptionBlock).toContain(".vue");
|
|
383
|
+
});
|
|
363
384
|
});
|
|
@@ -10,7 +10,7 @@ import { parseWorkspaceAuthType, getDefaultAuthHeader, WorkspaceAuthType, readWo
|
|
|
10
10
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
11
11
|
import { StateManager, registerSession, storeSessionData, setTestsRepoDir, } from "../../utils/AnalysisStateManager.js";
|
|
12
12
|
import { buildRecommendationPrompt } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
|
|
13
|
-
import { isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
13
|
+
import { hasFlutterSdkDep, isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
14
14
|
import { enumerateCandidateUiPages } from "../../utils/uiPageEnumerator.js";
|
|
15
15
|
import { MAX_RECOMMENDATIONS, MAX_TESTS_TO_GENERATE } from "../../prompts/test-recommendation/recommendationSections.js";
|
|
16
16
|
import { TestDiscoveryService } from "../../services/TestDiscoveryService.js";
|
|
@@ -1123,7 +1123,13 @@ Combines API endpoint scanning, branch diff computation, and test discovery into
|
|
|
1123
1123
|
// pass isFrontendFile (any .ts under a frontend directory matches
|
|
1124
1124
|
// the tier-3 rule) but aren't UI source we'd want to ground page
|
|
1125
1125
|
// enumeration in.
|
|
1126
|
-
|
|
1126
|
+
// Compute hasFlutterSdkDep once and pass it through so .dart files
|
|
1127
|
+
// in a Flutter project are recognised as frontend. See Confluence
|
|
1128
|
+
// "Flutter support in Testbot" — this is the second budget-driving
|
|
1129
|
+
// call site that must thread the flag (the other is
|
|
1130
|
+
// uiAnalyzeChangesTool). Both must agree to avoid silent divergence.
|
|
1131
|
+
const flutterSdk = hasFlutterSdkDep(params.repositoryPath);
|
|
1132
|
+
const frontendFiles = changedFiles.filter((f) => isFrontendFile(f, { hasFlutterSdkDep: flutterSdk }) && !isTestFile(f));
|
|
1127
1133
|
if (frontendFiles.length === 0)
|
|
1128
1134
|
return undefined;
|
|
1129
1135
|
const candidateUiPages = await enumerateCandidateUiPages(params.repositoryPath, frontendFiles);
|
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
5
|
import { enumerateCandidateUiPages } from "../../utils/uiPageEnumerator.js";
|
|
6
|
-
import { isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
6
|
+
import { hasFlutterSdkDep, isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
7
7
|
import { parseChangedFilesFromDiff } from "../../utils/branchDiff.js";
|
|
8
8
|
import { toolText } from "../../utils/utils.js";
|
|
9
9
|
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
@@ -49,7 +49,13 @@ export async function runUiAnalyzeChanges(params) {
|
|
|
49
49
|
instructions: DIFF_FILE_MISSING_INSTRUCTIONS,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// Compute hasFlutterSdkDep once at the tool boundary; pass into isFrontendFile
|
|
53
|
+
// so .dart files are recognised as frontend in Flutter repos. See Confluence
|
|
54
|
+
// "Flutter support in Testbot" — this is one of two budget-driving call sites
|
|
55
|
+
// that must thread the flag (the other is analyzeChangesTool). Without it,
|
|
56
|
+
// a Flutter PR shows zero frontend files and never enters the UI pipeline.
|
|
57
|
+
const flutterSdk = hasFlutterSdkDep(repoPath);
|
|
58
|
+
const frontendFiles = changedFiles.filter((f) => isFrontendFile(f, { hasFlutterSdkDep: flutterSdk }) && !isTestFile(f));
|
|
53
59
|
if (frontendFiles.length === 0) {
|
|
54
60
|
const uiContext = {
|
|
55
61
|
changedFrontendFiles: [],
|
|
@@ -97,4 +97,51 @@ describe("runUiAnalyzeChanges", () => {
|
|
|
97
97
|
expect(result.instructions).toMatch(/source-grounded/i);
|
|
98
98
|
fs.rmSync(repo, { recursive: true, force: true });
|
|
99
99
|
});
|
|
100
|
+
// Flutter support — the headline case the Confluence plan validates.
|
|
101
|
+
// Without the .dart classifier change, a Flutter PR returns
|
|
102
|
+
// changedFrontendFiles: [] → "No UI changes detected" → agent skips
|
|
103
|
+
// browser_navigate entirely. With the change + a Flutter pubspec, the
|
|
104
|
+
// diff registers as frontend and the agent gets told to navigate.
|
|
105
|
+
it("recognises .dart files as frontend in a Flutter project (with pubspec.yaml)", async () => {
|
|
106
|
+
readSpy.mockResolvedValue({
|
|
107
|
+
services: [
|
|
108
|
+
{
|
|
109
|
+
serviceName: "birdle-frontend",
|
|
110
|
+
language: "typescript",
|
|
111
|
+
framework: "playwright",
|
|
112
|
+
api: { baseUrl: "http://localhost:8080" },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const repo = makeRepoWithDiff(["lib/main.dart"]);
|
|
117
|
+
// Write a Flutter pubspec.yaml at the repo root so hasFlutterSdkDep returns true
|
|
118
|
+
fs.writeFileSync(path.join(repo, "pubspec.yaml"), `name: birdle\ndependencies:\n flutter:\n sdk: flutter\n`);
|
|
119
|
+
const result = await runUiAnalyzeChanges({ repositoryPath: repo });
|
|
120
|
+
expect(result.uiContext.changedFrontendFiles).toContain("lib/main.dart");
|
|
121
|
+
expect(result.uiContext.candidateUiPages.length).toBeGreaterThan(0);
|
|
122
|
+
expect(result.instructions).toMatch(/browser_navigate/);
|
|
123
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
it("does NOT recognise .dart files as frontend without a Flutter pubspec.yaml", async () => {
|
|
126
|
+
// Pure Dart project (server/CLI) — pubspec.yaml absent or missing the SDK dep.
|
|
127
|
+
// .dart files should NOT enter the UI pipeline.
|
|
128
|
+
readSpy.mockResolvedValue({ services: [] });
|
|
129
|
+
const repo = makeRepoWithDiff(["bin/server.dart"]);
|
|
130
|
+
// No pubspec.yaml written at all
|
|
131
|
+
const result = await runUiAnalyzeChanges({ repositoryPath: repo });
|
|
132
|
+
expect(result.uiContext.changedFrontendFiles).toEqual([]);
|
|
133
|
+
expect(result.uiContext.candidateUiPages).toEqual([]);
|
|
134
|
+
expect(result.instructions).toMatch(/no UI changes/i);
|
|
135
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
it("does NOT recognise .dart files as frontend in a pure Dart project (pubspec without flutter SDK dep)", async () => {
|
|
138
|
+
// pubspec.yaml exists but declares a Dart server (e.g. shelf) — no flutter SDK
|
|
139
|
+
readSpy.mockResolvedValue({ services: [] });
|
|
140
|
+
const repo = makeRepoWithDiff(["bin/server.dart"]);
|
|
141
|
+
fs.writeFileSync(path.join(repo, "pubspec.yaml"), `name: my_dart_server\ndependencies:\n shelf: ^1.4.0\n`);
|
|
142
|
+
const result = await runUiAnalyzeChanges({ repositoryPath: repo });
|
|
143
|
+
expect(result.uiContext.changedFrontendFiles).toEqual([]);
|
|
144
|
+
expect(result.uiContext.candidateUiPages).toEqual([]);
|
|
145
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
146
|
+
});
|
|
100
147
|
});
|