@skyramp/mcp 0.2.9 → 0.2.10-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +1 -1
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
- package/build/prompts/personas.d.ts +2 -2
- package/build/prompts/personas.js +2 -2
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
- package/build/prompts/sut-setup/shared.js +1 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
- package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
- package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
- package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
- package/build/prompts/test-recommendation/recommendationShared.js +1 -1
- package/build/prompts/testbot/testbot-prompts.js +8 -8
- package/build/resources/testbotResource.js +1 -1
- package/build/services/TestDiscoveryService.d.ts +0 -1
- package/build/services/TestDiscoveryService.js +13 -33
- package/build/services/TestDiscoveryService.test.js +136 -1
- package/build/services/TestExecutionService.d.ts +1 -1
- package/build/tools/executeSkyrampTestTool.js +40 -71
- package/build/tools/submitReportTool.d.ts +20 -20
- package/build/tools/submitReportTool.js +113 -31
- package/build/tools/submitReportTool.test.js +411 -94
- package/build/tools/test-management/actionsTool.js +135 -53
- package/build/tools/test-management/actionsTool.test.d.ts +1 -0
- package/build/tools/test-management/actionsTool.test.js +297 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
- package/build/tools/test-management/analyzeChangesTool.js +63 -152
- package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
- package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
- package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
- package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
- package/build/types/FrontendIntegration.d.ts +25 -0
- package/build/types/FrontendIntegration.js +19 -0
- package/build/types/RepositoryAnalysis.d.ts +8 -8
- package/build/types/TestAnalysis.d.ts +16 -50
- package/build/types/TestAnalysis.js +0 -28
- package/build/types/TestTypes.d.ts +3 -1
- package/build/types/TestTypes.js +3 -1
- package/build/utils/AnalysisStateManager.d.ts +13 -1
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +1 -1
- package/build/utils/featureFlags.js +1 -1
- package/build/utils/frontendIntegration.d.ts +9 -0
- package/build/utils/frontendIntegration.js +237 -0
- package/build/utils/frontendIntegration.test.d.ts +1 -0
- package/build/utils/frontendIntegration.test.js +229 -0
- package/build/utils/pr-comment-parser.d.ts +3 -3
- package/build/utils/pr-comment-parser.js +6 -6
- package/build/utils/pr-comment-parser.test.js +3 -3
- package/build/utils/repoScanner.d.ts +7 -0
- package/build/utils/repoScanner.js +14 -6
- package/build/utils/repoScanner.test.js +9 -9
- package/build/utils/routeParsers.d.ts +32 -0
- package/build/utils/routeParsers.js +205 -2
- package/build/utils/routeParsers.test.js +279 -29
- package/build/utils/sourceRouteExtractor.js +174 -5
- package/build/utils/sourceRouteExtractor.test.js +173 -0
- package/build/utils/utils.d.ts +11 -0
- package/build/utils/utils.js +19 -0
- package/build/utils/utils.test.js +23 -1
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck - Jest ESM type inference issues
|
|
2
2
|
import { jest } from "@jest/globals";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
// Mock all heavy dependencies so the module can be loaded in isolation.
|
|
5
4
|
// Under native ESM, jest.mock is not hoisted — every mock the subject's import
|
|
6
5
|
// graph touches must use unstable_mockModule before the dynamic import below.
|
|
@@ -27,7 +26,10 @@ jest.unstable_mockModule("../../utils/branchDiff.js", () => ({
|
|
|
27
26
|
jest.unstable_mockModule("../../utils/routeParsers.js", () => ({
|
|
28
27
|
classifyEndpointsByChangedFiles: jest.fn(),
|
|
29
28
|
extractResourceFromPath: jest.fn(),
|
|
29
|
+
extractTopLevelResourceFromPath: jest.fn(),
|
|
30
30
|
parseFileEndpoints: jest.fn(),
|
|
31
|
+
selectRemovalCandidateFiles: jest.fn(() => []),
|
|
32
|
+
recoverRemovedEndpointsFromBase: jest.fn(async () => []),
|
|
31
33
|
}));
|
|
32
34
|
jest.unstable_mockModule("../../utils/repoScanner.js", () => ({
|
|
33
35
|
scanAllRepoEndpoints: jest.fn(),
|
|
@@ -87,7 +89,7 @@ jest.unstable_mockModule("@modelcontextprotocol/sdk/shared/protocol.js", () => (
|
|
|
87
89
|
// All subject + mocked-module imports must be dynamic and live AFTER the
|
|
88
90
|
// unstable_mockModule calls above — static imports hoist above the mocks under
|
|
89
91
|
// native ESM, loading the real (heavy) modules unmocked.
|
|
90
|
-
const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
|
|
92
|
+
const { analyzeChangesInputSchema, isSecurityRelevantDiff, filterEndpointsBySpec, filterUnsupportedGraphQLEndpoints, isGraphQLFile, registerAnalyzeChangesTool, buildTraceFileEntry, buildCaptureInstructions, buildUnintegratedInstructions, NO_UI_INSTRUCTIONS, NO_RESOLVABLE_URLS_INSTRUCTIONS, } = await import("./analyzeChangesTool.js");
|
|
91
93
|
const { computeBranchDiff } = await import("../../utils/branchDiff.js");
|
|
92
94
|
const { classifyEndpointsByChangedFiles } = await import("../../utils/routeParsers.js");
|
|
93
95
|
const { scanRelatedEndpoints, scanAllRepoEndpoints } = await import("../../utils/repoScanner.js");
|
|
@@ -96,7 +98,6 @@ const os = await import("os");
|
|
|
96
98
|
const path = await import("path");
|
|
97
99
|
const fsSync = await import("fs");
|
|
98
100
|
const fsModule = fsSync;
|
|
99
|
-
const schema = z.object(analyzeChangesInputSchema);
|
|
100
101
|
describe("isSecurityRelevantDiff", () => {
|
|
101
102
|
it("matches auth-specific middleware and admin-key signals", () => {
|
|
102
103
|
expect(isSecurityRelevantDiff("const authMiddleware = requireAuth();")).toBe(true);
|
|
@@ -454,3 +455,33 @@ describe("UI blueprint-capture instructions (merged from ui_analyze_changes)", (
|
|
|
454
455
|
expect(text).not.toMatch(/ui-credentials/i);
|
|
455
456
|
});
|
|
456
457
|
});
|
|
458
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
459
|
+
// buildUnintegratedInstructions — SKYR-3855 server-side integration check.
|
|
460
|
+
// Per .claude/CLAUDE.md, only assert structural branching (which files are
|
|
461
|
+
// listed), not prose wording.
|
|
462
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
463
|
+
describe("buildUnintegratedInstructions", () => {
|
|
464
|
+
it("lists a single unintegrated file when only one is passed (all-unintegrated case)", () => {
|
|
465
|
+
const text = buildUnintegratedInstructions([
|
|
466
|
+
{ file: "src/components/EntityExplorerSidebar.tsx", integrated: false, importers: [], reason: "no-importers" },
|
|
467
|
+
]);
|
|
468
|
+
expect(text).toContain("src/components/EntityExplorerSidebar.tsx");
|
|
469
|
+
expect(text).not.toContain("src/components/OtherPanel.tsx");
|
|
470
|
+
});
|
|
471
|
+
it("lists every file passed in — mixed-PR call sites scope this to just the unintegrated subset", () => {
|
|
472
|
+
const text = buildUnintegratedInstructions([
|
|
473
|
+
{ file: "src/components/A.tsx", integrated: false, importers: [], reason: "no-importers" },
|
|
474
|
+
{ file: "src/components/B.tsx", integrated: false, importers: [], reason: "no-importers" },
|
|
475
|
+
]);
|
|
476
|
+
expect(text).toContain("src/components/A.tsx");
|
|
477
|
+
expect(text).toContain("src/components/B.tsx");
|
|
478
|
+
});
|
|
479
|
+
it("filters out integrated files even if accidentally included by a caller", () => {
|
|
480
|
+
const text = buildUnintegratedInstructions([
|
|
481
|
+
{ file: "src/components/Unintegrated.tsx", integrated: false, importers: [], reason: "no-importers" },
|
|
482
|
+
{ file: "src/pages/Dashboard.tsx", integrated: true, importers: ["src/App.tsx"], reason: "imported" },
|
|
483
|
+
]);
|
|
484
|
+
expect(text).toContain("src/components/Unintegrated.tsx");
|
|
485
|
+
expect(text).not.toContain("src/pages/Dashboard.tsx");
|
|
486
|
+
});
|
|
487
|
+
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { logger } from "../../utils/logger.js";
|
|
3
3
|
import { StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import * as fs from "fs/promises";
|
|
6
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
7
5
|
import { buildDriftAnalysisPrompt } from "../../prompts/test-maintenance/drift-analysis-prompt.js";
|
|
8
6
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
@@ -22,110 +20,11 @@ const UI_COMPONENT_FRAMEWORKS = new Set([
|
|
|
22
20
|
]);
|
|
23
21
|
// No REST API test is ever written in TSX/JSX — extension alone is sufficient.
|
|
24
22
|
const UI_TEST_EXTENSIONS = /\.(test|spec)\.(tsx|jsx)$/;
|
|
25
|
-
// Snapshot files (.snap) are secondary artifacts of UI tests — send them through
|
|
26
|
-
// the UI drift path so stale snapshots are detected when frontend selectors change.
|
|
27
23
|
const UI_SNAPSHOT_EXT = /\.snap$/;
|
|
28
|
-
// Playwright/Cypress without the "-ui" suffix = REST API usage — must not be routed
|
|
29
|
-
// to UI drift even when detectExternalTestType() labels the file as "e2e".
|
|
30
|
-
const API_ONLY_FRAMEWORKS = new Set(["playwright", "cypress"]);
|
|
31
24
|
function isUiTest(t) {
|
|
32
25
|
const fw = t.framework?.toLowerCase() ?? "";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (t.testType === "e2e" && API_ONLY_FRAMEWORKS.has(fw))
|
|
36
|
-
return false;
|
|
37
|
-
// .snap files are secondary artifacts — only route them to UI drift when
|
|
38
|
-
// accompanied by a UI testType or framework signal. API test snapshots
|
|
39
|
-
// (e.g. __snapshots__/orders_smoke.test.ts.snap) must remain in the API path.
|
|
40
|
-
const isUiSnap = UI_SNAPSHOT_EXT.test(t.testFile) &&
|
|
41
|
-
(t.testType === "ui" || UI_COMPONENT_FRAMEWORKS.has(fw));
|
|
42
|
-
return t.testType === "ui" || t.testType === "e2e" ||
|
|
43
|
-
UI_COMPONENT_FRAMEWORKS.has(fw) ||
|
|
44
|
-
UI_TEST_EXTENSIONS.test(t.testFile) ||
|
|
45
|
-
isUiSnap;
|
|
46
|
-
}
|
|
47
|
-
// Grep exported symbols + diff UI signals across external tests to find tests that
|
|
48
|
-
// reference changed frontend files when relevance scoring returns nothing (UI-only PRs).
|
|
49
|
-
const SYMBOL_GREP_MAX_READS = 150;
|
|
50
|
-
const SYMBOL_GREP_MAX_RESULTS = 20;
|
|
51
|
-
const GENERIC_SLUGS = new Set([
|
|
52
|
-
"index", "app", "main", "utils", "util", "lib", "helpers", "helper",
|
|
53
|
-
"common", "shared", "base", "core", "types", "constants", "config",
|
|
54
|
-
"hooks", "store", "context", "layout", "page", "view", "component",
|
|
55
|
-
]);
|
|
56
|
-
async function findUiTestsBySymbolGrep(changedFrontendFiles, allTests, repositoryPath, alreadyIncluded, diffFilePath) {
|
|
57
|
-
const symbols = new Set();
|
|
58
|
-
for (const file of changedFrontendFiles) {
|
|
59
|
-
const abs = path.isAbsolute(file) ? file : path.resolve(repositoryPath, file);
|
|
60
|
-
const content = await fs.readFile(abs, "utf-8").catch(() => "");
|
|
61
|
-
for (const m of content.matchAll(/export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w{3,})/g)) {
|
|
62
|
-
symbols.add(m[1]);
|
|
63
|
-
}
|
|
64
|
-
for (const m of content.matchAll(/(?:name|__name):\s*['"](\w[\w-]{2,})['"]/g)) {
|
|
65
|
-
symbols.add(m[1]);
|
|
66
|
-
}
|
|
67
|
-
const slug = path.basename(file).replace(/\.\w+$/, "");
|
|
68
|
-
if (!GENERIC_SLUGS.has(slug.toLowerCase()) && (slug.includes("-") || slug.length > 6)) {
|
|
69
|
-
symbols.add(slug);
|
|
70
|
-
const pascal = slug.replace(/(^|[-_])(\w)/g, (_, _sep, c) => c.toUpperCase());
|
|
71
|
-
symbols.add(pascal);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Also extract quoted string literals from diff +/- lines for changed frontend files.
|
|
75
|
-
// This catches tests referencing OLD values (renamed testid, aria-label, CSS class) that
|
|
76
|
-
// only appear in `-` diff lines, not in the current file content.
|
|
77
|
-
if (diffFilePath) {
|
|
78
|
-
const diffContent = await fs.readFile(diffFilePath, "utf-8").catch(() => "");
|
|
79
|
-
// Exact path markers prevent basename substring false-positives
|
|
80
|
-
// (e.g. "personal-calendar-view.test.tsx" matching "personal-calendar-view.tsx").
|
|
81
|
-
const changedFileMarkers = changedFrontendFiles.flatMap(f => {
|
|
82
|
-
const rel = f.replace(/\\/g, "/");
|
|
83
|
-
return [` a/${rel} `, ` b/${rel}`];
|
|
84
|
-
});
|
|
85
|
-
let inChangedFile = false;
|
|
86
|
-
for (const line of diffContent.split("\n")) {
|
|
87
|
-
if (line.startsWith("diff --git ")) {
|
|
88
|
-
inChangedFile = changedFileMarkers.length > 0 &&
|
|
89
|
-
changedFileMarkers.some(marker => line.includes(marker));
|
|
90
|
-
}
|
|
91
|
-
if (!inChangedFile)
|
|
92
|
-
continue;
|
|
93
|
-
if (!line.startsWith("+") && !line.startsWith("-"))
|
|
94
|
-
continue;
|
|
95
|
-
// Min 10 chars skips generic values ("true","false","continue") that match
|
|
96
|
-
// hundreds of unrelated test files.
|
|
97
|
-
for (const m of line.matchAll(/["']([^"'\n]{10,60})["']/g)) {
|
|
98
|
-
symbols.add(m[1]);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (symbols.size === 0)
|
|
103
|
-
return [];
|
|
104
|
-
const symArray = [...symbols];
|
|
105
|
-
logger.debug(`Symbol grep: ${symArray.length} symbol(s) — ${symArray.slice(0, 5).join(", ")}${symArray.length > 5 ? "..." : ""}`);
|
|
106
|
-
// Normalize paths — stateData.existingTests may contain relative paths in
|
|
107
|
-
// older state files. Mirror the re-absolutization done for relevantExternalPaths.
|
|
108
|
-
const candidates = allTests
|
|
109
|
-
.filter(t => (t.source ?? TestSource.Skyramp) === TestSource.External &&
|
|
110
|
-
isUiTest(t) &&
|
|
111
|
-
!alreadyIncluded.has(t.testFile))
|
|
112
|
-
.map(t => ({
|
|
113
|
-
...t,
|
|
114
|
-
testFile: path.isAbsolute(t.testFile)
|
|
115
|
-
? t.testFile
|
|
116
|
-
: path.resolve(repositoryPath, t.testFile),
|
|
117
|
-
}))
|
|
118
|
-
.slice(0, SYMBOL_GREP_MAX_READS);
|
|
119
|
-
// Read in batches to avoid EMFILE/ENFILE errors on large test suites.
|
|
120
|
-
const BATCH = 20;
|
|
121
|
-
const contents = [];
|
|
122
|
-
for (let i = 0; i < candidates.length; i += BATCH) {
|
|
123
|
-
const batch = await Promise.all(candidates.slice(i, i + BATCH).map(t => fs.readFile(t.testFile, "utf-8").catch(() => "")));
|
|
124
|
-
contents.push(...batch);
|
|
125
|
-
}
|
|
126
|
-
return candidates
|
|
127
|
-
.filter((_, i) => symArray.some(sym => contents[i].includes(sym)))
|
|
128
|
-
.slice(0, SYMBOL_GREP_MAX_RESULTS);
|
|
26
|
+
const isUiSnap = UI_SNAPSHOT_EXT.test(t.testFile);
|
|
27
|
+
return UI_COMPONENT_FRAMEWORKS.has(fw) || UI_TEST_EXTENSIONS.test(t.testFile) || isUiSnap;
|
|
129
28
|
}
|
|
130
29
|
export function registerAnalyzeTestHealthTool(server) {
|
|
131
30
|
server.registerTool(TOOL_NAME, {
|
|
@@ -170,19 +69,10 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
170
69
|
if (!Array.isArray(stateData.existingTests)) {
|
|
171
70
|
return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt.`);
|
|
172
71
|
}
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// relevantExternalTestPaths are stored relative to repositoryPath in the state file.
|
|
178
|
-
// Re-absolutize here so has() comparisons against t.testFile (absolute) work correctly.
|
|
179
|
-
const relevantExternalPaths = new Set((stateData.repositoryAnalysis?.relevantExternalTestPaths ?? []).map((p) => path.isAbsolute(p) ? p : path.resolve(repositoryPath, p)));
|
|
180
|
-
const existingTests = (stateData.existingTests || []).filter((t) => {
|
|
181
|
-
if ((t.source ?? TestSource.Skyramp) !== TestSource.External)
|
|
182
|
-
return true;
|
|
183
|
-
return relevantExternalPaths.has(t.testFile);
|
|
184
|
-
});
|
|
185
|
-
const skyrampCount = existingTests.filter((t) => (t.source ?? TestSource.Skyramp) !== TestSource.External).length;
|
|
72
|
+
// External tests: UPDATE only — REGENERATE/DELETE are report-only (enforced in
|
|
73
|
+
// skyramp_actions). Already scoped to relevant ones by discovery.
|
|
74
|
+
const existingTests = stateData.existingTests || [];
|
|
75
|
+
const skyrampCount = existingTests.filter((t) => t.source !== TestSource.External).length;
|
|
186
76
|
const externalCount = existingTests.length - skyrampCount;
|
|
187
77
|
logger.info(`Loaded ${skyrampCount} Skyramp + ${externalCount} relevant external tests from state file`);
|
|
188
78
|
// Delete stale diff files only — state files must remain for the caller's skyramp_actions call.
|
|
@@ -192,49 +82,15 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
192
82
|
catch (error) {
|
|
193
83
|
logger.warning(`Failed to cleanup old diff files: ${error.message}`);
|
|
194
84
|
}
|
|
85
|
+
const uiContext = stateData.uiContext;
|
|
86
|
+
const hasUiFrontend = !!(uiContext?.changedFrontendFiles?.length);
|
|
195
87
|
const uiTests = [];
|
|
196
88
|
const apiTests = [];
|
|
197
89
|
for (const t of existingTests) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// When frontend files changed, use the symbol grep to rank uiTests by relevance
|
|
203
|
-
// and cap the list to avoid token overflow in the drift prompt.
|
|
204
|
-
// All bulk-promoted UI tests are now in relevantExternalTestPaths and therefore
|
|
205
|
-
// in uiTests — the grep selects the most relevant subset rather than being the
|
|
206
|
-
// sole discovery path (which could silently drop tests that need UPDATE).
|
|
207
|
-
if (hasUiFrontend && uiTests.length >= SYMBOL_GREP_MAX_RESULTS) {
|
|
208
|
-
// Skyramp-generated UI tests always pass through — grep only ranks externals.
|
|
209
|
-
const skyrampUi = uiTests.filter(t => (t.source ?? TestSource.Skyramp) !== TestSource.External);
|
|
210
|
-
const externalUi = uiTests.filter(t => (t.source ?? TestSource.Skyramp) === TestSource.External);
|
|
211
|
-
const externalCap = Math.max(0, SYMBOL_GREP_MAX_RESULTS - skyrampUi.length);
|
|
212
|
-
// Grep the full discovered pool with empty alreadyIncluded so both:
|
|
213
|
-
// (a) externalUi members are ranked by relevance, and
|
|
214
|
-
// (b) tests not yet in uiTests (e.g. onboarding.e2e.ts missing from
|
|
215
|
-
// relevantExternalTestPaths) can still be found via diff signals.
|
|
216
|
-
// Previous bug: alreadyInUiTests excluded all candidates → grep returned [].
|
|
217
|
-
const grepped = await findUiTestsBySymbolGrep(uiContext.changedFrontendFiles, stateData.existingTests || [], repositoryPath, new Set(), stateData.repositoryAnalysis?.diffFilePath);
|
|
218
|
-
const rankedExternal = grepped.length > 0
|
|
219
|
-
? grepped.slice(0, externalCap)
|
|
220
|
-
: externalUi.slice(0, externalCap); // fallback: discovery order
|
|
221
|
-
if (grepped.length > 0) {
|
|
222
|
-
logger.info(`Symbol grep ranked ${Math.min(grepped.length, externalCap)} external UI test(s) from ${stateData.existingTests?.length ?? 0} candidates`);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
logger.info(`Symbol grep found no matches; capping ${externalUi.length} external UI tests to ${externalCap}`);
|
|
226
|
-
}
|
|
227
|
-
uiTests.splice(0, uiTests.length, ...skyrampUi, ...rankedExternal);
|
|
228
|
-
}
|
|
229
|
-
else if (hasUiFrontend && uiTests.length === 0) {
|
|
230
|
-
// No UI tests found — grep the full discovered pool.
|
|
231
|
-
// Pass empty alreadyIncluded: we want to discover tests from stateData.existingTests,
|
|
232
|
-
// not exclude them. uiTests is empty so there's nothing to avoid re-adding.
|
|
233
|
-
const grepped = await findUiTestsBySymbolGrep(uiContext.changedFrontendFiles, stateData.existingTests || [], repositoryPath, new Set(), stateData.repositoryAnalysis?.diffFilePath);
|
|
234
|
-
if (grepped.length > 0) {
|
|
235
|
-
logger.info(`Symbol grep found ${grepped.length} UI test(s) referencing changed frontend files`);
|
|
236
|
-
uiTests.push(...grepped);
|
|
237
|
-
}
|
|
90
|
+
// Only route to UI drift when frontend files changed — otherwise UI tests
|
|
91
|
+
// (playwright-ui, RTL, etc.) fall through to API drift so they are not
|
|
92
|
+
// silently dropped on API-only PRs.
|
|
93
|
+
(hasUiFrontend && isUiTest(t) ? uiTests : apiTests).push(t);
|
|
238
94
|
}
|
|
239
95
|
let uiDriftParams;
|
|
240
96
|
if (hasUiFrontend) {
|
|
@@ -245,7 +101,18 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
245
101
|
blueprintCaptured: args.blueprintCaptured,
|
|
246
102
|
};
|
|
247
103
|
}
|
|
248
|
-
|
|
104
|
+
// Collect all checked-out repo paths so the grep instruction covers
|
|
105
|
+
// every repo in a multi-repo run (endpoints in Repo A, tests in Repo B).
|
|
106
|
+
// TODO(multi-repo): existingTests is scoped to the current repo only — related
|
|
107
|
+
// repo tests are not pre-loaded, discoverable only via the grep instruction.
|
|
108
|
+
// Skip the async lookups when there are no tests — symbol discovery is a no-op.
|
|
109
|
+
let allRepoPaths = [repositoryPath];
|
|
110
|
+
if (existingTests.length > 0) {
|
|
111
|
+
const relatedRepoKeys = await stateManager.listRelatedRepos();
|
|
112
|
+
const relatedRepoPaths = (await Promise.all(relatedRepoKeys.map(r => stateManager.getRepoRepositoryPath(r)))).filter((p) => typeof p === "string");
|
|
113
|
+
allRepoPaths = [repositoryPath, ...relatedRepoPaths];
|
|
114
|
+
}
|
|
115
|
+
const promptText = buildDriftAnalysisPrompt(stateManager.getStatePath(), apiTests.map((t) => ({ testFile: t.testFile, source: t.source })), uiDriftParams, allRepoPaths);
|
|
249
116
|
return {
|
|
250
117
|
structuredContent: { prompt: promptText },
|
|
251
118
|
content: [{ type: "text", text: "Drift analysis prompt generated. Follow the prompt field to assess each test." }],
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
-
import { jest, describe, it, expect, beforeEach
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import * as os from "os";
|
|
5
|
-
import * as path from "path";
|
|
2
|
+
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
|
6
3
|
jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
|
|
7
4
|
McpServer: jest.fn(),
|
|
8
5
|
}));
|
|
@@ -68,6 +65,7 @@ function makeStateManager(stateData) {
|
|
|
68
65
|
// Multi-repo API added in SKYR-3786 — analyzeTestHealthTool now uses these.
|
|
69
66
|
readRepoData: jest.fn().mockResolvedValue(stateData),
|
|
70
67
|
getRepoRepositoryPath: jest.fn().mockResolvedValue("/repo"),
|
|
68
|
+
listRelatedRepos: jest.fn().mockResolvedValue([]),
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +91,7 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
93
91
|
it("includes UI drift when uiContext has changedFrontendFiles and a UI test", async () => {
|
|
94
92
|
const mockStateManager = makeStateManager({
|
|
95
93
|
existingTests: [
|
|
96
|
-
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" },
|
|
94
|
+
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
|
|
97
95
|
],
|
|
98
96
|
uiContext: {
|
|
99
97
|
changedFrontendFiles: ["/repo/src/components/Cart.tsx"],
|
|
@@ -109,7 +107,7 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
109
107
|
});
|
|
110
108
|
it("passes blueprintCaptured=true to buildUiDriftAnalysisPrompt", async () => {
|
|
111
109
|
const mockStateManager = makeStateManager({
|
|
112
|
-
existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" }],
|
|
110
|
+
existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" }],
|
|
113
111
|
uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
|
|
114
112
|
});
|
|
115
113
|
mockFromStatePath.mockReturnValue(mockStateManager);
|
|
@@ -119,7 +117,7 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
119
117
|
});
|
|
120
118
|
it("passes blueprintCaptured=false to buildUiDriftAnalysisPrompt", async () => {
|
|
121
119
|
const mockStateManager = makeStateManager({
|
|
122
|
-
existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" }],
|
|
120
|
+
existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" }],
|
|
123
121
|
uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
|
|
124
122
|
});
|
|
125
123
|
mockFromStatePath.mockReturnValue(mockStateManager);
|
|
@@ -196,7 +194,7 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
196
194
|
it("passes only UI tests to buildUiDriftAnalysisPrompt when mixed test types exist", async () => {
|
|
197
195
|
const mockStateManager = makeStateManager({
|
|
198
196
|
existingTests: [
|
|
199
|
-
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" },
|
|
197
|
+
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
|
|
200
198
|
{ testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
|
|
201
199
|
{ testFile: "/repo/tests/smoke.py", source: "skyramp", testType: "smoke", framework: "pytest" },
|
|
202
200
|
],
|
|
@@ -212,7 +210,7 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
212
210
|
it("excludes UI tests from API drift prompt when UI drift is also running", async () => {
|
|
213
211
|
const mockStateManager = makeStateManager({
|
|
214
212
|
existingTests: [
|
|
215
|
-
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" },
|
|
213
|
+
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
|
|
216
214
|
{ testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
|
|
217
215
|
],
|
|
218
216
|
uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
|
|
@@ -223,20 +221,21 @@ describe("analyzeTestHealthTool — UI drift gate logic", () => {
|
|
|
223
221
|
expect(apiTestsArg).toHaveLength(1);
|
|
224
222
|
expect(apiTestsArg[0]).toMatchObject({ testFile: "/repo/tests/products_contract.py" });
|
|
225
223
|
});
|
|
226
|
-
it("
|
|
224
|
+
it("includes UI tests in API drift prompt when no frontend changes", async () => {
|
|
227
225
|
const mockStateManager = makeStateManager({
|
|
228
226
|
existingTests: [
|
|
229
|
-
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" },
|
|
227
|
+
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
|
|
230
228
|
{ testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
|
|
231
229
|
],
|
|
232
230
|
});
|
|
233
231
|
mockFromStatePath.mockReturnValue(mockStateManager);
|
|
234
232
|
await server.getHandler()({ stateFile: "/tmp/state.json" });
|
|
235
|
-
//
|
|
236
|
-
//
|
|
233
|
+
// No frontend changes → no UI drift section → UI tests fall through to API drift
|
|
234
|
+
// so they are not silently dropped on API-only PRs.
|
|
237
235
|
const apiTestsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][1];
|
|
238
|
-
expect(apiTestsArg).toHaveLength(
|
|
239
|
-
expect(apiTestsArg
|
|
236
|
+
expect(apiTestsArg).toHaveLength(2);
|
|
237
|
+
expect(apiTestsArg).toContainEqual(expect.objectContaining({ testFile: "/repo/tests/cart.spec.ts" }));
|
|
238
|
+
expect(apiTestsArg).toContainEqual(expect.objectContaining({ testFile: "/repo/tests/products_contract.py" }));
|
|
240
239
|
});
|
|
241
240
|
});
|
|
242
241
|
describe("analyzeTestHealthTool — prompt concatenation", () => {
|
|
@@ -249,7 +248,7 @@ describe("analyzeTestHealthTool — prompt concatenation", () => {
|
|
|
249
248
|
it("concatenates API and UI drift prompts when both are included", async () => {
|
|
250
249
|
const mockStateManager = makeStateManager({
|
|
251
250
|
existingTests: [
|
|
252
|
-
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright" },
|
|
251
|
+
{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
|
|
253
252
|
{ testFile: "/repo/tests/contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
|
|
254
253
|
],
|
|
255
254
|
uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
|
|
@@ -267,202 +266,3 @@ describe("analyzeTestHealthTool — prompt concatenation", () => {
|
|
|
267
266
|
expect(result.structuredContent?.prompt).not.toContain("UI drift");
|
|
268
267
|
});
|
|
269
268
|
});
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Discovery layer tests — catch regressions in findUiTestsBySymbolGrep
|
|
272
|
-
// These tests use real temp files on disk to exercise the actual file-reading
|
|
273
|
-
// logic, catching bugs that mock-only tests would miss.
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
describe("analyzeTestHealthTool — findUiTestsBySymbolGrep discovery", () => {
|
|
276
|
-
let server;
|
|
277
|
-
let tmpDir;
|
|
278
|
-
beforeEach(() => {
|
|
279
|
-
jest.clearAllMocks();
|
|
280
|
-
server = makeFakeServer();
|
|
281
|
-
registerAnalyzeTestHealthTool(server);
|
|
282
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-grep-test-"));
|
|
283
|
-
});
|
|
284
|
-
afterEach(() => {
|
|
285
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
286
|
-
});
|
|
287
|
-
function writeFile(name, content) {
|
|
288
|
-
const filePath = path.join(tmpDir, name);
|
|
289
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
290
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
291
|
-
return filePath;
|
|
292
|
-
}
|
|
293
|
-
function makeState(opts) {
|
|
294
|
-
return {
|
|
295
|
-
existingTests: opts.existingTests ?? [],
|
|
296
|
-
repositoryAnalysis: {
|
|
297
|
-
relevantExternalTestPaths: (opts.existingTests ?? []).map(t => t.testFile),
|
|
298
|
-
diffFilePath: opts.diffFilePath,
|
|
299
|
-
},
|
|
300
|
-
uiContext: {
|
|
301
|
-
changedFrontendFiles: opts.changedFrontendFiles,
|
|
302
|
-
candidateUiPages: [],
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
async function runHandler(state, repoPath) {
|
|
307
|
-
const stateManager = {
|
|
308
|
-
getStatePath: jest.fn().mockReturnValue("/tmp/state.json"),
|
|
309
|
-
readData: jest.fn().mockResolvedValue(state),
|
|
310
|
-
readFullState: jest.fn().mockResolvedValue({ metadata: { repositoryPath: repoPath } }),
|
|
311
|
-
readRepoData: jest.fn().mockResolvedValue(state),
|
|
312
|
-
getRepoRepositoryPath: jest.fn().mockResolvedValue(repoPath),
|
|
313
|
-
};
|
|
314
|
-
mockFromStatePath.mockReturnValue(stateManager);
|
|
315
|
-
await server.getHandler()({ stateFile: "/tmp/state.json" });
|
|
316
|
-
return mockBuildDriftAnalysisPrompt.mock.calls[0]?.[2]?.tests ?? [];
|
|
317
|
-
}
|
|
318
|
-
it("CC-UC02: finds e2e test via diff signal when testid was renamed", async () => {
|
|
319
|
-
// Use relative paths for changedFrontendFiles — matches how diffs are formatted
|
|
320
|
-
const relFrontend = "apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx";
|
|
321
|
-
writeFile(relFrontend, `data-testid="onboarding-submit-btn"`);
|
|
322
|
-
const e2eTest = writeFile("apps/web/playwright/onboarding.e2e.ts", `await page.getByTestId("onboarding-continue-btn").click();`);
|
|
323
|
-
const diffFile = writeFile("cc-uc02.diff", [
|
|
324
|
-
`diff --git a/${relFrontend} b/${relFrontend}`,
|
|
325
|
-
`- data-testid="onboarding-continue-btn"`,
|
|
326
|
-
`+ data-testid="onboarding-submit-btn"`,
|
|
327
|
-
].join("\n"));
|
|
328
|
-
const state = makeState({
|
|
329
|
-
changedFrontendFiles: [relFrontend], // relative — code resolves against repositoryPath
|
|
330
|
-
existingTests: [{ testFile: e2eTest, testType: "e2e", framework: "playwright-ui", source: "external" }],
|
|
331
|
-
diffFilePath: diffFile,
|
|
332
|
-
});
|
|
333
|
-
state.repositoryAnalysis.relevantExternalTestPaths = []; // grep fallback fires
|
|
334
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
335
|
-
expect(uiTests.some(t => t.testFile === e2eTest)).toBe(true);
|
|
336
|
-
});
|
|
337
|
-
it("does not extract diff strings shorter than 10 chars (min-length filter)", async () => {
|
|
338
|
-
const frontendFile = writeFile("button.tsx", ``);
|
|
339
|
-
const genericTest = writeFile("Button.test.tsx", `it("renders", () => { expect(true).toBe(true); /* continue finish true */ });`);
|
|
340
|
-
const diffFile = writeFile("short.diff", [
|
|
341
|
-
`diff --git a/button.tsx b/button.tsx`,
|
|
342
|
-
`- label="continue"`,
|
|
343
|
-
`+ label="finish"`,
|
|
344
|
-
`- value={true}`,
|
|
345
|
-
].join("\n"));
|
|
346
|
-
const state = makeState({
|
|
347
|
-
changedFrontendFiles: [frontendFile],
|
|
348
|
-
existingTests: [{ testFile: genericTest, testType: "unknown", framework: "", source: "external" }],
|
|
349
|
-
diffFilePath: diffFile,
|
|
350
|
-
});
|
|
351
|
-
state.repositoryAnalysis.relevantExternalTestPaths = [];
|
|
352
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
353
|
-
expect(uiTests.some(t => t.testFile === genericTest)).toBe(false);
|
|
354
|
-
});
|
|
355
|
-
it("GENERIC_SLUGS blocks generic slugs longer than 6 chars (operator precedence)", async () => {
|
|
356
|
-
// "component" is in GENERIC_SLUGS, length=9 — must NOT be added to symbols
|
|
357
|
-
const frontendFile = writeFile("component.tsx", ``);
|
|
358
|
-
const unrelatedTest = writeFile("Component.test.tsx", `import { component } from './utils'; describe("component", () => {});`);
|
|
359
|
-
const state = makeState({
|
|
360
|
-
changedFrontendFiles: [frontendFile],
|
|
361
|
-
existingTests: [{ testFile: unrelatedTest, testType: "unknown", framework: "rtl", source: "external" }],
|
|
362
|
-
});
|
|
363
|
-
state.repositoryAnalysis.relevantExternalTestPaths = [];
|
|
364
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
365
|
-
expect(uiTests.some(t => t.testFile === unrelatedTest)).toBe(false);
|
|
366
|
-
});
|
|
367
|
-
it("grep fires when uiTests.length === SYMBOL_GREP_MAX_RESULTS (boundary >= not >)", async () => {
|
|
368
|
-
const relFrontend = "apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx";
|
|
369
|
-
writeFile(relFrontend, `export const PersonalCalendarView = () => {};`);
|
|
370
|
-
const targetTest = writeFile("apps/web/playwright/onboarding.e2e.ts", `await page.getByTestId("onboarding-continue-btn").click();`);
|
|
371
|
-
const diffFile = writeFile("rename.diff", [
|
|
372
|
-
`diff --git a/${relFrontend} b/${relFrontend}`,
|
|
373
|
-
`- data-testid="onboarding-continue-btn"`,
|
|
374
|
-
`+ data-testid="onboarding-submit-btn"`,
|
|
375
|
-
].join("\n"));
|
|
376
|
-
// Exactly 20 wrong tests in relevantExternalTestPaths
|
|
377
|
-
const wrongTests = Array.from({ length: 20 }, (_, i) => {
|
|
378
|
-
const f = writeFile(`packages/ui/Test${i}.test.tsx`, `describe("Test${i}", () => {});`);
|
|
379
|
-
return { testFile: f, testType: "unknown", framework: "rtl", source: "external" };
|
|
380
|
-
});
|
|
381
|
-
const state = makeState({
|
|
382
|
-
changedFrontendFiles: [relFrontend],
|
|
383
|
-
existingTests: [
|
|
384
|
-
...wrongTests,
|
|
385
|
-
// targetTest is in existingTests (discovered) but NOT in relevantExternalTestPaths
|
|
386
|
-
// (it's a .e2e.ts file not yet in the promoted set) — the >= branch must find it
|
|
387
|
-
{ testFile: targetTest, testType: "e2e", framework: "playwright-ui", source: "external" },
|
|
388
|
-
],
|
|
389
|
-
diffFilePath: diffFile,
|
|
390
|
-
});
|
|
391
|
-
// 20 wrong tests in relevantExternalTestPaths — targetTest deliberately excluded
|
|
392
|
-
state.repositoryAnalysis.relevantExternalTestPaths = wrongTests.map(t => t.testFile);
|
|
393
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
394
|
-
// Grep must have fired (>= condition) and ranked targetTest to top
|
|
395
|
-
expect(uiTests.some(t => t.testFile === targetTest)).toBe(true);
|
|
396
|
-
expect(uiTests.length).toBeLessThanOrEqual(20);
|
|
397
|
-
});
|
|
398
|
-
it("exact path match prevents test diff sections from contaminating symbols", async () => {
|
|
399
|
-
const relFrontend = "apps/web/modules/onboarding/personal/calendar/personal-calendar-view.tsx";
|
|
400
|
-
writeFile(relFrontend, `export const PersonalCalendarView = () => {};`);
|
|
401
|
-
const targetTest = writeFile("apps/web/playwright/onboarding.e2e.ts", `await page.getByTestId("onboarding-continue-btn").click();`);
|
|
402
|
-
const falsePositiveTest = writeFile("apps/web/playwright/false-positive.e2e.ts", `await page.getByTestId("injected-from-test-file").click();`);
|
|
403
|
-
// Diff has TWO sections: the test file first (substring match trap), then the frontend file
|
|
404
|
-
const diffFile = writeFile("both-sections.diff", [
|
|
405
|
-
`diff --git a/personal-calendar-view.test.tsx b/personal-calendar-view.test.tsx`,
|
|
406
|
-
`+ expect(screen.getByTestId("injected-from-test-file")).toBeTruthy();`,
|
|
407
|
-
`diff --git a/${relFrontend} b/${relFrontend}`,
|
|
408
|
-
`- data-testid="onboarding-continue-btn"`,
|
|
409
|
-
].join("\n"));
|
|
410
|
-
const state = makeState({
|
|
411
|
-
changedFrontendFiles: [relFrontend],
|
|
412
|
-
existingTests: [
|
|
413
|
-
{ testFile: targetTest, testType: "e2e", framework: "playwright-ui", source: "external" },
|
|
414
|
-
{ testFile: falsePositiveTest, testType: "e2e", framework: "playwright-ui", source: "external" },
|
|
415
|
-
],
|
|
416
|
-
diffFilePath: diffFile,
|
|
417
|
-
});
|
|
418
|
-
state.repositoryAnalysis.relevantExternalTestPaths = [];
|
|
419
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
420
|
-
expect(uiTests.some(t => t.testFile === targetTest)).toBe(true);
|
|
421
|
-
expect(uiTests.some(t => t.testFile === falsePositiveTest)).toBe(false);
|
|
422
|
-
});
|
|
423
|
-
// CC-UC04: bookings-results testid (>=10 chars) in diff → booking-pages.e2e.ts found
|
|
424
|
-
it("CC-UC04: finds e2e test when new data-testid added to diff + lines", async () => {
|
|
425
|
-
const relFrontend = "apps/web/modules/bookings/components/BookingList.tsx";
|
|
426
|
-
writeFile(relFrontend, `export function BookingList() { return <div data-testid="bookings-results" />; }`);
|
|
427
|
-
const bookingPagesTest = writeFile("apps/web/playwright/booking-pages.e2e.ts", `await page.getByTestId("bookings-results").waitFor({ state: "visible" });`);
|
|
428
|
-
const diffFile = writeFile("cc-uc04.diff", [
|
|
429
|
-
`diff --git a/${relFrontend} b/${relFrontend}`,
|
|
430
|
-
`+ <div data-testid="bookings-results" data-state={dataState}>`,
|
|
431
|
-
].join("\n"));
|
|
432
|
-
const state = makeState({
|
|
433
|
-
changedFrontendFiles: [relFrontend],
|
|
434
|
-
existingTests: [{ testFile: bookingPagesTest, testType: "e2e", framework: "playwright-ui", source: "external" }],
|
|
435
|
-
diffFilePath: diffFile,
|
|
436
|
-
});
|
|
437
|
-
state.repositoryAnalysis.relevantExternalTestPaths = [];
|
|
438
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
439
|
-
expect(uiTests.some(t => t.testFile === bookingPagesTest)).toBe(true);
|
|
440
|
-
});
|
|
441
|
-
// CC-UC03 known gap: booking-sheet-keyboard.e2e.ts NOT found for additive PR
|
|
442
|
-
// The PR adds aria-selected and booking-row-cell (new + lines, no - lines with old values).
|
|
443
|
-
// The test uses [role="dialog"] and booking-sheet-title — no overlap with diff signals.
|
|
444
|
-
// This documents the known limitation: additive PRs with no renamed selectors
|
|
445
|
-
// cannot be discovered via diff-signal extraction alone.
|
|
446
|
-
it("CC-UC03 known gap: additive-only diff (no removed selectors) does not find e2e test", async () => {
|
|
447
|
-
const relFrontend = "apps/web/components/booking/BookingListItem.tsx";
|
|
448
|
-
writeFile(relFrontend, `export function BookingListItem() { return <div aria-selected="false" />; }`);
|
|
449
|
-
const keyboardTest = writeFile("apps/web/playwright/booking-sheet-keyboard.e2e.ts", `const sheet = page.locator('[role="dialog"]');\nawait expect(sheet.getByTestId("booking-sheet-title")).toHaveText("Booking 1");`);
|
|
450
|
-
// Only + lines (additive) — no - lines with old values to match
|
|
451
|
-
const diffFile = writeFile("cc-uc03.diff", [
|
|
452
|
-
`diff --git a/${relFrontend} b/${relFrontend}`,
|
|
453
|
-
`+ aria-selected={isSelected ? "true" : "false"}`,
|
|
454
|
-
`+ data-testid="booking-row-cell"`,
|
|
455
|
-
].join("\n"));
|
|
456
|
-
const state = makeState({
|
|
457
|
-
changedFrontendFiles: [relFrontend],
|
|
458
|
-
existingTests: [{ testFile: keyboardTest, testType: "e2e", framework: "playwright-ui", source: "external" }],
|
|
459
|
-
diffFilePath: diffFile,
|
|
460
|
-
});
|
|
461
|
-
state.repositoryAnalysis.relevantExternalTestPaths = [];
|
|
462
|
-
const uiTests = await runHandler(state, tmpDir);
|
|
463
|
-
// Known gap: additive-only PRs cannot surface tests via diff signals.
|
|
464
|
-
// The test content ([role="dialog"], booking-sheet-title) has no overlap with
|
|
465
|
-
// the added attributes (aria-selected, booking-row-cell).
|
|
466
|
-
expect(uiTests.some(t => t.testFile === keyboardTest)).toBe(false);
|
|
467
|
-
});
|
|
468
|
-
});
|