@skyramp/mcp 0.3.0-rc.1 → 0.3.0
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/prompts/test-maintenance/actionsInstructions.js +4 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +9 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +8 -7
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -5
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +11 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +22 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +23 -31
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +7 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +20 -6
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +10 -1
- package/build/prompts/test-recommendation/scopeAssessment.js +27 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.js +35 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +5 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +61 -0
- package/build/prompts/testbot/testbot-prompts.js +7 -5
- package/build/prompts/testbot/testbot-prompts.test.js +1 -1
- package/build/services/AnalyticsService.js +37 -2
- package/build/services/TestDiscoveryService.d.ts +38 -4
- package/build/services/TestDiscoveryService.js +187 -16
- package/build/services/TestDiscoveryService.test.js +232 -10
- package/build/services/TestExecutionService.d.ts +12 -1
- package/build/services/TestExecutionService.js +20 -41
- package/build/services/TestGenerationService.d.ts +8 -0
- package/build/services/TestGenerationService.js +62 -17
- package/build/services/TestGenerationService.test.js +67 -1
- package/build/tools/auth/loginTool.js +1 -1
- package/build/tools/auth/logoutTool.js +1 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +30 -1
- package/build/tools/fixErrorTool.js +1 -1
- package/build/tools/one-click/oneClickTool.js +1 -1
- package/build/tools/test-management/actionsTool.js +76 -41
- package/build/tools/test-management/actionsTool.test.js +104 -38
- package/build/tools/test-management/analyzeChangesTool.js +102 -4
- package/build/tools/test-management/analyzeChangesTool.test.js +3 -1
- package/build/tools/test-management/analyzeTestHealthTool.js +9 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +1 -1
- package/build/tools/trace/startTraceCollectionTool.js +1 -1
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +6 -6
- package/build/types/TestAnalysis.d.ts +13 -0
- package/build/utils/dartRouteExtractor.js +8 -30
- package/build/utils/docker.test.js +1 -1
- package/build/utils/fileWalk.d.ts +71 -0
- package/build/utils/fileWalk.js +79 -0
- package/build/utils/fileWalk.test.d.ts +1 -0
- package/build/utils/fileWalk.test.js +252 -0
- package/build/utils/frontendIntegration.js +20 -36
- package/build/utils/frontendSelectors.d.ts +34 -0
- package/build/utils/frontendSelectors.js +124 -0
- package/build/utils/frontendSelectors.test.d.ts +1 -0
- package/build/utils/frontendSelectors.test.js +118 -0
- package/build/utils/importerHop.d.ts +18 -0
- package/build/utils/importerHop.js +48 -3
- package/build/utils/importerHop.test.js +71 -1
- package/build/utils/pythonMountPrefixes.d.ts +25 -0
- package/build/utils/pythonMountPrefixes.js +347 -0
- package/build/utils/pythonMountPrefixes.test.d.ts +1 -0
- package/build/utils/pythonMountPrefixes.test.js +113 -0
- package/build/utils/repoScanner.js +30 -23
- package/build/utils/repoScanner.test.js +98 -0
- package/build/utils/routeParsers.d.ts +7 -1
- package/build/utils/routeParsers.js +32 -18
- package/build/utils/sourceRouteExtractor.js +17 -40
- package/build/utils/trace-parser.js +7 -19
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/walkerCharacterization.test.d.ts +1 -0
- package/build/utils/walkerCharacterization.test.js +233 -0
- package/node_modules/playwright/lib/common/config.js +1 -1
- package/node_modules/playwright/lib/common/configLoader.js +1 -1
- package/node_modules/playwright/lib/common/fixtures.js +1 -1
- package/node_modules/playwright/lib/common/testType.js +1 -1
- package/node_modules/playwright/lib/index.js +2 -2
- package/node_modules/playwright/lib/isomorphic/testTree.js +1 -1
- package/node_modules/playwright/lib/matchers/expect.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tab.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/tracing.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +7 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +3 -1
- package/node_modules/playwright/lib/reporters/base.js +2 -2
- package/node_modules/playwright/lib/reporters/list.js +1 -1
- package/node_modules/playwright/lib/reporters/teleEmitter.js +1 -1
- package/node_modules/playwright/lib/worker/testInfo.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Cqp3cwEJ.js → codeMirrorModule-aszq5EdG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-CYf9adZh.js → defaultSettingsView-BxS7Jm4s.js} +94 -94
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.d4gkVSou.js → index.D4JTTy4R.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.u4_8VnCV.js → uiMode.DaRMQKOI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/._cli.js +0 -0
- package/node_modules/playwright/._index.d.ts +0 -0
- package/node_modules/playwright/._index.js +0 -0
- package/node_modules/playwright/._index.mjs +0 -0
- package/node_modules/playwright/._jsx-runtime.js +0 -0
- package/node_modules/playwright/._jsx-runtime.mjs +0 -0
- package/node_modules/playwright/._lib +0 -0
- package/node_modules/playwright/._node_modules +0 -0
- package/node_modules/playwright/._package.json +0 -0
- package/node_modules/playwright/._test.d.ts +0 -0
- package/node_modules/playwright/._test.js +0 -0
- package/node_modules/playwright/._test.mjs +0 -0
- package/node_modules/playwright/._types +0 -0
- package/node_modules/playwright/._vitest.config.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._.npmignore +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._README.md +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._ThirdPartyNotices.txt +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bin +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._browsers.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._bundles +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._cli.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.d.ts +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.js +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._index.mjs +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._lib +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._package.json +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._src +0 -0
- package/node_modules/playwright/node_modules/playwright-core/._types +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { collectFiles, walkDir, walkSourceFiles } from "./fileWalk.js";
|
|
5
|
+
/** Materialize a file tree under a fresh temp dir. Keys are relative paths;
|
|
6
|
+
* a value of `null` creates an (empty) directory instead of a file. */
|
|
7
|
+
function makeTree(files) {
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "filewalk-"));
|
|
9
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
10
|
+
const abs = path.join(root, rel);
|
|
11
|
+
if (content === null) {
|
|
12
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
16
|
+
fs.writeFileSync(abs, content);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
/** Sort + make paths relative to root for stable comparison. */
|
|
22
|
+
function rel(root, abs) {
|
|
23
|
+
return abs.map((p) => path.relative(root, p).replace(/\\/g, "/")).sort();
|
|
24
|
+
}
|
|
25
|
+
// `chmod 000` only makes a directory unreadable where the OS enforces the
|
|
26
|
+
// permission bits: not on Windows, and not for root (which bypasses them). Skip
|
|
27
|
+
// the best-effort-on-unreadable-dir tests where the setup can't actually fail a
|
|
28
|
+
// readdir, rather than let them false-fail. `it`/`it.skip` chosen at load time.
|
|
29
|
+
const permsEnforced = process.platform !== "win32" &&
|
|
30
|
+
(typeof process.getuid !== "function" || process.getuid() !== 0);
|
|
31
|
+
const itIfPerms = permsEnforced ? it : it.skip;
|
|
32
|
+
/** Collect the absolute file paths a walkDir generator yields. */
|
|
33
|
+
function walkPaths(root, opts) {
|
|
34
|
+
const seen = [];
|
|
35
|
+
for (const [, full] of walkDir(root, opts))
|
|
36
|
+
seen.push(full);
|
|
37
|
+
return seen;
|
|
38
|
+
}
|
|
39
|
+
describe("walkDir", () => {
|
|
40
|
+
it("yields every file depth-first as [entry, absolute path]", () => {
|
|
41
|
+
const root = makeTree({
|
|
42
|
+
"a.txt": "a",
|
|
43
|
+
"sub/b.txt": "b",
|
|
44
|
+
"sub/deep/c.txt": "c",
|
|
45
|
+
});
|
|
46
|
+
const seen = [];
|
|
47
|
+
for (const [entry, full] of walkDir(root)) {
|
|
48
|
+
expect(entry.isFile()).toBe(true);
|
|
49
|
+
expect(path.isAbsolute(full)).toBe(true);
|
|
50
|
+
seen.push(full);
|
|
51
|
+
}
|
|
52
|
+
expect(rel(root, seen)).toEqual(["a.txt", "sub/b.txt", "sub/deep/c.txt"]);
|
|
53
|
+
});
|
|
54
|
+
it("yields only files, never directory entries", () => {
|
|
55
|
+
const root = makeTree({ "sub/b.txt": "b", emptydir: null });
|
|
56
|
+
const names = [...walkDir(root)].map(([entry]) => entry.name);
|
|
57
|
+
expect(names).toEqual(["b.txt"]);
|
|
58
|
+
});
|
|
59
|
+
it("honors maxDepth = 0 (root files only, no descent)", () => {
|
|
60
|
+
const root = makeTree({ "a.txt": "a", "sub/b.txt": "b" });
|
|
61
|
+
expect(rel(root, walkPaths(root, { maxDepth: 0 }))).toEqual(["a.txt"]);
|
|
62
|
+
});
|
|
63
|
+
it("honors maxDepth = 1 (root + one level down)", () => {
|
|
64
|
+
const root = makeTree({
|
|
65
|
+
"a.txt": "a",
|
|
66
|
+
"sub/b.txt": "b",
|
|
67
|
+
"sub/deep/c.txt": "c",
|
|
68
|
+
});
|
|
69
|
+
expect(rel(root, walkPaths(root, { maxDepth: 1 }))).toEqual([
|
|
70
|
+
"a.txt",
|
|
71
|
+
"sub/b.txt",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
it("skips directories for which shouldSkipDir returns true", () => {
|
|
75
|
+
const root = makeTree({
|
|
76
|
+
"keep/a.txt": "a",
|
|
77
|
+
"node_modules/x.txt": "x",
|
|
78
|
+
});
|
|
79
|
+
const out = walkPaths(root, {
|
|
80
|
+
shouldSkipDir: (entry) => entry.name === "node_modules",
|
|
81
|
+
});
|
|
82
|
+
expect(rel(root, out)).toEqual(["keep/a.txt"]);
|
|
83
|
+
});
|
|
84
|
+
it("passes the directory's absolute path to shouldSkipDir", () => {
|
|
85
|
+
const root = makeTree({ "sub/a.txt": "a" });
|
|
86
|
+
[
|
|
87
|
+
...walkDir(root, {
|
|
88
|
+
shouldSkipDir: (entry, full) => {
|
|
89
|
+
expect(path.isAbsolute(full)).toBe(true);
|
|
90
|
+
expect(path.basename(full)).toBe(entry.name);
|
|
91
|
+
return false;
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
];
|
|
95
|
+
});
|
|
96
|
+
it("stops immediately when the consumer breaks (no further reads)", () => {
|
|
97
|
+
const root = makeTree({
|
|
98
|
+
"1.txt": "",
|
|
99
|
+
"2.txt": "",
|
|
100
|
+
"3.txt": "",
|
|
101
|
+
"sub/4.txt": "",
|
|
102
|
+
});
|
|
103
|
+
const seen = [];
|
|
104
|
+
for (const [, full] of walkDir(root)) {
|
|
105
|
+
seen.push(full);
|
|
106
|
+
break; // stop after the very first file
|
|
107
|
+
}
|
|
108
|
+
expect(seen.length).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
it("yields all files when the consumer never breaks", () => {
|
|
111
|
+
const root = makeTree({ "1.txt": "", "2.txt": "" });
|
|
112
|
+
expect(walkPaths(root).length).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
itIfPerms("is best-effort: an unreadable subdirectory is skipped, not fatal", () => {
|
|
115
|
+
const root = makeTree({ "a.txt": "a", "bad/b.txt": "b", "c.txt": "c" });
|
|
116
|
+
const badDir = path.join(root, "bad");
|
|
117
|
+
fs.chmodSync(badDir, 0o000);
|
|
118
|
+
try {
|
|
119
|
+
// Should not throw despite `bad` being unreadable, and should skip its
|
|
120
|
+
// contents while still visiting siblings.
|
|
121
|
+
expect(rel(root, walkPaths(root))).toEqual(["a.txt", "c.txt"]);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
fs.chmodSync(badDir, 0o755);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
it("does not throw when the root itself is unreadable/missing", () => {
|
|
128
|
+
expect(() => walkPaths(path.join(os.tmpdir(), "filewalk-does-not-exist-xyz"))).not.toThrow();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("walkSourceFiles (characterization — existing contract)", () => {
|
|
132
|
+
it("collects only files matching one of the extensions, as absolute paths", () => {
|
|
133
|
+
const root = makeTree({
|
|
134
|
+
"a.ts": "",
|
|
135
|
+
"b.tsx": "",
|
|
136
|
+
"c.js": "",
|
|
137
|
+
"d.py": "",
|
|
138
|
+
"sub/e.ts": "",
|
|
139
|
+
});
|
|
140
|
+
const out = walkSourceFiles(root, {
|
|
141
|
+
extensions: [".ts", ".tsx"],
|
|
142
|
+
skipDirs: new Set(),
|
|
143
|
+
});
|
|
144
|
+
expect(out.every((p) => path.isAbsolute(p))).toBe(true);
|
|
145
|
+
expect(rel(root, out)).toEqual(["a.ts", "b.tsx", "sub/e.ts"]);
|
|
146
|
+
});
|
|
147
|
+
it("skips directories named in skipDirs (matched on basename)", () => {
|
|
148
|
+
const root = makeTree({
|
|
149
|
+
"a.ts": "",
|
|
150
|
+
"node_modules/x.ts": "",
|
|
151
|
+
"src/b.ts": "",
|
|
152
|
+
});
|
|
153
|
+
const out = walkSourceFiles(root, {
|
|
154
|
+
extensions: [".ts"],
|
|
155
|
+
skipDirs: new Set(["node_modules"]),
|
|
156
|
+
});
|
|
157
|
+
expect(rel(root, out)).toEqual(["a.ts", "src/b.ts"]);
|
|
158
|
+
});
|
|
159
|
+
it("stops at exactly `limit` files when the cap is hit", () => {
|
|
160
|
+
const root = makeTree({
|
|
161
|
+
"a.ts": "",
|
|
162
|
+
"b.ts": "",
|
|
163
|
+
"c.ts": "",
|
|
164
|
+
"d.ts": "",
|
|
165
|
+
});
|
|
166
|
+
const out = walkSourceFiles(root, {
|
|
167
|
+
extensions: [".ts"],
|
|
168
|
+
skipDirs: new Set(),
|
|
169
|
+
limit: 2,
|
|
170
|
+
});
|
|
171
|
+
expect(out.length).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
it("returns all matches when under the limit", () => {
|
|
174
|
+
const root = makeTree({ "a.ts": "", "b.ts": "" });
|
|
175
|
+
const out = walkSourceFiles(root, {
|
|
176
|
+
extensions: [".ts"],
|
|
177
|
+
skipDirs: new Set(),
|
|
178
|
+
limit: 100,
|
|
179
|
+
});
|
|
180
|
+
expect(out.length).toBe(2);
|
|
181
|
+
});
|
|
182
|
+
it("does NOT skip dotfile directories by default", () => {
|
|
183
|
+
const root = makeTree({ ".hidden/a.ts": "", "b.ts": "" });
|
|
184
|
+
const out = walkSourceFiles(root, {
|
|
185
|
+
extensions: [".ts"],
|
|
186
|
+
skipDirs: new Set(),
|
|
187
|
+
});
|
|
188
|
+
expect(rel(root, out)).toEqual([".hidden/a.ts", "b.ts"]);
|
|
189
|
+
});
|
|
190
|
+
itIfPerms("is best-effort on unreadable directories", () => {
|
|
191
|
+
const root = makeTree({ "a.ts": "", "bad/b.ts": "" });
|
|
192
|
+
const badDir = path.join(root, "bad");
|
|
193
|
+
fs.chmodSync(badDir, 0o000);
|
|
194
|
+
try {
|
|
195
|
+
const out = walkSourceFiles(root, {
|
|
196
|
+
extensions: [".ts"],
|
|
197
|
+
skipDirs: new Set(),
|
|
198
|
+
});
|
|
199
|
+
expect(rel(root, out)).toEqual(["a.ts"]);
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
fs.chmodSync(badDir, 0o755);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe("collectFiles", () => {
|
|
207
|
+
it("collects files matching the predicate as absolute paths", () => {
|
|
208
|
+
const root = makeTree({ "a.dart": "", "b.txt": "", "sub/c.dart": "" });
|
|
209
|
+
const out = collectFiles(root, {
|
|
210
|
+
match: (entry) => entry.name.endsWith(".dart"),
|
|
211
|
+
});
|
|
212
|
+
expect(out.every((p) => path.isAbsolute(p))).toBe(true);
|
|
213
|
+
expect(rel(root, out)).toEqual(["a.dart", "sub/c.dart"]);
|
|
214
|
+
});
|
|
215
|
+
it("passes the absolute path to the matcher", () => {
|
|
216
|
+
const root = makeTree({ "keep/x.ts": "", "modules/y/index.ts": "" });
|
|
217
|
+
const out = collectFiles(root, {
|
|
218
|
+
match: (_entry, full) => full.includes(`${path.sep}modules${path.sep}`),
|
|
219
|
+
});
|
|
220
|
+
expect(rel(root, out)).toEqual(["modules/y/index.ts"]);
|
|
221
|
+
});
|
|
222
|
+
it("returns exactly `limit` files when the cap is hit", () => {
|
|
223
|
+
const root = makeTree({ "a.ts": "", "b.ts": "", "c.ts": "" });
|
|
224
|
+
const out = collectFiles(root, {
|
|
225
|
+
match: (entry) => entry.name.endsWith(".ts"),
|
|
226
|
+
limit: 2,
|
|
227
|
+
});
|
|
228
|
+
expect(out.length).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
it("returns nothing for `limit: 0` (does not collect a single file)", () => {
|
|
231
|
+
const root = makeTree({ "a.ts": "", "b.ts": "" });
|
|
232
|
+
const out = collectFiles(root, {
|
|
233
|
+
match: () => true,
|
|
234
|
+
limit: 0,
|
|
235
|
+
});
|
|
236
|
+
expect(out).toEqual([]);
|
|
237
|
+
});
|
|
238
|
+
it("forwards maxDepth and shouldSkipDir to the walk", () => {
|
|
239
|
+
const root = makeTree({
|
|
240
|
+
"a.ts": "",
|
|
241
|
+
"skip/b.ts": "",
|
|
242
|
+
"sub/deep/c.ts": "",
|
|
243
|
+
});
|
|
244
|
+
const out = collectFiles(root, {
|
|
245
|
+
match: (entry) => entry.name.endsWith(".ts"),
|
|
246
|
+
maxDepth: 1,
|
|
247
|
+
shouldSkipDir: (entry) => entry.name === "skip",
|
|
248
|
+
});
|
|
249
|
+
// maxDepth: 1 excludes sub/deep/c.ts; shouldSkipDir excludes skip/b.ts.
|
|
250
|
+
expect(rel(root, out)).toEqual(["a.ts"]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -28,6 +28,7 @@ import * as fs from "fs";
|
|
|
28
28
|
import * as path from "path";
|
|
29
29
|
import { isTestFile } from "../prompts/test-recommendation/scopeAssessment.js";
|
|
30
30
|
import { logger } from "./logger.js";
|
|
31
|
+
import { walkDir } from "./fileWalk.js";
|
|
31
32
|
import { buildPathSignatures } from "./pathSignatures.js";
|
|
32
33
|
import { IntegrationReason } from "../types/FrontendIntegration.js";
|
|
33
34
|
const MAX_IMPORTERS = 10;
|
|
@@ -117,48 +118,31 @@ function manualWalkGrep(repositoryPath, signatures) {
|
|
|
117
118
|
const results = [];
|
|
118
119
|
let filesScanned = 0;
|
|
119
120
|
let truncated = false;
|
|
120
|
-
const
|
|
121
|
-
|
|
121
|
+
const shouldSkipDir = (_entry, full) => {
|
|
122
|
+
// EXCLUDED_DIR_PATTERN matches on the "/"-normalized relative path.
|
|
123
|
+
const rel = path.relative(repositoryPath, full).replace(/\\/g, "/");
|
|
124
|
+
return EXCLUDED_DIR_PATTERN.test(rel + "/");
|
|
125
|
+
};
|
|
126
|
+
for (const [entry, abs] of walkDir(repositoryPath, { shouldSkipDir })) {
|
|
127
|
+
if (!FRONTEND_SOURCE_EXT_PATTERN.test(entry.name))
|
|
128
|
+
continue;
|
|
122
129
|
if (filesScanned >= MAX_FILES_WALKED) {
|
|
123
130
|
truncated = true;
|
|
124
|
-
break;
|
|
131
|
+
break; // stop the entire walk
|
|
125
132
|
}
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
filesScanned++;
|
|
134
|
+
// Normalize to forward slashes: on Windows, path.relative() returns
|
|
135
|
+
// "\\"-separated paths, but isTestFile assumes "/".
|
|
136
|
+
const rel = path.relative(repositoryPath, abs).replace(/\\/g, "/");
|
|
128
137
|
try {
|
|
129
|
-
|
|
138
|
+
if (fs.statSync(abs).size > MAX_FILE_SIZE_BYTES)
|
|
139
|
+
continue;
|
|
140
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
141
|
+
if (signatures.some((s) => content.includes(s)))
|
|
142
|
+
results.push(rel);
|
|
130
143
|
}
|
|
131
144
|
catch {
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
for (const entry of entries) {
|
|
135
|
-
const abs = path.join(dir, entry.name);
|
|
136
|
-
// Normalize to forward slashes: on Windows, path.relative() returns
|
|
137
|
-
// "\\"-separated paths, but EXCLUDED_DIR_PATTERN/isTestFile assume "/".
|
|
138
|
-
const rel = path.relative(repositoryPath, abs).replace(/\\/g, "/");
|
|
139
|
-
if (entry.isDirectory()) {
|
|
140
|
-
if (EXCLUDED_DIR_PATTERN.test(rel + "/"))
|
|
141
|
-
continue;
|
|
142
|
-
stack.push(abs);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (!FRONTEND_SOURCE_EXT_PATTERN.test(entry.name))
|
|
146
|
-
continue;
|
|
147
|
-
if (filesScanned >= MAX_FILES_WALKED) {
|
|
148
|
-
truncated = true;
|
|
149
|
-
break walk;
|
|
150
|
-
}
|
|
151
|
-
filesScanned++;
|
|
152
|
-
try {
|
|
153
|
-
if (fs.statSync(abs).size > MAX_FILE_SIZE_BYTES)
|
|
154
|
-
continue;
|
|
155
|
-
const content = fs.readFileSync(abs, "utf-8");
|
|
156
|
-
if (signatures.some((s) => content.includes(s)))
|
|
157
|
-
results.push(rel);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
145
|
+
// Unreadable/vanished file — skip it, keep scanning.
|
|
162
146
|
}
|
|
163
147
|
}
|
|
164
148
|
if (truncated && results.length === 0) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend selector extraction for UI-test-maintenance discovery (Pass 3, "selector-edge").
|
|
3
|
+
*
|
|
4
|
+
* Playwright/Cypress page objects couple to a changed component by SELECTOR — a
|
|
5
|
+
* `data-testid` value or a CSS class token — not by filename or by `import`. So the
|
|
6
|
+
* filename-token (pass 1) and import-edge (pass 2) promotion signals in
|
|
7
|
+
* TestDiscoveryService Step 2 both miss them: a page object `table.component.ts` shares
|
|
8
|
+
* no name token with the changed `CascadeFields.tsx` and does not import it — it selects
|
|
9
|
+
* `.t--table-filter-columns-dropdown` at runtime.
|
|
10
|
+
*
|
|
11
|
+
* This module extracts the selector literals that a frontend diff added OR removed, so
|
|
12
|
+
* discovery can promote any existing test/page-object whose body still references one of
|
|
13
|
+
* them. Both sides of a rename are captured deliberately: a page object that has NOT been
|
|
14
|
+
* updated still contains the OLD value, so matching the removed literal is what surfaces
|
|
15
|
+
* the test that needs maintenance.
|
|
16
|
+
*
|
|
17
|
+
* This is the frontend analog of importerHop's `extractChangedSymbols` (backend type
|
|
18
|
+
* names) — kept in its own frontend-scoped module rather than in the backend-resource
|
|
19
|
+
* importerHop, per the codebase convention of keeping domain vocabularies local.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_CHANGED_SELECTORS = 80;
|
|
22
|
+
/**
|
|
23
|
+
* Extract selector literals added or removed in a single file's diff slice.
|
|
24
|
+
*
|
|
25
|
+
* Two complementary passes over the changed (`+`/`-`) lines, excluding the `+++`/`---`
|
|
26
|
+
* file headers:
|
|
27
|
+
* 1. Explicit test-facing attribute/API values: `data-testid` / `data-test` /
|
|
28
|
+
* `data-test-id` / `data-cy` / `data-qa` attribute values and `getByTestId(...)`
|
|
29
|
+
* arguments (trusted — accepts camelCase or single-delimiter ids).
|
|
30
|
+
* 2. Distinctive kebab / BEM class tokens anywhere on the line (>= 2 hyphens or `--`),
|
|
31
|
+
* which catches CSS-class-based coupling (`t--table-filter-columns-dropdown`,
|
|
32
|
+
* `ads-button--primary`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractChangedSelectors(fileDiff: string): string[];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend selector extraction for UI-test-maintenance discovery (Pass 3, "selector-edge").
|
|
3
|
+
*
|
|
4
|
+
* Playwright/Cypress page objects couple to a changed component by SELECTOR — a
|
|
5
|
+
* `data-testid` value or a CSS class token — not by filename or by `import`. So the
|
|
6
|
+
* filename-token (pass 1) and import-edge (pass 2) promotion signals in
|
|
7
|
+
* TestDiscoveryService Step 2 both miss them: a page object `table.component.ts` shares
|
|
8
|
+
* no name token with the changed `CascadeFields.tsx` and does not import it — it selects
|
|
9
|
+
* `.t--table-filter-columns-dropdown` at runtime.
|
|
10
|
+
*
|
|
11
|
+
* This module extracts the selector literals that a frontend diff added OR removed, so
|
|
12
|
+
* discovery can promote any existing test/page-object whose body still references one of
|
|
13
|
+
* them. Both sides of a rename are captured deliberately: a page object that has NOT been
|
|
14
|
+
* updated still contains the OLD value, so matching the removed literal is what surfaces
|
|
15
|
+
* the test that needs maintenance.
|
|
16
|
+
*
|
|
17
|
+
* This is the frontend analog of importerHop's `extractChangedSymbols` (backend type
|
|
18
|
+
* names) — kept in its own frontend-scoped module rather than in the backend-resource
|
|
19
|
+
* importerHop, per the codebase convention of keeping domain vocabularies local.
|
|
20
|
+
*/
|
|
21
|
+
// Bound the extracted set so a large diff can't produce an unbounded match list. The
|
|
22
|
+
// downstream promotion is separately capped by MAX_UI_PROMOTED, so this is a belt on the
|
|
23
|
+
// literal set itself. Exported so the caller can detect + log when a single file's
|
|
24
|
+
// extraction is truncated by this cap.
|
|
25
|
+
export const MAX_CHANGED_SELECTORS = 80;
|
|
26
|
+
// Utility / design-token CSS classes (Tailwind/Bootstrap-style) are ALSO multi-hyphen kebab
|
|
27
|
+
// tokens, but they are shared *styling* vocabulary, not component identity — matching them
|
|
28
|
+
// floods Pass 3, because snapshot files (`__snapshots__/*.snap`) serialize `className`
|
|
29
|
+
// verbatim, so an unrelated component's snapshot reuses the same class (e.g. `text-gray-500`).
|
|
30
|
+
// Reject the two dominant utility shapes on the untrusted path: a numeric scale suffix
|
|
31
|
+
// (`text-gray-500`, `bg-blue-600`, `grid-cols-3`) and common non-numeric utility roots
|
|
32
|
+
// (`flex-row-reverse`, `space-x-…`). BEM component classes (`ads-button--primary`, `t--…`)
|
|
33
|
+
// and semantic identifiers (`confirmation-modal-confirm-button`) are kept.
|
|
34
|
+
const COMMON_CSS_SELECTOR_TOKENS = /-\d+$|^(?:flex|grid|space|gap|order|col|row|inset|translate|scale|rotate|divide|place|self|content|justify|items|auto)-/;
|
|
35
|
+
/**
|
|
36
|
+
* True if a token is a distinctive selector literal worth matching against test bodies.
|
|
37
|
+
* `trusted` = extracted from an explicit test-facing attribute (data-testid / getByTestId),
|
|
38
|
+
* where a single delimiter or camelCase boundary is enough. Untrusted (a bare kebab/BEM token
|
|
39
|
+
* found anywhere on the line) requires a BEM `--`, or >= 2 hyphens that don't look like a
|
|
40
|
+
* utility/design-token class — otherwise shared styling classes flood promotion.
|
|
41
|
+
*/
|
|
42
|
+
function isDistinctiveSelector(token, trusted) {
|
|
43
|
+
if (token.length < 6)
|
|
44
|
+
return false;
|
|
45
|
+
const hyphens = (token.match(/-/g) ?? []).length;
|
|
46
|
+
const hasBEM = token.includes("--");
|
|
47
|
+
if (trusted) {
|
|
48
|
+
// A test id: any structural signal (delimiter or camelCase) qualifies — single-word
|
|
49
|
+
// ids like "submit" (6 chars, so past the length gate) are dropped by the structural
|
|
50
|
+
// requirement alone: no hyphen, underscore, or camelCase boundary.
|
|
51
|
+
return hyphens > 0 || token.includes("_") || /[a-z][A-Z]/.test(token);
|
|
52
|
+
}
|
|
53
|
+
return hasBEM || (hyphens >= 2 && !COMMON_CSS_SELECTOR_TOKENS.test(token));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract selector literals added or removed in a single file's diff slice.
|
|
57
|
+
*
|
|
58
|
+
* Two complementary passes over the changed (`+`/`-`) lines, excluding the `+++`/`---`
|
|
59
|
+
* file headers:
|
|
60
|
+
* 1. Explicit test-facing attribute/API values: `data-testid` / `data-test` /
|
|
61
|
+
* `data-test-id` / `data-cy` / `data-qa` attribute values and `getByTestId(...)`
|
|
62
|
+
* arguments (trusted — accepts camelCase or single-delimiter ids).
|
|
63
|
+
* 2. Distinctive kebab / BEM class tokens anywhere on the line (>= 2 hyphens or `--`),
|
|
64
|
+
* which catches CSS-class-based coupling (`t--table-filter-columns-dropdown`,
|
|
65
|
+
* `ads-button--primary`).
|
|
66
|
+
*/
|
|
67
|
+
export function extractChangedSelectors(fileDiff) {
|
|
68
|
+
const out = new Set();
|
|
69
|
+
// Require a quote immediately before the value so only string LITERALS are captured —
|
|
70
|
+
// `data-testid="x"`, `data-testid={'x'}`, `data-testid={`x`}`. This deliberately skips
|
|
71
|
+
// non-literal expressions like `data-testid={someVar}`, which would otherwise inject a
|
|
72
|
+
// variable name into the selector set and cause noisy promotion matches.
|
|
73
|
+
// Require a quote/backtick on BOTH sides so only FULLY-literal values are captured —
|
|
74
|
+
// `data-testid="x"`, `data-testid={'x'}`, `data-testid={`x`}`. The closing delimiter
|
|
75
|
+
// deliberately rejects interpolated template literals (`data-testid={`members-${id}`}`),
|
|
76
|
+
// which would otherwise leak a partial prefix (`members-`) into the selector set.
|
|
77
|
+
// eslint-disable-next-line no-useless-escape
|
|
78
|
+
const ATTR = /\bdata-(?:test-?id|test|cy|qa)\b\s*=\s*\{?\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
|
|
79
|
+
const GET_BY_TEST_ID = /getByTestId\(\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
|
|
80
|
+
const KEBAB = /[a-z][a-z0-9]*(?:-{1,2}[a-z0-9]+)+/g;
|
|
81
|
+
// BEM base whose modifier is composed DYNAMICALLY, so no concrete class literal exists
|
|
82
|
+
// in the diff to grep: `ads-button--${kind}`, "ads-button--" + kind, clsx(base && `x--${y}`).
|
|
83
|
+
// Match a kebab base + `--` followed by a NON-alphanumeric (interpolation `$`, quote,
|
|
84
|
+
// paren, whitespace) and capture the `<base>--` prefix. The page objects' concrete
|
|
85
|
+
// modifier classes (ads-button--primary, ads-button--secondary) all start with that
|
|
86
|
+
// prefix, so a substring match on it promotes them — the shared/design-system component
|
|
87
|
+
// case a static-literal scan (KEBAB) misses. (A STATIC modifier like `foo--bar` is left
|
|
88
|
+
// to KEBAB, which captures the whole class.)
|
|
89
|
+
const BEM_DYNAMIC = /([a-z][a-z0-9]*(?:-[a-z0-9]+)+--)(?![a-z0-9])/g;
|
|
90
|
+
for (const raw of fileDiff.split("\n")) {
|
|
91
|
+
if (!(raw.startsWith("+") || raw.startsWith("-")))
|
|
92
|
+
continue;
|
|
93
|
+
if (raw.startsWith("+++") || raw.startsWith("---"))
|
|
94
|
+
continue;
|
|
95
|
+
const line = raw.slice(1);
|
|
96
|
+
for (const re of [ATTR, GET_BY_TEST_ID]) {
|
|
97
|
+
re.lastIndex = 0;
|
|
98
|
+
let m;
|
|
99
|
+
while ((m = re.exec(line)) !== null) {
|
|
100
|
+
if (isDistinctiveSelector(m[1], true))
|
|
101
|
+
out.add(m[1]);
|
|
102
|
+
if (out.size >= MAX_CHANGED_SELECTORS)
|
|
103
|
+
return [...out];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
KEBAB.lastIndex = 0;
|
|
107
|
+
let k;
|
|
108
|
+
while ((k = KEBAB.exec(line)) !== null) {
|
|
109
|
+
if (isDistinctiveSelector(k[0], false))
|
|
110
|
+
out.add(k[0]);
|
|
111
|
+
if (out.size >= MAX_CHANGED_SELECTORS)
|
|
112
|
+
return [...out];
|
|
113
|
+
}
|
|
114
|
+
BEM_DYNAMIC.lastIndex = 0;
|
|
115
|
+
let b;
|
|
116
|
+
while ((b = BEM_DYNAMIC.exec(line)) !== null) {
|
|
117
|
+
if (isDistinctiveSelector(b[1], false))
|
|
118
|
+
out.add(b[1]);
|
|
119
|
+
if (out.size >= MAX_CHANGED_SELECTORS)
|
|
120
|
+
return [...out];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...out];
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { extractChangedSelectors } from "./frontendSelectors.js";
|
|
3
|
+
describe("extractChangedSelectors", () => {
|
|
4
|
+
it("captures both the old (removed) and new (added) value of a renamed data-testid", () => {
|
|
5
|
+
const diff = [
|
|
6
|
+
"@@ -175,7 +175,7 @@",
|
|
7
|
+
'- dataTestId="confirmation-modal-confirm-button"',
|
|
8
|
+
'+ dataTestId="confirmation-modal-primary-button"',
|
|
9
|
+
].join("\n");
|
|
10
|
+
const sel = extractChangedSelectors(diff);
|
|
11
|
+
// dataTestId isn't a data-* attribute, but both are distinctive kebab class-ish tokens
|
|
12
|
+
expect(sel).toEqual(expect.arrayContaining([
|
|
13
|
+
"confirmation-modal-confirm-button",
|
|
14
|
+
"confirmation-modal-primary-button",
|
|
15
|
+
]));
|
|
16
|
+
});
|
|
17
|
+
it("captures data-testid attribute values", () => {
|
|
18
|
+
const diff = '+ <input data-testid="members-search-field" />\n- <input data-testid="members-search-input" />';
|
|
19
|
+
const sel = extractChangedSelectors(diff);
|
|
20
|
+
expect(sel).toEqual(expect.arrayContaining(["members-search-field", "members-search-input"]));
|
|
21
|
+
});
|
|
22
|
+
it("captures CSS class rename with BEM `--` and multi-segment kebab", () => {
|
|
23
|
+
const diff = [
|
|
24
|
+
'- <div className="t--table-filter-columns-dropdown">',
|
|
25
|
+
'+ <div className="t--tbl-filter-column-select">',
|
|
26
|
+
'+ <button className="ads-button--primary">',
|
|
27
|
+
].join("\n");
|
|
28
|
+
const sel = extractChangedSelectors(diff);
|
|
29
|
+
expect(sel).toEqual(expect.arrayContaining([
|
|
30
|
+
"t--table-filter-columns-dropdown",
|
|
31
|
+
"t--tbl-filter-column-select",
|
|
32
|
+
"ads-button--primary",
|
|
33
|
+
]));
|
|
34
|
+
});
|
|
35
|
+
it("captures the BEM base prefix of a dynamically-composed modifier class", () => {
|
|
36
|
+
const diff = [
|
|
37
|
+
"- className={clsx(ButtonClassName, className)}",
|
|
38
|
+
"+ className={clsx(ButtonClassName, kind && `ads-button--${kind}`, className)}",
|
|
39
|
+
].join("\n");
|
|
40
|
+
const sel = extractChangedSelectors(diff);
|
|
41
|
+
// no concrete `ads-button--primary` literal exists in the diff — capture the base prefix
|
|
42
|
+
expect(sel).toContain("ads-button--");
|
|
43
|
+
});
|
|
44
|
+
it("captures the BEM base from string-concatenation too, but leaves STATIC modifiers to the kebab pass", () => {
|
|
45
|
+
const concat = extractChangedSelectors('+ const cls = "ads-button--" + kind;');
|
|
46
|
+
expect(concat).toContain("ads-button--");
|
|
47
|
+
// a fully static modifier is captured whole (not as a bare `foo-bar--` prefix)
|
|
48
|
+
const staticMod = extractChangedSelectors('+ <div className="t--tbl-filter-column-select" />');
|
|
49
|
+
expect(staticMod).toContain("t--tbl-filter-column-select");
|
|
50
|
+
expect(staticMod).not.toContain("t--tbl-filter-column-select--");
|
|
51
|
+
});
|
|
52
|
+
it("captures getByTestId arguments", () => {
|
|
53
|
+
const diff = "+ const btn = page.getByTestId('workflow-settings-modal');";
|
|
54
|
+
expect(extractChangedSelectors(diff)).toContain("workflow-settings-modal");
|
|
55
|
+
});
|
|
56
|
+
it("drops single-hyphen styling utilities and short/generic tokens", () => {
|
|
57
|
+
const diff = [
|
|
58
|
+
'+ <div className="flex-start space-between text-align" />',
|
|
59
|
+
'+ const gap = "sans-serif";',
|
|
60
|
+
].join("\n");
|
|
61
|
+
expect(extractChangedSelectors(diff)).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
it("drops multi-hyphen utility / design-token classes (numeric scale + utility roots)", () => {
|
|
64
|
+
const diff = [
|
|
65
|
+
'- <div className="text-gray-500 bg-blue-600 flex-row-reverse" />',
|
|
66
|
+
'+ <div className="text-gray-600 bg-blue-700 grid-cols-3" />',
|
|
67
|
+
].join("\n");
|
|
68
|
+
const sel = extractChangedSelectors(diff);
|
|
69
|
+
for (const util of ["text-gray-500", "text-gray-600", "bg-blue-600", "bg-blue-700", "flex-row-reverse", "grid-cols-3"]) {
|
|
70
|
+
expect(sel).not.toContain(util);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
it("keeps semantic multi-hyphen identifiers and BEM classes (not utility)", () => {
|
|
74
|
+
const diff = [
|
|
75
|
+
'+ <div className="confirmation-modal-confirm-button user-profile-header" />',
|
|
76
|
+
'+ <div className="t--tbl-filter-column-select" />',
|
|
77
|
+
].join("\n");
|
|
78
|
+
const sel = extractChangedSelectors(diff);
|
|
79
|
+
expect(sel).toEqual(expect.arrayContaining([
|
|
80
|
+
"confirmation-modal-confirm-button",
|
|
81
|
+
"user-profile-header",
|
|
82
|
+
"t--tbl-filter-column-select",
|
|
83
|
+
]));
|
|
84
|
+
});
|
|
85
|
+
it("ignores context (unchanged) lines and file headers", () => {
|
|
86
|
+
const diff = [
|
|
87
|
+
"--- a/src/Component.tsx",
|
|
88
|
+
"+++ b/src/Component.tsx",
|
|
89
|
+
' <div className="t--unchanged-context-selector" />', // context line (leading space)
|
|
90
|
+
'+ <div className="t--added-real-selector" />',
|
|
91
|
+
].join("\n");
|
|
92
|
+
const sel = extractChangedSelectors(diff);
|
|
93
|
+
expect(sel).toContain("t--added-real-selector");
|
|
94
|
+
expect(sel).not.toContain("t--unchanged-context-selector");
|
|
95
|
+
expect(sel).not.toContain("Component"); // +++/--- headers ignored
|
|
96
|
+
});
|
|
97
|
+
it("does not leak a partial prefix from an interpolated template-literal attribute value", () => {
|
|
98
|
+
const diff = [
|
|
99
|
+
"+ <input data-testid={`members-search-${id}`} />",
|
|
100
|
+
"+ <button data-testid={`confirmation-modal-${kind}`} />",
|
|
101
|
+
].join("\n");
|
|
102
|
+
const sel = extractChangedSelectors(diff);
|
|
103
|
+
expect(sel).not.toContain("members-search-");
|
|
104
|
+
expect(sel).not.toContain("confirmation-modal-");
|
|
105
|
+
});
|
|
106
|
+
it("does not capture non-literal expression attribute values (data-testid={someVar})", () => {
|
|
107
|
+
const diff = [
|
|
108
|
+
"+ <div data-testid={someVariableName} />",
|
|
109
|
+
"+ <div data-testid={'members-search-field'} />",
|
|
110
|
+
].join("\n");
|
|
111
|
+
const sel = extractChangedSelectors(diff);
|
|
112
|
+
expect(sel).not.toContain("someVariableName"); // variable name, not a literal
|
|
113
|
+
expect(sel).toContain("members-search-field"); // quoted literal still captured
|
|
114
|
+
});
|
|
115
|
+
it("returns [] for an empty diff", () => {
|
|
116
|
+
expect(extractChangedSelectors("")).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -31,6 +31,14 @@ export declare function deriveResourceToken(file: string): string;
|
|
|
31
31
|
* `[deriveResourceToken(file)]` (or `[]` if that's generic/unknown) —
|
|
32
32
|
* identical to today's filename-only behavior.
|
|
33
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* Declared symbol names changed in a unified diff — from added-line declarations
|
|
36
|
+
* AND from the hunk-header enclosing type (so a field-only edit to an existing class,
|
|
37
|
+
* e.g. `DeploymentCreate.work_pool_name` → required, still yields `DeploymentCreate`).
|
|
38
|
+
* Returns raw names, used both for resource-token derivation (via `tokensFromSymbol`)
|
|
39
|
+
* and for content-grep matching against test bodies (SKYR-3924).
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractChangedSymbols(diffHunk: string): string[];
|
|
34
42
|
export declare function deriveResourceTokensForFile(file: string, diffHunk?: string): string[];
|
|
35
43
|
/**
|
|
36
44
|
* All singular resource tokens implied by an endpoint path, used for affinity
|
|
@@ -114,6 +122,16 @@ export declare function extractFieldNamesFromAddedLines(lines: string[]): string
|
|
|
114
122
|
* without re-implementing the diff-header walk.
|
|
115
123
|
*/
|
|
116
124
|
export declare function sliceAddedLinesByFile(diffContent: string): Map<string, string[]>;
|
|
125
|
+
/**
|
|
126
|
+
* Splits a unified diff into per-file raw sections — every line after a file's
|
|
127
|
+
* `diff --git` header up to the next one — keyed by the `b/` (post-change) path.
|
|
128
|
+
* Unlike `sliceAddedLinesByFile` (added lines only), this preserves the `@@`
|
|
129
|
+
* hunk headers, so `extractChangedSymbols`' enclosing-type extraction (which
|
|
130
|
+
* reads `@@ ... class Foo`) still works when a single file's section is passed
|
|
131
|
+
* to `deriveResourceTokensForFile`. Use this for per-file token derivation that
|
|
132
|
+
* must not mix symbols across files in a multi-file diff (SKYR-3924).
|
|
133
|
+
*/
|
|
134
|
+
export declare function sliceDiffByFile(diffContent: string): Map<string, string>;
|
|
117
135
|
/**
|
|
118
136
|
* Computes added-field names per file for the given files, from a full
|
|
119
137
|
* unified diff. Files with no added-line matches are omitted from the result
|