@skyramp/mcp 0.3.6 → 0.3.7
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/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 +81 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/ReuseOutcome.d.ts +16 -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/reportVerification.d.ts +4 -0
- package/build/utils/reportVerification.js +32 -4
- 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
|
@@ -51,6 +51,26 @@ export async function listChangedFilesAcross(roots) {
|
|
|
51
51
|
}
|
|
52
52
|
return files;
|
|
53
53
|
}
|
|
54
|
+
/** Like listChangedFilesAcross, but ABSOLUTE: each changed path resolved against
|
|
55
|
+
* the root it came from, so a consumer never has to guess which checkout a
|
|
56
|
+
* repo-relative path belongs to. */
|
|
57
|
+
export async function listChangedFilesAbs(roots) {
|
|
58
|
+
const seenRoots = new Set();
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const root of roots) {
|
|
61
|
+
if (!root || seenRoots.has(root))
|
|
62
|
+
continue;
|
|
63
|
+
seenRoots.add(root);
|
|
64
|
+
try {
|
|
65
|
+
for (const f of await listChangedFiles(root))
|
|
66
|
+
files.push(path.resolve(root, f));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Non-git or missing tree — nothing to contribute.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
54
74
|
export async function listChangedFiles(repoRoot) {
|
|
55
75
|
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot });
|
|
56
76
|
const files = [];
|
|
@@ -273,7 +293,9 @@ export async function findInvalidSourceCitations(input) {
|
|
|
273
293
|
continue; // Could not look — accept.
|
|
274
294
|
const suggestions = await suggestSimilarPaths(roots, cited);
|
|
275
295
|
invalid.push(`issuesFound[${i}] cites ${cited} but that file does not exist in the repository` +
|
|
276
|
-
(suggestions.length > 0
|
|
296
|
+
(suggestions.length > 0
|
|
297
|
+
? ` (closest matches: ${suggestions.join(", ")})`
|
|
298
|
+
: "") +
|
|
277
299
|
`. ${CITATION_REMEDY}`);
|
|
278
300
|
continue;
|
|
279
301
|
}
|
|
@@ -322,7 +344,9 @@ export async function findInvalidSourceCitations(input) {
|
|
|
322
344
|
* A checkout the run could not name contributes nothing: it can be neither
|
|
323
345
|
* stamped nor offered as a candidate. */
|
|
324
346
|
function namesOf(resolved) {
|
|
325
|
-
return [
|
|
347
|
+
return [
|
|
348
|
+
...new Set(resolved.map((c) => c.repository).filter((r) => !!r)),
|
|
349
|
+
];
|
|
326
350
|
}
|
|
327
351
|
/**
|
|
328
352
|
* Reconcile where a citation resolved with what the entry says about it, giving
|
|
@@ -339,7 +363,9 @@ function attributeCitation(args) {
|
|
|
339
363
|
// than sent back for a rewrite that would change nothing.
|
|
340
364
|
const match = resolved.find((c) => c.repository && c.repository.toLowerCase() === declared.toLowerCase());
|
|
341
365
|
if (match)
|
|
342
|
-
return match.repository === declared
|
|
366
|
+
return match.repository === declared
|
|
367
|
+
? {}
|
|
368
|
+
: { repository: match.repository };
|
|
343
369
|
// It resolved somewhere else. Only say so when the run can name where —
|
|
344
370
|
// otherwise there is nothing to correct the attribution to.
|
|
345
371
|
if (names.length === 0)
|
|
@@ -362,5 +388,7 @@ function attributeCitation(args) {
|
|
|
362
388
|
// Exactly one checkout. Stamp it unless it is the primary, whose findings are
|
|
363
389
|
// identified downstream by having NO repository — see SourceCitationCheck.
|
|
364
390
|
const [only] = resolved;
|
|
365
|
-
return only.primary || !only.repository
|
|
391
|
+
return only.primary || !only.repository
|
|
392
|
+
? {}
|
|
393
|
+
: { repository: only.repository };
|
|
366
394
|
}
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
2
|
-
export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name"
|
|
3
|
-
|
|
2
|
+
export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenario-name"
|
|
3
|
+
/** The delivered spec (plus the helpers it imports) holds fewer assertions than at
|
|
4
|
+
* the modularization hand-out. Marker-eligible: a deliberate consolidation is
|
|
5
|
+
* legitimate once documented. */
|
|
6
|
+
| "assertion-loss"
|
|
7
|
+
/** A page-error/console guard present at hand-out is gone. Not marker-eligible. */
|
|
8
|
+
| "guard-removed"
|
|
9
|
+
/** Python only: the spec imports the shared module in a form that cannot resolve
|
|
10
|
+
* in the customer's repo. Not marker-eligible — a broken import has no reason to
|
|
11
|
+
* stay. */
|
|
12
|
+
| "import-form";
|
|
13
|
+
/** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
|
|
14
|
+
* meaning "a kind the allow grammar and the gate know about" — an advisory can
|
|
15
|
+
* never fail a verify and never needs (or accepts) an allow marker. */
|
|
16
|
+
export type UtilsAdvisoryKind = "untyped-parameter";
|
|
17
|
+
export declare const MARKERABLE_UTILS_KINDS: readonly ["duplicate-helper", "body-assertion", "scenario-name", "assertion-loss"];
|
|
18
|
+
/** A kind the allow grammar accepts. `guard-removed` is a gate kind but never
|
|
19
|
+
* markerable — a guard is restored, not documented away — so the grammar itself
|
|
20
|
+
* cannot spell it; the regex below is built from this list. */
|
|
21
|
+
export type MarkerableUtilsKind = (typeof MARKERABLE_UTILS_KINDS)[number];
|
|
4
22
|
export interface UtilsAllow {
|
|
5
|
-
kind:
|
|
23
|
+
kind: MarkerableUtilsKind;
|
|
6
24
|
helper: string;
|
|
7
25
|
reason: string;
|
|
8
26
|
}
|
|
9
27
|
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
10
|
-
export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind:
|
|
28
|
+
export declare function allowMarkerLine(spec: UtilsLanguageSpec, kind: MarkerableUtilsKind, helper: string): string;
|
|
11
29
|
export declare function parseUtilsAllows(content: string, spec: UtilsLanguageSpec): {
|
|
12
30
|
allows: UtilsAllow[];
|
|
13
31
|
/** Marker lines the loose pattern recognises but the grammar cannot read — a decline
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { blankStrings } from "./parse.js";
|
|
2
|
-
export const
|
|
2
|
+
export const MARKERABLE_UTILS_KINDS = [
|
|
3
3
|
"duplicate-helper",
|
|
4
4
|
"body-assertion",
|
|
5
5
|
"scenario-name",
|
|
6
|
+
"assertion-loss",
|
|
6
7
|
];
|
|
7
8
|
/**
|
|
8
9
|
* The documented-decline marker for a utils-file invariant, written in the utils file:
|
|
@@ -21,7 +22,7 @@ export const UTILS_VIOLATION_KINDS = [
|
|
|
21
22
|
* paste-ready line from the failure text when it was pasted unedited: publishing the
|
|
22
23
|
* placeholder as the stated reason is worse than no marker.
|
|
23
24
|
*/
|
|
24
|
-
const ALLOW_RE =
|
|
25
|
+
const ALLOW_RE = new RegExp(`reuse-verify:\\s*allow\\s+(${MARKERABLE_UTILS_KINDS.join("|")})\\s+([A-Za-z_$][\\w$]*)\\s*(?:[—–]+|\\s+-+\\s+)\\s*(?!<reason)(\\S.*?)\\s*$`);
|
|
25
26
|
const ALLOW_LOOSE_RE = /reuse-verify:\s*allow\b/;
|
|
26
27
|
/** The exact line to paste for a violation — what the failure text hands the agent. */
|
|
27
28
|
export function allowMarkerLine(spec, kind, helper) {
|
|
@@ -55,6 +56,11 @@ export function parseUtilsAllows(content, spec) {
|
|
|
55
56
|
.trim();
|
|
56
57
|
const m = ALLOW_RE.exec(line);
|
|
57
58
|
if (!m) {
|
|
59
|
+
// A marker naming an advisory kind is inert, not malformed: advisories never
|
|
60
|
+
// block, so "documenting" one must not fail the gate either (the agent sees
|
|
61
|
+
// the kind in a kind-shaped slot and reasonably tries).
|
|
62
|
+
if (/allow\s+untyped-parameter\b/.test(code))
|
|
63
|
+
continue;
|
|
58
64
|
malformed.push(raw.trim());
|
|
59
65
|
continue;
|
|
60
66
|
}
|
|
@@ -4,6 +4,23 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
|
4
4
|
* Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
|
|
5
5
|
* The same discriminator STEP 4 of the reuse prompt hands the agent. */
|
|
6
6
|
export declare const CODEGEN_MARKER_RE: RegExp;
|
|
7
|
+
/** One Skyramp-generated test beside the spec, read once. */
|
|
8
|
+
export interface SiblingFile {
|
|
9
|
+
/** Absolute path. */
|
|
10
|
+
file: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
|
+
* walk, shared by the inline-call-site scan (which excludes the spec itself) and the
|
|
16
|
+
* importer count (which includes it). Utils files carry no codegen version marker,
|
|
17
|
+
* so the marker check alone excludes them. Fails open to [] on any read error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generatedSiblings(testFile: string, spec: UtilsLanguageSpec, opts: {
|
|
20
|
+
includeSelf: boolean;
|
|
21
|
+
}): Promise<SiblingFile[]>;
|
|
22
|
+
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
23
|
+
export declare const REQUEST_CALL_RE: RegExp;
|
|
7
24
|
export interface InlineCallSite {
|
|
8
25
|
/** Absolute path of the sibling test. */
|
|
9
26
|
file: string;
|
|
@@ -28,7 +45,29 @@ export interface InlineCallSite {
|
|
|
28
45
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
29
46
|
* flow owns, so it informs rather than gates. Never throws.
|
|
30
47
|
*/
|
|
31
|
-
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec): Promise<InlineCallSite[]>;
|
|
48
|
+
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<InlineCallSite[]>;
|
|
49
|
+
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
50
|
+
* immediately after the status-code assertion. */
|
|
51
|
+
export declare const SCHEMA_SCAN_LINES = 12;
|
|
52
|
+
/** True when a `check_schema` / `checkSchema` assertion sits in the given 1-based line
|
|
53
|
+
* range — the response-shape contract a status-only helper does not carry. */
|
|
54
|
+
export declare function hasSchemaCheck(content: string, from: number, to: number): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The request's body/override FIELD names — leaf names only, so a dotted override key
|
|
57
|
+
* (`posts.0.publish_at`) and the body literal it targets (`"publish_at":`) are one
|
|
58
|
+
* field. Reads the body through its variable: generated code assigns the template to
|
|
59
|
+
* an identifier and passes `body=<ident>`, so the fields live in the assignment, not
|
|
60
|
+
* in the call.
|
|
61
|
+
*
|
|
62
|
+
* `callAt` is the call's offset in `content`. The body variable resolves to the
|
|
63
|
+
* assignment NEAREST BEFORE it, because generated code reuses the name `body` per
|
|
64
|
+
* request: a file-wide first match gives every later request the first one's fields,
|
|
65
|
+
* so two different payloads compare equal. Omit it only when `content` holds a single
|
|
66
|
+
* request.
|
|
67
|
+
*/
|
|
68
|
+
export declare function requestFields(content: string, call: string, callAt?: number): Set<string>;
|
|
32
69
|
/** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
|
|
33
70
|
* object keys), less the liftable ones — a cheap normalised call shape. */
|
|
34
71
|
export declare function shapeKeys(call: string): Set<string>;
|
|
72
|
+
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
73
|
+
export declare function callText(content: string, open: number): string;
|
|
@@ -4,12 +4,52 @@ import { SKYRAMP_UTILS_HEADER } from "../utils.js";
|
|
|
4
4
|
import { parseUtilsFile } from "./parse.js";
|
|
5
5
|
import { normalizePath } from "./language-spec.js";
|
|
6
6
|
import { realpath } from "./locate.js";
|
|
7
|
+
import { escapeRegExp } from "../regex.js";
|
|
7
8
|
/** The codegen marker every Skyramp-GENERATED TEST carries on line 1 (`Generated by
|
|
8
9
|
* Skyramp v<version>`), as opposed to the utils header (`Generated by Skyramp on …`).
|
|
9
10
|
* The same discriminator STEP 4 of the reuse prompt hands the agent. */
|
|
10
11
|
export const CODEGEN_MARKER_RE = new RegExp(`${SKYRAMP_UTILS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+v\\d`);
|
|
11
12
|
const HEADER_SCAN_LINES = 5;
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The Skyramp-generated tests in the spec's directory, each read once — THE sibling
|
|
15
|
+
* walk, shared by the inline-call-site scan (which excludes the spec itself) and the
|
|
16
|
+
* importer count (which includes it). Utils files carry no codegen version marker,
|
|
17
|
+
* so the marker check alone excludes them. Fails open to [] on any read error.
|
|
18
|
+
*/
|
|
19
|
+
export async function generatedSiblings(testFile, spec, opts) {
|
|
20
|
+
let dir;
|
|
21
|
+
let entries;
|
|
22
|
+
let self;
|
|
23
|
+
try {
|
|
24
|
+
self = await realpath(testFile);
|
|
25
|
+
dir = path.dirname(self);
|
|
26
|
+
entries = await fs.promises.readdir(dir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const name of entries.sort()) {
|
|
33
|
+
const file = path.join(dir, name);
|
|
34
|
+
if (!spec.extensions.some((e) => name.toLowerCase().endsWith(e)))
|
|
35
|
+
continue;
|
|
36
|
+
if (!opts.includeSelf && path.resolve(file) === self)
|
|
37
|
+
continue;
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = await fs.promises.readFile(file, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
|
|
46
|
+
continue;
|
|
47
|
+
out.push({ file, content });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
52
|
+
export const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
13
53
|
/**
|
|
14
54
|
* Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
|
|
15
55
|
* utils helper already wraps — the reuse the prompt asks for and the agent does not
|
|
@@ -24,10 +64,12 @@ const REQUEST_CALL_RE = /\b(?:send_request|sendRequest)\s*\(/g;
|
|
|
24
64
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
25
65
|
* flow owns, so it informs rather than gates. Never throws.
|
|
26
66
|
*/
|
|
27
|
-
export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec) {
|
|
67
|
+
export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers, spec, preReadSiblings) {
|
|
28
68
|
// route → helper name + the keyword set of the helper's own request call, so a
|
|
29
69
|
// sibling call is reported only when its SHAPE matches too (same keywords once the
|
|
30
70
|
// liftable ones are set aside) — criterion 2 of the API near-duplicate definition.
|
|
71
|
+
// `fields` and `schemaChecked` carry the two things keyword shape cannot see; see
|
|
72
|
+
// substitutable().
|
|
31
73
|
const byRoute = new Map();
|
|
32
74
|
for (const utilsFile of utilsFiles) {
|
|
33
75
|
let content;
|
|
@@ -50,39 +92,20 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
50
92
|
byRoute.set(`${owner.method} ${owner.normalizedPath}`, {
|
|
51
93
|
helper: owner.name,
|
|
52
94
|
keys: shapeKeys(call),
|
|
95
|
+
fields: requestFields(content, call, start),
|
|
96
|
+
schemaChecked: hasSchemaCheck(content, owner.line, owner.endLine),
|
|
53
97
|
});
|
|
54
98
|
}
|
|
55
99
|
}
|
|
56
100
|
if (byRoute.size === 0)
|
|
57
101
|
return [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// own sibling.
|
|
63
|
-
const exclude = new Set(await Promise.all([testFile, ...utilsFiles].map((f) => realpath(f))));
|
|
64
|
-
let entries;
|
|
65
|
-
try {
|
|
66
|
-
entries = await fs.promises.readdir(dir);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
102
|
+
const utilsSet = new Set(await Promise.all(utilsFiles.map((f) => realpath(f))));
|
|
103
|
+
const siblings = (preReadSiblings ??
|
|
104
|
+
(await generatedSiblings(testFile, spec, { includeSelf: false }))).filter((sib) => !utilsSet.has(path.resolve(sib.file)));
|
|
105
|
+
const self = await realpath(testFile);
|
|
71
106
|
const out = [];
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
-
if (exclude.has(path.resolve(file)))
|
|
75
|
-
continue;
|
|
76
|
-
if (!spec.extensions.some((e) => name.toLowerCase().endsWith(e)))
|
|
77
|
-
continue;
|
|
78
|
-
let content;
|
|
79
|
-
try {
|
|
80
|
-
content = await fs.promises.readFile(file, "utf8");
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (!CODEGEN_MARKER_RE.test(content.split("\n").slice(0, HEADER_SCAN_LINES).join("\n")))
|
|
107
|
+
for (const { file, content } of siblings) {
|
|
108
|
+
if (path.resolve(file) === self)
|
|
86
109
|
continue;
|
|
87
110
|
// Line ranges of the sibling's own non-test helpers: calls inside them are not inline.
|
|
88
111
|
const localHelpers = parseUtilsFile(content, spec).filter((h) => !/^test/i.test(h.name));
|
|
@@ -102,11 +125,154 @@ export async function findSiblingInlineCallSites(testFile, utilsFiles, _helpers,
|
|
|
102
125
|
continue;
|
|
103
126
|
if (!sameShape(shapeKeys(call), match.keys))
|
|
104
127
|
continue;
|
|
128
|
+
if (!substitutable(match, {
|
|
129
|
+
fields: requestFields(content, call, start),
|
|
130
|
+
schemaChecked: hasSchemaCheck(content, line, line + SCHEMA_SCAN_LINES),
|
|
131
|
+
}))
|
|
132
|
+
continue;
|
|
105
133
|
out.push({ file, line, method, path: p, helper: match.helper });
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
136
|
return out;
|
|
109
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether the helper can stand in for this call site with LITERALS as the only
|
|
140
|
+
* difference — the condition the advisory itself states. Keyword shape (`sameShape`)
|
|
141
|
+
* cannot see either of these, and both were measured naming sites an agent then
|
|
142
|
+
* correctly declined, which trains it to ignore the advisory:
|
|
143
|
+
*
|
|
144
|
+
* - **Fields the site does not carry.** A helper whose body/override sets
|
|
145
|
+
* `publish_at` cannot replace a POST whose body has only `status` and `title`:
|
|
146
|
+
* substituting ADDS a field, changing the sibling's request. `body` and
|
|
147
|
+
* `dataOverride` are liftable *keywords*, so their mere presence matched; their
|
|
148
|
+
* contents never got compared. (ghost `pr-scheduled-publishing`: a scheduled-post
|
|
149
|
+
* helper offered for a draft-post call, declined for exactly this reason.)
|
|
150
|
+
* - **A schema-checked site.** When the site feeds its response to `check_schema`
|
|
151
|
+
* against a per-file expected body and the helper wraps no such check, replacing
|
|
152
|
+
* the block either drops the contract assertion or redesigns the file. That is a
|
|
153
|
+
* structural difference, not a literal one. (demoshop `P08`: the contract-file
|
|
154
|
+
* sites, declined as "a design change beyond this PR's scope".)
|
|
155
|
+
*
|
|
156
|
+
* Both directions are conservative — they only ever drop a candidate — so precision
|
|
157
|
+
* rises and the advisory cannot start over-reporting because of them.
|
|
158
|
+
*/
|
|
159
|
+
function substitutable(helper, site) {
|
|
160
|
+
for (const f of helper.fields)
|
|
161
|
+
if (!site.fields.has(f))
|
|
162
|
+
return false;
|
|
163
|
+
return helper.schemaChecked === site.schemaChecked;
|
|
164
|
+
}
|
|
165
|
+
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
166
|
+
* immediately after the status-code assertion. */
|
|
167
|
+
export const SCHEMA_SCAN_LINES = 12;
|
|
168
|
+
/** True when a `check_schema` / `checkSchema` assertion sits in the given 1-based line
|
|
169
|
+
* range — the response-shape contract a status-only helper does not carry. */
|
|
170
|
+
export function hasSchemaCheck(content, from, to) {
|
|
171
|
+
const lines = content.split("\n").slice(Math.max(0, from - 1), to);
|
|
172
|
+
return /\bcheck_?[Ss]chema\s*\(/.test(lines.join("\n"));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The request's body/override FIELD names — leaf names only, so a dotted override key
|
|
176
|
+
* (`posts.0.publish_at`) and the body literal it targets (`"publish_at":`) are one
|
|
177
|
+
* field. Reads the body through its variable: generated code assigns the template to
|
|
178
|
+
* an identifier and passes `body=<ident>`, so the fields live in the assignment, not
|
|
179
|
+
* in the call.
|
|
180
|
+
*
|
|
181
|
+
* `callAt` is the call's offset in `content`. The body variable resolves to the
|
|
182
|
+
* assignment NEAREST BEFORE it, because generated code reuses the name `body` per
|
|
183
|
+
* request: a file-wide first match gives every later request the first one's fields,
|
|
184
|
+
* so two different payloads compare equal. Omit it only when `content` holds a single
|
|
185
|
+
* request.
|
|
186
|
+
*/
|
|
187
|
+
export function requestFields(content, call, callAt) {
|
|
188
|
+
const out = new Set();
|
|
189
|
+
const add = (raw) => {
|
|
190
|
+
const leaf = raw.split(".").pop() ?? "";
|
|
191
|
+
// Array indices are positions, not fields.
|
|
192
|
+
if (leaf && !/^\d+$/.test(leaf))
|
|
193
|
+
out.add(leaf);
|
|
194
|
+
};
|
|
195
|
+
// FIELDS COME FROM THE PAYLOAD ONLY. Every other liftable argument carries quoted
|
|
196
|
+
// keys of its own that are not body fields: `path_params={"orders": id}` names a
|
|
197
|
+
// placeholder (the same `{}` in the same normalised path), `headers={"Authorization":
|
|
198
|
+
// t}` names a header, `query_params={"page": 1}` a query key. Reading any of them as
|
|
199
|
+
// a field reports a correct retrofit as changing the request — measured on a demoshop
|
|
200
|
+
// DELETE whose helper labelled the path parameter `orders` where the baseline said
|
|
201
|
+
// `order_id`. shapeKeys already ignores these keywords; ignoring the keyword while
|
|
202
|
+
// reading its contents was the incoherence. `body` and `data_override` stay, because
|
|
203
|
+
// their keys ARE payload paths.
|
|
204
|
+
const body = call.replace(/\b(?:path_params|pathParams|query_params|queryParams|headers|url)\s*[=:]\s*\{[^{}]*\}/g, " ");
|
|
205
|
+
// Dotted override keys, and any body literal written inline in the call.
|
|
206
|
+
for (const m of body.matchAll(/["'`]([A-Za-z_][\w.]*)["'`]\s*:/g))
|
|
207
|
+
add(m[1]);
|
|
208
|
+
// TS shorthand inside an override object — `dataOverride: { status, title }` names
|
|
209
|
+
// the same fields as `{ "status": status }` but carries neither quotes nor a colon,
|
|
210
|
+
// so the pattern above cannot see it and the field would compare as absent.
|
|
211
|
+
for (const region of overrideObjects(body))
|
|
212
|
+
for (const m of region.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?=[,}])/g))
|
|
213
|
+
add(m[1]);
|
|
214
|
+
// `body=IDENT` / `body: IDENT` → the identifier's assignment elsewhere in the file.
|
|
215
|
+
const ref = /\bbody\s*[=:]\s*([A-Za-z_$][\w$]*)/.exec(call);
|
|
216
|
+
if (ref) {
|
|
217
|
+
const assignRe = new RegExp(`\\b${escapeRegExp(ref[1])}\\b\\s*(?::[^=]*)?=`, "g");
|
|
218
|
+
// The last assignment before the call; the first in the file when the offset is
|
|
219
|
+
// unknown or nothing precedes it.
|
|
220
|
+
let assign = null;
|
|
221
|
+
for (const m of content.matchAll(assignRe)) {
|
|
222
|
+
if (callAt !== undefined && (m.index ?? 0) > callAt)
|
|
223
|
+
break;
|
|
224
|
+
assign = m;
|
|
225
|
+
if (callAt === undefined)
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
if (assign) {
|
|
229
|
+
// From the assignment to the end of its braced literal — the template is one
|
|
230
|
+
// string, so the first balanced `{`…`}` after it is the body.
|
|
231
|
+
const tail = content.slice(assign.index + assign[0].length);
|
|
232
|
+
const open = tail.indexOf("{");
|
|
233
|
+
if (open !== -1) {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
let end = tail.length;
|
|
236
|
+
for (let i = open; i < tail.length; i++) {
|
|
237
|
+
if (tail[i] === "{")
|
|
238
|
+
depth++;
|
|
239
|
+
else if (tail[i] === "}") {
|
|
240
|
+
depth--;
|
|
241
|
+
if (depth === 0) {
|
|
242
|
+
end = i + 1;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const m of tail
|
|
248
|
+
.slice(open, end)
|
|
249
|
+
.matchAll(/["'`]([A-Za-z_][\w.]*)["'`]\s*:/g))
|
|
250
|
+
add(m[1]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
/** The `{…}` text of each `data_override` / `dataOverride` argument in a call. */
|
|
257
|
+
function overrideObjects(call) {
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const m of call.matchAll(/\b(?:data_override|dataOverride)\s*[=:]\s*\{/g)) {
|
|
260
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
261
|
+
let depth = 0;
|
|
262
|
+
for (let i = open; i < call.length; i++) {
|
|
263
|
+
if (call[i] === "{")
|
|
264
|
+
depth++;
|
|
265
|
+
else if (call[i] === "}") {
|
|
266
|
+
depth--;
|
|
267
|
+
if (depth === 0) {
|
|
268
|
+
out.push(call.slice(open, i + 1));
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
110
276
|
/** Keywords whose presence differs legitimately between two versions of one request:
|
|
111
277
|
* the literals the merge rule lifts to parameters. Everything else is call shape. */
|
|
112
278
|
const LIFTABLE_KEYS = new Set([
|
|
@@ -138,7 +304,7 @@ function sameShape(a, b) {
|
|
|
138
304
|
return true;
|
|
139
305
|
}
|
|
140
306
|
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
141
|
-
function callText(content, open) {
|
|
307
|
+
export function callText(content, open) {
|
|
142
308
|
let depth = 0;
|
|
143
309
|
for (let i = open; i < content.length; i++) {
|
|
144
310
|
const ch = content[i];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SiblingFile } from "./call-sites.js";
|
|
2
|
+
import type { UtilsHelper } from "./parse.js";
|
|
3
|
+
import type { UtilsLanguageSpec } from "./language-spec.js";
|
|
4
|
+
/** A helper imported by at most one delivered test. */
|
|
5
|
+
export interface SingleImporterHelper {
|
|
6
|
+
/** Absolute path of the utils file. */
|
|
7
|
+
file: string;
|
|
8
|
+
helper: string;
|
|
9
|
+
line: number;
|
|
10
|
+
/** Delivered tests importing it (0 or 1). */
|
|
11
|
+
importers: number;
|
|
12
|
+
}
|
|
13
|
+
/** The strings a sibling's source would contain if it imports the module at
|
|
14
|
+
* `modulePath` (relative to the sibling's directory): the path form for TS/JS
|
|
15
|
+
* specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
|
|
16
|
+
* `shared/X` → `shared.X` / `.shared.X`. */
|
|
17
|
+
export declare function moduleMentionCandidates(modulePath: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Count, per utils helper, how many Skyramp-generated tests in the spec's directory
|
|
20
|
+
* import it — the spec under verification included.
|
|
21
|
+
*
|
|
22
|
+
* Findings:
|
|
23
|
+
* - 0 importers: nothing imports the helper, not even the spec that wrote it — a
|
|
24
|
+
* dead helper, actionable on any run.
|
|
25
|
+
* - 1 importer: not (yet) shared. Reported only when the directory holds at least
|
|
26
|
+
* TWO delivered tests: on a first run every helper has exactly one importer by
|
|
27
|
+
* construction (it seeds the utils file), so listing them would be guaranteed
|
|
28
|
+
* noise on the most common path.
|
|
29
|
+
* Fails open to "no findings" on any read error.
|
|
30
|
+
*/
|
|
31
|
+
export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<SingleImporterHelper[]>;
|
|
@@ -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;
|