@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
|
@@ -15,11 +15,13 @@ export function buildFileRenameStrategy() {
|
|
|
15
15
|
}
|
|
16
16
|
/** Strategy string for in-place UPDATE edits. */
|
|
17
17
|
export function buildUpdateStrategy() {
|
|
18
|
-
return `
|
|
18
|
+
return `Apply each file in update_context yourself with the Edit tool, using its current_content and the changes described in context. Preserve all existing test logic — only add or adjust what context describes.
|
|
19
|
+
|
|
20
|
+
When a file provides edit_sites, apply each site directly using its anchor as the Edit target — do not re-grep or re-page the file to relocate the sites; they were already found for you.
|
|
19
21
|
|
|
20
22
|
UPDATE is an in-place edit to the existing baseline file — do not create a new test file to cover the same page or component.
|
|
21
23
|
|
|
22
|
-
After
|
|
24
|
+
After editing, re-read each file to confirm the change landed, then call skyramp_enhance_assertions with each updated file path.`;
|
|
23
25
|
}
|
|
24
26
|
/** Strategy string for REGENERATE — call generation tool to overwrite the file. */
|
|
25
27
|
export function buildRegenerateStrategy() {
|
|
@@ -4,6 +4,15 @@ export type UiDriftParams = {
|
|
|
4
4
|
}>;
|
|
5
5
|
changedFrontendFiles: string[];
|
|
6
6
|
blueprintCaptured?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Changed frontend files imported by 2+ production files, with their importer
|
|
9
|
+
* lists (server-computed). Drives the shared-component drift rule: a test may not
|
|
10
|
+
* be assigned IGNORE for one of these files until every importer has been read.
|
|
11
|
+
*/
|
|
12
|
+
sharedComponents?: Array<{
|
|
13
|
+
file: string;
|
|
14
|
+
importers: string[];
|
|
15
|
+
}>;
|
|
7
16
|
};
|
|
8
17
|
/**
|
|
9
18
|
* Build the drift analysis prompt.
|
|
@@ -61,14 +61,15 @@ export function buildDriftAnalysisPrompt(stateFile, apiTests, ui, repoPaths) {
|
|
|
61
61
|
${ui.changedFrontendFiles.length > 0
|
|
62
62
|
? ui.changedFrontendFiles.map(f => `- ${f}`).join("\n")
|
|
63
63
|
: "(none)"}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
${ui.sharedComponents && ui.sharedComponents.length > 0
|
|
65
|
+
? `
|
|
66
|
+
**Shared frontend components (imported by 2+ production files — cross-consumer blast radius):**
|
|
67
|
+
${ui.sharedComponents.map(c => `- ${c.file} — ${c.importers.length} importer(s): ${c.importers.join(", ")}`).join("\n")}
|
|
68
|
+
For any test whose changed target appears here, you MUST NOT assign IGNORE, and may assign UPDATE only after reading every listed importer (see "Component behavior changes").
|
|
69
|
+
`
|
|
70
|
+
: ""}
|
|
70
71
|
**Instructions:**
|
|
71
|
-
For each test
|
|
72
|
+
For each test in \`<tests_to_assess>\`, read it to determine its type, then apply the scope gate from the decision tree. Assign the appropriate action (DELETE, REGENERATE, UPDATE, VERIFY, IGNORE).
|
|
72
73
|
</ui_analysis_context>
|
|
73
74
|
|
|
74
75
|
`;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* mirroring the recommendationSections.ts pattern.
|
|
4
4
|
*/
|
|
5
5
|
import { AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
|
|
6
|
-
import { buildApiAssignAction } from "./driftAnalysisShared.js";
|
|
6
|
+
import { buildApiAssignAction, EMPTY_TEST_LIST_PLACEHOLDER, NO_PRELOADED_TESTS_NOTE } from "./driftAnalysisShared.js";
|
|
7
7
|
// TODO: Replace the open-ended diff-line categories below with a two-tier structure:
|
|
8
8
|
// Tier 1 — mechanical patterns (specific token + file-path criteria, zero judgment needed).
|
|
9
9
|
// Tier 2 — residual open-ended scan for anything observable not covered by tier 1.
|
|
@@ -173,6 +173,7 @@ Diff signals and actions:
|
|
|
173
173
|
}
|
|
174
174
|
export function buildCheckAssignAction() {
|
|
175
175
|
return buildApiAssignAction(`- If the Additive Fields check flagged a new field with output layer signal confirmed in the diff → action is UPDATE. If the Additive Fields check returned VERIFY (model-only signal, no output layer change) → action remains VERIFY.
|
|
176
|
+
- **Request/response contract change** (a field made required, a type change, or a removal): the changed schema/DTO line is the quotable trigger. You MUST read the body of every candidate test that could reach the changed endpoint before assigning it an action — a speculative "may/uncertain" rationale on a file you have not read is not allowed. After reading, assign UPDATE if the test sends a request the new contract now rejects (e.g. it POSTs without the now-required field — it need not name the changed symbol); assign IGNORE only with a concrete reason quoted from the file (it already supplies the field, or never calls the endpoint). Use VERIFY only when the test's behavior genuinely cannot be determined from its own body. When a single UPDATE file needs the same change at more than a couple of places, record each one in \`editSites\` (line + a verbatim anchor copied from that line) as you read it — this list is handed to the apply step so it edits each site directly instead of re-scanning the file.
|
|
176
177
|
- **Scope gate:** If the changed code is clearly not reachable through the service or base URL this test targets, assign IGNORE.
|
|
177
178
|
- **When uncertain, use VERIFY not IGNORE:** If the diff touches a model or migration and this test's endpoint reads from that model, assign VERIFY — you cannot confirm from the diff alone whether the field is exposed.`);
|
|
178
179
|
}
|
|
@@ -200,10 +201,8 @@ export function buildDriftOutputChecklist(stateFile, existingTests) {
|
|
|
200
201
|
const label = (t.source ?? "skyramp") === "external" ? " [external]" : "";
|
|
201
202
|
return `- ${t.testFile}${label}`;
|
|
202
203
|
}).join("\n")
|
|
203
|
-
:
|
|
204
|
-
const noTestsNote = !hasTests
|
|
205
|
-
? `\nNo existing tests were found. Call \`skyramp_actions\` with \`recommendations: []\` and explain in your report why no maintenance was needed (e.g. "no existing tests cover the changed endpoints" or "PR adds a new endpoint with no prior coverage").\n`
|
|
206
|
-
: "";
|
|
204
|
+
: EMPTY_TEST_LIST_PLACEHOLDER;
|
|
205
|
+
const noTestsNote = !hasTests ? NO_PRELOADED_TESTS_NOTE : "";
|
|
207
206
|
const existingTestSection = `**Existing tests (${existingTests?.length ?? 0} total) — assess ALL of the following:**
|
|
208
207
|
${testList}
|
|
209
208
|
${noTestsNote}
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
* covering aliases, indirect renders, and shared hooks the diff doesn't mention.
|
|
6
6
|
*/
|
|
7
7
|
export declare function buildSymbolDiscoveryStep(hasTests: boolean, repoPaths?: string[]): string;
|
|
8
|
+
/** Placeholder shown in place of an empty pre-loaded test list — the agent must still
|
|
9
|
+
* discover tests via the shared Symbol discovery step before concluding none apply. */
|
|
10
|
+
export declare const EMPTY_TEST_LIST_PLACEHOLDER = "- (none pre-loaded \u2014 discover them via the Symbol discovery step above before assessing)";
|
|
11
|
+
/**
|
|
12
|
+
* Shared note for the drift output checklist when no tests were pre-loaded. An empty
|
|
13
|
+
* pre-load is not evidence that no tests exist — in PR mode external (user-written) tests
|
|
14
|
+
* are discovered by the agent via the Symbol discovery step, not pre-loaded. Defers the
|
|
15
|
+
* empty-recommendations bail to genuine grep discovery finding nothing. Used by both the
|
|
16
|
+
* API and UI checklists so the wording stays in one place.
|
|
17
|
+
*/
|
|
18
|
+
export declare const NO_PRELOADED_TESTS_NOTE = "\nNo tests were pre-loaded into the list above. This does NOT mean none exist \u2014 external (user-written) tests are discovered by you via the Symbol discovery step, not pre-loaded. First complete that grep, then assess every test file it surfaces using the actions below. Only call `skyramp_actions` with `recommendations: []` if that discovery genuinely finds no test exercising the changed code \u2014 and in that case state in your report exactly what you grepped for (endpoints, function/class names, changed type names).\n";
|
|
8
19
|
/** One source of truth for the recommendations[] contract passed to skyramp_actions. */
|
|
9
20
|
export declare const RECOMMENDATIONS_INSTRUCTION = "Pass VERIFY, UPDATE, REGENERATE, and DELETE entries to `recommendations[]`. Omit IGNORE.";
|
|
10
21
|
export declare function buildSharedExternalTestPolicy(): string;
|
|
@@ -5,23 +5,39 @@
|
|
|
5
5
|
* covering aliases, indirect renders, and shared hooks the diff doesn't mention.
|
|
6
6
|
*/
|
|
7
7
|
export function buildSymbolDiscoveryStep(hasTests, repoPaths) {
|
|
8
|
-
if (!hasTests)
|
|
9
|
-
return "";
|
|
10
8
|
const repoList = repoPaths && repoPaths.length > 1
|
|
11
9
|
? repoPaths.map(p => `\`${p}\``).join(", ")
|
|
12
10
|
: repoPaths?.[0] ? `\`${repoPaths[0]}\`` : "the repository";
|
|
13
|
-
|
|
11
|
+
// When no tests were pre-loaded this step is REQUIRED, not optional: in PR mode external
|
|
12
|
+
// (user-written) tests are discovered by the agent, not pre-loaded, so an empty list means
|
|
13
|
+
// "not yet discovered", not "none exist". Suppressing this step in that case is the bug that
|
|
14
|
+
// let schema-only diffs silently maintain nothing (SKYR-3924).
|
|
15
|
+
const intro = hasTests
|
|
16
|
+
? `**Symbol discovery (before assessing):** Find *additional* test files that reference changed code beyond those already listed:`
|
|
17
|
+
: `**Symbol discovery (REQUIRED — do this before concluding anything):** No tests were pre-loaded. That does NOT mean none exist — external (user-written) tests are discovered here, not pre-loaded. Find the test files that exercise the changed code:`;
|
|
18
|
+
return `${intro}
|
|
14
19
|
|
|
15
20
|
1. **Hunk context:** Extract function and class names from \`@@\` context lines and changed endpoint URL path segments. Grep ${repoList} for each.
|
|
16
|
-
2. **Full file exports:** For each changed source file in the diff, read it and extract all exported symbols (component names, function names, hooks, class names). Grep ${repoList} for each (case-insensitive). This catches tests using aliased imports, indirect renders,
|
|
21
|
+
2. **Full file exports:** For each changed source file in the diff, read it and extract all exported symbols (component names, function names, hooks, class names) — and for schema/model/DTO files, the changed type/schema names. Grep ${repoList} for each (case-insensitive). This catches tests using aliased imports, indirect renders, shared hooks, or a changed type referenced by name that the diff hunk doesn't mention.
|
|
17
22
|
|
|
18
|
-
Add
|
|
23
|
+
Add every matched test file to your assessment below${hasTests ? " (if not already listed)" : ""}, and include the resulting VERIFY/UPDATE/REGENERATE/DELETE entries in \`recommendations[]\`.`;
|
|
19
24
|
}
|
|
25
|
+
/** Placeholder shown in place of an empty pre-loaded test list — the agent must still
|
|
26
|
+
* discover tests via the shared Symbol discovery step before concluding none apply. */
|
|
27
|
+
export const EMPTY_TEST_LIST_PLACEHOLDER = "- (none pre-loaded — discover them via the Symbol discovery step above before assessing)";
|
|
28
|
+
/**
|
|
29
|
+
* Shared note for the drift output checklist when no tests were pre-loaded. An empty
|
|
30
|
+
* pre-load is not evidence that no tests exist — in PR mode external (user-written) tests
|
|
31
|
+
* are discovered by the agent via the Symbol discovery step, not pre-loaded. Defers the
|
|
32
|
+
* empty-recommendations bail to genuine grep discovery finding nothing. Used by both the
|
|
33
|
+
* API and UI checklists so the wording stays in one place.
|
|
34
|
+
*/
|
|
35
|
+
export const NO_PRELOADED_TESTS_NOTE = `\nNo tests were pre-loaded into the list above. This does NOT mean none exist — external (user-written) tests are discovered by you via the Symbol discovery step, not pre-loaded. First complete that grep, then assess every test file it surfaces using the actions below. Only call \`skyramp_actions\` with \`recommendations: []\` if that discovery genuinely finds no test exercising the changed code — and in that case state in your report exactly what you grepped for (endpoints, function/class names, changed type names).\n`;
|
|
20
36
|
/** One source of truth for the recommendations[] contract passed to skyramp_actions. */
|
|
21
37
|
export const RECOMMENDATIONS_INSTRUCTION = `Pass VERIFY, UPDATE, REGENERATE, and DELETE entries to \`recommendations[]\`. Omit IGNORE.`;
|
|
22
38
|
export function buildSharedExternalTestPolicy() {
|
|
23
39
|
return `**For user-written (external) tests** marked \`[external]\` in the test list:
|
|
24
|
-
- UPDATE is permitted — pass the exact file that needs editing as \`testFile\`. This can be the test file itself, a snapshot file, a page object, or a fixture. \`skyramp_actions\`
|
|
40
|
+
- UPDATE is permitted — pass the exact file that needs editing as \`testFile\`. This can be the test file itself, a snapshot file, a page object, or a fixture. \`skyramp_actions\` returns edit instructions for that file; application source files are automatically rejected.
|
|
25
41
|
- REGENERATE and DELETE: assign these actions when the evidence clearly warrants them (page gone → DELETE, component structure fundamentally changed → REGENERATE). \`skyramp_actions\` handles execution restrictions at apply time — assign the action that the evidence supports. Leave user-authored test files unchanged; edits go only through \`skyramp_actions\` with explicit \`updateInstructions\`. VERIFY means "uncertain, need more context" — assign DELETE or REGENERATE when you have confirmed the failure, and VERIFY only when you genuinely cannot determine impact without more information.`;
|
|
26
42
|
}
|
|
27
43
|
/** Shared boilerplate: VERIFY rule, pre-commit structure, and external policy. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (RTL/Flutter widget/vue-test-utils/angular-testbed) go through the same gate.
|
|
8
8
|
* Component tests always use source-only analysis regardless of blueprintCaptured.
|
|
9
9
|
*/
|
|
10
|
-
import { buildUiAssignAction } from "./driftAnalysisShared.js";
|
|
10
|
+
import { buildUiAssignAction, EMPTY_TEST_LIST_PLACEHOLDER, NO_PRELOADED_TESTS_NOTE } from "./driftAnalysisShared.js";
|
|
11
11
|
/**
|
|
12
12
|
* Build the UI Action Decision Tree section.
|
|
13
13
|
* This is the main entry point for UI drift analysis.
|
|
@@ -20,10 +20,12 @@ For each UI test listed in the \`<ui_analysis_context>\` above, assess whether t
|
|
|
20
20
|
- Route paths (URL changes)
|
|
21
21
|
- Component behavior (state changes, visibility conditions)
|
|
22
22
|
|
|
23
|
-
**Scope gate:** Read each test file to determine its type
|
|
24
|
-
- **Browser test** (has \`page.goto()\`, \`cy.visit()\`, \`driver.get()\`):
|
|
25
|
-
- **Component/widget test** (has \`render(<…>)\`, \`mount()\`, \`shallowMount()\`):
|
|
26
|
-
|
|
23
|
+
**Scope gate:** Read each test file to determine its type. A test is in scope if the diff's changed file reaches it, directly or transitively:
|
|
24
|
+
- **Browser test** (has \`page.goto()\`, \`cy.visit()\`, \`driver.get()\`): via a page/route it navigates to — the file *is* that page/route (infer from its directory path, e.g. \`modules/bookings/\` → bookings page, or from route-path diff signals), or a component that page renders.
|
|
25
|
+
- **Component/widget test** (has \`render(<…>)\`, \`mount()\`, \`shallowMount()\`): via the component it mounts.
|
|
26
|
+
- **Shared / design-system component:** the changed file is a shared component (e.g. a \`design-system\`/\`ui\` package, or one imported by many consumers), so it reaches every consumer — use symbol discovery to find the consumer tests and treat each as in scope. A different package or directory does not make a test unrelated when it consumes the changed component.
|
|
27
|
+
|
|
28
|
+
Matching none does **not** mean IGNORE — first run the selector safety net: match the class / \`data-testid\` / \`aria-*\` literals the diff changed or added against the test body and any page object it imports. A match puts the test in scope. Assign **IGNORE** only when a test matches none of the conditions **and** the selector safety net finds nothing. Assess every in-scope test under the rules below.
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
**Severity order (highest first): DELETE > REGENERATE > UPDATE > VERIFY > IGNORE**
|
|
@@ -37,24 +39,12 @@ If neither condition applies, assign **IGNORE** and skip remaining checks.
|
|
|
37
39
|
|
|
38
40
|
Build a detection list first: for each matched diff line, write one line: \`{pattern type} — "{diff line}" — affects {route/component}\`.
|
|
39
41
|
|
|
40
|
-
**
|
|
41
|
-
- **DELETE
|
|
42
|
-
- **REGENERATE
|
|
43
|
-
- **UPDATE
|
|
44
|
-
- **VERIFY
|
|
45
|
-
- **IGNORE
|
|
46
|
-
|
|
47
|
-
**Rules:**
|
|
48
|
-
- DELETE when all routes the test covers were removed or all critical selectors are gone.
|
|
49
|
-
- REGENERATE when the interaction model changed fundamentally — the test's sequence of actions is broken with no migration path (e.g. a click-to-update button replaced by an auto-updating stepper; a remove button moved into a dropdown). The number of broken selectors alone does not determine REGENERATE; what matters is whether the flow can still be expressed with targeted edits.
|
|
50
|
-
- UPDATE when specific selectors changed (renamed classes, moved data-testid) but the test flow remains valid, or when new elements were added to a page the test already covers — append the assertion to the existing file.
|
|
51
|
-
- VERIFY when a page object or indirect import hides the affected selectors — read the imported file before escalating to UPDATE.
|
|
52
|
-
- IGNORE when the diff touches unrelated frontend code (different routes, different components).
|
|
53
|
-
|
|
54
|
-
**Additional rule for component/widget tests** ([rtl], [flutter-widget], [vue-test-utils], [angular-testbed]):
|
|
55
|
-
- Rule #7: Component deleted — source file removed with no re-export → **DELETE** (report-only for external tests). These tests have no navigation; the rendered component is their only target.
|
|
56
|
-
- Skip the route-existence check (step 1.1) for component/widget tests — they have no \`page.goto()\` or equivalent navigation calls.
|
|
57
|
-
- Primary matching signal: read the test to check if it directly renders a changed component — if so, it is impacted.
|
|
42
|
+
**Rules (assign exactly one action per test; severity order above):**
|
|
43
|
+
- **DELETE** when the test's target is gone: all routes/pages it covers no longer exist; all critical selectors were removed with no migration path; or, for a component/widget test, the component it renders was deleted (source removed with no re-export; report-only for external tests).
|
|
44
|
+
- **REGENERATE** when the interaction model changed so fundamentally that the test's action sequence is broken with no migration path: the flow itself changed, not just selector values (e.g. a form replaced by a modal, buttons replaced by a dropdown, a click-to-update button replaced by an auto-updating stepper). Re-recording from scratch is the only viable approach. Broken-selector count alone does not decide REGENERATE; if the flow still works with targeted edits, it is UPDATE.
|
|
45
|
+
- **UPDATE** when the test's flow still holds but something it relies on shifted on a page or component it exercises: a selector (className, data-testid, aria-*) was renamed or moved, a new selector was added that the test should adopt or assert (including a dynamic class like \`+ \`btn--\${variant}\`\`), or an assertion needs adjusting. Fix it in place in the existing file, never a new spec for the same page or component. An added selector is still drift, not a no-op. Read the test/POM to confirm the component is targeted and prefer UPDATE; fall back to VERIFY only when reading leaves impact undetermined.
|
|
46
|
+
- **VERIFY** when the test may be impacted but you cannot confirm breakage without reading more context (e.g. the affected selectors live in a page object or indirect import you have not read). Read that file before escalating to UPDATE.
|
|
47
|
+
- **IGNORE** when the changed code cannot reach this test's routes or rendered components (the diff touches different routes or components). If you cannot point to a diff line affecting this test's routes, selectors, or rendered components, the action is IGNORE or VERIFY, not UPDATE.
|
|
58
48
|
</ui_decision_rules>`;
|
|
59
49
|
}
|
|
60
50
|
/**
|
|
@@ -89,6 +79,7 @@ Diff signals to look for:
|
|
|
89
79
|
- Tailwind changes: \`- tw="p-4"\` / \`+ tw="p-6"\`
|
|
90
80
|
- Test ID changes: \`- data-testid="old-id"\` / \`+ data-testid="new-id"\`
|
|
91
81
|
- ARIA changes: \`- aria-label="Old"\` / \`+ aria-label="New"\`
|
|
82
|
+
- Selector ADDED with no paired removal: \`+ data-testid="new-id"\`, \`+ className="new-class"\`, or a dynamically-composed class \`+ \`btn--\${variant}\`\`
|
|
92
83
|
- Component restructuring: \`+ <NewWrapper>\` around existing content, \`- <OldWrapper>\`
|
|
93
84
|
|
|
94
85
|
**When blueprints are available** (\`blueprintCaptured=true\`): compare extracted selectors against \`browser_blueprint_diff\` for the affected page — elements absent from the new blueprint signal breakage.
|
|
@@ -96,9 +87,10 @@ Diff signals to look for:
|
|
|
96
87
|
**When blueprints are not available** (\`blueprintCaptured=false\`): use diff signals only. If a selector the test uses matches a value renamed or removed in the diff, assign UPDATE. If you cannot confirm breakage from the diff alone (e.g. selectors live in a page object you have not read), assign VERIFY rather than guessing.
|
|
97
88
|
|
|
98
89
|
Actions:
|
|
99
|
-
- Selectors confirmed unchanged (diff or blueprint) → **IGNORE** (no drift)
|
|
90
|
+
- Selectors confirmed unchanged (diff or blueprint) → **IGNORE** (no drift). "Absent from the diff" is NOT "confirmed unchanged": a \`data-testid\` or selector can MOVE to a different element while keeping its value, producing no \`-\`/\`+\` literal to match. If the test renders or navigates a changed component but no selector literal appears in the diff, the selector may have moved — assign **VERIFY** (confirm via blueprint or by reading the changed component), not IGNORE.
|
|
100
91
|
- Selectors renamed (class, testid, aria-label changed) → **UPDATE** (patch selector values)
|
|
101
92
|
- Selectors removed with no confirmed replacement → **UPDATE** (find replacement; use blueprint if available, diff otherwise)
|
|
93
|
+
- Selector ADDED with no paired removal (new class, testid, or \`+ \`btn--\${variant}\`\`) → **UPDATE** (adopt the new selector or assert the new variant)
|
|
102
94
|
- Major restructuring (most selectors gone) → **REGENERATE** (page structure fundamentally changed)
|
|
103
95
|
- All selectors gone, no replacement elements → **DELETE** (page no longer testable)
|
|
104
96
|
- Cannot confirm without reading page object or additional files → **VERIFY**
|
|
@@ -151,7 +143,7 @@ Actions:
|
|
|
151
143
|
- New conditional rendering → **UPDATE** (update assertions to match new conditions)
|
|
152
144
|
- Auth behavior changed → **UPDATE** (adjust login flow in test)
|
|
153
145
|
- Async delays added → **UPDATE** (add appropriate waits/timeouts)
|
|
154
|
-
- Shared component changed (
|
|
146
|
+
- Shared component changed: if the changed file appears under **Shared frontend components** in \`<ui_analysis_context>\` (imported by 2+ production files), the importer list there is authoritative — do NOT rely on your own count. You may NOT assign **IGNORE** for such a file. Assign **UPDATE** only after reading every importer listed there and confirming the change is isolated to this test's consumer; until you have read all of them, assign **VERIFY** — the blast radius across the other consumers is indeterminate.`;
|
|
155
147
|
}
|
|
156
148
|
/**
|
|
157
149
|
* Assign the final UI drift action.
|
|
@@ -170,25 +162,25 @@ export function buildUiDriftOutputChecklist(stateFile, tests) {
|
|
|
170
162
|
const hasTests = (tests?.length ?? 0) > 0;
|
|
171
163
|
const testList = hasTests
|
|
172
164
|
? tests.map(t => `- ${t.testFile}${t.source === "external" ? " [external]" : ""}`).join("\n")
|
|
173
|
-
:
|
|
174
|
-
const noTestsNote = !hasTests
|
|
175
|
-
? `\nNo existing tests were found. Call \`skyramp_actions\` with \`recommendations: []\`.\n`
|
|
176
|
-
: "";
|
|
165
|
+
: EMPTY_TEST_LIST_PLACEHOLDER;
|
|
166
|
+
const noTestsNote = !hasTests ? NO_PRELOADED_TESTS_NOTE : "";
|
|
177
167
|
const existingTestSection = `**Tests (${tests?.length ?? 0} total) — assess ALL of the following:**
|
|
168
|
+
<tests_to_assess>
|
|
178
169
|
${testList}
|
|
170
|
+
</tests_to_assess>
|
|
179
171
|
${noTestsNote}
|
|
180
172
|
|
|
181
173
|
**Execution policy:**
|
|
182
174
|
- Skyramp-generated tests: UPDATE/REGENERATE/DELETE applied automatically by \`skyramp_actions\`. Run via \`skyramp_execute_test\`.
|
|
183
175
|
- External tests (Playwright, Cypress, RTL, etc.): \`skyramp_actions\` applies UPDATE edits to \`testFile\` — which can be the test itself, a snapshot file, a page object, or a fixture. Application source files are automatically rejected. After a successful UPDATE, call \`skyramp_execute_test\` to confirm the patched test passes (the test environment is already running). Record the result as the after-state in the report. For DELETE and REGENERATE on external tests, see the execution policy in the decision rules above.
|
|
184
176
|
|
|
185
|
-
For each UI test
|
|
177
|
+
For each UI test in \`<tests_to_assess>\`, output one structured entry in \`recommendations[]\`:
|
|
186
178
|
- **IGNORE**: \`{testFile} — IGNORE: {specific reason the diff cannot reach this test — name the route or component that differs}\` — do not pass to \`skyramp_actions\`
|
|
187
179
|
- **VERIFY**: \`{testFile} — VERIFY: {uncertain element, e.g. "selectors hidden in page object not yet read"}\`
|
|
188
180
|
\`\`\`
|
|
189
181
|
Test: {testFile}
|
|
190
182
|
Action: UPDATE
|
|
191
|
-
Rationale: UPDATE because {quoted diff line};
|
|
183
|
+
Rationale: UPDATE because {quoted diff line}; name the concrete before→after value (e.g. data-testid "old-id" → "new-id", class "btn-old" → "btn-new", route "/old" → "/new") — this rationale is the report's record of what was maintained, so a vague "selector changed" is not enough
|
|
192
184
|
UpdateInstructions: {what must change — specific selectors to update, routes to patch, assertions to add}
|
|
193
185
|
\`\`\`
|
|
194
186
|
\`\`\`
|
|
@@ -14,4 +14,10 @@ export declare function buildExecutionPlan(scored: Array<{
|
|
|
14
14
|
scenario: DraftedScenario;
|
|
15
15
|
priority: PriorityTier;
|
|
16
16
|
novelty: string;
|
|
17
|
-
}>, maxGen: number, topN: number, baseUrl: string, authHeaderValue: string, authSchemeSnippet: string, authTypeValue: string, seed: string, endpointCount: number, isUIOnlyPR: boolean, hasFrontendChanges?: boolean, hasTraces?: boolean, externalCoverage?: Set<string>, relevantExternalTestPaths?: string[]
|
|
17
|
+
}>, maxGen: number, topN: number, baseUrl: string, authHeaderValue: string, authSchemeSnippet: string, authTypeValue: string, seed: string, endpointCount: number, isUIOnlyPR: boolean, hasFrontendChanges?: boolean, hasTraces?: boolean, externalCoverage?: Set<string>, relevantExternalTestPaths?: string[],
|
|
18
|
+
/**
|
|
19
|
+
* Whether the diff classified at least one new/modified/removed endpoint.
|
|
20
|
+
* Defaults to true so existing callers keep the fixed-budget rendering.
|
|
21
|
+
* See buildScopeAssessmentSection's hasClassifiedApiChanges (SKYR-3820).
|
|
22
|
+
*/
|
|
23
|
+
hasApiChanges?: boolean): string;
|
|
@@ -217,8 +217,18 @@ function roundRobinByType(rankOrdered, count) {
|
|
|
217
217
|
}
|
|
218
218
|
return selected;
|
|
219
219
|
}
|
|
220
|
-
export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges = false, hasTraces = false, externalCoverage = new Set(), relevantExternalTestPaths = []
|
|
220
|
+
export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges = false, hasTraces = false, externalCoverage = new Set(), relevantExternalTestPaths = [],
|
|
221
|
+
/**
|
|
222
|
+
* Whether the diff classified at least one new/modified/removed endpoint.
|
|
223
|
+
* Defaults to true so existing callers keep the fixed-budget rendering.
|
|
224
|
+
* See buildScopeAssessmentSection's hasClassifiedApiChanges (SKYR-3820).
|
|
225
|
+
*/
|
|
226
|
+
hasApiChanges = true) {
|
|
221
227
|
const frontendUrl = "<frontend_url>";
|
|
228
|
+
// SKYR-3820: backend diff with zero classified endpoint changes — the Budget
|
|
229
|
+
// Plan defaults to 0 (see scopeAssessment), so every "fill the budget" nudge
|
|
230
|
+
// in this plan must flip to its abstention-aware variant.
|
|
231
|
+
const zeroClassifiedPR = !isUIOnlyPR && !hasFrontendChanges && !hasApiChanges;
|
|
222
232
|
// Slot allocation:
|
|
223
233
|
// - UI-only PR: all GENERATE slots are UI placeholders (no pre-ranked backend scenarios)
|
|
224
234
|
// - Mixed PR: last GENERATE slot is a UI placeholder; remaining slots are backend
|
|
@@ -443,7 +453,9 @@ export function buildExecutionPlan(scored, maxGen, topN, baseUrl, authHeaderValu
|
|
|
443
453
|
- **UI**: ${hasTraces ? "Use an existing Playwright `.zip` trace with `skyramp_ui_test_generation`." : `Record a trace using \`browser_navigate\` + \`browser_snapshot\` + \`skyramp_export_zip\`, then call \`skyramp_ui_test_generation({ playwrightInput: "<zip_path>", outputDir: "<frontend_output_dir>" })\` — set \`outputDir\` to ${SERVICE_REFS.frontendTestDirRef}.`}
|
|
444
454
|
Derive scenario names and steps from the actual changed frontend files. If your Budget Plan calls for 0% UI/E2E, omit this entirely.`
|
|
445
455
|
: "";
|
|
446
|
-
const supplementNote =
|
|
456
|
+
const supplementNote = zeroClassifiedPR
|
|
457
|
+
? `\n**Supplements:** With a 0-total Budget Plan, do not draft supplements. If you claimed the Scope Assessment ceiling, draft tests ONLY for endpoints whose observable behavior your code review showed actually changed in this diff — never generic patterns on unchanged endpoints.`
|
|
458
|
+
: `\n**If your Budget Plan total exceeds the pre-ranked items listed above:** draft additional tests from source-code enrichment (Step ${EXEC_STEP_ENRICH}). For each new or changed endpoint, identify boundary or variation scenarios — formula parameters, search/filter constraints, required field validation. Only after exhausting PR-specific scenarios, add generic patterns (auth boundary → 401, non-existent ID → 404). Do NOT supplement with tests whose endpoint + test type match a GENERATE item.`;
|
|
447
459
|
// ── PR / branch-diff mode: execution plan ────────────────────────────────
|
|
448
460
|
const externalTestFilesList = relevantExternalTestPaths.length > 0
|
|
449
461
|
? `**Read these external test files first** (paths are relative to the \`repositoryPath\` you passed to \`skyramp_analyze_changes\` — prepend it to get the absolute path). Determine exactly which HTTP methods + paths each one covers. This is the definitive source of truth for external coverage:\n${relevantExternalTestPaths.map((p) => `- \`${p}\``).join("\n")}\n\n`
|
|
@@ -455,9 +467,9 @@ Derive scenario names and steps from the actual changed frontend files. If your
|
|
|
455
467
|
enrichStepLabel: EXEC_STEP_ENRICH,
|
|
456
468
|
};
|
|
457
469
|
return `## Execution Plan
|
|
458
|
-
Seed: ${seed} | Endpoints: ${endpointCount} | Max: ${maxGen} generate + up to ${Math.max(topN - maxGen, 0)} additional (your Budget Plan determines the exact count)
|
|
470
|
+
Seed: ${seed} | Endpoints: ${endpointCount} | Max: ${zeroClassifiedPR ? `0 generate by default (the Scope Assessment exception may raise it to ${maxGen} generate + up to ${Math.max(topN - maxGen, 0)} additional)` : `${maxGen} generate + up to ${Math.max(topN - maxGen, 0)} additional (your Budget Plan determines the exact count)`}
|
|
459
471
|
|
|
460
|
-
${buildScopeAssessmentSection(topN, maxGen, isUIOnlyPR, isUIOnlyPR ? 100 : hasFrontendChanges ? undefined : 0, hasFrontendChanges)}
|
|
472
|
+
${buildScopeAssessmentSection(topN, maxGen, isUIOnlyPR, isUIOnlyPR ? 100 : hasFrontendChanges ? undefined : 0, hasFrontendChanges, hasApiChanges)}
|
|
461
473
|
|
|
462
474
|
${_execPlan.render(_ctx)}
|
|
463
475
|
|
|
@@ -467,7 +479,9 @@ ${isUIOnlyPR
|
|
|
467
479
|
? uiGenerateBlocks ||
|
|
468
480
|
" (no UI generate items — derive scenarios from changed frontend files)"
|
|
469
481
|
: [generateBlocks, uiPlaceholderBlock].filter(Boolean).join("\n\n") ||
|
|
470
|
-
|
|
482
|
+
(zeroClassifiedPR
|
|
483
|
+
? " (no pre-ranked generate items — the Budget Plan defaults to 0 total; add items only under the Scope Assessment exception)"
|
|
484
|
+
: " (no pre-ranked generate items — draft your own based on endpoint analysis)")}
|
|
471
485
|
|
|
472
486
|
### ADDITIONAL (list in additionalRecommendations in this order after Step ${EXEC_STEP_ENRICH} insertion)
|
|
473
487
|
|
|
@@ -476,7 +490,7 @@ ${uiGroundingGuidance}
|
|
|
476
490
|
${uiGuidance}
|
|
477
491
|
${supplementNote}
|
|
478
492
|
|
|
479
|
-
**Honor your Budget Plan: produce exactly the total you committed to (GENERATE + ADDITIONAL). No fewer, no padding with low-value tests.**
|
|
493
|
+
**Honor your Budget Plan: produce exactly the total you committed to (GENERATE + ADDITIONAL). No fewer, no padding with low-value tests. A Budget Plan of 0 total means zero tests — abstain and follow the zero-test report path; do not backfill.**
|
|
480
494
|
|
|
481
495
|
## Recommendation Stability
|
|
482
496
|
- **Carry forward** previous additionalRecommendations that still apply — match by scenarioName (multi-step) or endpoint (single-endpoint). Re-derive category and priority from test content.
|
|
@@ -52,4 +52,13 @@ export declare function buildScopeAssessmentSection(maxTotal?: number, maxGenera
|
|
|
52
52
|
/** Server-determined UI/E2E percentage. `undefined` = mixed PR or unknown. */
|
|
53
53
|
precomputedUIPct?: number,
|
|
54
54
|
/** Whether the diff contains frontend files (true ↔ mixed PR when precomputedUIPct is undefined). */
|
|
55
|
-
hasFrontendChanges?: boolean
|
|
55
|
+
hasFrontendChanges?: boolean,
|
|
56
|
+
/**
|
|
57
|
+
* PR-mode: whether the diff classified at least one new/modified/removed endpoint.
|
|
58
|
+
* When false on the backend branch (precomputedUIPct === 0), the Budget Plan
|
|
59
|
+
* defaults to 0 total with a conditional ceiling instead of a fixed mandate —
|
|
60
|
+
* SKYR-3820: a config-only PR must not be pushed to fill the budget with
|
|
61
|
+
* baseline tests for unchanged endpoints. Defaults to true so existing callers
|
|
62
|
+
* keep the unconditional fixed-budget rendering.
|
|
63
|
+
*/
|
|
64
|
+
hasClassifiedApiChanges?: boolean): string;
|
|
@@ -155,7 +155,9 @@ export function isFrontendFile(filePath, { hasFlutterSdkDep = false } = {}) {
|
|
|
155
155
|
export function isTestFile(filePath) {
|
|
156
156
|
return (/(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$/.test(filePath) ||
|
|
157
157
|
/scenario_[^/]+\.json$/.test(filePath) ||
|
|
158
|
-
|
|
158
|
+
// Include the `.e2e.` dot form so Playwright/Cypress specs discovery treats as UI tests
|
|
159
|
+
// (UI_TEST_EXT matches `.(test|spec|e2e).`) are recognized here too — e.g. `login.e2e.ts`.
|
|
160
|
+
/\.(spec|test|e2e)\.(tsx?|jsx?)$/.test(filePath) ||
|
|
159
161
|
/(?:^|\/)__tests__\//.test(filePath));
|
|
160
162
|
}
|
|
161
163
|
// ── LLM scope assessment ──────────────────────────────────────────────────────
|
|
@@ -174,9 +176,32 @@ export function buildScopeAssessmentSection(maxTotal = MAX_RECOMMENDATIONS, maxG
|
|
|
174
176
|
/** Server-determined UI/E2E percentage. `undefined` = mixed PR or unknown. */
|
|
175
177
|
precomputedUIPct,
|
|
176
178
|
/** Whether the diff contains frontend files (true ↔ mixed PR when precomputedUIPct is undefined). */
|
|
177
|
-
hasFrontendChanges
|
|
179
|
+
hasFrontendChanges,
|
|
180
|
+
/**
|
|
181
|
+
* PR-mode: whether the diff classified at least one new/modified/removed endpoint.
|
|
182
|
+
* When false on the backend branch (precomputedUIPct === 0), the Budget Plan
|
|
183
|
+
* defaults to 0 total with a conditional ceiling instead of a fixed mandate —
|
|
184
|
+
* SKYR-3820: a config-only PR must not be pushed to fill the budget with
|
|
185
|
+
* baseline tests for unchanged endpoints. Defaults to true so existing callers
|
|
186
|
+
* keep the unconditional fixed-budget rendering.
|
|
187
|
+
*/
|
|
188
|
+
hasClassifiedApiChanges = true) {
|
|
178
189
|
const effectiveGenerate = Math.min(maxGenerate, maxTotal);
|
|
179
190
|
const additional = Math.max(0, maxTotal - effectiveGenerate);
|
|
191
|
+
// Backend-only diff with zero classified endpoint changes (SKYR-3820): the
|
|
192
|
+
// budget defaults to 0 so the tool output agrees with the testbot prompt's
|
|
193
|
+
// zero-test report path, instead of mandating a fixed total the diff cannot
|
|
194
|
+
// justify. The ceiling stays claimable because a zero-classified diff can be
|
|
195
|
+
// a classification miss on a real behavior change (e.g. a DTO/serializer
|
|
196
|
+
// change the endpoint scanner cannot attribute — SKYR-3855's immich case).
|
|
197
|
+
if (precomputedUIPct === 0 && !hasClassifiedApiChanges) {
|
|
198
|
+
return `### PR Scope Assessment
|
|
199
|
+
Budget Plan: 0 total — no new, modified, or removed endpoints were classified from this diff, and no frontend files changed.
|
|
200
|
+
|
|
201
|
+
With a 0-total Budget Plan: generate zero tests, recommend zero tests, and follow the zero-test report path. Do NOT draft baseline or generic tests for unchanged endpoints to fill a budget — an empty diff surface is a valid, expected outcome.
|
|
202
|
+
|
|
203
|
+
**Exception — claim the ceiling only with evidence:** if your code review of the changed files shows an observable API behavior change the classifier missed (e.g. a DTO/serializer/service change that alters a response shape, or a deployment/config change that newly exposes or removes endpoints), raise your Budget Plan to cover exactly those affected endpoints, up to ${maxTotal} total (${effectiveGenerate} generate + ${additional} additional), 0% UI/E2E. Every test must name the changed file that justifies it. State your raised plan now in the canonical format — \`Budget Plan: <total> total (<generate> generate + <additional> additional), 0% UI/E2E\` — and use those exact numbers throughout the rest of the prompt; the raised generate count is your committed generate count.`;
|
|
204
|
+
}
|
|
180
205
|
// Unambiguous backend-only or UI-only: emit a single Budget Plan line — no LLM counting needed.
|
|
181
206
|
if (precomputedUIPct !== undefined) {
|
|
182
207
|
return `### PR Scope Assessment
|
|
@@ -204,6 +204,9 @@ describe("isTestFile", () => {
|
|
|
204
204
|
expect(isTestFile("src/components/Button.test.tsx")).toBe(true);
|
|
205
205
|
expect(isTestFile("e2e/login.spec.js")).toBe(true);
|
|
206
206
|
expect(isTestFile("packages/foo/Bar.test.jsx")).toBe(true);
|
|
207
|
+
// `.e2e.` dot form (Playwright/Cypress) — discovery's UI_TEST_EXT treats it as a UI test.
|
|
208
|
+
expect(isTestFile("e2e/login.e2e.ts")).toBe(true);
|
|
209
|
+
expect(isTestFile("packages/foo/Members.e2e.tsx")).toBe(true);
|
|
207
210
|
});
|
|
208
211
|
it("matches files under __tests__/ directories", () => {
|
|
209
212
|
expect(isTestFile("src/__tests__/utils.ts")).toBe(true);
|
|
@@ -280,6 +283,38 @@ describe("buildScopeAssessmentSection", () => {
|
|
|
280
283
|
expect(section).not.toContain("Step C");
|
|
281
284
|
expect(section).not.toContain("Step D");
|
|
282
285
|
});
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// SKYR-3820 — zero-classified backend diff: budget defaults to 0
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
it("defaults the Budget Plan to 0 total when precomputedUIPct=0 and hasClassifiedApiChanges=false (SKYR-3820)", () => {
|
|
290
|
+
const section = buildScopeAssessmentSection(20, 3, false, 0, false, false);
|
|
291
|
+
expect(section).toContain("Budget Plan: 0 total");
|
|
292
|
+
// The fixed budget mandate must NOT be asserted
|
|
293
|
+
expect(section).not.toContain("Budget Plan: 20 total (3 generate + 17 additional)");
|
|
294
|
+
expect(section).not.toContain("Use these exact numbers throughout the rest of the prompt.");
|
|
295
|
+
// The conditional ceiling names the numbers the agent may claim after code review
|
|
296
|
+
expect(section).toContain("up to 20 total (3 generate + 17 additional)");
|
|
297
|
+
});
|
|
298
|
+
it("keeps the fixed budget line when hasClassifiedApiChanges is omitted (back-compat default true)", () => {
|
|
299
|
+
const withDefault = buildScopeAssessmentSection(10, 3, false, 0, false);
|
|
300
|
+
const explicitTrue = buildScopeAssessmentSection(10, 3, false, 0, false, true);
|
|
301
|
+
expect(withDefault).toBe(explicitTrue);
|
|
302
|
+
expect(withDefault).toContain("Budget Plan: 10 total (3 generate + 7 additional), 0% UI/E2E");
|
|
303
|
+
expect(withDefault).not.toContain("Budget Plan: 0 total");
|
|
304
|
+
});
|
|
305
|
+
it("does not apply the zero-classified override to UI-only PRs (precomputedUIPct=100)", () => {
|
|
306
|
+
// UI-only PRs have hasApiChanges=false by construction — the override must be
|
|
307
|
+
// gated on the backend branch (precomputedUIPct === 0) only.
|
|
308
|
+
const section = buildScopeAssessmentSection(10, 3, true, 100, false, false);
|
|
309
|
+
expect(section).toContain("Budget Plan: 10 total (3 generate + 7 additional), 100% UI/E2E");
|
|
310
|
+
expect(section).not.toContain("Budget Plan: 0 total");
|
|
311
|
+
});
|
|
312
|
+
it("does not apply the zero-classified override to the mixed-PR branch", () => {
|
|
313
|
+
const section = buildScopeAssessmentSection(10, 3, false, undefined, true, false);
|
|
314
|
+
expect(section).toContain("Budget Plan (total already determined)");
|
|
315
|
+
expect(section).toContain("Cosmetic-only override");
|
|
316
|
+
expect(section).not.toContain("Budget Plan: 0 total");
|
|
317
|
+
});
|
|
283
318
|
it("emits a single Budget Plan line for UI-only PR (precomputedUIPct=100)", () => {
|
|
284
319
|
const section = buildScopeAssessmentSection(10, 3, true, 100, false);
|
|
285
320
|
expect(section).toContain("Budget Plan: 10 total (3 generate + 7 additional), 100% UI/E2E");
|
|
@@ -342,7 +342,11 @@ ${detailBlocks}
|
|
|
342
342
|
if (externalCoverage.size > 0) {
|
|
343
343
|
logger.info(`External test coverage keys: ${[...externalCoverage].join(", ")}`);
|
|
344
344
|
}
|
|
345
|
-
mainSection = buildExecutionPlan(scored, maxGen, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges, hasTraces, externalCoverage, analysis.existingTests.relevantExternalTestPaths ?? []
|
|
345
|
+
mainSection = buildExecutionPlan(scored, maxGen, topN, analysis.apiEndpoints.baseUrl, authHeaderValue, authSchemeSnippet, authTypeValue, seed, endpointCount, isUIOnlyPR, hasFrontendChanges, hasTraces, externalCoverage, analysis.existingTests.relevantExternalTestPaths ?? [],
|
|
346
|
+
// Zero-classified gating (SKYR-3820) requires an actual diff that classified
|
|
347
|
+
// nothing — a missing diffContext is absence of evidence, not an empty
|
|
348
|
+
// change surface, so it keeps the legacy fixed-budget rendering.
|
|
349
|
+
diffContext ? hasApiChanges : true);
|
|
346
350
|
}
|
|
347
351
|
else {
|
|
348
352
|
// Full-repo scope with no scored items — rare fallback
|
|
@@ -685,6 +685,67 @@ describe("buildRecommendationPrompt — LLM scope assessment for UI/E2E split",
|
|
|
685
685
|
});
|
|
686
686
|
});
|
|
687
687
|
// ---------------------------------------------------------------------------
|
|
688
|
+
// Tests — zero-classified diff: budget defaults to 0 (SKYR-3820)
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
describe("buildRecommendationPrompt — zero-classified diff (SKYR-3820)", () => {
|
|
691
|
+
function zeroClassifiedAnalysis(changedFiles) {
|
|
692
|
+
return minimalAnalysis({
|
|
693
|
+
businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
|
|
694
|
+
branchDiffContext: {
|
|
695
|
+
baseBranch: "main",
|
|
696
|
+
currentBranch: "skyramp/testbot-setup",
|
|
697
|
+
changedFiles,
|
|
698
|
+
newEndpoints: [],
|
|
699
|
+
modifiedEndpoints: [],
|
|
700
|
+
affectedServices: [],
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
it("config-only PR (netbox shape): Budget Plan defaults to 0 total, fixed budget mandate absent", () => {
|
|
705
|
+
const prompt = buildRecommendationPrompt(zeroClassifiedAnalysis([
|
|
706
|
+
".github/workflows/skyramp-testbot.yml",
|
|
707
|
+
"deployment-testbot/Dockerfile",
|
|
708
|
+
"deployment-testbot/configuration.py",
|
|
709
|
+
"deployment-testbot/setup.sh",
|
|
710
|
+
]), AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
|
|
711
|
+
expect(prompt).toContain("Budget Plan: 0 total");
|
|
712
|
+
expect(prompt).not.toContain("Budget Plan: 20 total (3 generate + 17 additional)");
|
|
713
|
+
expect(prompt).not.toContain("Use these exact numbers throughout the rest of the prompt.");
|
|
714
|
+
// The "draft your own" pressure valves must not fire on a zero-classified diff
|
|
715
|
+
expect(prompt).not.toContain("draft your own based on endpoint analysis");
|
|
716
|
+
expect(prompt).not.toContain("If your Budget Plan total exceeds the pre-ranked items listed above");
|
|
717
|
+
});
|
|
718
|
+
it("backend source change with 0 classified endpoints keeps the conditional ceiling (SKYR-3855 guard)", () => {
|
|
719
|
+
// immich user.dto.ts shape — classification blind to a real behavior change.
|
|
720
|
+
// The agent must still be able to claim the budget from its own code review.
|
|
721
|
+
const prompt = buildRecommendationPrompt(zeroClassifiedAnalysis(["server/src/dtos/user.dto.ts"]), AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
|
|
722
|
+
expect(prompt).toContain("Budget Plan: 0 total");
|
|
723
|
+
expect(prompt).toContain("up to 20 total (3 generate + 17 additional)");
|
|
724
|
+
});
|
|
725
|
+
it("classified backend PR is unchanged: fixed budget line still rendered", () => {
|
|
726
|
+
const analysis = minimalAnalysis({
|
|
727
|
+
businessContext: { mainPurpose: "Test", userFlows: [], dataFlows: [], integrationPatterns: [], draftedScenarios: [] },
|
|
728
|
+
branchDiffContext: {
|
|
729
|
+
baseBranch: "main",
|
|
730
|
+
currentBranch: "feature/test",
|
|
731
|
+
changedFiles: ["backend/routes.py"],
|
|
732
|
+
newEndpoints: [{ path: "/api/items", methods: [{ method: "POST", sourceFile: "routes.py", interactionCount: 0 }] }],
|
|
733
|
+
modifiedEndpoints: [],
|
|
734
|
+
affectedServices: [],
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
const prompt = buildRecommendationPrompt(analysis, AnalysisScope.CurrentBranchDiff, 20, undefined, undefined, undefined, undefined, 3);
|
|
738
|
+
expect(prompt).toContain("Budget Plan: 20 total (3 generate + 17 additional), 0% UI/E2E");
|
|
739
|
+
expect(prompt).toContain("Use these exact numbers throughout the rest of the prompt.");
|
|
740
|
+
expect(prompt).not.toContain("Budget Plan: 0 total");
|
|
741
|
+
});
|
|
742
|
+
it("buildExecutionPlan omitting hasApiChanges renders the fixed budget (back-compat)", () => {
|
|
743
|
+
const plan = buildExecutionPlan([], 3, 20, "http://localhost:3000", "", "", "", "seed", 100, false, false);
|
|
744
|
+
expect(plan).toContain("Budget Plan: 20 total (3 generate + 17 additional), 0% UI/E2E");
|
|
745
|
+
expect(plan).not.toContain("Budget Plan: 0 total");
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
688
749
|
// Tests — GENERATE slot allocation (UI vs backend slots)
|
|
689
750
|
// ---------------------------------------------------------------------------
|
|
690
751
|
describe("buildRecommendationPrompt — GENERATE slot allocation", () => {
|