@skyramp/mcp 0.3.5 → 0.3.6-rc.2.ac20
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/adapters/jestAdapter.js +3 -0
- package/build/adapters/mochaAdapter.js +2 -0
- package/build/adapters/playwrightAdapter.js +3 -0
- package/build/adapters/pytestAdapter.js +12 -0
- package/build/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +81 -35
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/runExistingTestsTool.d.ts +34 -2
- package/build/tools/runExistingTestsTool.js +104 -4
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +537 -120
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +71 -10
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -12
- package/build/types/ExternalTestExecution.d.ts +67 -1
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +10 -1
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +57 -1
- package/build/utils/AnalysisStateManager.js +54 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportVerification.d.ts +64 -4
- package/build/utils/reportVerification.js +228 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- 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.VEfqi1qN.js +5 -0
- 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/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TestType } from "../../types/TestTypes.js";
|
|
2
2
|
import { inferScenarioType, prioritizeAttackSurfaceBundles } from "../diversity.js";
|
|
3
|
-
import {
|
|
3
|
+
import { externalDedupKeys, scenarioCoverageKeys, isAttackSurfaceSecurityBoundary, } from "../../prompts/test-recommendation/recommendationShared.js";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
5
|
/**
|
|
6
6
|
* Backend GENERATE slot count:
|
|
@@ -20,25 +20,39 @@ export function reservedUISlots(ctx) {
|
|
|
20
20
|
return ctx.hasFrontendChanges && ctx.maxGenerate > 0 ? 1 : 0;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Split candidates by whether an external test already covers them. Protected
|
|
24
|
+
* `bug_caught` / attack-surface scenarios always count as uncovered — they
|
|
25
|
+
* require semantic flaw coverage the external test may not provide.
|
|
26
|
+
*
|
|
27
|
+
* A candidate is covered only when the set holds EVERY one of its keys. One
|
|
28
|
+
* match is not coverage: a scenario exercising two changed endpoints would lose
|
|
29
|
+
* the endpoint the external test does not reach (SKYR-4214). An empty key list
|
|
30
|
+
* carries no information, so it never removes anything.
|
|
31
|
+
*
|
|
32
|
+
* The covered group is held back, not discarded. `runBudget` moves it into
|
|
33
|
+
* ADDITIONAL when GENERATE's backend half would otherwise be empty (SKYR-4024),
|
|
34
|
+
* and records it as dropped, with a reason, when it does not.
|
|
26
35
|
*/
|
|
27
|
-
function
|
|
36
|
+
function partitionByExternalCoverage(ranked, externalCoverage) {
|
|
28
37
|
if (externalCoverage.size === 0)
|
|
29
|
-
return ranked;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
logger.info(`External dedup: skipping "${item.scenario.scenarioName}" (${key}) — covered by external test`);
|
|
38
|
-
return false;
|
|
38
|
+
return { uncovered: ranked, covered: [] };
|
|
39
|
+
const uncovered = [];
|
|
40
|
+
const covered = [];
|
|
41
|
+
for (const item of ranked) {
|
|
42
|
+
const keys = externalDedupKeys(item.scenario);
|
|
43
|
+
if (keys.length === 0 || !keys.every((key) => externalCoverage.has(key))) {
|
|
44
|
+
uncovered.push(item);
|
|
45
|
+
continue;
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
if (item.scenario.category === "bug_caught" || isAttackSurfaceSecurityBoundary(item.scenario)) {
|
|
48
|
+
logger.info(`External dedup: preserving "${item.scenario.scenarioName}" (${keys.join(", ")}) — protected bug/attack-surface scenario requires semantic flaw coverage`);
|
|
49
|
+
uncovered.push(item);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
logger.info(`External dedup: "${item.scenario.scenarioName}" (${keys.join(", ")}) covered by an external test — eligible for the reserve`);
|
|
53
|
+
covered.push({ item, keys });
|
|
54
|
+
}
|
|
55
|
+
return { uncovered, covered };
|
|
42
56
|
}
|
|
43
57
|
/**
|
|
44
58
|
* Shared budgeting pipeline. All Budgeters run the same external-dedup,
|
|
@@ -49,10 +63,11 @@ function applyExternalDedup(ranked, externalCoverage) {
|
|
|
49
63
|
* diffExecutionPlan.ts exactly.
|
|
50
64
|
*/
|
|
51
65
|
export function runBudget(ranked, ctx, pick) {
|
|
66
|
+
const dropped = [];
|
|
52
67
|
const backend = backendGenerateCount(ctx);
|
|
53
68
|
const uiSlots = reservedUISlots(ctx);
|
|
54
|
-
const
|
|
55
|
-
const slotOrdered = prioritizeAttackSurfaceBundles(
|
|
69
|
+
const { uncovered, covered } = partitionByExternalCoverage(ranked, ctx.externalCoverage);
|
|
70
|
+
const slotOrdered = prioritizeAttackSurfaceBundles(uncovered);
|
|
56
71
|
// When UI slots are reserved, keep UI candidates out of the backend pick so the
|
|
57
72
|
// reserved slot can always be filled below. Otherwise a high-ranked (e.g.
|
|
58
73
|
// CRITICAL) UI candidate could be consumed by the backend pick on a mixed PR,
|
|
@@ -77,14 +92,77 @@ export function runBudget(ranked, ctx, pick) {
|
|
|
77
92
|
chosen.add(item);
|
|
78
93
|
}
|
|
79
94
|
const generate = [...backendGenerate, ...uiGenerate];
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
// SKYR-4024. A covered candidate is one an existing test already reaches, so
|
|
96
|
+
// generating a second test for it is wrong — maintenance updates the existing
|
|
97
|
+
// one. But dropping it outright removed it from the RECOMMENDATIONS too, and an
|
|
98
|
+
// empty GENERATE is authoritative downstream. The reserve surfaces it as
|
|
99
|
+
// ADDITIONAL instead. The emptiness test reads the backend half: on a mixed PR
|
|
100
|
+
// one UI candidate fills the reserved slot while every covered backend
|
|
101
|
+
// candidate would otherwise vanish.
|
|
102
|
+
//
|
|
103
|
+
// Two conditions keep the reserve shut where recommending nothing is right.
|
|
104
|
+
// Backend slots must exist, so a UI-only PR is left alone. And the PR must not
|
|
105
|
+
// touch a test file of its own: coverage is read from the working tree, and a
|
|
106
|
+
// test file without the Skyramp marker counts as external, so a PR that adds
|
|
107
|
+
// its own tests would have that coverage used as grounds to recommend
|
|
108
|
+
// duplicates of it.
|
|
109
|
+
const spendReserve = backendGenerate.length === 0 && backend > 0 && covered.length > 0 && !ctx.diffChangesTestFiles;
|
|
110
|
+
if (spendReserve) {
|
|
111
|
+
logger.info(`External dedup: GENERATE empty after dedup — recommending ${covered.length} held-back candidate(s) as ADDITIONAL rather than approving an empty plan`);
|
|
112
|
+
}
|
|
113
|
+
else if (covered.length > 0) {
|
|
114
|
+
// SKYR-4214. The reserve stayed shut, so these really are dropped — say so,
|
|
115
|
+
// with the reason, instead of leaving the agent an unexplained short list.
|
|
116
|
+
const reason = backendGenerate.length > 0
|
|
117
|
+
? `the backend half of GENERATE is filled (${backendGenerate.length})`
|
|
118
|
+
: backend === 0
|
|
119
|
+
? "no backend slots (UI-only PR)"
|
|
120
|
+
: "the PR changes test files of its own";
|
|
121
|
+
logger.info(`External dedup: dropping ${covered.length} covered candidate(s) — reserve shut because ${reason}`);
|
|
122
|
+
for (const { item, keys } of covered) {
|
|
123
|
+
dropped.push({
|
|
124
|
+
candidateId: item.candidateId,
|
|
125
|
+
reason: `covered by an external test (${keys.join(", ")})`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ADDITIONAL = everything not chosen for GENERATE, in rank order, minus any
|
|
130
|
+
// whose subject endpoints are ALL already covered by GENERATE, then capped at
|
|
131
|
+
// the remaining budget. All, not any: an item that also covers an endpoint no
|
|
132
|
+
// GENERATE item reaches is not a duplicate. An empty key list never removes
|
|
133
|
+
// an item.
|
|
134
|
+
//
|
|
135
|
+
// The duplicate filter runs BEFORE the cap. In the other order a duplicate
|
|
136
|
+
// took a slot and then vacated it, and nothing moved up: eval run
|
|
137
|
+
// 32543498147, fixture cc15-org-reviewer-role, filled 19 of a maxTotal of 20
|
|
138
|
+
// while 16 candidates carried the reason "beyond the maxTotal budget of 20".
|
|
139
|
+
// A candidate that is both a duplicate and past the cap now reports the
|
|
140
|
+
// duplicate — the stronger reason, and the only one that does not depend on
|
|
141
|
+
// where the cap happened to fall.
|
|
142
|
+
const additionalSource = spendReserve
|
|
143
|
+
? [...slotOrdered, ...prioritizeAttackSurfaceBundles(covered.map((c) => c.item))]
|
|
144
|
+
: slotOrdered;
|
|
145
|
+
const notChosen = additionalSource.filter((it) => !chosen.has(it));
|
|
146
|
+
const remainingBudget = Math.max(0, ctx.maxTotal - generate.length);
|
|
147
|
+
const generatedCoverage = new Set(generate.flatMap((item) => scenarioCoverageKeys(item.scenario)));
|
|
148
|
+
const notDuplicate = notChosen.filter((item) => {
|
|
149
|
+
const keys = scenarioCoverageKeys(item.scenario);
|
|
150
|
+
if (keys.length === 0 || !keys.every((key) => generatedCoverage.has(key)))
|
|
151
|
+
return true;
|
|
152
|
+
dropped.push({
|
|
153
|
+
candidateId: item.candidateId,
|
|
154
|
+
reason: `covered by GENERATE (${keys.join(", ")})`,
|
|
155
|
+
});
|
|
156
|
+
return false;
|
|
157
|
+
});
|
|
158
|
+
const additional = notDuplicate.slice(0, remainingBudget);
|
|
159
|
+
for (const item of notDuplicate.slice(remainingBudget)) {
|
|
160
|
+
dropped.push({
|
|
161
|
+
candidateId: item.candidateId,
|
|
162
|
+
reason: `beyond the maxTotal budget of ${ctx.maxTotal}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
87
165
|
// Budgeting itself produces no demotions; the register-plan selection stage
|
|
88
166
|
// (planRanker.selectPlan) fills this channel from discriminator verification.
|
|
89
|
-
return { generate, additional, reservedUISlots: uiSlots, demotions: [] };
|
|
167
|
+
return { generate, additional, reservedUISlots: uiSlots, demotions: [], dropped };
|
|
90
168
|
}
|
|
@@ -281,9 +281,20 @@ function isNegativeAssertStep(step) {
|
|
|
281
281
|
if (step?.expectedStatusCode === 404)
|
|
282
282
|
return true;
|
|
283
283
|
const assertions = [...toStringArray(step?.expectedResponseFields), ...toStringArray(step?.bodyMustInclude)];
|
|
284
|
-
return assertsEmptyOrZero(assertions);
|
|
284
|
+
return assertsEmptyOrZero(assertions, step?.responseBody);
|
|
285
285
|
}
|
|
286
|
-
|
|
286
|
+
/**
|
|
287
|
+
* True when the step asserts an empty or zero result, reading the assertion
|
|
288
|
+
* strings AND `responseBody`. An EMPTY ARRAY body is itself the "no rows came
|
|
289
|
+
* back" assertion, and can be the only empty-result signal a step carries.
|
|
290
|
+
*
|
|
291
|
+
* `{}` deliberately does NOT count: an unspecified body is filled in as `{}`
|
|
292
|
+
* (ScenarioGenerationService), so it cannot be told apart from a body the agent
|
|
293
|
+
* never wrote. `isAssertStep` above already reads `{}` as asserting nothing.
|
|
294
|
+
*/
|
|
295
|
+
function assertsEmptyOrZero(strings, responseBody) {
|
|
296
|
+
if (Array.isArray(responseBody) && responseBody.length === 0)
|
|
297
|
+
return true;
|
|
287
298
|
return strings.some((raw) => {
|
|
288
299
|
const s = raw.toLowerCase();
|
|
289
300
|
if (/\bcount\b/.test(s) && /\b0\b/.test(s))
|
|
@@ -4,9 +4,9 @@ import { ScenarioCategory } from "../types/TestRecommendation.js";
|
|
|
4
4
|
export interface RankOptions {
|
|
5
5
|
/**
|
|
6
6
|
* Categories that take the top carve-out tier ahead of everything else.
|
|
7
|
-
* Defaults to the `CATEGORY_PRIORITY === "CRITICAL"` categories (
|
|
8
|
-
*
|
|
9
|
-
* the agent's priority tag as a ranking input.
|
|
7
|
+
* Defaults to the `CATEGORY_PRIORITY === "CRITICAL"` categories (bug_caught only —
|
|
8
|
+
* new_endpoint is MEDIUM, not carved out). Exposed so phase 2 can tune the carve-out
|
|
9
|
+
* WITHOUT reintroducing the agent's priority tag as a ranking input.
|
|
10
10
|
*/
|
|
11
11
|
carveOutCategories?: ScenarioCategory[];
|
|
12
12
|
/**
|
|
@@ -32,9 +32,9 @@ export interface SelectPlanContext extends BudgetContext {
|
|
|
32
32
|
* independent of input order (the final tiebreak is the stable `candidateId`).
|
|
33
33
|
*
|
|
34
34
|
* Ordering (highest first):
|
|
35
|
-
* 1. Carve-out —
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
35
|
+
* 1. Carve-out — CRITICAL-category scenarios (bug_caught), preserving the
|
|
36
|
+
* protected-first convention of `roundRobinByType` /
|
|
37
|
+
* `prioritizeAttackSurfaceBundles`.
|
|
38
38
|
* 2. Verified discriminators — candidates whose declared discriminator survived
|
|
39
39
|
* `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
|
|
40
40
|
* unverified peers in the same tier.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CATEGORY_PRIORITY, PriorityTier } from "../types/TestRecommendation.js";
|
|
2
2
|
import { diversityBalancedBudgeter } from "./budgeters/diversityBalancedBudgeter.js";
|
|
3
|
-
import {
|
|
3
|
+
import { collectChangedRouteLines, findStepOnChangedRoute } from "../utils/changedRoutes.js";
|
|
4
4
|
const PRIORITY_RANK = {
|
|
5
5
|
CRITICAL: 0,
|
|
6
6
|
HIGH: 1,
|
|
@@ -14,9 +14,9 @@ const DEFAULT_CARVE_OUT_CATEGORIES = Object.keys(CATEGORY_PRIORITY).filter((cate
|
|
|
14
14
|
* independent of input order (the final tiebreak is the stable `candidateId`).
|
|
15
15
|
*
|
|
16
16
|
* Ordering (highest first):
|
|
17
|
-
* 1. Carve-out —
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
17
|
+
* 1. Carve-out — CRITICAL-category scenarios (bug_caught), preserving the
|
|
18
|
+
* protected-first convention of `roundRobinByType` /
|
|
19
|
+
* `prioritizeAttackSurfaceBundles`.
|
|
20
20
|
* 2. Verified discriminators — candidates whose declared discriminator survived
|
|
21
21
|
* `validateDiscriminator` (marked via `verifiedDiscriminator`) float ahead of
|
|
22
22
|
* unverified peers in the same tier.
|
|
@@ -77,62 +77,7 @@ function categoryRank(candidate) {
|
|
|
77
77
|
const tier = CATEGORY_PRIORITY[candidate.scenario?.category] ?? PriorityTier.LOW;
|
|
78
78
|
return PRIORITY_RANK[tier];
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Extract method+path from every changed (`+`/`-`, non-header) line of a raw
|
|
82
|
-
* unified diff. Reuses `parseRouteLine`, which already strips the leading
|
|
83
|
-
* `+`/`-` marker and matches the same route-decorator patterns the endpoint
|
|
84
|
-
* scanner does.
|
|
85
|
-
*/
|
|
86
|
-
function collectChangedRouteLines(diffText) {
|
|
87
|
-
const routes = [];
|
|
88
|
-
let currentFile = "";
|
|
89
|
-
for (const line of diffText.split("\n")) {
|
|
90
|
-
// Track the current file from the unified-diff header so parseRouteLine
|
|
91
|
-
// gets the real path — its UI-component guard (UI_COMPONENT_EXT) depends
|
|
92
|
-
// on it, or a route-shaped line inside a .tsx/.jsx file (e.g. a client
|
|
93
|
-
// router registration) gets misparsed as a changed backend route.
|
|
94
|
-
if (line.startsWith("+++ ")) {
|
|
95
|
-
const spec = line.slice(4).trim().split("\t")[0];
|
|
96
|
-
currentFile = spec === "/dev/null" ? "" : normalizeDiffPath(spec);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (line.startsWith("--- ") || line.startsWith("diff --git") || line.startsWith("index "))
|
|
100
|
-
continue;
|
|
101
|
-
if (!(line.startsWith("+") || line.startsWith("-")))
|
|
102
|
-
continue;
|
|
103
|
-
const parsed = parseRouteLine(line, currentFile);
|
|
104
|
-
if (parsed)
|
|
105
|
-
routes.push({ method: parsed.method, path: parsed.path });
|
|
106
|
-
}
|
|
107
|
-
return routes;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Whether any step of `scenario` targets a method+path on the changed hunk.
|
|
111
|
-
* Diff-extracted paths are local to the file's own router declaration (e.g.
|
|
112
|
-
* "/suggestions"); scenario step paths are fully mounted (e.g.
|
|
113
|
-
* "/api/recipes/suggestions") once drafted from a scanned/recovered endpoint.
|
|
114
|
-
* A local path matches when the step path ends with it, so the cross-file
|
|
115
|
-
* mount prefix difference (see recoverRemovedEndpointsFromBase, SKYR-4026)
|
|
116
|
-
* doesn't prevent the match.
|
|
117
|
-
*/
|
|
80
|
+
/** Whether any step of `scenario` targets a method+path on the changed hunk. */
|
|
118
81
|
function scenarioOnChangedHunk(scenario, changedRoutes) {
|
|
119
|
-
|
|
120
|
-
return false;
|
|
121
|
-
for (const step of scenario.steps ?? []) {
|
|
122
|
-
const stepMethod = (step.method ?? "").toUpperCase();
|
|
123
|
-
const stepPath = (step.path ?? "").replace(/\/+$/, "");
|
|
124
|
-
for (const route of changedRoutes) {
|
|
125
|
-
if (route.method.toUpperCase() !== stepMethod)
|
|
126
|
-
continue;
|
|
127
|
-
const routePath = route.path.replace(/\/+$/, "");
|
|
128
|
-
if (routePath === "") {
|
|
129
|
-
if (stepPath === "" || stepPath === "/")
|
|
130
|
-
return true;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (stepPath === routePath || stepPath.endsWith(routePath))
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
82
|
+
return !!findStepOnChangedRoute(scenario.steps, changedRoutes);
|
|
138
83
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
export declare class AnalyticsService {
|
|
3
|
+
/**
|
|
4
|
+
* Only the fields named here reach the analytics backend. The tool's own
|
|
5
|
+
* params are NOT forwarded wholesale: they carry the user's prompt, which is
|
|
6
|
+
* free text about a private codebase and has no business leaving the machine.
|
|
7
|
+
* Anything added here is sent verbatim -- PushToolEvent on the Go side does no
|
|
8
|
+
* redaction of its own -- so add a field only when it is safe to publish.
|
|
9
|
+
*/
|
|
3
10
|
static pushTestGenerationToolEvent(toolName: string, result: CallToolResult, params: Record<string, any>): Promise<void>;
|
|
4
11
|
static pushMCPToolEvent(toolName: string, result: CallToolResult | undefined, params: Record<string, any>): Promise<void>;
|
|
5
12
|
/**
|
|
@@ -40,9 +40,15 @@ async function safePushToolEvent(entryPoint, toolName, errorMessage, params) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
export class AnalyticsService {
|
|
43
|
+
/**
|
|
44
|
+
* Only the fields named here reach the analytics backend. The tool's own
|
|
45
|
+
* params are NOT forwarded wholesale: they carry the user's prompt, which is
|
|
46
|
+
* free text about a private codebase and has no business leaving the machine.
|
|
47
|
+
* Anything added here is sent verbatim -- PushToolEvent on the Go side does no
|
|
48
|
+
* redaction of its own -- so add a field only when it is safe to publish.
|
|
49
|
+
*/
|
|
43
50
|
static async pushTestGenerationToolEvent(toolName, result, params) {
|
|
44
51
|
const analyticsResult = {};
|
|
45
|
-
analyticsResult["prompt"] = params.prompt;
|
|
46
52
|
analyticsResult["language"] = params.language;
|
|
47
53
|
this.pushMCPToolEvent(toolName, result, analyticsResult);
|
|
48
54
|
}
|
|
@@ -28,10 +28,8 @@ The test file \`${params.testFile}\` will remain unchanged. No further action is
|
|
|
28
28
|
let prompt = "";
|
|
29
29
|
switch (testType) {
|
|
30
30
|
case TestType.UI:
|
|
31
|
-
prompt = getModularizationPromptForUI(params.testFile);
|
|
32
|
-
break;
|
|
33
31
|
case TestType.E2E:
|
|
34
|
-
prompt = getModularizationPromptForUI(params.testFile);
|
|
32
|
+
prompt = getModularizationPromptForUI(params.testFile, testType, params.language);
|
|
35
33
|
break;
|
|
36
34
|
case TestType.INTEGRATION:
|
|
37
35
|
prompt = getModularizationPromptForIntegration(params.testFile);
|
|
@@ -45,8 +45,6 @@ export declare class TestDiscoveryService {
|
|
|
45
45
|
private readonly SUPPORTED_EXTENSIONS;
|
|
46
46
|
private readonly MAX_CONCURRENT_OPERATIONS;
|
|
47
47
|
private readonly MAX_EXTERNAL_FULL_REPO;
|
|
48
|
-
private readonly TEST_FILE_PATTERNS;
|
|
49
|
-
private readonly TEST_DIR_PATTERNS;
|
|
50
48
|
/**
|
|
51
49
|
* Discover all tests under testDir — both Skyramp-generated and external (user-written).
|
|
52
50
|
* Uses fast-glob for cross-platform file scanning, then classifies discovered files
|
|
@@ -5,6 +5,7 @@ import { TestSource } from "../types/TestAnalysis.js";
|
|
|
5
5
|
import { TestType } from "../types/TestTypes.js";
|
|
6
6
|
import { buildPathSignatures } from "../utils/pathSignatures.js";
|
|
7
7
|
import fg from "fast-glob";
|
|
8
|
+
import { isDiscoveredTestFile } from "../utils/testFileClassification.js";
|
|
8
9
|
export class TestDiscoveryService {
|
|
9
10
|
EXCLUDED_DIRS = [
|
|
10
11
|
"node_modules",
|
|
@@ -34,37 +35,6 @@ export class TestDiscoveryService {
|
|
|
34
35
|
// In PR mode this hard limit is replaced by relevance-based partitioning which
|
|
35
36
|
// naturally bounds the read set to files relevant to the changed endpoints.
|
|
36
37
|
MAX_EXTERNAL_FULL_REPO = 100;
|
|
37
|
-
// Test file naming patterns — match against basename.
|
|
38
|
-
// Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
|
|
39
|
-
// outside a recognized test directory are still discovered.
|
|
40
|
-
TEST_FILE_PATTERNS = [
|
|
41
|
-
/^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
|
|
42
|
-
/.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
|
|
43
|
-
/.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
|
|
44
|
-
/.*\..*spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.e2e-spec.ts, *.unit-spec.ts
|
|
45
|
-
/.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
|
|
46
|
-
/.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
|
|
47
|
-
/.*_spec\.rb$/, // *_spec.rb (RSpec)
|
|
48
|
-
/.*\.test\.php$/, // *.test.php
|
|
49
|
-
/.*Test\.php$/, // *Test.php (PHPUnit)
|
|
50
|
-
// Skyramp-generated test suffixes — must be matched even outside test dirs
|
|
51
|
-
/.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
|
|
52
|
-
];
|
|
53
|
-
// Directory patterns that signal test-owned files — works with both / and \ separators.
|
|
54
|
-
// Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
|
|
55
|
-
// Playwright, Cypress, and other frameworks for page objects and support code.
|
|
56
|
-
TEST_DIR_PATTERNS = [
|
|
57
|
-
/[\\/]tests?[\\/]/,
|
|
58
|
-
/[\\/]__tests__[\\/]/,
|
|
59
|
-
/[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
|
|
60
|
-
/[\\/]spec[\\/]/,
|
|
61
|
-
/[\\/]e2e[\\/]/,
|
|
62
|
-
/[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
|
|
63
|
-
/[\\/]playwright[\\/]/, // Playwright fixtures and helpers
|
|
64
|
-
/[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
|
|
65
|
-
/[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
|
|
66
|
-
/[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
|
|
67
|
-
];
|
|
68
38
|
/**
|
|
69
39
|
* Discover all tests under testDir — both Skyramp-generated and external (user-written).
|
|
70
40
|
* Uses fast-glob for cross-platform file scanning, then classifies discovered files
|
|
@@ -478,12 +448,7 @@ export class TestDiscoveryService {
|
|
|
478
448
|
* or directory placement.
|
|
479
449
|
*/
|
|
480
450
|
isExternalTestFile(filePath) {
|
|
481
|
-
|
|
482
|
-
if (this.TEST_FILE_PATTERNS.some(p => p.test(basename)))
|
|
483
|
-
return true;
|
|
484
|
-
if (this.TEST_DIR_PATTERNS.some(p => p.test(filePath)))
|
|
485
|
-
return true;
|
|
486
|
-
return false;
|
|
451
|
+
return isDiscoveredTestFile(filePath);
|
|
487
452
|
}
|
|
488
453
|
/**
|
|
489
454
|
* Extract metadata from a test file
|
|
@@ -44,6 +44,15 @@ export interface BaseTestParams {
|
|
|
44
44
|
* empty-string edge (SKYR-3899).
|
|
45
45
|
*/
|
|
46
46
|
export declare function deriveEffectiveFramework(language: string | undefined, framework: string | undefined): string;
|
|
47
|
+
/** The files codegen reports it wrote — the block every generation result carries:
|
|
48
|
+
*
|
|
49
|
+
* Successfully generated/updated tests files:
|
|
50
|
+
* \t/abs/path/to/orders_integration_test.py
|
|
51
|
+
*
|
|
52
|
+
* Read from the result rather than inferred from the filesystem, so a leftover from an
|
|
53
|
+
* earlier attempt or a sibling written by another call is never mistaken for this call's
|
|
54
|
+
* output. Empty when the block is absent (nothing to hand off). */
|
|
55
|
+
export declare function generatedFilesFrom(result: string): string[];
|
|
47
56
|
export declare abstract class TestGenerationService {
|
|
48
57
|
protected client: SkyrampClient;
|
|
49
58
|
constructor();
|
|
@@ -51,6 +60,13 @@ export declare abstract class TestGenerationService {
|
|
|
51
60
|
protected validateInputs(params: BaseTestParams): CallToolResult;
|
|
52
61
|
protected abstract buildGenerationOptions(params: BaseTestParams & Record<string, any>): any;
|
|
53
62
|
protected abstract getTestType(): TestType | typeof MOCK_TYPE;
|
|
63
|
+
/**
|
|
64
|
+
* Whether this generation's codeReuse hand-off sequences skyramp_modularization
|
|
65
|
+
* before skyramp_reuse_code (routing: isModularizeFirstTarget). Subclasses that
|
|
66
|
+
* add their own post-generation steps consult this so modularization is
|
|
67
|
+
* instructed exactly once.
|
|
68
|
+
*/
|
|
69
|
+
protected emitsModularizeFirstHandOff(params: BaseTestParams): boolean;
|
|
54
70
|
protected handleApiAnalysis(params: BaseTestParams): Promise<CallToolResult | null>;
|
|
55
71
|
private static readonly STANDARD_HEADERS;
|
|
56
72
|
private static simpleWildcardMatch;
|
|
@@ -3,7 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import { SkyrampClient } from "@skyramp/skyramp";
|
|
4
4
|
import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
|
|
5
5
|
import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, resolveAuthFromWorkspace, getWorkspaceSkipTLSVerify, getWorkspaceScopedQueryParams, mergeQueryParamsString, } from "../utils/workspaceAuth.js";
|
|
6
|
-
import { extractPathname, resolveQueryParamsForPath, contributesNothing } from "../workspace/queryParamResolution.js";
|
|
6
|
+
import { extractPathname, resolveQueryParamsForPath, contributesNothing, } from "../workspace/queryParamResolution.js";
|
|
7
7
|
import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
|
|
8
8
|
import { getEntryPoint } from "../utils/telemetry.js";
|
|
9
9
|
import { getLanguageSteps } from "../utils/language-helper.js";
|
|
@@ -12,6 +12,8 @@ import { logger } from "../utils/logger.js";
|
|
|
12
12
|
import { normalizeLanguageParams } from "../utils/normalizeParams.js";
|
|
13
13
|
import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
|
|
14
14
|
import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
|
|
15
|
+
import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
|
|
16
|
+
import { isModularizeFirstTarget } from "../utils/reuseRouting.js";
|
|
15
17
|
/**
|
|
16
18
|
* Derive the framework actually used for generation from the (normalized)
|
|
17
19
|
* language + framework params. A blank/whitespace-only framework is treated as
|
|
@@ -35,10 +37,24 @@ export function deriveEffectiveFramework(language, framework) {
|
|
|
35
37
|
* defaultQueryParams value into that path is not what the design intends
|
|
36
38
|
* (SKYR-4050).
|
|
37
39
|
*/
|
|
38
|
-
const SKIP_DEFAULT_QUERY_PARAMS_TEST_TYPES = new Set([
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const SKIP_DEFAULT_QUERY_PARAMS_TEST_TYPES = new Set([TestType.UI, TestType.E2E]);
|
|
41
|
+
/** The files codegen reports it wrote — the block every generation result carries:
|
|
42
|
+
*
|
|
43
|
+
* Successfully generated/updated tests files:
|
|
44
|
+
* \t/abs/path/to/orders_integration_test.py
|
|
45
|
+
*
|
|
46
|
+
* Read from the result rather than inferred from the filesystem, so a leftover from an
|
|
47
|
+
* earlier attempt or a sibling written by another call is never mistaken for this call's
|
|
48
|
+
* output. Empty when the block is absent (nothing to hand off). */
|
|
49
|
+
export function generatedFilesFrom(result) {
|
|
50
|
+
const m = /Successfully generated\/updated tests? files?:\s*\n((?:[ \t]+\S[^\n]*\n?)+)/i.exec(result);
|
|
51
|
+
if (!m)
|
|
52
|
+
return [];
|
|
53
|
+
return m[1]
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((l) => l.trim())
|
|
56
|
+
.filter((l) => l.length > 0 && path.isAbsolute(l));
|
|
57
|
+
}
|
|
42
58
|
export class TestGenerationService {
|
|
43
59
|
client;
|
|
44
60
|
constructor() {
|
|
@@ -74,6 +90,9 @@ export class TestGenerationService {
|
|
|
74
90
|
return apiAnalysisResult;
|
|
75
91
|
}
|
|
76
92
|
const result = await this.executeGeneration(generateOptions);
|
|
93
|
+
// The files this call wrote, as codegen reports them — the reuse hand-off is
|
|
94
|
+
// recorded per FILE (SKYR-4220).
|
|
95
|
+
const written = generatedFilesFrom(result);
|
|
77
96
|
const testType = this.getTestType();
|
|
78
97
|
const languageSteps = getLanguageSteps({
|
|
79
98
|
language: params.language || "",
|
|
@@ -83,12 +102,57 @@ export class TestGenerationService {
|
|
|
83
102
|
// Orchestrate code optimization workflow based on flags
|
|
84
103
|
let postGenerationMessage = "";
|
|
85
104
|
if (params.codeReuse) {
|
|
86
|
-
//
|
|
87
|
-
|
|
105
|
+
// Name the framework generation actually used, not the raw parameter:
|
|
106
|
+
// TS/JS derive to playwright when the caller omitted it, and
|
|
107
|
+
// skyramp_reuse_code requires framework to route correctly.
|
|
108
|
+
const effectiveFramework = deriveEffectiveFramework(params.language || "", params.framework);
|
|
109
|
+
const reuseArgs = `\`testFile\` set to the absolute path of the generated test file, \`language: "${params.language}"\`, \`testType: "${testType}"\`${effectiveFramework ? `, and \`framework: "${effectiveFramework}"\`` : ""}`;
|
|
110
|
+
// Modularize-first: fresh codegen carries no helper functions (each
|
|
111
|
+
// endpoint called once, each browser step inline), so modularization is
|
|
112
|
+
// what creates the named helpers that reuse then consolidates into the
|
|
113
|
+
// shared utils file. Emitted here, not in the testbot prompt, so the
|
|
114
|
+
// instruction exists only on runs that actually generated this test type
|
|
115
|
+
// and needs no hand-maintained step numbering.
|
|
116
|
+
if (this.emitsModularizeFirstHandOff(params)) {
|
|
117
|
+
// SKYR-4220: record the hand-off at the site that emits it, under the same
|
|
118
|
+
// condition, so skyramp_execute_test can refuse a test that skipped the pair
|
|
119
|
+
// and the report can mark it — without ever owing it for a test that was not
|
|
120
|
+
// told to run it. Keyed by the files this call wrote: a test generated into
|
|
121
|
+
// the same directory without codeReuse owes nothing.
|
|
122
|
+
await recordReuseHandOff(written, testType, params.language || "", effectiveFramework);
|
|
123
|
+
// Browser tests: the assertion enhancer anchors new assertions to
|
|
124
|
+
// selectors present in the test file, and after modularize → reuse
|
|
125
|
+
// those selectors live in the utils file. So for UI the enhancer runs
|
|
126
|
+
// FIRST, on the raw file; the extraction rules then keep every value
|
|
127
|
+
// assertion in the test body. API enhancers key off response paths,
|
|
128
|
+
// so integration keeps its validated modularize → reuse → enhance order.
|
|
129
|
+
const enhanceFirst = testType === TestType.UI && !!params.enhanceAssertions;
|
|
130
|
+
const steps = [
|
|
131
|
+
...(enhanceFirst
|
|
132
|
+
? [
|
|
133
|
+
`Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated test file, \`testType: "${testType}"\`, and \`enhanceType: "generation"\`, and apply every instruction returned to that file — BEFORE modularization, while the file's selectors are still inline.`,
|
|
134
|
+
]
|
|
135
|
+
: []),
|
|
136
|
+
`Call \`skyramp_modularization\` with \`testFile\`, \`testType: "${testType}"\`, and \`isTraceBased: true\`, and apply the returned instructions — this extracts the test's steps into named helper functions, which is what makes cross-test reuse possible.`,
|
|
137
|
+
`Immediately after writing the modularized file — as your very NEXT skyramp tool call — call \`skyramp_reuse_code\` with ${reuseArgs}. The modularization call does NOT count as this step: reuse is a separate tool and MUST be invoked even when you expect it to find nothing. Follow the returned steps to completion. Sibling tests offering nothing to reuse is a normal outcome — the returned steps still move THIS test's own helpers into the shared utils file and import them back, so the test file IS expected to change; finish those steps, then continue (do NOT retry).`,
|
|
138
|
+
];
|
|
139
|
+
const ordering = enhanceFirst
|
|
140
|
+
? "three sub-steps, STRICTLY SEQUENTIAL (never in the same tool-call batch)"
|
|
141
|
+
: "two sub-steps, STRICTLY SEQUENTIAL (never in the same tool-call batch), BOTH BEFORE `skyramp_enhance_assertions`";
|
|
142
|
+
postGenerationMessage += `
|
|
143
|
+
Test generated successfully!
|
|
144
|
+
|
|
145
|
+
⏭️**CRITICAL NEXT STEPS — ${ordering}**:
|
|
146
|
+
${steps.map((text, i) => `${String.fromCharCode(97 + i)}. ${text}`).join("\n")}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
postGenerationMessage += `
|
|
88
151
|
Test generated successfully!
|
|
89
152
|
|
|
90
|
-
⏭️**CRITICAL NEXT STEP**:
|
|
153
|
+
⏭️**CRITICAL NEXT STEP**: Run code reuse analysis by calling the skyramp_reuse_code tool with ${reuseArgs}.
|
|
91
154
|
`;
|
|
155
|
+
}
|
|
92
156
|
}
|
|
93
157
|
else if (params.modularizeCode) {
|
|
94
158
|
// Check if test type should be modularized
|
|
@@ -210,6 +274,16 @@ The generated test file remains unchanged and ready to use as-is.
|
|
|
210
274
|
? { content: [], isError: false }
|
|
211
275
|
: errList;
|
|
212
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Whether this generation's codeReuse hand-off sequences skyramp_modularization
|
|
279
|
+
* before skyramp_reuse_code (routing: isModularizeFirstTarget). Subclasses that
|
|
280
|
+
* add their own post-generation steps consult this so modularization is
|
|
281
|
+
* instructed exactly once.
|
|
282
|
+
*/
|
|
283
|
+
emitsModularizeFirstHandOff(params) {
|
|
284
|
+
return (!!params.codeReuse &&
|
|
285
|
+
isModularizeFirstTarget(this.getTestType(), params.language));
|
|
286
|
+
}
|
|
213
287
|
async handleApiAnalysis(params) {
|
|
214
288
|
if (params.apiSchema && params.endpointURL && params.method === "") {
|
|
215
289
|
try {
|
|
@@ -274,7 +348,9 @@ The generated test file remains unchanged and ready to use as-is.
|
|
|
274
348
|
// KNOWN_AUTH_HEADERS is imported from workspaceAuth.ts — single source of truth
|
|
275
349
|
// shared with trace-parser.ts for redaction. See workspaceAuth.ts for the full list.
|
|
276
350
|
static simpleWildcardMatch(pattern, value) {
|
|
277
|
-
const regex = new RegExp("^" +
|
|
351
|
+
const regex = new RegExp("^" +
|
|
352
|
+
pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
|
|
353
|
+
"$");
|
|
278
354
|
return regex.test(value);
|
|
279
355
|
}
|
|
280
356
|
static traceMatchesFilters(entry, include, exclude) {
|
|
@@ -421,7 +497,7 @@ The generated test file remains unchanged and ready to use as-is.
|
|
|
421
497
|
// authType handles the Authorization header implicitly — the Go CLI treats
|
|
422
498
|
// --auth-type and --auth-header as mutually exclusive, so clear authHeader
|
|
423
499
|
// when it's the standard Authorization header to avoid the conflict.
|
|
424
|
-
if (isAuthorizationHeaderName(generateOptions.authHeader ||
|
|
500
|
+
if (isAuthorizationHeaderName(generateOptions.authHeader || "")) {
|
|
425
501
|
delete generateOptions.authHeader;
|
|
426
502
|
}
|
|
427
503
|
}
|
|
@@ -106,23 +106,24 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
|
|
|
106
106
|
// carrying as literals (SKYR-3862). Without this the container never sees
|
|
107
107
|
// them and every generated login fails its guard.
|
|
108
108
|
//
|
|
109
|
+
// Matched by prefix, like the service URLs above: a multi-credential
|
|
110
|
+
// `uiCredentials` input exports one keyed pair per credential —
|
|
111
|
+
// SKYRAMP_UI_USERNAME_<KEY> / SKYRAMP_UI_PASSWORD_<KEY> — alongside the
|
|
112
|
+
// unkeyed first-credential pair, and codegen binds each generated test to
|
|
113
|
+
// the pair it was recorded with (SKYR-4110). Dropping a keyed pair here
|
|
114
|
+
// would fail that test's guard even though the run provided the credential.
|
|
115
|
+
//
|
|
109
116
|
// Forwarded on presence, not truthiness: the generated guard deliberately
|
|
110
117
|
// distinguishes an unset variable (a misconfiguration, fails fast) from one
|
|
111
118
|
// set to "" (valid on forms that accept a blank password). A truthy check —
|
|
112
119
|
// as used for the URL vars above — would drop the empty case and collapse it
|
|
113
120
|
// into the failure case.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// (SKYRAMP_UI_PASSWORD_<KEY>), which needs no new forwarding logic here: the
|
|
121
|
-
// loop above already forwards SKYRAMP_TEST_SERVICE_URL_* by prefix, so the
|
|
122
|
-
// same approach applies. Keep the presence-not-truthiness rule when it lands.
|
|
123
|
-
for (const key of ["SKYRAMP_UI_USERNAME", "SKYRAMP_UI_PASSWORD"]) {
|
|
124
|
-
const val = hostEnv[key];
|
|
125
|
-
if (val !== undefined) {
|
|
121
|
+
for (const [key, val] of Object.entries(hostEnv)) {
|
|
122
|
+
const isCredential = key === "SKYRAMP_UI_USERNAME" ||
|
|
123
|
+
key === "SKYRAMP_UI_PASSWORD" ||
|
|
124
|
+
key.startsWith("SKYRAMP_UI_USERNAME_") ||
|
|
125
|
+
key.startsWith("SKYRAMP_UI_PASSWORD_");
|
|
126
|
+
if (isCredential && val !== undefined) {
|
|
126
127
|
env.push(`${key}=${val}`);
|
|
127
128
|
}
|
|
128
129
|
}
|