@skyramp/mcp 0.4.0 → 0.4.1-rc.2
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/enhance-assertions/contractProviderAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
- package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
- package/build/prompts/testbot/testbot-prompts.js +19 -12
- package/build/recommendation/answers.d.ts +11 -7
- package/build/recommendation/answers.js +14 -10
- package/build/recommendation/pullRequestText.d.ts +18 -0
- package/build/recommendation/pullRequestText.js +31 -0
- package/build/recommendation/registerPlan.d.ts +9 -1
- package/build/recommendation/registerPlan.js +8 -1
- package/build/recommendation/runVerifiers.js +6 -0
- package/build/recommendation/types.d.ts +58 -0
- package/build/recommendation/verifierContracts.d.ts +108 -13
- package/build/recommendation/verifierContracts.js +147 -29
- package/build/recommendation/verifiers/coverage.d.ts +10 -0
- package/build/recommendation/verifiers/coverage.js +144 -22
- package/build/recommendation/verifiers/defects.d.ts +9 -0
- package/build/recommendation/verifiers/defects.js +117 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
- package/build/recommendation/verifiers/existingCoverage.js +53 -0
- package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +246 -0
- package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
- package/build/recommendation/verifiers/issueTraceability.js +197 -0
- package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
- package/build/recommendation/verifiers/requirementSourced.js +168 -0
- package/build/services/TestGenerationService.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
- package/build/tools/code-refactor/reuse-state.d.ts +85 -7
- package/build/tools/code-refactor/reuse-state.js +239 -34
- package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
- package/build/tools/code-refactor/utils-verify-gates.js +103 -11
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
- package/build/tools/submitReportTool.js +276 -39
- package/build/tools/test-management/actionsTool.js +5 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
- package/build/tools/test-management/registerTestPlanTool.js +105 -16
- package/build/types/ReuseOutcome.d.ts +73 -7
- package/build/types/TestAnalysis.d.ts +6 -0
- package/build/types/TestbotReport.d.ts +22 -4
- package/build/utils/AnalysisStateManager.d.ts +7 -1
- package/build/utils/AnalysisStateManager.js +5 -1
- package/build/utils/assertion-verify/api-shared-lints.js +118 -11
- package/build/utils/assertion-verify/format.js +2 -2
- package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
- package/build/utils/assertion-verify/helper-imports.js +119 -27
- package/build/utils/assertion-verify/lint-types.d.ts +31 -2
- package/build/utils/assertion-verify/lint-types.js +66 -0
- package/build/utils/assertion-verify/metrics.d.ts +13 -0
- package/build/utils/assertion-verify/metrics.js +16 -0
- package/build/utils/assertion-verify/verify.d.ts +11 -6
- package/build/utils/assertion-verify/verify.js +56 -15
- package/build/utils/canonicalJson.d.ts +11 -0
- package/build/utils/canonicalJson.js +17 -0
- package/build/utils/utils-verify/action-key.d.ts +27 -0
- package/build/utils/utils-verify/action-key.js +292 -0
- package/build/utils/utils-verify/allow.d.ts +8 -1
- package/build/utils/utils-verify/allow.js +14 -1
- package/build/utils/utils-verify/call-sites.d.ts +76 -8
- package/build/utils/utils-verify/call-sites.js +256 -70
- package/build/utils/utils-verify/language-spec.d.ts +3 -2
- package/build/utils/utils-verify/parse.d.ts +22 -3
- package/build/utils/utils-verify/parse.js +123 -52
- package/build/utils/utils-verify/verify.d.ts +33 -3
- package/build/utils/utils-verify/verify.js +126 -12
- package/build/utils/workspaceAuth.d.ts +59 -19
- package/build/utils/workspaceAuth.js +228 -31
- package/package.json +1 -1
- package/plugin/prompts/generate-tests/execution-plan.md +3 -3
- package/plugin/prompts/generate-tests/generation.md +1 -0
- package/plugin/prompts/plan-tests.md +41 -15
- package/plugin/prompts/testbot-task1.md +3 -9
- package/build/prompts/testbot/planDeclarations.d.ts +0 -6
- package/build/prompts/testbot/planDeclarations.js +0 -9
- package/plugin/prompts/declaring-a-plan.md +0 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { SKYRAMP_UTILS_HEADER } from "../utils.js";
|
|
4
|
-
import { parseUtilsFile } from "./parse.js";
|
|
4
|
+
import { blankCommentsAndStrings, isSingleLiteral, parseUtilsFile, } from "./parse.js";
|
|
5
5
|
import { normalizePath } from "./language-spec.js";
|
|
6
6
|
import { realpath } from "./locate.js";
|
|
7
7
|
import { escapeRegExp } from "../regex.js";
|
|
@@ -12,9 +12,10 @@ export const CODEGEN_MARKER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/[.*
|
|
|
12
12
|
const HEADER_SCAN_LINES = 5;
|
|
13
13
|
/**
|
|
14
14
|
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
|
-
* walk, shared by the
|
|
16
|
-
*
|
|
17
|
-
* so the marker check alone excludes them. Fails open to []
|
|
15
|
+
* walk, shared by the two sibling call-site scans and the importer count (the inline
|
|
16
|
+
* scan excludes the spec itself; the other two include it). Utils files carry no
|
|
17
|
+
* codegen version marker, so the marker check alone excludes them. Fails open to []
|
|
18
|
+
* on any read error.
|
|
18
19
|
*/
|
|
19
20
|
export async function generatedSiblings(testFile, spec, opts) {
|
|
20
21
|
let dir;
|
|
@@ -55,21 +56,94 @@ export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
|
55
56
|
* utils helper already wraps — the reuse the prompt asks for and the agent does not
|
|
56
57
|
* perform (measured 0/2 seeded eval runs with the rule as its own mandatory step).
|
|
57
58
|
*
|
|
58
|
-
* A call
|
|
59
|
-
*
|
|
60
|
-
* helper
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* A call inside a module-level helper of the sibling's own counts too, named by that
|
|
60
|
+
* helper (`localHelper`): a sibling that defines `create_order` locally beside a
|
|
61
|
+
* shared `create_order` is the shared helper not used, however it is wrapped. The
|
|
62
|
+
* first design left that to STEP 4b of the prompt (helper-vs-helper) and nothing
|
|
63
|
+
* detected it across spec files, so the copies a half-done extraction leaves behind
|
|
64
|
+
* — one spec rewired, the others still defining the helper — went silent the moment
|
|
65
|
+
* the route became covered. Equivalence is `c3b9157b`'s: same method and same
|
|
66
|
+
* normalised path, nothing else — literals are what the call's arguments are for.
|
|
63
67
|
*
|
|
64
68
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
65
69
|
* flow owns, so it informs rather than gates. Never throws.
|
|
66
70
|
*/
|
|
67
71
|
export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec, preReadSiblings) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const byRoute = await helperRoutes(utilsFiles, spec);
|
|
73
|
+
if (byRoute.size === 0)
|
|
74
|
+
return [];
|
|
75
|
+
const siblings = await nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, false);
|
|
76
|
+
const self = await realpath(testFile);
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const { file, content } of siblings) {
|
|
79
|
+
if (path.resolve(file) === self)
|
|
80
|
+
continue;
|
|
81
|
+
for (const c of requestCallsIn(content, spec)) {
|
|
82
|
+
const match = byRoute.get(c.key);
|
|
83
|
+
if (!match)
|
|
84
|
+
continue;
|
|
85
|
+
if (!sameShape(callShape(c.call), match.keys))
|
|
86
|
+
continue;
|
|
87
|
+
if (!substitutable(match, {
|
|
88
|
+
fields: requestFields(content, c.call, c.start),
|
|
89
|
+
query: queryKeys(c.call),
|
|
90
|
+
schemaChecked: hasSchemaCheck(content, c.line, c.line + SCHEMA_SCAN_LINES),
|
|
91
|
+
}))
|
|
92
|
+
continue;
|
|
93
|
+
out.push({
|
|
94
|
+
file,
|
|
95
|
+
line: c.line,
|
|
96
|
+
method: c.method,
|
|
97
|
+
path: c.path,
|
|
98
|
+
helper: match.helper,
|
|
99
|
+
utilsFile: match.utilsFile,
|
|
100
|
+
...(c.localHelper ? { localHelper: c.localHelper } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Request calls in the Skyramp-generated tests beside `testFile` — the delivered spec
|
|
108
|
+
* INCLUDED, since it is one of the copies — grouped by route and call shape, less the
|
|
109
|
+
* routes a utils helper covers. The same walk as findSiblingInlineCallSites keyed the
|
|
110
|
+
* other way: that scan starts from the helpers and finds a helper not USED; this one
|
|
111
|
+
* starts from the sites and finds a helper not CREATED. Every existing check takes a
|
|
112
|
+
* defined helper as one side of its comparison, so an operation never extracted was
|
|
113
|
+
* invisible to all of them (measured: four per-spec copies of the operation a PR was
|
|
114
|
+
* about, differing only in expected status, beside a module that shared two others).
|
|
115
|
+
*
|
|
116
|
+
* Two sites are one operation when they share method, normalised path and keyword
|
|
117
|
+
* shape — the API near-duplicate definition. Body fields and a schema check do not
|
|
118
|
+
* split a group: with no incumbent helper there is nothing to be substitutable FOR,
|
|
119
|
+
* a body difference is one more literal to lift, and a schema check stays in the spec
|
|
120
|
+
* after the helper call because the response variable is kept.
|
|
121
|
+
*
|
|
122
|
+
* A group must span two or more FILES: a spec repeating one request twice is a
|
|
123
|
+
* different question. Advisory by design — consolidation is a judgement. Never throws.
|
|
124
|
+
*/
|
|
125
|
+
export async function findUnextractedDuplicates(testFile, utilsFiles, spec, preReadSiblings) {
|
|
126
|
+
const covered = new Set((await helperRoutes(utilsFiles, spec)).keys());
|
|
127
|
+
const siblings = await nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, true);
|
|
128
|
+
const groups = new Map();
|
|
129
|
+
for (const { file, content } of siblings) {
|
|
130
|
+
for (const c of requestCallsIn(content, spec)) {
|
|
131
|
+
if (covered.has(c.key))
|
|
132
|
+
continue;
|
|
133
|
+
const shape = [...callShape(c.call)].sort().join(",");
|
|
134
|
+
const k = `${c.key}\0${shape}`;
|
|
135
|
+
const group = groups.get(k) ?? {
|
|
136
|
+
method: c.method,
|
|
137
|
+
path: c.path,
|
|
138
|
+
sites: [],
|
|
139
|
+
};
|
|
140
|
+
group.sites.push({ file, line: c.line, localHelper: c.localHelper });
|
|
141
|
+
groups.set(k, group);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return [...groups.values()].filter((g) => new Set(g.sites.map((s) => s.file)).size >= 2);
|
|
145
|
+
}
|
|
146
|
+
async function helperRoutes(utilsFiles, spec) {
|
|
73
147
|
const byRoute = new Map();
|
|
74
148
|
for (const utilsFile of utilsFiles) {
|
|
75
149
|
let content;
|
|
@@ -91,47 +165,56 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
91
165
|
const call = callText(content, start + m[0].length - 1);
|
|
92
166
|
byRoute.set(`${owner.method} ${owner.normalizedPath}`, {
|
|
93
167
|
helper: owner.name,
|
|
94
|
-
|
|
168
|
+
utilsFile,
|
|
169
|
+
keys: callShape(call),
|
|
95
170
|
fields: requestFields(content, call, start),
|
|
171
|
+
query: queryKeys(call),
|
|
96
172
|
schemaChecked: hasSchemaCheck(content, owner.line, owner.endLine),
|
|
97
173
|
});
|
|
98
174
|
}
|
|
99
175
|
}
|
|
100
|
-
|
|
101
|
-
|
|
176
|
+
return byRoute;
|
|
177
|
+
}
|
|
178
|
+
/** The generated siblings less the utils files themselves (a utils file carries no
|
|
179
|
+
* codegen marker, so this is belt and braces). Pre-read siblings are used as given —
|
|
180
|
+
* the caller's walk decides whether the spec itself is among them. */
|
|
181
|
+
async function nonUtilsSiblings(testFile, utilsFiles, spec, preReadSiblings, includeSelf) {
|
|
102
182
|
const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
|
|
103
|
-
|
|
104
|
-
(await generatedSiblings(testFile, spec, { includeSelf
|
|
105
|
-
|
|
183
|
+
return (preReadSiblings ??
|
|
184
|
+
(await generatedSiblings(testFile, spec, { includeSelf }))).filter((sib) => !utilsSet.has(path.resolve(sib.file)));
|
|
185
|
+
}
|
|
186
|
+
/** Every keyable request call in a generated test. Calls whose method or path cannot
|
|
187
|
+
* be read are skipped: they have no route to compare. Calls are located, and their
|
|
188
|
+
* parentheses balanced, on a comment/string-blanked copy (offsets preserved), so a
|
|
189
|
+
* commented-out or quoted request is never a site and a `)` inside a string never
|
|
190
|
+
* truncates the argument text, which is read from the original. */
|
|
191
|
+
function requestCallsIn(content, spec) {
|
|
192
|
+
const localHelpers = parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
|
|
193
|
+
const scan = blankCommentsAndStrings(content, spec);
|
|
106
194
|
const out = [];
|
|
107
|
-
for (const
|
|
108
|
-
|
|
195
|
+
for (const m of scan.matchAll(REQUEST_CALL_RE)) {
|
|
196
|
+
const start = m.index ?? 0;
|
|
197
|
+
const line = content.slice(0, start).split("\n").length;
|
|
198
|
+
const open = start + m[0].length - 1;
|
|
199
|
+
const call = content.slice(open, callEnd(scan, open));
|
|
200
|
+
const method = spec.methodRe.exec(call)?.[1]?.toUpperCase();
|
|
201
|
+
const pm = spec.pathRe.exec(call);
|
|
202
|
+
// Keyed on ONE literal, as helpers are (parseUtilsFile): a concatenated path's
|
|
203
|
+
// first literal is not the operation — it would read as copies of a route that
|
|
204
|
+
// does not exist, or match a helper for the parent route.
|
|
205
|
+
if (!method || !pm || !isSingleLiteral(call, pm))
|
|
109
206
|
continue;
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!method || !p)
|
|
122
|
-
continue;
|
|
123
|
-
const match = byRoute.get(`${method} ${normalizePath(p)}`);
|
|
124
|
-
if (!match)
|
|
125
|
-
continue;
|
|
126
|
-
if (!sameShape(shapeKeys(call), match.keys))
|
|
127
|
-
continue;
|
|
128
|
-
if (!substitutable(match, {
|
|
129
|
-
fields: requestFields(content, call, start),
|
|
130
|
-
schemaChecked: hasSchemaCheck(content, line, line + SCHEMA_SCAN_LINES),
|
|
131
|
-
}))
|
|
132
|
-
continue;
|
|
133
|
-
out.push({ file, line, method, path: p, helper: match.helper });
|
|
134
|
-
}
|
|
207
|
+
const p = pm[1];
|
|
208
|
+
const owner = localHelpers.find((h) => line >= h.line && line <= h.endLine);
|
|
209
|
+
out.push({
|
|
210
|
+
line,
|
|
211
|
+
start,
|
|
212
|
+
call,
|
|
213
|
+
method,
|
|
214
|
+
path: p,
|
|
215
|
+
key: `${method} ${normalizePath(p)}`,
|
|
216
|
+
localHelper: owner?.name,
|
|
217
|
+
});
|
|
135
218
|
}
|
|
136
219
|
return out;
|
|
137
220
|
}
|
|
@@ -147,6 +230,10 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
147
230
|
* `dataOverride` are liftable *keywords*, so their mere presence matched; their
|
|
148
231
|
* contents never got compared. (ghost `pr-scheduled-publishing`: a scheduled-post
|
|
149
232
|
* helper offered for a draft-post call, declined for exactly this reason.)
|
|
233
|
+
* - **Query keys the site does not send.** The same as fields, for the query: a
|
|
234
|
+
* helper sending `?page=` offered for a site sending `?status=` adds a parameter.
|
|
235
|
+
* Query keys were in neither the shape (quoted keys never matched) nor the fields
|
|
236
|
+
* (`query_params` is stripped there on purpose), so nothing told the two apart.
|
|
150
237
|
* - **A schema-checked site.** When the site feeds its response to `check_schema`
|
|
151
238
|
* against a per-file expected body and the helper wraps no such check, replacing
|
|
152
239
|
* the block either drops the contract assertion or redesigns the file. That is a
|
|
@@ -160,6 +247,11 @@ function substitutable(helper, site) {
|
|
|
160
247
|
for (const f of helper.fields)
|
|
161
248
|
if (!site.fields.has(f))
|
|
162
249
|
return false;
|
|
250
|
+
// The body rule, applied to the query: a `?page=` helper offered for a `?status=`
|
|
251
|
+
// site would add a parameter to the sibling's request. See queryKeys().
|
|
252
|
+
for (const q of helper.query)
|
|
253
|
+
if (!site.query.has(q))
|
|
254
|
+
return false;
|
|
163
255
|
return helper.schemaChecked === site.schemaChecked;
|
|
164
256
|
}
|
|
165
257
|
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
@@ -208,9 +300,18 @@ export function requestFields(content, call, callAt) {
|
|
|
208
300
|
// TS shorthand inside an override object — `dataOverride: { status, title }` names
|
|
209
301
|
// the same fields as `{ "status": status }` but carries neither quotes nor a colon,
|
|
210
302
|
// so the pattern above cannot see it and the field would compare as absent.
|
|
211
|
-
for (const region of
|
|
212
|
-
for (const m of region.matchAll(
|
|
303
|
+
for (const region of keyedObjects(body, OVERRIDE_OBJECT_RE))
|
|
304
|
+
for (const m of region.matchAll(SHORTHAND_KEY_RE))
|
|
213
305
|
add(m[1]);
|
|
306
|
+
// An inline TS body literal — `body: { title: t, status: "draft", tags }` — carries
|
|
307
|
+
// UNQUOTED keys (and shorthand), which the quoted pattern above cannot see either.
|
|
308
|
+
// With no fields read, `substitutable` had nothing to compare and offered a helper
|
|
309
|
+
// whose substitution would ADD a field to the sibling's request — the very case it
|
|
310
|
+
// exists to refuse. Python passes `body=IDENT` (read below) or a quoted dict, so
|
|
311
|
+
// nothing changes there.
|
|
312
|
+
for (const region of keyedObjects(body, BODY_OBJECT_RE))
|
|
313
|
+
for (const k of literalKeys(region))
|
|
314
|
+
add(k);
|
|
214
315
|
// `body=IDENT` / `body: IDENT` → the identifier's assignment elsewhere in the file.
|
|
215
316
|
const ref = /\bbody\s*[=:]\s*([A-Za-z_$][\w$]*)/.exec(call);
|
|
216
317
|
if (ref) {
|
|
@@ -253,26 +354,108 @@ export function requestFields(content, call, callAt) {
|
|
|
253
354
|
}
|
|
254
355
|
return out;
|
|
255
356
|
}
|
|
256
|
-
|
|
257
|
-
|
|
357
|
+
const OVERRIDE_OBJECT_RE = /\b(?:data_override|dataOverride)\s*[=:]\s*\{/g;
|
|
358
|
+
const BODY_OBJECT_RE = /\b(?:body|data)\s*[=:]\s*\{/g;
|
|
359
|
+
const QUERY_OBJECT_RE = /\b(?:query_params|queryParams)\s*[=:]\s*\{/g;
|
|
360
|
+
/** A bare identifier between `{`/`,` and `,`/`}` — TS object shorthand. */
|
|
361
|
+
const SHORTHAND_KEY_RE = /(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?=[,}])/g;
|
|
362
|
+
/** One quoted string, any of the three quote kinds, escapes honoured. */
|
|
363
|
+
const STRING_LITERAL_RE = /(["'`])(?:\\.|(?!\1)[^\\])*\1/g;
|
|
364
|
+
/** The key names written in one `{…}` object literal: quoted (`"page":`, either
|
|
365
|
+
* language), unquoted (`page:`, TS) and shorthand (`page,`, TS). The unquoted and
|
|
366
|
+
* shorthand passes run with string CONTENTS blanked, so `q: "a, b: c"` names one
|
|
367
|
+
* key, not two. Nested keys are included; callers decide leaf semantics. */
|
|
368
|
+
function literalKeys(region) {
|
|
369
|
+
const out = [];
|
|
370
|
+
for (const m of region.matchAll(/["'`]([A-Za-z_][\w.]*)["'`]\s*:/g))
|
|
371
|
+
out.push(m[1]);
|
|
372
|
+
const code = blankStringContents(region);
|
|
373
|
+
for (const m of code.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*:(?!=)/g))
|
|
374
|
+
out.push(m[1]);
|
|
375
|
+
for (const m of code.matchAll(SHORTHAND_KEY_RE))
|
|
376
|
+
out.push(m[1]);
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The query-parameter NAMES a request call sends (`query_params={"page": 1}` /
|
|
381
|
+
* `queryParams: { page }`), or the empty set. Kept apart from requestFields, which
|
|
382
|
+
* strips `query_params` on purpose because it is shared with the retrofit gate and a
|
|
383
|
+
* query key is not a body field; here the question is the advisory's own — whether a
|
|
384
|
+
* helper can stand in for a site without ADDING a query parameter — and the rule is
|
|
385
|
+
* the body rule: every helper key must be present at the site.
|
|
386
|
+
*/
|
|
387
|
+
export function queryKeys(call) {
|
|
388
|
+
const out = new Set();
|
|
389
|
+
for (const region of keyedObjects(call, QUERY_OBJECT_RE))
|
|
390
|
+
for (const k of literalKeys(region))
|
|
391
|
+
out.add(k);
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
/** The `{…}` text of each argument in a call whose opener `keyRe` matches (a
|
|
395
|
+
* `KEY: {` / `KEY={` pattern ending at the brace). */
|
|
396
|
+
function keyedObjects(call, keyRe) {
|
|
397
|
+
// Boundaries come from the string-blanked copy (offsets preserved): a `{` inside
|
|
398
|
+
// a value (`q: "{"`) is payload, and counting it would run the object into the
|
|
399
|
+
// next argument. The text itself is taken from the original.
|
|
400
|
+
const scan = blankStringContents(call);
|
|
258
401
|
const out = [];
|
|
259
|
-
for (const m of
|
|
402
|
+
for (const m of scan.matchAll(keyRe)) {
|
|
260
403
|
const open = (m.index ?? 0) + m[0].length - 1;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
404
|
+
out.push(call.slice(open, braceEnd(scan, open)));
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
/** `text` with the CONTENTS of every string literal replaced by spaces, quotes kept,
|
|
409
|
+
* length preserved — so offsets found on the copy index into the original. */
|
|
410
|
+
function blankStringContents(text) {
|
|
411
|
+
return text.replace(STRING_LITERAL_RE, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0]);
|
|
412
|
+
}
|
|
413
|
+
/** The call shape both sibling scans compare: shapeKeys of the call with the interior
|
|
414
|
+
* of every keyed `{…}` value blanked, so the keys INSIDE a body, path-params or
|
|
415
|
+
* query-params object never read as shape. shapeKeys sees every `name:` in the text,
|
|
416
|
+
* and an unquoted TS object key (`body: { discountPercent: 10 }`) is one: a helper
|
|
417
|
+
* inlining `body: { title }` never matched a site whose body added a field, and two
|
|
418
|
+
* copies whose bodies named different fields were two operations — in both scans the
|
|
419
|
+
* fields are the literals to lift, and `substitutable` is what judges them. Python
|
|
420
|
+
* quoted dict keys (`{"order_id": id}`) never matched the identifier pattern, so
|
|
421
|
+
* this is a TS/JS-only change. */
|
|
422
|
+
export function callShape(call) {
|
|
423
|
+
return shapeKeys(withoutKeyedLiterals(call));
|
|
424
|
+
}
|
|
425
|
+
/** `call` with the interior of every keyed `{…}` value blanked, offsets kept. */
|
|
426
|
+
function withoutKeyedLiterals(call) {
|
|
427
|
+
// Balanced on the string-blanked copy (see keyedObjects); the copy and the output
|
|
428
|
+
// are the same length, so one offset serves both.
|
|
429
|
+
let scan = blankStringContents(call);
|
|
430
|
+
let out = call;
|
|
431
|
+
const re = /\b[A-Za-z_$][\w$]*\s*[=:]\s*\{/g;
|
|
432
|
+
let m;
|
|
433
|
+
while ((m = re.exec(scan)) !== null) {
|
|
434
|
+
const open = m.index + m[0].length - 1;
|
|
435
|
+
const end = braceEnd(scan, open);
|
|
436
|
+
const blank = (t) => t.slice(0, open + 1) +
|
|
437
|
+
" ".repeat(Math.max(0, end - open - 2)) +
|
|
438
|
+
t.slice(Math.max(open + 1, end - 1));
|
|
439
|
+
out = blank(out);
|
|
440
|
+
scan = blank(scan);
|
|
441
|
+
re.lastIndex = end;
|
|
273
442
|
}
|
|
274
443
|
return out;
|
|
275
444
|
}
|
|
445
|
+
/** Index just past the `}` balancing the `{` at `open` (the text's end if unbalanced). */
|
|
446
|
+
function braceEnd(text, open) {
|
|
447
|
+
let depth = 0;
|
|
448
|
+
for (let i = open; i < text.length; i++) {
|
|
449
|
+
if (text[i] === "{")
|
|
450
|
+
depth++;
|
|
451
|
+
else if (text[i] === "}") {
|
|
452
|
+
depth--;
|
|
453
|
+
if (depth === 0)
|
|
454
|
+
return i + 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return text.length;
|
|
458
|
+
}
|
|
276
459
|
/** Keywords whose presence differs legitimately between two versions of one request:
|
|
277
460
|
* the literals the merge rule lifts to parameters. Everything else is call shape. */
|
|
278
461
|
const LIFTABLE_KEYS = new Set([
|
|
@@ -305,16 +488,19 @@ function sameShape(a, b) {
|
|
|
305
488
|
}
|
|
306
489
|
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
307
490
|
export function callText(content, open) {
|
|
491
|
+
return content.slice(open, callEnd(content, open));
|
|
492
|
+
}
|
|
493
|
+
/** Index just past the `)` balancing the `(` at `open` (the text's end if unbalanced). */
|
|
494
|
+
function callEnd(text, open) {
|
|
308
495
|
let depth = 0;
|
|
309
|
-
for (let i = open; i <
|
|
310
|
-
|
|
311
|
-
if (ch === "(")
|
|
496
|
+
for (let i = open; i < text.length; i++) {
|
|
497
|
+
if (text[i] === "(")
|
|
312
498
|
depth++;
|
|
313
|
-
else if (
|
|
499
|
+
else if (text[i] === ")") {
|
|
314
500
|
depth--;
|
|
315
501
|
if (depth === 0)
|
|
316
|
-
return
|
|
502
|
+
return i + 1;
|
|
317
503
|
}
|
|
318
504
|
}
|
|
319
|
-
return
|
|
505
|
+
return text.length;
|
|
320
506
|
}
|
|
@@ -46,8 +46,9 @@ export declare const UTILS_LANGUAGE_SPECS: Record<UtilsLanguage, UtilsLanguageSp
|
|
|
46
46
|
* split the modularization and reuse prompts make (API_HELPERS / BROWSER_HELPERS):
|
|
47
47
|
* - `api` (integration, contract, load …): one SDK request; equivalence is
|
|
48
48
|
* method+path; the only assertion is the status code.
|
|
49
|
-
* - `browser` (ui, e2e): a Playwright action sequence;
|
|
50
|
-
*
|
|
49
|
+
* - `browser` (ui, e2e): a Playwright action sequence; equivalence is the action
|
|
50
|
+
* sequence itself (verb + selector per step, see actionKeyOf) and is advised,
|
|
51
|
+
* not enforced; the only assertions are structural waits.
|
|
51
52
|
*/
|
|
52
53
|
export type HelperFamily = "api" | "browser";
|
|
53
54
|
export interface HelperFamilySpec {
|
|
@@ -25,10 +25,26 @@ export interface UtilsHelper {
|
|
|
25
25
|
/** TypeScript only: signature parameters with no type annotation (`page`, not
|
|
26
26
|
* `page: Page`). Empty for other languages. */
|
|
27
27
|
untypedParams: string[];
|
|
28
|
+
/** Declared parameter count — defaulted and rest parameters included. Two
|
|
29
|
+
* definitions of one name with different arities are the collision a caller
|
|
30
|
+
* written against either signature hits. `undefined` when no signature could
|
|
31
|
+
* be read. */
|
|
32
|
+
arity?: number;
|
|
33
|
+
/** The helper's Playwright action sequence — the duplicate key of a browser
|
|
34
|
+
* helper, where an API helper has method+path. See actionKeyOf. Read for
|
|
35
|
+
* TypeScript, JavaScript and Python (through the JavaScript spelling); absent
|
|
36
|
+
* for a helper with no readable action. */
|
|
37
|
+
actionKey?: string;
|
|
28
38
|
}
|
|
29
|
-
/** The
|
|
30
|
-
* from the definition line to its
|
|
31
|
-
* first balanced `(...)`, and
|
|
39
|
+
/** The declared parameters of a signature, raw text trimmed, empty entries dropped
|
|
40
|
+
* (a trailing comma). Reads the signature from the definition line to its body
|
|
41
|
+
* opener (Prettier wraps long signatures), takes the first balanced `(...)`, and
|
|
42
|
+
* splits it on top-level commas — so a comma inside a default value, a generic, or
|
|
43
|
+
* a binding pattern never splits. `undefined` when the text holds no readable
|
|
44
|
+
* parameter list. Language-neutral: both a TS `{` and a Python `:` terminator lie
|
|
45
|
+
* outside the parentheses this reads. */
|
|
46
|
+
export declare function signatureParams(sigLines: string[]): string[] | undefined;
|
|
47
|
+
/** The parameter names in a TS signature that carry no `: type`. */
|
|
32
48
|
export declare function untypedParamsOf(sigLines: string[]): string[];
|
|
33
49
|
/**
|
|
34
50
|
* Structured extraction of the module-level helpers in a utils file.
|
|
@@ -41,6 +57,9 @@ export declare function untypedParamsOf(sigLines: string[]): string[];
|
|
|
41
57
|
* the definition returns to zero.
|
|
42
58
|
*/
|
|
43
59
|
export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec): UtilsHelper[];
|
|
60
|
+
/** True when the `path` value is a single string literal ending the argument — a
|
|
61
|
+
* concatenation (`"/api/orders/" + str(id) + "/items"`) has no route this can name. */
|
|
62
|
+
export declare function isSingleLiteral(line: string, m: RegExpExecArray): boolean;
|
|
44
63
|
/** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
|
|
45
64
|
* comments are `#…`; TS/JS go through the shared stripper. */
|
|
46
65
|
/** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
|