@skyramp/mcp 0.4.2 → 0.4.3-rc.1
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/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +91 -29
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.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.BPopbasy.js → uiMode.GLqsx5cI.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/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -7,7 +7,7 @@ function actionSequence(body, language) {
|
|
|
7
7
|
return keys ? { actionKey: keys.join("; "), actionSteps: keys } : {};
|
|
8
8
|
}
|
|
9
9
|
import { fixedSleepsIn, pythonSleepBindings, } from "./fixed-sleep.js";
|
|
10
|
-
import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
10
|
+
import { stringSpans, stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
|
|
11
11
|
/** Whether the `<` at `i` opens a generic (`Record<string, …>`, `Array<{…}>`) rather
|
|
12
12
|
* than comparing (`i < 3`): a generic follows an identifier or `>` and is followed
|
|
13
13
|
* by a type start; a comparison is followed by a number, a space and a value, or
|
|
@@ -266,6 +266,7 @@ export function parseUtilsFile(content, spec) {
|
|
|
266
266
|
const helper = {
|
|
267
267
|
name,
|
|
268
268
|
line: start + 1,
|
|
269
|
+
startLine: decoratorStart(lines, structure, start) + 1,
|
|
269
270
|
endLine: Math.max(end, start + 1),
|
|
270
271
|
wrapsRequest: false,
|
|
271
272
|
hasGuard: hasPageErrorGuard(body.join("\n"), spec),
|
|
@@ -355,69 +356,100 @@ export function isSingleLiteral(line, m) {
|
|
|
355
356
|
after.startsWith(")") ||
|
|
356
357
|
after.startsWith("}"));
|
|
357
358
|
}
|
|
358
|
-
/**
|
|
359
|
-
*
|
|
360
|
-
|
|
361
|
-
export function
|
|
362
|
-
|
|
363
|
-
return stripStrings(src);
|
|
364
|
-
return blankPythonStrings(src);
|
|
359
|
+
/** The string literals in `src`, quotes included, in order. TS/JS go through the
|
|
360
|
+
* shared scanner; Python goes through the one below, so a caller sees one definition
|
|
361
|
+
* of a literal for each language. */
|
|
362
|
+
export function stringSpansOf(src, spec) {
|
|
363
|
+
return spec.language === "python" ? pythonStringSpans(src) : stringSpans(src);
|
|
365
364
|
}
|
|
366
365
|
/**
|
|
367
|
-
*
|
|
366
|
+
* The string literals in Python source, quotes included.
|
|
368
367
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
368
|
+
* Triple quotes are tested BEFORE single ones, and this is the whole reason the scan
|
|
369
|
+
* is not a regex: a `"""` read one quote at a time matches an empty `""` and leaves
|
|
370
|
+
* the payload to be treated as code. A multiline payload's own indentation is then
|
|
371
|
+
* read as formatting, and a change to it disappears.
|
|
372
|
+
*
|
|
373
|
+
* A single-quoted string that never closes before the end of its line is a stray
|
|
374
|
+
* apostrophe in code (`# don't`, or a regex), not an opener — the same rule the TS
|
|
375
|
+
* scanner applies. A prefix (`r`, `f`, `b`, `rb`) is code; only the quote opens the
|
|
376
|
+
* literal.
|
|
377
|
+
*
|
|
378
|
+
* COMMENTS ARE SKIPPED while looking for an opener, in the same pass rather than a
|
|
379
|
+
* prior one: a comment that merely mentions a quote (`# a docstring starts with """`)
|
|
380
|
+
* would otherwise open a span that runs to the next real delimiter, blanking every
|
|
381
|
+
* definition in between so its helpers vanish from every check that reads the
|
|
382
|
+
* structure. A `#` inside a literal is not a comment, which is why one pass has to
|
|
383
|
+
* decide both.
|
|
373
384
|
*/
|
|
374
|
-
function
|
|
375
|
-
const out =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
let i = 0;
|
|
382
|
-
const n = src.length;
|
|
383
|
-
while (i < n) {
|
|
384
|
-
const c = src[i];
|
|
385
|
-
// A comment runs to the end of the line and holds no string opener.
|
|
386
|
-
if (c === "#") {
|
|
387
|
-
while (i < n && src[i] !== "\n")
|
|
385
|
+
function pythonStringSpans(src) {
|
|
386
|
+
const out = [];
|
|
387
|
+
for (let i = 0; i < src.length; i++) {
|
|
388
|
+
const q = src[i];
|
|
389
|
+
if (q === "#") {
|
|
390
|
+
// Outside a literal by construction — a literal advances `i` past its end.
|
|
391
|
+
while (i < src.length && src[i] !== "\n")
|
|
388
392
|
i++;
|
|
389
393
|
continue;
|
|
390
394
|
}
|
|
391
|
-
if (
|
|
392
|
-
i++;
|
|
395
|
+
if (q !== '"' && q !== "'")
|
|
393
396
|
continue;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
while (i < n) {
|
|
402
|
-
if (src[i] === "\\") {
|
|
403
|
-
i += 2;
|
|
397
|
+
const triple = src.startsWith(q + q + q, i);
|
|
398
|
+
const close = triple ? q + q + q : q;
|
|
399
|
+
let j = i + close.length;
|
|
400
|
+
let end = -1;
|
|
401
|
+
while (j < src.length) {
|
|
402
|
+
if (src[j] === "\\") {
|
|
403
|
+
j += 2;
|
|
404
404
|
continue;
|
|
405
405
|
}
|
|
406
|
-
if (src.startsWith(
|
|
407
|
-
|
|
408
|
-
closed = true;
|
|
406
|
+
if (src.startsWith(close, j)) {
|
|
407
|
+
end = j + close.length;
|
|
409
408
|
break;
|
|
410
409
|
}
|
|
411
410
|
// Only a triple-quoted literal may hold a raw newline.
|
|
412
|
-
if (src[
|
|
411
|
+
if (src[j] === "\n" && !triple)
|
|
413
412
|
break;
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
if (!closed) {
|
|
417
|
-
i = start + 1;
|
|
418
|
-
continue;
|
|
413
|
+
j++;
|
|
419
414
|
}
|
|
420
|
-
|
|
415
|
+
if (end === -1)
|
|
416
|
+
continue; // never closed: not a literal
|
|
417
|
+
out.push({ start: i, end });
|
|
418
|
+
i = end - 1;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
/** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
|
|
423
|
+
* comments are `#…`; TS/JS go through the shared stripper. */
|
|
424
|
+
/** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
|
|
425
|
+
export function blankStrings(src, spec) {
|
|
426
|
+
if (spec.language !== "python")
|
|
427
|
+
return stripStrings(src);
|
|
428
|
+
return blankPythonStrings(src);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* String CONTENTS blanked in Python source, quotes kept, offsets and line count
|
|
432
|
+
* preserved.
|
|
433
|
+
*
|
|
434
|
+
* Reads its literals from `pythonStringSpans` — the same scan `stringSpansOf` serves —
|
|
435
|
+
* so ONE definition decides what a Python literal is, for the callers that blank a
|
|
436
|
+
* literal and the callers that must copy one through unchanged.
|
|
437
|
+
*
|
|
438
|
+
* A line-by-line regex cannot do this. A `"""…"""` block spans lines, so a `#` inside a
|
|
439
|
+
* docstring or an embedded SQL block read as a comment, and every scanner built on this
|
|
440
|
+
* then treated quoted text as code the file executes — the delivery-time marker strip
|
|
441
|
+
* deleted a line out of a value rather than out of a comment, and a `def` at column 0
|
|
442
|
+
* inside a docstring ended a helper early and reappeared as a second definition of the
|
|
443
|
+
* same name.
|
|
444
|
+
*/
|
|
445
|
+
function blankPythonStrings(src) {
|
|
446
|
+
const out = src.split("");
|
|
447
|
+
for (const { start, end } of pythonStringSpans(src)) {
|
|
448
|
+
// The quote delimiters stay: callers read this copy for offsets and expect a
|
|
449
|
+
// literal to still look like one.
|
|
450
|
+
const q = src.startsWith(src[start].repeat(3), start) ? 3 : 1;
|
|
451
|
+
for (let i = start + q; i < end - q; i++)
|
|
452
|
+
out[i] = src[i] === "\n" ? "\n" : " ";
|
|
421
453
|
}
|
|
422
454
|
return out.join("");
|
|
423
455
|
}
|
|
@@ -508,6 +540,47 @@ function signatureLines(body) {
|
|
|
508
540
|
}
|
|
509
541
|
return out;
|
|
510
542
|
}
|
|
543
|
+
/** The index of the first line of the decorator stack above `start`, or `start` when
|
|
544
|
+
* there is none. Comments and blank lines may sit between decorators; anything else
|
|
545
|
+
* ends the stack.
|
|
546
|
+
*
|
|
547
|
+
* A decorator may span lines — `@retry(` … `)` — so the walk goes up by EXPRESSION,
|
|
548
|
+
* not by line: from a line that closes more brackets than it opens, it keeps going
|
|
549
|
+
* until the brackets balance, and only then asks whether that line starts with `@`.
|
|
550
|
+
* A line-at-a-time walk sees `)` above the definition, stops there, and drops the
|
|
551
|
+
* whole decorator out of the comparison.
|
|
552
|
+
*
|
|
553
|
+
* Brackets are counted on `structure`, the copy with comments and string contents
|
|
554
|
+
* blanked, for the same reason every other structural read here uses it: a bracket
|
|
555
|
+
* typed inside a string (`retry_if_message(")")`) is not a bracket. The `@` itself is
|
|
556
|
+
* read from `lines`, where it is still legible. Both copies keep the same line count,
|
|
557
|
+
* so one index walks both.
|
|
558
|
+
*
|
|
559
|
+
* Language-neutral: a definition with no `@` line simply gets `start` back. */
|
|
560
|
+
function decoratorStart(lines, structure, start) {
|
|
561
|
+
let first = start;
|
|
562
|
+
let depth = 0;
|
|
563
|
+
for (let i = start - 1; i >= 0; i--) {
|
|
564
|
+
const l = lines[i].trim();
|
|
565
|
+
// Blank, or a comment: `structure` has comments blanked already, so the test
|
|
566
|
+
// is one and the same in every language — no prefix list to keep in step.
|
|
567
|
+
if (depth === 0 && (structure[i] ?? "").trim() === "")
|
|
568
|
+
continue;
|
|
569
|
+
for (const ch of structure[i] ?? "") {
|
|
570
|
+
if (")]}".includes(ch))
|
|
571
|
+
depth++;
|
|
572
|
+
else if ("([{".includes(ch))
|
|
573
|
+
depth--;
|
|
574
|
+
}
|
|
575
|
+
if (depth > 0)
|
|
576
|
+
continue; // inside a wrapped expression; keep walking up
|
|
577
|
+
depth = 0;
|
|
578
|
+
if (!l.startsWith("@"))
|
|
579
|
+
break;
|
|
580
|
+
first = i;
|
|
581
|
+
}
|
|
582
|
+
return first;
|
|
583
|
+
}
|
|
511
584
|
/** Whether the def at `start` carries `@overload` / `@typing.overload` anywhere in
|
|
512
585
|
* its decorator stack (comments and other decorators may sit between). */
|
|
513
586
|
function isOverloadStub(lines, start) {
|
|
@@ -41,8 +41,3 @@ export interface RetrofitMismatch {
|
|
|
41
41
|
* than a refusal the agent cannot act on.
|
|
42
42
|
*/
|
|
43
43
|
export declare function retrofitMismatches(retrofitFile: string, utilsFiles: string[], spec: UtilsLanguageSpec): Promise<RetrofitMismatch[]>;
|
|
44
|
-
/** The file's content at HEAD, or undefined when there is nothing to compare against
|
|
45
|
-
* (untracked, no git root, or git unavailable). Never throws. The ONE reader of a
|
|
46
|
-
* file's committed baseline on the utils path: the incumbent-signature gate reads
|
|
47
|
-
* the utils module through it too. */
|
|
48
|
-
export declare function committedBaseline(file: string): Promise<string | undefined>;
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { execFile } from "child_process";
|
|
2
1
|
import * as fs from "fs";
|
|
3
|
-
import
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
import { logger } from "../logger.js";
|
|
6
|
-
import { detectGitRoot } from "../gitStaging.js";
|
|
2
|
+
import { contentAtHead } from "./head.js";
|
|
7
3
|
import { blankCommentsAndStrings, parseUtilsFile } from "./parse.js";
|
|
8
4
|
import { normalizePath } from "./language-spec.js";
|
|
9
5
|
import { callText, hasSchemaCheck, requestFields, REQUEST_CALL_RE, SCHEMA_SCAN_LINES, shapeKeys, } from "./call-sites.js";
|
|
10
6
|
import { escapeRegExp } from "../regex.js";
|
|
11
|
-
const exec = promisify(execFile);
|
|
12
7
|
/**
|
|
13
8
|
* Whether a retrofit changed the request the rewired test sends.
|
|
14
9
|
*
|
|
@@ -41,7 +36,7 @@ const exec = promisify(execFile);
|
|
|
41
36
|
* than a refusal the agent cannot act on.
|
|
42
37
|
*/
|
|
43
38
|
export async function retrofitMismatches(retrofitFile, utilsFiles, spec) {
|
|
44
|
-
const baseline = await
|
|
39
|
+
const baseline = await contentAtHead(retrofitFile);
|
|
45
40
|
if (baseline === undefined)
|
|
46
41
|
return [];
|
|
47
42
|
let delivered;
|
|
@@ -191,30 +186,3 @@ function sameSet(a, b) {
|
|
|
191
186
|
return false;
|
|
192
187
|
return true;
|
|
193
188
|
}
|
|
194
|
-
/** The file's content at HEAD, or undefined when there is nothing to compare against
|
|
195
|
-
* (untracked, no git root, or git unavailable). Never throws. The ONE reader of a
|
|
196
|
-
* file's committed baseline on the utils path: the incumbent-signature gate reads
|
|
197
|
-
* the utils module through it too. */
|
|
198
|
-
export async function committedBaseline(file) {
|
|
199
|
-
const root = detectGitRoot(file);
|
|
200
|
-
if (!root)
|
|
201
|
-
return undefined;
|
|
202
|
-
const rel = path.relative(root, file);
|
|
203
|
-
if (!rel || rel.startsWith(".."))
|
|
204
|
-
return undefined;
|
|
205
|
-
try {
|
|
206
|
-
const { stdout } = await exec("git", ["show", `HEAD:${rel}`], {
|
|
207
|
-
cwd: root,
|
|
208
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
209
|
-
});
|
|
210
|
-
return stdout;
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
// A file added by this run has no HEAD version — the common case, not an error.
|
|
214
|
-
logger.debug("No committed baseline for a retrofit; skipping equivalence", {
|
|
215
|
-
file,
|
|
216
|
-
error: String(err),
|
|
217
|
-
});
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { execFile } from "child_process";
|
|
2
|
-
import { promisify } from "util";
|
|
3
1
|
import * as fs from "fs";
|
|
4
2
|
import * as path from "path";
|
|
5
3
|
import { detectGitRoot, stageGeneratedPaths } from "../gitStaging.js";
|
|
6
4
|
import { logger } from "../logger.js";
|
|
7
5
|
import { isTestbotEnabled } from "../featureFlags.js";
|
|
8
6
|
import { locateUtilsFiles, realpath } from "./locate.js";
|
|
7
|
+
import { changedSinceHead } from "./head.js";
|
|
9
8
|
import { locateInHouseModules } from "./in-house.js";
|
|
10
9
|
import { CODEGEN_MARKER_RE } from "./call-sites.js";
|
|
11
10
|
import { utilsSpecFor, utilsSpecForFile } from "./language-spec.js";
|
|
12
|
-
const execFileAsync = promisify(execFile);
|
|
13
11
|
export async function stageUtilsArtifacts(testFile, language) {
|
|
14
12
|
const staged = [];
|
|
15
13
|
const failed = [];
|
|
@@ -99,10 +97,7 @@ async function modifiedInHouseModules(testFile, language) {
|
|
|
99
97
|
const modules = await locateInHouseModules({ testFile, spec, cwd: root });
|
|
100
98
|
if (modules.length === 0)
|
|
101
99
|
return [];
|
|
102
|
-
const
|
|
103
|
-
const modified = new Set();
|
|
104
|
-
for (const rel of stdout.split("\0").filter(Boolean))
|
|
105
|
-
modified.add(await realpath(path.resolve(root, rel)));
|
|
100
|
+
const modified = new Set(await changedSinceHead(root, { filter: "M" }));
|
|
106
101
|
return modules.map((m) => m.file).filter((f) => modified.has(f));
|
|
107
102
|
}
|
|
108
103
|
catch (err) {
|
|
@@ -117,18 +112,12 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
|
|
|
117
112
|
return [];
|
|
118
113
|
let modified;
|
|
119
114
|
try {
|
|
120
|
-
// `-z`: unquoted, NUL-separated — git C-quotes non-ASCII paths otherwise.
|
|
121
|
-
const { stdout } = await execFileAsync("git",
|
|
122
115
|
// Against HEAD, modified only: a test THIS run created is staged at discovery, so
|
|
123
116
|
// its later STEP 5b edits show in a plain `git diff` and it would masquerade as a
|
|
124
117
|
// pre-existing sibling (observed: the report gate then demanded an execution no
|
|
125
118
|
// record could satisfy, and the agent un-shared the helpers to escape). Files
|
|
126
119
|
// added since HEAD are excluded; they are staged by their own tool calls.
|
|
127
|
-
|
|
128
|
-
cwd: root,
|
|
129
|
-
encoding: "utf8",
|
|
130
|
-
});
|
|
131
|
-
modified = stdout.split("\0").filter(Boolean);
|
|
120
|
+
modified = await changedSinceHead(root, { filter: "M" });
|
|
132
121
|
}
|
|
133
122
|
catch (err) {
|
|
134
123
|
logger.warning("Could not list modified files — edited sibling tests will not be staged", {
|
|
@@ -140,8 +129,7 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
|
|
|
140
129
|
const stems = utilsFiles.map((f) => path.basename(f).replace(/\.[^.]+$/, ""));
|
|
141
130
|
const self = await realpath(testFile);
|
|
142
131
|
const out = [];
|
|
143
|
-
for (const
|
|
144
|
-
const abs = await realpath(path.resolve(root, rel));
|
|
132
|
+
for (const abs of modified) {
|
|
145
133
|
if (abs === self || utilsFiles.includes(abs))
|
|
146
134
|
continue;
|
|
147
135
|
if (!spec.extensions.some((e) => abs.toLowerCase().endsWith(e)))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type UtilsTypecheck } from "./typecheck.js";
|
|
2
2
|
import { type InlineCallSite, type UnextractedDuplicate } from "./call-sites.js";
|
|
3
3
|
import { type UtilsAdvisoryKind, type UtilsViolationKind } from "./allow.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type HelperImporters } from "./importers.js";
|
|
5
5
|
import { type HelperFamilySpec, type UtilsLanguageSpec } from "./language-spec.js";
|
|
6
6
|
export interface UtilsViolation {
|
|
7
7
|
kind: UtilsViolationKind;
|
|
@@ -60,8 +60,14 @@ export interface UtilsVerifyResult {
|
|
|
60
60
|
* findUnextractedDuplicates. API family only. */
|
|
61
61
|
unextractedDuplicates: UnextractedDuplicate[];
|
|
62
62
|
/** Helpers imported by 0 or 1 delivered test (SKYR-4276 A6). Advisory: a first run
|
|
63
|
-
* seeds every helper single-use by design; the count is the trend to watch.
|
|
64
|
-
|
|
63
|
+
* seeds every helper single-use by design; the count is the trend to watch. Empty
|
|
64
|
+
* whenever `importersComplete` is false — see there. */
|
|
65
|
+
singleImporters: HelperImporters[];
|
|
66
|
+
/** The importer walk read the whole directory. When false, `singleImporters` is
|
|
67
|
+
* empty because the walk could not support a finding, NOT because every helper has
|
|
68
|
+
* importers: a file it could not read may be the one importer of a helper. A
|
|
69
|
+
* caller that states "nothing imports this" must consult it. */
|
|
70
|
+
importersComplete: boolean;
|
|
65
71
|
/** The numbers the baseline assertion check compared — present only when a
|
|
66
72
|
* baseline was supplied and the spec was readable. The check is a COUNT: it
|
|
67
73
|
* cannot see one assertion swapped for another. The caller's text says so.
|
|
@@ -7,19 +7,20 @@ import { typecheckDelivered } from "./typecheck.js";
|
|
|
7
7
|
import { blankComments, blankCommentsAndStrings, countAssertions, hasPageErrorGuard, parseUtilsFile, } from "./parse.js";
|
|
8
8
|
import { escapeRegExp } from "../regex.js";
|
|
9
9
|
import { detectGitRoot } from "../gitStaging.js";
|
|
10
|
-
import { inlineCallSiteFamily, isModularizeFirstTarget, } from "../reuseRouting.js";
|
|
10
|
+
import { inlineCallSiteFamily, isModularizeFirstTarget, isPomFirstTarget, } from "../reuseRouting.js";
|
|
11
11
|
import { describeActionKey } from "./action-key.js";
|
|
12
12
|
import { findDuplicateBodyReach, findUnreachableBodies } from "./body-reach.js";
|
|
13
13
|
import { misnamedNewModules } from "./module-name.js";
|
|
14
|
+
import { pageObjectCandidates } from "../pom-scope/ownership.js";
|
|
14
15
|
import { additiveChanges, locateInHouseModules, resolveInHouseCalls, } from "./in-house.js";
|
|
15
16
|
import { findSiblingInlineActionSites } from "./action-sites.js";
|
|
16
17
|
import { findSiblingInlineCallSites, findUnextractedDuplicates, generatedSiblings, } from "./call-sites.js";
|
|
17
18
|
import { parseUtilsAllows, } from "./allow.js";
|
|
18
19
|
import { describeFixedSleeps, fixedSleepReplacement } from "./fixed-sleep.js";
|
|
19
|
-
import { singleImporterHelpers
|
|
20
|
+
import { singleImporterHelpers } from "./importers.js";
|
|
20
21
|
import { assertsStatusEquality, defaultedStatusParam, pinnedStatusLiteral, restatedStatusAssertions, } from "./status-once.js";
|
|
21
22
|
import { STATUS_ONCE_RULE } from "../../prompts/shared-helper-policy.js";
|
|
22
|
-
import {
|
|
23
|
+
import { contentAtHead } from "./head.js";
|
|
23
24
|
import { incumbentSignatureViolations } from "./incumbent.js";
|
|
24
25
|
import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
|
|
25
26
|
/** What the compiler says about a name defined twice, which `helper-redefined`
|
|
@@ -80,9 +81,16 @@ export async function verifyUtils(params) {
|
|
|
80
81
|
// locateInHouseModules). Found BEFORE the utils files so a customer module that
|
|
81
82
|
// gained our header this run is judged by the additive gate, never by the Skyramp
|
|
82
83
|
// invariants: the header locator would otherwise claim it.
|
|
83
|
-
const
|
|
84
|
+
const located = spec
|
|
84
85
|
? await locateInHouseModules({ testFile: params.testFile, spec, cwd: root })
|
|
85
86
|
: [];
|
|
87
|
+
// Both reuse settings on: a module that declares a class is a page object, which
|
|
88
|
+
// the page-object pass owned and its verifier checked. The in-house parser cannot
|
|
89
|
+
// read a class, and its additive gate would judge a page-object edit it did not
|
|
90
|
+
// prescribe — so the split is by shape, the one pom-scope/ownership.ts states.
|
|
91
|
+
const pomFirst = !!spec && isPomFirstTarget(spec.language, params.testType);
|
|
92
|
+
const pageObjectFiles = new Set(pomFirst ? (await pageObjectCandidates(located)).map((m) => m.file) : []);
|
|
93
|
+
const inHouse = located.filter((m) => !pageObjectFiles.has(m.file));
|
|
86
94
|
const inHouseFiles = new Set(inHouse.map((m) => m.file));
|
|
87
95
|
const utilsFiles = spec
|
|
88
96
|
? (await locateUtilsFiles({
|
|
@@ -107,6 +115,7 @@ export async function verifyUtils(params) {
|
|
|
107
115
|
inlineCallSites: [],
|
|
108
116
|
unextractedDuplicates: [],
|
|
109
117
|
singleImporters: [],
|
|
118
|
+
importersComplete: false,
|
|
110
119
|
};
|
|
111
120
|
if (!spec)
|
|
112
121
|
return result;
|
|
@@ -161,7 +170,7 @@ export async function verifyUtils(params) {
|
|
|
161
170
|
// helpers this run writes, so a defaulted status on an incumbent is not advised
|
|
162
171
|
// — and an incumbent's signature is held to what it was (incumbent.ts). Absent
|
|
163
172
|
// for a module this run created, or outside git: then every helper is this run's.
|
|
164
|
-
const baseline = await
|
|
173
|
+
const baseline = await contentAtHead(file);
|
|
165
174
|
const headHelpers = baseline === undefined ? [] : parseUtilsFile(baseline, fileSpec);
|
|
166
175
|
const incumbents = new Set(headHelpers.map((h) => h.name));
|
|
167
176
|
incumbentsOfFile.set(file, incumbents);
|
|
@@ -321,6 +330,9 @@ export async function verifyUtils(params) {
|
|
|
321
330
|
}
|
|
322
331
|
if (utilsSet.has(imp.resolved) || imp.resolved === realTestFile)
|
|
323
332
|
continue;
|
|
333
|
+
// The page-object pass's module: checked by its verifier, not this one.
|
|
334
|
+
if (pageObjectFiles.has(imp.resolved))
|
|
335
|
+
continue;
|
|
324
336
|
if (inHouseFiles.has(imp.resolved)) {
|
|
325
337
|
if (!inHouseResult)
|
|
326
338
|
unchecked.push(imp.resolved);
|
|
@@ -372,7 +384,7 @@ export async function verifyUtils(params) {
|
|
|
372
384
|
if (!inHouseResult && inHouse.length > 0)
|
|
373
385
|
result.inHouseChecksSkipped = `this pass ran ${params.testType
|
|
374
386
|
? `as a ${params.testType.toLowerCase()} test`
|
|
375
|
-
: "with no test type supplied"}, and they run for a UI test with
|
|
387
|
+
: "with no test type supplied"}, and they run for a UI test in TypeScript or JavaScript with utils reuse on`;
|
|
376
388
|
}
|
|
377
389
|
// The spec asserts a status a helper it calls already asserted on the response it
|
|
378
390
|
// returned. Spec-level, like assertion-loss: the finding names the spec and the
|
|
@@ -478,12 +490,13 @@ export async function verifyUtils(params) {
|
|
|
478
490
|
});
|
|
479
491
|
}
|
|
480
492
|
// One directory walk feeds every sibling advisory: each sibling is read exactly once.
|
|
481
|
-
let siblings = [];
|
|
493
|
+
let siblings = { files: [], complete: false };
|
|
482
494
|
try {
|
|
483
495
|
siblings = await generatedSiblings(params.testFile, spec, {
|
|
484
496
|
includeSelf: true,
|
|
485
497
|
});
|
|
486
498
|
result.singleImporters = await singleImporterHelpers(params.testFile, utilsFiles, helpersByFile, spec, siblings);
|
|
499
|
+
result.importersComplete = siblings.complete;
|
|
487
500
|
}
|
|
488
501
|
catch {
|
|
489
502
|
/* advisory only — never fails a verify */
|
|
@@ -864,9 +877,9 @@ incumbents = new Set()) {
|
|
|
864
877
|
});
|
|
865
878
|
}
|
|
866
879
|
else if (sameName &&
|
|
867
|
-
h.
|
|
868
|
-
sameName.
|
|
869
|
-
h.
|
|
880
|
+
h.params !== undefined &&
|
|
881
|
+
sameName.params !== undefined &&
|
|
882
|
+
h.params.length !== sameName.params.length &&
|
|
870
883
|
!(routeDuplicateOf?.file === sameName.file &&
|
|
871
884
|
routeDuplicateOf?.line === sameName.line)) {
|
|
872
885
|
nameReported = true;
|
|
@@ -875,7 +888,7 @@ incumbents = new Set()) {
|
|
|
875
888
|
file,
|
|
876
889
|
helper: h.name,
|
|
877
890
|
line: h.line,
|
|
878
|
-
detail: `${h.name} is defined in two modules with different parameter lists — ${h.
|
|
891
|
+
detail: `${h.name} is defined in two modules with different parameter lists — ${h.params.length} parameter${h.params.length === 1 ? "" : "s"} here, ${sameName.params.length} at ${path.basename(sameName.file)}:${sameName.line}; the two modules disagree on one operation's signature, and the next test to import it gets whichever it finds first. Keep ONE definition, give every parameter the other lacked a default, and point every importer at it`,
|
|
879
892
|
});
|
|
880
893
|
}
|
|
881
894
|
if (!inFile)
|
|
@@ -898,7 +911,8 @@ incumbents = new Set()) {
|
|
|
898
911
|
line: h.line,
|
|
899
912
|
// Only the parameter COUNT is compared, and only when both were read.
|
|
900
913
|
detail: first.name === h.name
|
|
901
|
-
? `${h.name} is defined twice (also at ${path.basename(first.file)}:${first.line}) with the same ${h.
|
|
914
|
+
? `${h.name} is defined twice (also at ${path.basename(first.file)}:${first.line}) with the same ${h.params !== undefined &&
|
|
915
|
+
h.params.length === first.params?.length
|
|
902
916
|
? "parameter count and the same "
|
|
903
917
|
: ""}action sequence: ${describeActionKey(h.actionKey)}; keep one definition and point every importer at it`
|
|
904
918
|
: `${h.name} and ${first.name} (${path.basename(first.file)}:${first.line}) perform the same action sequence: ${describeActionKey(h.actionKey)}`,
|
|
@@ -608,6 +608,7 @@ async function domEvaluationScript(options) {
|
|
|
608
608
|
if (m && /[0-9]/.test(m[1])) return true;
|
|
609
609
|
if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
|
|
610
610
|
if (id.includes(":")) return true;
|
|
611
|
+
if (/«r[0-9a-z]*»|(^|[-_])_?r_[0-9a-z]+_/.test(id)) return true;
|
|
611
612
|
if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
|
|
612
613
|
return false;
|
|
613
614
|
}
|
|
@@ -34,6 +34,7 @@ function isDynamicId(id) {
|
|
|
34
34
|
if (shortHexMatch && /[0-9]/.test(shortHexMatch[1])) return true;
|
|
35
35
|
if (/[a-zA-Z][0-9]{3,}$/.test(id)) return true;
|
|
36
36
|
if (id.includes(":")) return true;
|
|
37
|
+
if (/«r[0-9a-z]*»|(^|[-_])_?r_[0-9a-z]+_/.test(id)) return true;
|
|
37
38
|
if (/^.+__search_[a-zA-Z0-9]{4,}$/.test(id)) return true;
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
@@ -65,6 +65,12 @@ test("isDynamicId: single trailing digit after letter is not dynamic", () => {
|
|
|
65
65
|
assertFalse((0, import_dynamicId.isDynamicId)("item1"), "item1 (single digit)");
|
|
66
66
|
assertFalse((0, import_dynamicId.isDynamicId)("item12"), "item12 (2 digits, below 3-digit threshold)");
|
|
67
67
|
});
|
|
68
|
+
test("isDynamicId: React 19 useId forms", () => {
|
|
69
|
+
assertTrue((0, import_dynamicId.isDynamicId)("\xABr2\xBB-form-item"), "\xABr2\xBB-form-item (React 19.0)");
|
|
70
|
+
assertTrue((0, import_dynamicId.isDynamicId)("radix-_r_16_"), "radix-_r_16_ (React 19.1 inside Radix)");
|
|
71
|
+
assertTrue((0, import_dynamicId.isDynamicId)("_r_1k_"), "_r_1k_ (bare React 19.1)");
|
|
72
|
+
assertFalse((0, import_dynamicId.isDynamicId)("user_r_name"), "user_r_name (word, not a useId)");
|
|
73
|
+
});
|
|
68
74
|
let failed = 0;
|
|
69
75
|
for (const { name, run } of cases) {
|
|
70
76
|
try {
|