@skyramp/mcp 0.3.6 → 0.3.8
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/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +25 -8
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +87 -9
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/reportVerification.d.ts +9 -0
- package/build/utils/reportVerification.js +39 -6
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.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.VEfqi1qN.js → uiMode.BPopbasy.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/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { generatedSiblings } from "./call-sites.js";
|
|
3
|
+
/** The strings a sibling's source would contain if it imports the module at
|
|
4
|
+
* `modulePath` (relative to the sibling's directory): the path form for TS/JS
|
|
5
|
+
* specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
|
|
6
|
+
* `shared/X` → `shared.X` / `.shared.X`. */
|
|
7
|
+
export function moduleMentionCandidates(modulePath) {
|
|
8
|
+
const posix = modulePath.split(path.sep).join("/");
|
|
9
|
+
const m = /^((?:\.\.\/)*)(.*)$/.exec(posix);
|
|
10
|
+
const ups = (m?.[1] ?? "").split("/").filter(Boolean).length;
|
|
11
|
+
const rest = (m?.[2] ?? posix).split("/").filter(Boolean).join(".");
|
|
12
|
+
const dotted = ".".repeat(ups + 1) + rest; // one leading dot = current package
|
|
13
|
+
return [posix, "./" + posix, rest, dotted, "." + rest];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Count, per utils helper, how many Skyramp-generated tests in the spec's directory
|
|
17
|
+
* import it — the spec under verification included.
|
|
18
|
+
*
|
|
19
|
+
* Findings:
|
|
20
|
+
* - 0 importers: nothing imports the helper, not even the spec that wrote it — a
|
|
21
|
+
* dead helper, actionable on any run.
|
|
22
|
+
* - 1 importer: not (yet) shared. Reported only when the directory holds at least
|
|
23
|
+
* TWO delivered tests: on a first run every helper has exactly one importer by
|
|
24
|
+
* construction (it seeds the utils file), so listing them would be guaranteed
|
|
25
|
+
* noise on the most common path.
|
|
26
|
+
* Fails open to "no findings" on any read error.
|
|
27
|
+
*/
|
|
28
|
+
export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
|
|
29
|
+
let dir;
|
|
30
|
+
let siblings;
|
|
31
|
+
try {
|
|
32
|
+
dir = path.dirname(testFile);
|
|
33
|
+
siblings =
|
|
34
|
+
preReadSiblings ??
|
|
35
|
+
(await generatedSiblings(testFile, spec, { includeSelf: true }));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
// `importedNames` matches by basename stem, so a bare `from SkyrampUtils import …`
|
|
41
|
+
// in a sibling would also be credited to `../shared/SkyrampUtils`. A sibling's
|
|
42
|
+
// bare or `./` import resolves to the module BESIDE it, so an out-of-directory
|
|
43
|
+
// utils file is credited only when the sibling's content names its module path;
|
|
44
|
+
// a miss under-counts, which keeps the helper IN the advisory rather than
|
|
45
|
+
// hiding it.
|
|
46
|
+
const stems = utilsFiles.map((f) => ({
|
|
47
|
+
file: f,
|
|
48
|
+
stem: path.basename(f).replace(/\.[^.]+$/, ""),
|
|
49
|
+
inDir: path.dirname(f) === dir,
|
|
50
|
+
mentions: moduleMentionCandidates(path.relative(dir, f).replace(/\.[^.]+$/, "")),
|
|
51
|
+
}));
|
|
52
|
+
// helper key = `${utilsFile}\0${name}` → number of importing tests
|
|
53
|
+
const counts = new Map();
|
|
54
|
+
for (const [file, helpers] of helpersByFile)
|
|
55
|
+
for (const h of helpers)
|
|
56
|
+
counts.set(`${file}\0${h.name}`, 0);
|
|
57
|
+
for (const { content } of siblings) {
|
|
58
|
+
for (const { file: utilsFile, stem, inDir, mentions } of stems) {
|
|
59
|
+
if (!inDir && !mentions.some((c) => content.includes(c)))
|
|
60
|
+
continue;
|
|
61
|
+
const imported = new Set(spec.importedNames(content, stem));
|
|
62
|
+
for (const h of helpersByFile.get(utilsFile) ?? [])
|
|
63
|
+
if (imported.has(h.name)) {
|
|
64
|
+
const k = `${utilsFile}\0${h.name}`;
|
|
65
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const secondRunExists = siblings.length >= 2;
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [file, helpers] of helpersByFile)
|
|
72
|
+
for (const h of helpers) {
|
|
73
|
+
const n = counts.get(`${file}\0${h.name}`) ?? 0;
|
|
74
|
+
if (n === 0 || (n === 1 && secondRunExists))
|
|
75
|
+
out.push({ file, helper: h.name, line: h.line, importers: n });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -54,12 +54,13 @@ export interface HelperFamilySpec {
|
|
|
54
54
|
family: HelperFamily;
|
|
55
55
|
/** Whether two helpers wrapping the same method+path are a violation. */
|
|
56
56
|
routeDuplicates: boolean;
|
|
57
|
-
/** Picks the permitted-assertion pattern out of a language spec. */
|
|
58
|
-
allowedAssertion(spec: UtilsLanguageSpec): RegExp;
|
|
59
57
|
/** Whether one complete assertion expression is permitted for this family. */
|
|
60
58
|
isAllowed(spec: UtilsLanguageSpec, expr: string): boolean;
|
|
61
59
|
/** One clause for the failure text: what a helper of this family may assert. */
|
|
62
60
|
assertionRule: string;
|
|
61
|
+
/** One clause for the fix-instruction sentence — composed by the gate, never
|
|
62
|
+
* restated there (the browser clause reads as a continuation of the api one). */
|
|
63
|
+
fixRule: string;
|
|
63
64
|
}
|
|
64
65
|
export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
|
|
65
66
|
/** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
|
|
@@ -67,6 +68,16 @@ export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
|
|
|
67
68
|
* exists, and the browser rules are the more permissive of the two on duplicates, so
|
|
68
69
|
* guessing "browser" would silently switch a check off. */
|
|
69
70
|
export declare function helperFamilyFor(testType: string | undefined): HelperFamilySpec;
|
|
71
|
+
/** The family ONE helper is judged by. A helper that issues an SDK request follows
|
|
72
|
+
* the api rules whatever the test type: UI flows routinely keep a sign-in or seed
|
|
73
|
+
* request beside their browser helpers, and judging that request by the browser
|
|
74
|
+
* rules made its status assertion a false violation. Keyed on the request itself,
|
|
75
|
+
* not the duplicate key — a two-request sign-in, a concatenated path, and a
|
|
76
|
+
* parameterised route are all API helpers with no keyable route. A helper with no
|
|
77
|
+
* request falls back to the test type's family. */
|
|
78
|
+
export declare function helperFamilyOf(helper: {
|
|
79
|
+
wrapsRequest?: boolean;
|
|
80
|
+
}, fallback: HelperFamilySpec): HelperFamilySpec;
|
|
70
81
|
/** The spec for a language string as the tools receive it, or `undefined` for a
|
|
71
82
|
* language this verifier does not cover (Java: utils reuse is disabled for it). */
|
|
72
83
|
export declare function utilsSpecFor(language: string | undefined): UtilsLanguageSpec | undefined;
|
|
@@ -125,20 +125,20 @@ export const HELPER_FAMILIES = {
|
|
|
125
125
|
api: {
|
|
126
126
|
family: "api",
|
|
127
127
|
routeDuplicates: true,
|
|
128
|
-
allowedAssertion: (spec) => spec.allowedAssertion,
|
|
129
128
|
isAllowed: (spec, expr) => spec.allowedAssertion.test(expr),
|
|
130
129
|
assertionRule: "a shared API helper asserts only the status code; move this assertion to the test body, applied to the response the helper returns",
|
|
130
|
+
fixRule: "a helper that wraps an SDK request asserts only the status code — one helper per method+path, extended with defaulted parameters, named method + resource",
|
|
131
131
|
},
|
|
132
132
|
browser: {
|
|
133
133
|
family: "browser",
|
|
134
134
|
routeDuplicates: false,
|
|
135
|
-
allowedAssertion: (spec) => spec.allowedBrowserAssertion,
|
|
136
135
|
// A structural wait on a locator that carries a scenario LITERAL
|
|
137
136
|
// (`getByText("Total: $899.98")`) is a value assertion in disguise: digits or
|
|
138
137
|
// currency in any string inside the expect(...) arguments disqualify it.
|
|
139
138
|
isAllowed: (spec, expr) => spec.allowedBrowserAssertion.test(expr) &&
|
|
140
139
|
!SCENARIO_LITERAL_RE.test(expectArgs(expr)),
|
|
141
140
|
assertionRule: "a shared browser helper holds actions and structural waits only (toBeVisible, toBeEnabled, …); move this value assertion to the test body immediately after the helper call, selector verbatim",
|
|
141
|
+
fixRule: "every other helper holds actions and structural waits only, named for the user intent",
|
|
142
142
|
},
|
|
143
143
|
};
|
|
144
144
|
/** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
|
|
@@ -149,6 +149,16 @@ export function helperFamilyFor(testType) {
|
|
|
149
149
|
const t = (testType ?? "").toLowerCase();
|
|
150
150
|
return HELPER_FAMILIES[t === "ui" || t === "e2e" ? "browser" : "api"];
|
|
151
151
|
}
|
|
152
|
+
/** The family ONE helper is judged by. A helper that issues an SDK request follows
|
|
153
|
+
* the api rules whatever the test type: UI flows routinely keep a sign-in or seed
|
|
154
|
+
* request beside their browser helpers, and judging that request by the browser
|
|
155
|
+
* rules made its status assertion a false violation. Keyed on the request itself,
|
|
156
|
+
* not the duplicate key — a two-request sign-in, a concatenated path, and a
|
|
157
|
+
* parameterised route are all API helpers with no keyable route. A helper with no
|
|
158
|
+
* request falls back to the test type's family. */
|
|
159
|
+
export function helperFamilyOf(helper, fallback) {
|
|
160
|
+
return helper.wrapsRequest ? HELPER_FAMILIES.api : fallback;
|
|
161
|
+
}
|
|
152
162
|
/** The spec for a language string as the tools receive it, or `undefined` for a
|
|
153
163
|
* language this verifier does not cover (Java: utils reuse is disabled for it). */
|
|
154
164
|
export function utilsSpecFor(language) {
|
|
@@ -2,8 +2,6 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
|
2
2
|
export interface HelperAssertion {
|
|
3
3
|
line: number;
|
|
4
4
|
text: string;
|
|
5
|
-
/** True for the one permitted shape — a status-code comparison. */
|
|
6
|
-
allowed: boolean;
|
|
7
5
|
}
|
|
8
6
|
export interface UtilsHelper {
|
|
9
7
|
name: string;
|
|
@@ -15,8 +13,23 @@ export interface UtilsHelper {
|
|
|
15
13
|
path?: string;
|
|
16
14
|
/** `path` with every path parameter collapsed to `{}`; the duplicate key with `method`. */
|
|
17
15
|
normalizedPath?: string;
|
|
16
|
+
/** The helper issues at least one SDK request — what makes it an API helper,
|
|
17
|
+
* whatever the test type, and whether or not it has a keyable single route
|
|
18
|
+
* (a two-request sign-in, a concatenated path, and a parameterised route all
|
|
19
|
+
* wrap requests without one). */
|
|
20
|
+
wrapsRequest: boolean;
|
|
21
|
+
/** The body holds a live `page.on('pageerror', …)` registration. A guard moved
|
|
22
|
+
* into a shared helper is still delivered — see hasPageErrorGuard. */
|
|
23
|
+
hasGuard: boolean;
|
|
18
24
|
assertions: HelperAssertion[];
|
|
25
|
+
/** TypeScript only: signature parameters with no type annotation (`page`, not
|
|
26
|
+
* `page: Page`). Empty for other languages. */
|
|
27
|
+
untypedParams: string[];
|
|
19
28
|
}
|
|
29
|
+
/** The parameter names in a TS signature that carry no `: type`. Reads the signature
|
|
30
|
+
* from the definition line to its `{` (Prettier wraps long signatures), takes the
|
|
31
|
+
* first balanced `(...)`, and splits it on top-level commas. */
|
|
32
|
+
export declare function untypedParamsOf(sigLines: string[]): string[];
|
|
20
33
|
/**
|
|
21
34
|
* Structured extraction of the module-level helpers in a utils file.
|
|
22
35
|
*
|
|
@@ -27,8 +40,23 @@ export interface UtilsHelper {
|
|
|
27
40
|
* next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
|
|
28
41
|
* the definition returns to zero.
|
|
29
42
|
*/
|
|
30
|
-
export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec
|
|
43
|
+
export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec): UtilsHelper[];
|
|
31
44
|
/** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
|
|
32
45
|
* comments are `#…`; TS/JS go through the shared stripper. */
|
|
33
46
|
/** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
|
|
34
47
|
export declare function blankStrings(src: string, spec: UtilsLanguageSpec): string;
|
|
48
|
+
/** Assertions in a source file: lines (Python) / occurrences (TS) matching the
|
|
49
|
+
* language's assertion pattern, with comments and string contents blanked so a
|
|
50
|
+
* commented-out or quoted `expect(` never counts. */
|
|
51
|
+
export declare function countAssertions(content: string, spec: UtilsLanguageSpec): number;
|
|
52
|
+
/**
|
|
53
|
+
* A live `page.on('pageerror', …)` registration — the guard the enhancer adds and
|
|
54
|
+
* modularization must not drop. Exactly `pageerror`: a `console` listener is not an
|
|
55
|
+
* error collector, and accepting it would let an unrelated listener mask the removal.
|
|
56
|
+
* The call is located on a copy with comments AND string contents blanked (so a
|
|
57
|
+
* commented-out or quoted example never satisfies the check) and its argument is read
|
|
58
|
+
* from the comment-blanked copy, where the `'pageerror'` literal is still legible.
|
|
59
|
+
* Both copies preserve offsets, so a Prettier-wrapped registration matches too.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasPageErrorGuard(content: string, spec: UtilsLanguageSpec): boolean;
|
|
62
|
+
export declare function blankCommentsAndStrings(src: string, spec: UtilsLanguageSpec): string;
|
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
import { normalizePath } from "./language-spec.js";
|
|
2
|
-
import { stripCommentsAndStrings, stripStrings } from "../pom-scope/strip.js";
|
|
2
|
+
import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
3
|
+
/** The parameter names in a TS signature that carry no `: type`. Reads the signature
|
|
4
|
+
* from the definition line to its `{` (Prettier wraps long signatures), takes the
|
|
5
|
+
* first balanced `(...)`, and splits it on top-level commas. */
|
|
6
|
+
export function untypedParamsOf(sigLines) {
|
|
7
|
+
const sig = sigLines.join(" ");
|
|
8
|
+
const open = sig.indexOf("(");
|
|
9
|
+
if (open === -1) {
|
|
10
|
+
// Bare-arrow form: `export const open = async page => …` — one parameter,
|
|
11
|
+
// untyped by construction (a typed single parameter requires parentheses).
|
|
12
|
+
const bare = /(?:^|=)\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(sig);
|
|
13
|
+
return bare ? [bare[1]] : [];
|
|
14
|
+
}
|
|
15
|
+
// `>` closes a nesting level only when it is not the arrow token's tail:
|
|
16
|
+
// `(callback: (x: string) => void, page)` must not close at `=>`.
|
|
17
|
+
const closes = (ch, prev) => ch === ")" || ch === "]" || ch === "}" || (ch === ">" && prev !== "=");
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let close = -1;
|
|
20
|
+
for (let i = open; i < sig.length; i++) {
|
|
21
|
+
const ch = sig[i];
|
|
22
|
+
if ("([{<".includes(ch))
|
|
23
|
+
depth++;
|
|
24
|
+
else if (closes(ch, sig[i - 1] ?? "")) {
|
|
25
|
+
depth--;
|
|
26
|
+
if (depth === 0) {
|
|
27
|
+
close = i;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (close === -1)
|
|
33
|
+
return [];
|
|
34
|
+
const params = [];
|
|
35
|
+
let cur = "";
|
|
36
|
+
depth = 0;
|
|
37
|
+
for (let i = open + 1; i < close; i++) {
|
|
38
|
+
const ch = sig[i];
|
|
39
|
+
if ("([{<".includes(ch))
|
|
40
|
+
depth++;
|
|
41
|
+
else if (closes(ch, sig[i - 1] ?? ""))
|
|
42
|
+
depth--;
|
|
43
|
+
if (ch === "," && depth === 0) {
|
|
44
|
+
params.push(cur);
|
|
45
|
+
cur = "";
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
cur += ch;
|
|
49
|
+
}
|
|
50
|
+
params.push(cur);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const raw of params) {
|
|
53
|
+
const param = raw.trim().replace(/^\.\.\./, "");
|
|
54
|
+
if (!param)
|
|
55
|
+
continue;
|
|
56
|
+
// A default value means TypeScript infers the type — not a finding, even
|
|
57
|
+
// unannotated (`expectedStatus = 200`). `=>` inside a function-type
|
|
58
|
+
// annotation is not a default.
|
|
59
|
+
if (/=(?!>)/.test(param.replace(/\([^)]*\)/g, "")))
|
|
60
|
+
continue;
|
|
61
|
+
// A binding pattern's inner colons are renames, not annotations:
|
|
62
|
+
// `{ page: browserPage }` is implicitly any unless a `:` follows the
|
|
63
|
+
// complete pattern.
|
|
64
|
+
if (param.startsWith("{") || param.startsWith("[")) {
|
|
65
|
+
let d = 0;
|
|
66
|
+
let after = -1;
|
|
67
|
+
for (let i = 0; i < param.length; i++) {
|
|
68
|
+
if ("([{<".includes(param[i]))
|
|
69
|
+
d++;
|
|
70
|
+
else if (closes(param[i], param[i - 1] ?? "")) {
|
|
71
|
+
d--;
|
|
72
|
+
if (d === 0) {
|
|
73
|
+
after = i + 1;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (after === -1 || !param.slice(after).trimStart().startsWith(":"))
|
|
79
|
+
out.push(param.replace(/\s+/g, " "));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (param.includes(":")) {
|
|
83
|
+
// `page: any` compiles clean under noImplicitAny — the customer's compiler
|
|
84
|
+
// will NEVER report it, which makes it the one case this advisory can catch
|
|
85
|
+
// that tsc cannot. It is also the exact legacy convention SKYR-3324 wrote.
|
|
86
|
+
if (/:\s*any\s*$/.test(param))
|
|
87
|
+
out.push(param.split(":")[0].trim().replace(/\?$/, "") + ": any");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out.push(param.replace(/\?$/, ""));
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
3
94
|
/**
|
|
4
95
|
* Structured extraction of the module-level helpers in a utils file.
|
|
5
96
|
*
|
|
@@ -10,10 +101,7 @@ import { stripCommentsAndStrings, stripStrings } from "../pom-scope/strip.js";
|
|
|
10
101
|
* next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
|
|
11
102
|
* the definition returns to zero.
|
|
12
103
|
*/
|
|
13
|
-
export function parseUtilsFile(content, spec
|
|
14
|
-
const isAllowed = typeof allowedAssertion === "function"
|
|
15
|
-
? allowedAssertion
|
|
16
|
-
: (expr) => allowedAssertion.test(expr);
|
|
104
|
+
export function parseUtilsFile(content, spec) {
|
|
17
105
|
const lines = content.split("\n");
|
|
18
106
|
const helpers = [];
|
|
19
107
|
let i = 0;
|
|
@@ -37,7 +125,14 @@ export function parseUtilsFile(content, spec, allowedAssertion = spec.allowedAss
|
|
|
37
125
|
name,
|
|
38
126
|
line: start + 1,
|
|
39
127
|
endLine: Math.max(end, start + 1),
|
|
128
|
+
wrapsRequest: false,
|
|
129
|
+
hasGuard: hasPageErrorGuard(body.join("\n"), spec),
|
|
40
130
|
assertions: [],
|
|
131
|
+
untypedParams: spec.language === "typescript"
|
|
132
|
+
? // The blanked copy: a comment inside a wrapped signature must not
|
|
133
|
+
// reach the agent as a "parameter".
|
|
134
|
+
untypedParamsOf(signatureLines(scan))
|
|
135
|
+
: [],
|
|
41
136
|
};
|
|
42
137
|
let requests = 0;
|
|
43
138
|
for (let k = 0; k < body.length; k++) {
|
|
@@ -77,13 +172,13 @@ export function parseUtilsFile(content, spec, allowedAssertion = spec.allowedAss
|
|
|
77
172
|
helper.assertions.push({
|
|
78
173
|
line: start + k + 1,
|
|
79
174
|
text: original,
|
|
80
|
-
allowed: isAllowed(original),
|
|
81
175
|
});
|
|
82
176
|
k = last;
|
|
83
177
|
}
|
|
84
178
|
}
|
|
85
179
|
// A helper wrapping two requests is a sequence, not "one SDK call": it has no single
|
|
86
180
|
// route to be a duplicate of, and the shared-helper rules do not apply to it.
|
|
181
|
+
helper.wrapsRequest = requests > 0;
|
|
87
182
|
if (requests > 1) {
|
|
88
183
|
helper.method = undefined;
|
|
89
184
|
helper.path = undefined;
|
|
@@ -122,16 +217,88 @@ export function blankStrings(src, spec) {
|
|
|
122
217
|
.map((line) => line.replace(/(["'])(?:\\.|(?!\1).)*\1/g, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0]))
|
|
123
218
|
.join("\n");
|
|
124
219
|
}
|
|
125
|
-
|
|
220
|
+
/** Assertions in a source file: lines (Python) / occurrences (TS) matching the
|
|
221
|
+
* language's assertion pattern, with comments and string contents blanked so a
|
|
222
|
+
* commented-out or quoted `expect(` never counts. */
|
|
223
|
+
export function countAssertions(content, spec) {
|
|
224
|
+
const scan = blankCommentsAndStrings(content, spec);
|
|
225
|
+
if (spec.language === "python")
|
|
226
|
+
return scan.split("\n").filter((l) => spec.assertionRe.test(l)).length;
|
|
227
|
+
return (scan.match(new RegExp(spec.assertionRe.source, "g")) ?? []).length;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A live `page.on('pageerror', …)` registration — the guard the enhancer adds and
|
|
231
|
+
* modularization must not drop. Exactly `pageerror`: a `console` listener is not an
|
|
232
|
+
* error collector, and accepting it would let an unrelated listener mask the removal.
|
|
233
|
+
* The call is located on a copy with comments AND string contents blanked (so a
|
|
234
|
+
* commented-out or quoted example never satisfies the check) and its argument is read
|
|
235
|
+
* from the comment-blanked copy, where the `'pageerror'` literal is still legible.
|
|
236
|
+
* Both copies preserve offsets, so a Prettier-wrapped registration matches too.
|
|
237
|
+
*/
|
|
238
|
+
export function hasPageErrorGuard(content, spec) {
|
|
239
|
+
const noComments = blankComments(content, spec);
|
|
240
|
+
const code = blankStrings(noComments, spec);
|
|
241
|
+
for (const m of code.matchAll(/\bpage\.on\(/g)) {
|
|
242
|
+
const at = (m.index ?? 0) + m[0].length;
|
|
243
|
+
if (/^\s*["']pageerror["']/.test(noComments.slice(at)))
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
/** Comments blanked, strings preserved, offsets kept. TS/JS use the shared stripper;
|
|
249
|
+
* Python blanks each line from its first `#` outside a string. */
|
|
250
|
+
function blankComments(src, spec) {
|
|
251
|
+
if (spec.language !== "python")
|
|
252
|
+
return stripComments(src);
|
|
253
|
+
const blanked = blankStrings(src, spec).split("\n");
|
|
254
|
+
return src
|
|
255
|
+
.split("\n")
|
|
256
|
+
.map((line, i) => {
|
|
257
|
+
const at = blanked[i].indexOf("#");
|
|
258
|
+
return at === -1
|
|
259
|
+
? line
|
|
260
|
+
: line.slice(0, at) + " ".repeat(line.length - at);
|
|
261
|
+
})
|
|
262
|
+
.join("\n");
|
|
263
|
+
}
|
|
264
|
+
export function blankCommentsAndStrings(src, spec) {
|
|
126
265
|
if (spec.language !== "python")
|
|
127
266
|
return stripCommentsAndStrings(src);
|
|
128
267
|
return src
|
|
129
268
|
.split("\n")
|
|
130
269
|
.map((line) => line
|
|
131
270
|
.replace(/(["'])(?:\\.|(?!\1).)*\1/g, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0])
|
|
132
|
-
|
|
271
|
+
// Blanked, not deleted: callers scan this copy for OFFSETS and read the text
|
|
272
|
+
// back out of the original, so every line must keep its length.
|
|
273
|
+
.replace(/#.*$/, (c) => " ".repeat(c.length)))
|
|
133
274
|
.join("\n");
|
|
134
275
|
}
|
|
276
|
+
function signatureLines(body) {
|
|
277
|
+
// The signature runs until its parameter parentheses close: a `{` inside a
|
|
278
|
+
// wrapped object-type parameter (`opts: { a: string },`) is not the body
|
|
279
|
+
// opener, so a bare `/[{]|=>/` test truncated such signatures.
|
|
280
|
+
const out = [];
|
|
281
|
+
let depth = 0;
|
|
282
|
+
let opened = false;
|
|
283
|
+
for (const l of body) {
|
|
284
|
+
out.push(l);
|
|
285
|
+
for (let i = 0; i < l.length; i++) {
|
|
286
|
+
if (l[i] === "(") {
|
|
287
|
+
depth++;
|
|
288
|
+
opened = true;
|
|
289
|
+
}
|
|
290
|
+
else if (l[i] === ")")
|
|
291
|
+
depth--;
|
|
292
|
+
}
|
|
293
|
+
if (opened && depth <= 0)
|
|
294
|
+
break;
|
|
295
|
+
if (!opened && /=>/.test(l))
|
|
296
|
+
break; // bare-arrow form has no parentheses
|
|
297
|
+
if (out.length > 12)
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
135
302
|
/** Exclusive end index of a Python def starting at `start`: the next non-blank line at
|
|
136
303
|
* column 0 (a following def, import, or module statement). */
|
|
137
304
|
function pythonBodyEnd(lines, start) {
|
|
@@ -147,12 +314,26 @@ function pythonBodyEnd(lines, start) {
|
|
|
147
314
|
/** Exclusive end index of a brace-delimited definition starting at `start`. Strings and
|
|
148
315
|
* comments are not tracked — helper bodies contain request paths, not braces in
|
|
149
316
|
* strings — and an unbalanced body runs to end of file rather than throwing. */
|
|
317
|
+
/** The definition line through the one that opens the body (`{` or `=>`). */
|
|
150
318
|
function braceBodyEnd(lines, start) {
|
|
151
319
|
let depth = 0;
|
|
152
320
|
let opened = false;
|
|
321
|
+
// A `{` inside the signature's still-open parentheses is an object-type
|
|
322
|
+
// parameter (`opts: { a: string },` wrapped by Prettier), not the body opener.
|
|
323
|
+
let parens = 0;
|
|
153
324
|
for (let i = start; i < lines.length; i++) {
|
|
154
325
|
for (const ch of lines[i]) {
|
|
155
|
-
|
|
326
|
+
// Parens matter only until the body opens: they exist to get past a wrapped
|
|
327
|
+
// signature. Tracking them afterwards let one unbalanced `(` in a string or
|
|
328
|
+
// comment blind the brace count for the rest of the file — swallowing every
|
|
329
|
+
// later helper and misattributing its violations (the gate then never clears).
|
|
330
|
+
if (!opened && ch === "(")
|
|
331
|
+
parens++;
|
|
332
|
+
else if (!opened && ch === ")")
|
|
333
|
+
parens = Math.max(0, parens - 1);
|
|
334
|
+
else if (!opened && parens > 0)
|
|
335
|
+
continue;
|
|
336
|
+
else if (ch === "{") {
|
|
156
337
|
depth++;
|
|
157
338
|
opened = true;
|
|
158
339
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
+
export interface RetrofitMismatch {
|
|
3
|
+
/** Absolute path of the rewired pre-existing test. */
|
|
4
|
+
file: string;
|
|
5
|
+
/** The shared helper the file now calls in place of its own request block. */
|
|
6
|
+
helper: string;
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
/** What the substitution changed about the request. */
|
|
10
|
+
detail: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether a retrofit changed the request the rewired test sends.
|
|
14
|
+
*
|
|
15
|
+
* STEP 5c edits a PRE-EXISTING test — code the customer already owns and whose
|
|
16
|
+
* behaviour nothing in this run chose — to call a shared helper instead of its own
|
|
17
|
+
* inline request block. That is only safe if the helper sends the same request. It
|
|
18
|
+
* usually does, because the helper was extracted from an identical block; but nothing
|
|
19
|
+
* checked it, and a helper that hardcodes a field the block never sent (or omits one
|
|
20
|
+
* it did) silently changes what the customer's test exercises. The failure is quiet:
|
|
21
|
+
* the test still runs, and a red result reads like any other red.
|
|
22
|
+
*
|
|
23
|
+
* The advisory path already refuses to OFFER such a site (see `substitutable` in
|
|
24
|
+
* call-sites.ts). This is the same question asked after the fact, about the edit that
|
|
25
|
+
* was actually made, and the rule is stricter: offering tolerates a site with an extra
|
|
26
|
+
* field, because extending the helper with a defaulted parameter is the sanctioned
|
|
27
|
+
* move; a completed retrofit must leave the request byte-equivalent in shape, so the
|
|
28
|
+
* field sets must match exactly in both directions.
|
|
29
|
+
*
|
|
30
|
+
* LIMIT: this compares the request's SHAPE — which fields and options it carries —
|
|
31
|
+
* not the values passed. A baseline sending `status: "draft"` rewired to
|
|
32
|
+
* `createPost(..., "published")` has the same field set and is not reported. Value
|
|
33
|
+
* equivalence needs argument-to-parameter binding, default resolution and expression
|
|
34
|
+
* normalisation; a wrong answer here REFUSES the report, so it is deliberately left
|
|
35
|
+
* out rather than approximated. The value case is also visible in the rewired file's
|
|
36
|
+
* own diff, where a hardcoded helper field is not.
|
|
37
|
+
*
|
|
38
|
+
* Compares against the file's committed baseline (`git show HEAD:<path>`), which is
|
|
39
|
+
* the only record of what the block sent before the edit. Fails OPEN — an untracked
|
|
40
|
+
* file, a detached worktree, or an unreadable baseline yields no mismatches rather
|
|
41
|
+
* than a refusal the agent cannot act on.
|
|
42
|
+
*/
|
|
43
|
+
export declare function retrofitMismatches(retrofitFile: string, utilsFiles: string[], spec: UtilsLanguageSpec): Promise<RetrofitMismatch[]>;
|