@skyramp/mcp 0.4.2-rc.1 → 0.4.2-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/commands/localDevTestChangesCommand.js +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +10 -1
- package/build/commands/testThisEndpointCommand.js +19 -2
- package/build/execution/wrapperConfig.d.ts +56 -0
- package/build/execution/wrapperConfig.js +155 -0
- package/build/index.js +6 -6
- package/build/playwright/registerPlaywrightTools.js +14 -0
- package/build/playwright/traceExportStore.d.ts +22 -0
- package/build/playwright/traceExportStore.js +81 -0
- package/build/playwright/traceRecordingPrompt.js +2 -1
- package/build/prompts/code-reuse.js +24 -21
- package/build/prompts/local-dev/local-dev-plan.js +6 -23
- package/build/prompts/local-dev/local-dev-prompts.js +1 -1
- package/build/prompts/shared-helper-policy.d.ts +36 -0
- package/build/prompts/shared-helper-policy.js +33 -1
- package/build/prompts/startTraceCollectionPrompts.js +1 -1
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +6 -7
- package/build/prompts/sut-setup/shared.d.ts +1 -1
- package/build/prompts/sut-setup/shared.js +5 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +90 -36
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +1 -1
- package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationShared.js +0 -1
- package/build/prompts/testbot/testbot-prompts.js +11 -9
- package/build/services/TestDiscoveryService.js +32 -4
- package/build/skills/runTestSkill.d.ts +6 -0
- package/build/skills/runTestSkill.js +17 -0
- package/build/tool-phases.js +0 -1
- package/build/tools/budgetExcuse.d.ts +15 -0
- package/build/tools/budgetExcuse.js +113 -0
- package/build/tools/code-refactor/utils-verify-gates.js +17 -3
- package/build/tools/executeSkyrampTestTool.d.ts +97 -48
- package/build/tools/executeSkyrampTestTool.js +775 -449
- package/build/tools/submitReportTool.js +128 -0
- package/build/tools/test-management/actionsTool.js +31 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +4 -4
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -63
- package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
- package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +32 -32
- package/build/types/ReuseOutcome.d.ts +4 -3
- package/build/types/TestExecution.d.ts +2 -2
- package/build/types/TestTypes.d.ts +3 -0
- package/build/types/TestTypes.js +6 -0
- package/build/utils/AnalysisStateManager.d.ts +0 -7
- package/build/utils/AnalysisStateManager.js +1 -1
- package/build/utils/connectionErrors.d.ts +10 -0
- package/build/utils/connectionErrors.js +10 -0
- package/build/utils/language-helper.js +24 -3
- package/build/utils/progress.d.ts +1 -1
- package/build/utils/progress.js +1 -1
- package/build/utils/rebaselineSnapshots.d.ts +1 -1
- package/build/utils/rebaselineSnapshots.js +6 -16
- package/build/utils/reuseRouting.d.ts +10 -0
- package/build/utils/reuseRouting.js +15 -0
- package/build/utils/runContextGauge.d.ts +27 -0
- package/build/utils/runContextGauge.js +181 -0
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/skyrampSdkVersion.d.ts +9 -0
- package/build/utils/skyrampSdkVersion.js +16 -0
- package/build/utils/testDependencyPolicy.js +21 -0
- package/build/utils/testExecutionRecord.d.ts +5 -1
- package/build/utils/testExecutionRecord.js +3 -1
- package/build/utils/testFileClassification.d.ts +8 -0
- package/build/utils/testFileClassification.js +36 -3
- package/build/utils/utils-verify/action-key.d.ts +42 -0
- package/build/utils/utils-verify/action-key.js +118 -36
- package/build/utils/utils-verify/action-sites.d.ts +32 -0
- package/build/utils/utils-verify/action-sites.js +202 -0
- package/build/utils/utils-verify/body-reach.js +2 -4
- package/build/utils/utils-verify/call-sites.d.ts +25 -6
- package/build/utils/utils-verify/call-sites.js +8 -5
- 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 +25 -0
- package/build/utils/utils-verify/language-spec.js +16 -2
- package/build/utils/utils-verify/parse.d.ts +10 -1
- package/build/utils/utils-verify/parse.js +19 -2
- package/build/utils/utils-verify/verify.d.ts +3 -2
- package/build/utils/utils-verify/verify.js +16 -18
- package/build/workspace/workspace.d.ts +72 -52
- package/build/workspace/workspace.js +12 -8
- package/package.json +1 -1
- package/plugin/prompts/testbot-task1.md +0 -2
- package/plugin/skills/fix-test-import-errors/SKILL.md +2 -1
- package/plugin/skills/run-test/SKILL.md +16 -0
- package/build/adapters/jestAdapter.d.ts +0 -14
- package/build/adapters/jestAdapter.js +0 -131
- package/build/adapters/mochaAdapter.d.ts +0 -13
- package/build/adapters/mochaAdapter.js +0 -93
- package/build/adapters/playwrightAdapter.d.ts +0 -17
- package/build/adapters/playwrightAdapter.js +0 -184
- package/build/adapters/pytestAdapter.d.ts +0 -15
- package/build/adapters/pytestAdapter.js +0 -119
- package/build/tools/runExistingTestsTool.d.ts +0 -138
- package/build/tools/runExistingTestsTool.js +0 -666
- package/build/types/ExternalTestExecution.d.ts +0 -67
- package/build/types/ExternalTestExecution.js +0 -8
- package/build/workspace/testSuites.d.ts +0 -20
- package/build/workspace/testSuites.js +0 -17
|
@@ -231,6 +231,7 @@ fileHash) {
|
|
|
231
231
|
* per-file testExecutions record (every file, keyed by canonical path — see
|
|
232
232
|
* executionRecordFrom), plus the maintained-test executionBefore/After row
|
|
233
233
|
* when `testFile` has one. No-op when the repo section doesn't exist yet.
|
|
234
|
+
* Returns what was written, so the caller can say what was not.
|
|
234
235
|
*
|
|
235
236
|
* Extracted from skyramp_execute_test's own state-write block so the
|
|
236
237
|
* behavior that makes a file's result available to the post-execution
|
|
@@ -245,7 +246,7 @@ export async function persistTestExecutionResult(stateFile, repo, testType, phas
|
|
|
245
246
|
const stateManager = StateManager.fromStatePath(stateFile);
|
|
246
247
|
const stateData = await stateManager.readRepoData(repo);
|
|
247
248
|
if (!stateData)
|
|
248
|
-
return;
|
|
249
|
+
return { saved: false, matchedExistingTest: false };
|
|
249
250
|
const testIndex = stateData.existingTests?.findIndex((t) => canonicalTestPath(t.testFile) === canonicalTestPath(testFile)) ?? -1;
|
|
250
251
|
if (testIndex >= 0) {
|
|
251
252
|
if (phase === "before") {
|
|
@@ -263,5 +264,6 @@ export async function persistTestExecutionResult(stateFile, repo, testType, phas
|
|
|
263
264
|
!options.reserved),
|
|
264
265
|
};
|
|
265
266
|
await stateManager.updateRepoData(stateData, { repo });
|
|
267
|
+
return { saved: true, matchedExistingTest: testIndex >= 0 };
|
|
266
268
|
});
|
|
267
269
|
}
|
|
@@ -14,6 +14,14 @@ export declare function isDiscoveredTestFile(filePath: string): boolean;
|
|
|
14
14
|
* `isDiscoveredTestFile`'s rule, which also counts `tests/`, a page-object
|
|
15
15
|
* directory and `test_orders.py`. The maintenance tool wants this narrow one. */
|
|
16
16
|
export declare function isTestFile(filePath: string): boolean;
|
|
17
|
+
/** The file's own NAME says it is a test — `login.spec.ts`, `orders.test.js`,
|
|
18
|
+
* `test_orders.py`, `UserDAOTest.java`. Almost entirely a NAME rule: a helper,
|
|
19
|
+
* a fixture, a page object or a config sitting in `e2e/` or `cypress/support/`
|
|
20
|
+
* is not a test and cannot be run as one. The one exception is `__tests__/`,
|
|
21
|
+
* Jest's convention that everything inside it is a test whatever it is called.
|
|
22
|
+
* Exported for callers that must not act on a file merely because of where it
|
|
23
|
+
* lives. */
|
|
24
|
+
export declare function hasTestFileName(filePath: string): boolean;
|
|
17
25
|
/** Whether a path is a test file, by directory or by name. Read by the route
|
|
18
26
|
* checks: a route declared in a mock server or a spec fixture is not a surface
|
|
19
27
|
* anyone serves. */
|
|
@@ -76,10 +76,43 @@ const TEST_ROOT_DIR = /(^|[\\/])(tests?|__tests__|__mocks__|e2e|cypress|testing)
|
|
|
76
76
|
* `TEST_FILE_PATTERNS` rows it resembles — any extension, and `.`, `_` or `-`
|
|
77
77
|
* before `test` — so no row above can stand in for it. */
|
|
78
78
|
const TEST_FILE_NAME = /[._-](test|spec)\.[a-z]+$|(^|[\\/])test_[^\\/]*$/i;
|
|
79
|
+
/** The file's own NAME says it is a test — `login.spec.ts`, `orders.test.js`,
|
|
80
|
+
* `test_orders.py`, `UserDAOTest.java`. Almost entirely a NAME rule: a helper,
|
|
81
|
+
* a fixture, a page object or a config sitting in `e2e/` or `cypress/support/`
|
|
82
|
+
* is not a test and cannot be run as one. The one exception is `__tests__/`,
|
|
83
|
+
* Jest's convention that everything inside it is a test whatever it is called.
|
|
84
|
+
* Exported for callers that must not act on a file merely because of where it
|
|
85
|
+
* lives. */
|
|
86
|
+
export function hasTestFileName(filePath) {
|
|
87
|
+
return (
|
|
88
|
+
// Discovery's own name rule, applied to the basename so it admits no
|
|
89
|
+
// directory. It carries the framework spellings the regexes below miss:
|
|
90
|
+
// `UserDAOTest.java`, `orders_smoke.py`, `checkout_e2e.ts`.
|
|
91
|
+
TEST_FILE_PATTERNS.some((p) => p.test(path.basename(filePath))) ||
|
|
92
|
+
TEST_FILE_NAME.test(filePath) ||
|
|
93
|
+
CAMEL_TEST_FILE_NAME.test(filePath) ||
|
|
94
|
+
// Surefire's default includes are `Test*.java` as well as `*Test.java`, and
|
|
95
|
+
// Failsafe's are `*IT.java`. CAMEL_TEST_FILE_NAME needs a lowercase
|
|
96
|
+
// character before the suffix, so the acronym spelling (`APIIT.java`) and
|
|
97
|
+
// the prefix spelling (`TestOrder.java`) both fall through it.
|
|
98
|
+
/(^|[\\/])Test[A-Z][A-Za-z0-9]*\.(java|kt|kts|scala)$/.test(filePath) ||
|
|
99
|
+
/[A-Za-z0-9]IT\.(java|kt|kts|scala)$/.test(filePath) ||
|
|
100
|
+
// mocha's default spec glob is `./test/*.js` — one directory level, and no
|
|
101
|
+
// name rule at all, so `test/app.js` is a test to mocha.
|
|
102
|
+
/(^|[\\/])test[\\/][^\\/]+\.(js|cjs|mjs)$/.test(filePath) ||
|
|
103
|
+
// `login.e2e.ts` and Cypress's own `login.cy.ts` — the dot forms
|
|
104
|
+
// TEST_FILE_NAME does not carry. `.cy.` is to Cypress what `.spec.` is to
|
|
105
|
+
// Playwright.
|
|
106
|
+
/\.(cy|e2e)\.[a-z]+$/i.test(filePath) ||
|
|
107
|
+
// Jest's convention: everything under `__tests__/` is a test, whatever it
|
|
108
|
+
// is called. The only directory that means that on its own.
|
|
109
|
+
/(^|[\\/])__tests__[\\/]/.test(filePath));
|
|
110
|
+
}
|
|
79
111
|
/** The CamelCase spellings, which carry no separator before `Test`.
|
|
80
|
-
* Case-SENSITIVE and must stay so: with `i` it also matches `Latest.java
|
|
81
|
-
* `
|
|
82
|
-
* `*Test.java` / `*Tests.cs` rows above cannot serve
|
|
112
|
+
* Case-SENSITIVE and must stay so: with `i` it also matches `Latest.java`,
|
|
113
|
+
* `Audit.java`, `Edit.cs` and `Digit.ts`, ordinary source files. That is why
|
|
114
|
+
* the case-insensitive `*Test.java` / `*Tests.cs` rows above cannot serve
|
|
115
|
+
* here. */
|
|
83
116
|
const CAMEL_TEST_FILE_NAME = /[a-z0-9](Tests?|IT)\.[A-Za-z0-9]+$/;
|
|
84
117
|
/** Whether a path is a test file, by directory or by name. Read by the route
|
|
85
118
|
* checks: a route declared in a mock server or a spec fixture is not a surface
|
|
@@ -25,7 +25,49 @@ import type { UtilsLanguage } from "./language-spec.js";
|
|
|
25
25
|
* helper in either language gets the same key.
|
|
26
26
|
*/
|
|
27
27
|
export declare function actionKeyOf(body: string, language?: UtilsLanguage): string | undefined;
|
|
28
|
+
/** The keys of every step, in order, or `undefined` when there is no step or a
|
|
29
|
+
* step has no key — the sequence `actionKeyOf` joins, kept as steps for a caller
|
|
30
|
+
* that matches step by step (a key is not split back: a selector may carry the
|
|
31
|
+
* separator). */
|
|
32
|
+
export declare function actionStepKeys(steps: ActionStep[]): string[] | undefined;
|
|
33
|
+
/** One Playwright action in a body, in source order — the unit `actionKeyOf` joins. */
|
|
34
|
+
export interface ActionStep {
|
|
35
|
+
/** The step's key (`verb chain [target]`), or `undefined` when this action cannot
|
|
36
|
+
* be read: its locator is not built from plain string literals, or its arguments
|
|
37
|
+
* do not balance. A helper with one such step has no key; a spec's other steps
|
|
38
|
+
* are still read, so an unreadable step is a boundary no matched run crosses. */
|
|
39
|
+
key?: string;
|
|
40
|
+
/** 1-based line of the action verb. */
|
|
41
|
+
line: number;
|
|
42
|
+
/** The source between the previous action's closing `)` (or the start of the
|
|
43
|
+
* body) and this action's chain: what stands between two consecutive actions.
|
|
44
|
+
* Comments are as the caller left them; string contents are kept. Empty for
|
|
45
|
+
* the first step. See `betweenActionsIsTransparent`. */
|
|
46
|
+
gapBefore: string;
|
|
47
|
+
/** The source after this action's closing `)` up to the next action's chain, or
|
|
48
|
+
* to the end of the body for the last step — the next step's `gapBefore`. */
|
|
49
|
+
gapAfter: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Every action in `body`, in order, each keyed as `actionKeyOf` keys it. Where
|
|
53
|
+
* `actionKeyOf` answers "what does this helper do" with one string, this answers
|
|
54
|
+
* "which actions, where, and what sits between them" — what a scan for an inline
|
|
55
|
+
* copy of a helper's sequence needs. Lines survive the Python and alias rewrites:
|
|
56
|
+
* neither adds or removes a newline.
|
|
57
|
+
*/
|
|
58
|
+
export declare function actionStepsOf(body: string, language?: UtilsLanguage): ActionStep[];
|
|
59
|
+
/** `click empty-cart-btn; fill qty` → `click \`empty-cart-btn\`; fill \`qty\`` — the
|
|
60
|
+
* key as the agent reads it, selectors quoted so a `#id` never reads as prose. */
|
|
61
|
+
export declare function describeActionKey(key: string): string;
|
|
28
62
|
/** Index of the `)` that closes the `(` at `open`, on a string-blanked copy. */
|
|
29
63
|
export declare function matchingClose(structure: string, open: number): number;
|
|
64
|
+
/** Whitespace and braces dropped outside string literals, `'` normalised to `"` —
|
|
65
|
+
* the spelling a locator chain has inside a key. */
|
|
66
|
+
export declare function normalizeChain(text: string): string;
|
|
30
67
|
/** String contents replaced by spaces, delimiters kept, same length. */
|
|
31
68
|
export declare function blankStringContents(text: string): string;
|
|
69
|
+
/** The statements of a fragment as `[start, end)` spans on `structure` (a
|
|
70
|
+
* string-blanked copy): each cut where valueEnd cuts — a `;` at depth 0, or a line
|
|
71
|
+
* end at depth 0 that the next line does not continue with `.` — so a
|
|
72
|
+
* Prettier-wrapped chain is one statement, not one per line. */
|
|
73
|
+
export declare function statementSpans(structure: string): [number, number][];
|
|
@@ -34,52 +34,101 @@ const TARGET_ARG = new Set(["goto", "dragTo"]);
|
|
|
34
34
|
* helper in either language gets the same key.
|
|
35
35
|
*/
|
|
36
36
|
export function actionKeyOf(body, language = "typescript") {
|
|
37
|
+
return actionStepKeys(actionStepsOf(body, language))?.join("; ");
|
|
38
|
+
}
|
|
39
|
+
/** The keys of every step, in order, or `undefined` when there is no step or a
|
|
40
|
+
* step has no key — the sequence `actionKeyOf` joins, kept as steps for a caller
|
|
41
|
+
* that matches step by step (a key is not split back: a selector may carry the
|
|
42
|
+
* separator). */
|
|
43
|
+
export function actionStepKeys(steps) {
|
|
44
|
+
if (steps.length === 0 || steps.some((s) => s.key === undefined))
|
|
45
|
+
return undefined;
|
|
46
|
+
return steps.map((s) => s.key);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Every action in `body`, in order, each keyed as `actionKeyOf` keys it. Where
|
|
50
|
+
* `actionKeyOf` answers "what does this helper do" with one string, this answers
|
|
51
|
+
* "which actions, where, and what sits between them" — what a scan for an inline
|
|
52
|
+
* copy of a helper's sequence needs. Lines survive the Python and alias rewrites:
|
|
53
|
+
* neither adds or removes a newline.
|
|
54
|
+
*/
|
|
55
|
+
export function actionStepsOf(body, language = "typescript") {
|
|
37
56
|
const src = resolveLocatorAliases(language === "python" ? toPlaywrightJs(body) : body);
|
|
38
57
|
// Structure is read on a copy with string CONTENTS blanked (same length), so a
|
|
39
|
-
// `)` or `.` inside a literal never ends a chain
|
|
58
|
+
// `)` or `.` inside a literal never ends a chain, and a `.click(` written inside
|
|
59
|
+
// a string is not an action; text is taken from the original.
|
|
40
60
|
const structure = blankStringContents(src);
|
|
41
61
|
const steps = [];
|
|
42
|
-
|
|
62
|
+
let prevEnd = 0;
|
|
63
|
+
for (const m of structure.matchAll(ACTION_RE)) {
|
|
43
64
|
const verb = m[1];
|
|
44
65
|
const dot = m.index;
|
|
66
|
+
const line = src.slice(0, dot).split("\n").length;
|
|
45
67
|
const argsOpen = dot + m[0].length - 1;
|
|
46
68
|
const argsClose = matchingClose(structure, argsOpen);
|
|
47
|
-
if (argsClose === -1)
|
|
48
|
-
return undefined;
|
|
49
69
|
const chainStart = chainStartBefore(structure, dot);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!target)
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
else if (TARGET_ARG.has(verb))
|
|
61
|
-
target = args;
|
|
62
|
-
if (verb === "goto") {
|
|
63
|
-
chain = "";
|
|
64
|
-
// A template target is keyed on its text, `${…}` included: generated helpers
|
|
65
|
-
// prefix every path with one module constant (`${baseUrl}/login`), and
|
|
66
|
-
// `/orders/${id}` is one page whatever the id. A bare variable is not a
|
|
67
|
-
// target this can read.
|
|
68
|
-
if (/^\s*`/.test(target))
|
|
69
|
-
target = "`" + target.trim().slice(1, -1) + "`";
|
|
70
|
-
else if (!/^\s*["']/.test(target))
|
|
71
|
-
return undefined;
|
|
70
|
+
const gapBefore = steps.length === 0 ? "" : src.slice(prevEnd, chainStart);
|
|
71
|
+
// An action inside another action's arguments: its chain starts before the
|
|
72
|
+
// previous action ended, so there is no gap to judge and no run to join.
|
|
73
|
+
if (argsClose === -1 || chainStart < prevEnd) {
|
|
74
|
+
steps.push({ line, gapBefore, gapAfter: "" });
|
|
75
|
+
prevEnd = argsOpen + 1;
|
|
76
|
+
continue;
|
|
72
77
|
}
|
|
73
|
-
|
|
78
|
+
prevEnd = argsClose + 1;
|
|
79
|
+
steps.push({
|
|
80
|
+
line,
|
|
81
|
+
gapBefore,
|
|
82
|
+
gapAfter: "",
|
|
83
|
+
key: stepKey(verb, src, structure, chainStart, dot, argsOpen, argsClose),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < steps.length; i++)
|
|
87
|
+
steps[i].gapAfter =
|
|
88
|
+
i + 1 < steps.length ? steps[i + 1].gapBefore : src.slice(prevEnd);
|
|
89
|
+
return steps;
|
|
90
|
+
}
|
|
91
|
+
/** The key of one action, or `undefined` when its locator or target is unreadable. */
|
|
92
|
+
function stepKey(verb, src, structure, chainStart, dot, argsOpen, argsClose) {
|
|
93
|
+
let chain = src.slice(chainStart, dot);
|
|
94
|
+
const args = src.slice(argsOpen + 1, argsClose);
|
|
95
|
+
let target = "";
|
|
96
|
+
if (!/\(/.test(structure.slice(chainStart, dot))) {
|
|
97
|
+
// No locator call in the chain: the legacy `page.click("#save")` form, whose
|
|
98
|
+
// first argument is the selector; or `page.goto(url)`.
|
|
99
|
+
target = firstArgument(structure, src, argsOpen + 1, argsClose);
|
|
100
|
+
if (!target)
|
|
74
101
|
return undefined;
|
|
75
|
-
|
|
102
|
+
}
|
|
103
|
+
else if (TARGET_ARG.has(verb))
|
|
104
|
+
target = args;
|
|
105
|
+
if (verb === "goto") {
|
|
106
|
+
chain = "";
|
|
107
|
+
// A template target is keyed on its text, `${…}` included: generated helpers
|
|
108
|
+
// prefix every path with one module constant (`${baseUrl}/login`), and
|
|
109
|
+
// `/orders/${id}` is one page whatever the id. A bare variable is not a
|
|
110
|
+
// target this can read.
|
|
111
|
+
if (/^\s*`/.test(target))
|
|
112
|
+
target = "`" + target.trim().slice(1, -1) + "`";
|
|
113
|
+
else if (!/^\s*["']/.test(target))
|
|
76
114
|
return undefined;
|
|
77
|
-
const step = [verb, normalize(chain), normalize(target)]
|
|
78
|
-
.filter(Boolean)
|
|
79
|
-
.join(" ");
|
|
80
|
-
steps.push(step);
|
|
81
115
|
}
|
|
82
|
-
|
|
116
|
+
if (hasDynamicSelector(chain) || hasDynamicSelector(target))
|
|
117
|
+
return undefined;
|
|
118
|
+
if (!/["'`]/.test(chain) && !/["'`]/.test(target))
|
|
119
|
+
return undefined;
|
|
120
|
+
return [verb, normalize(chain), normalize(target)].filter(Boolean).join(" ");
|
|
121
|
+
}
|
|
122
|
+
/** `click empty-cart-btn; fill qty` → `click \`empty-cart-btn\`; fill \`qty\`` — the
|
|
123
|
+
* key as the agent reads it, selectors quoted so a `#id` never reads as prose. */
|
|
124
|
+
export function describeActionKey(key) {
|
|
125
|
+
return key
|
|
126
|
+
.split("; ")
|
|
127
|
+
.map((step) => {
|
|
128
|
+
const at = step.indexOf(" ");
|
|
129
|
+
return `${step.slice(0, at)} \`${step.slice(at + 1)}\``;
|
|
130
|
+
})
|
|
131
|
+
.join("; ");
|
|
83
132
|
}
|
|
84
133
|
/** Index of the `)` that closes the `(` at `open`, on a string-blanked copy. */
|
|
85
134
|
export function matchingClose(structure, open) {
|
|
@@ -170,7 +219,11 @@ function hasDynamicSelector(text) {
|
|
|
170
219
|
return true;
|
|
171
220
|
return false;
|
|
172
221
|
}
|
|
173
|
-
/** Whitespace and braces dropped outside string literals, `'` normalised to `"
|
|
222
|
+
/** Whitespace and braces dropped outside string literals, `'` normalised to `"` —
|
|
223
|
+
* the spelling a locator chain has inside a key. */
|
|
224
|
+
export function normalizeChain(text) {
|
|
225
|
+
return normalize(text);
|
|
226
|
+
}
|
|
174
227
|
function normalize(text) {
|
|
175
228
|
let out = "";
|
|
176
229
|
let quote = null;
|
|
@@ -243,7 +296,12 @@ function toPlaywrightJs(body) {
|
|
|
243
296
|
* semicolons, to the end of its `.`-continued lines — so a Prettier-wrapped chain
|
|
244
297
|
* is kept whole. A name bound more than once (`for (const row of rows)`), reassigned
|
|
245
298
|
* (`t = page.…`), or used as a parameter is ambiguous and is left alone; a variable
|
|
246
|
-
* receiver is unreadable.
|
|
299
|
+
* receiver is unreadable.
|
|
300
|
+
*
|
|
301
|
+
* Deliberately file-wide when the sibling scan hands it a whole spec: a name bound
|
|
302
|
+
* in two `test()` blocks is then ambiguous and its steps lose their key, and the
|
|
303
|
+
* final substitution runs on raw text, so an alias name inside a selector string
|
|
304
|
+
* is rewritten too. Both only ever lose a match, never invent one. */
|
|
247
305
|
function resolveLocatorAliases(body) {
|
|
248
306
|
const structure = blankStringContents(body);
|
|
249
307
|
const aliases = new Map();
|
|
@@ -251,7 +309,13 @@ function resolveLocatorAliases(body) {
|
|
|
251
309
|
const name = m[1];
|
|
252
310
|
const from = m.index + m[0].length;
|
|
253
311
|
const to = valueEnd(structure, from);
|
|
254
|
-
|
|
312
|
+
// Joined onto one line: the substitution must not move the lines of the
|
|
313
|
+
// actions after it (actionStepsOf reports each action's line).
|
|
314
|
+
aliases.set(name, body
|
|
315
|
+
.slice(from, to)
|
|
316
|
+
.trim()
|
|
317
|
+
.replace(/;$/, "")
|
|
318
|
+
.replace(/\s*\n\s*/g, " "));
|
|
255
319
|
}
|
|
256
320
|
for (const name of [...aliases.keys()]) {
|
|
257
321
|
const esc = name.replace(/\$/g, "\\$");
|
|
@@ -271,6 +335,24 @@ function resolveLocatorAliases(body) {
|
|
|
271
335
|
return body;
|
|
272
336
|
return body.replace(/(?<![\w$.])([A-Za-z_$][\w$]*)(?=\s*\.)/g, (id) => aliases.get(id) ?? id);
|
|
273
337
|
}
|
|
338
|
+
/** The statements of a fragment as `[start, end)` spans on `structure` (a
|
|
339
|
+
* string-blanked copy): each cut where valueEnd cuts — a `;` at depth 0, or a line
|
|
340
|
+
* end at depth 0 that the next line does not continue with `.` — so a
|
|
341
|
+
* Prettier-wrapped chain is one statement, not one per line. */
|
|
342
|
+
export function statementSpans(structure) {
|
|
343
|
+
const out = [];
|
|
344
|
+
let i = 0;
|
|
345
|
+
while (i < structure.length) {
|
|
346
|
+
while (i < structure.length && /[\s;]/.test(structure[i]))
|
|
347
|
+
i++;
|
|
348
|
+
if (i >= structure.length)
|
|
349
|
+
break;
|
|
350
|
+
const end = valueEnd(structure, i);
|
|
351
|
+
out.push([i, end]);
|
|
352
|
+
i = end + 1;
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
274
356
|
/** End of an initialiser that starts at `from`: the `;` at paren depth 0, or a
|
|
275
357
|
* newline at depth 0 whose next line does not continue the chain with `.`. */
|
|
276
358
|
function valueEnd(structure, from) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type UtilsHelper } from "./parse.js";
|
|
2
|
+
import { type UtilsLanguageSpec } from "./language-spec.js";
|
|
3
|
+
import { type InlineCallSite, type SiblingFile } from "./call-sites.js";
|
|
4
|
+
/**
|
|
5
|
+
* The browser family's sibling scan: a contiguous run of actions in OTHER
|
|
6
|
+
* Skyramp-generated tests beside `testFile` whose action sequence equals a utils
|
|
7
|
+
* helper's — the inline copy of a shared step (measured: three cohorts in a row, a
|
|
8
|
+
* run created the helper, rewired the two copies it happened to read and left the
|
|
9
|
+
* third, naming it in its own report).
|
|
10
|
+
*
|
|
11
|
+
* A browser step is a SEQUENCE, so the match is a sequence match: each helper's key
|
|
12
|
+
* (verb + locator chain per action, in body order — the same key STEP 3b matches a
|
|
13
|
+
* customer's helper on) is looked for as a run of consecutive actions in the
|
|
14
|
+
* sibling. Two things stand between "consecutive" and "one call can replace it":
|
|
15
|
+
*
|
|
16
|
+
* - **What sits between two actions of the run.** A structural wait (a presence
|
|
17
|
+
* matcher on a structural locator, a `waitFor…`), a fixed sleep or a comment is
|
|
18
|
+
* transparent — the helper's own body may hold the same, and the key ignores it
|
|
19
|
+
* either way. ANYTHING ELSE breaks the run: a value assertion (it cannot move
|
|
20
|
+
* across an action — the reuse policy's own boundary rule), a page-error guard,
|
|
21
|
+
* another call, a binding, or the end of the block the run started in. The test
|
|
22
|
+
* the gap is held to is the one the verify pass holds a helper body to, so a run
|
|
23
|
+
* is matched only where a helper could have held every line of it.
|
|
24
|
+
* - **A step this cannot read.** A helper whose key is undefined is no candidate;
|
|
25
|
+
* a sibling action with an undefined key equals nothing, so no run crosses it.
|
|
26
|
+
* Both fail open — no match, no finding — as STEP 3b's decline rule does.
|
|
27
|
+
*
|
|
28
|
+
* Where two helpers' keys start the same run, the longer wins; the run's actions are
|
|
29
|
+
* consumed and the scan continues after it. A run inside the sibling's own local
|
|
30
|
+
* helper is reported with that helper's name, as the API scan does.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findSiblingInlineActionSites(testFile: string, helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings: SiblingFile[] | undefined): Promise<InlineCallSite[]>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { blankComments, localHelperAt, localHelpersOf, } from "./parse.js";
|
|
3
|
+
import { expectArgs, HELPER_FAMILIES, } from "./language-spec.js";
|
|
4
|
+
import { actionStepsOf, blankStringContents, normalizeChain, statementSpans, } from "./action-key.js";
|
|
5
|
+
import { realpath } from "./locate.js";
|
|
6
|
+
import { nonUtilsSiblings, } from "./call-sites.js";
|
|
7
|
+
/** A presence assertion as `[normalised locator, matcher]`, or undefined when the
|
|
8
|
+
* text is not one this can read. */
|
|
9
|
+
function presenceWait(text, spec) {
|
|
10
|
+
if (!spec.assertionRe.test(text))
|
|
11
|
+
return undefined;
|
|
12
|
+
const matcher = /\)\s*\.\s*((?:not[._]\s*)?to\w+)\s*\(/.exec(text);
|
|
13
|
+
if (!matcher)
|
|
14
|
+
return undefined;
|
|
15
|
+
return [normalizeChain(expectArgs(text)), matcher[1].replace(/\s+/g, "")];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The browser family's sibling scan: a contiguous run of actions in OTHER
|
|
19
|
+
* Skyramp-generated tests beside `testFile` whose action sequence equals a utils
|
|
20
|
+
* helper's — the inline copy of a shared step (measured: three cohorts in a row, a
|
|
21
|
+
* run created the helper, rewired the two copies it happened to read and left the
|
|
22
|
+
* third, naming it in its own report).
|
|
23
|
+
*
|
|
24
|
+
* A browser step is a SEQUENCE, so the match is a sequence match: each helper's key
|
|
25
|
+
* (verb + locator chain per action, in body order — the same key STEP 3b matches a
|
|
26
|
+
* customer's helper on) is looked for as a run of consecutive actions in the
|
|
27
|
+
* sibling. Two things stand between "consecutive" and "one call can replace it":
|
|
28
|
+
*
|
|
29
|
+
* - **What sits between two actions of the run.** A structural wait (a presence
|
|
30
|
+
* matcher on a structural locator, a `waitFor…`), a fixed sleep or a comment is
|
|
31
|
+
* transparent — the helper's own body may hold the same, and the key ignores it
|
|
32
|
+
* either way. ANYTHING ELSE breaks the run: a value assertion (it cannot move
|
|
33
|
+
* across an action — the reuse policy's own boundary rule), a page-error guard,
|
|
34
|
+
* another call, a binding, or the end of the block the run started in. The test
|
|
35
|
+
* the gap is held to is the one the verify pass holds a helper body to, so a run
|
|
36
|
+
* is matched only where a helper could have held every line of it.
|
|
37
|
+
* - **A step this cannot read.** A helper whose key is undefined is no candidate;
|
|
38
|
+
* a sibling action with an undefined key equals nothing, so no run crosses it.
|
|
39
|
+
* Both fail open — no match, no finding — as STEP 3b's decline rule does.
|
|
40
|
+
*
|
|
41
|
+
* Where two helpers' keys start the same run, the longer wins; the run's actions are
|
|
42
|
+
* consumed and the scan continues after it. A run inside the sibling's own local
|
|
43
|
+
* helper is reported with that helper's name, as the API scan does.
|
|
44
|
+
*/
|
|
45
|
+
export async function findSiblingInlineActionSites(testFile, helpersByFile, spec, preReadSiblings) {
|
|
46
|
+
const sequences = helperSequences(helpersByFile, spec);
|
|
47
|
+
if (sequences.length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const siblings = await nonUtilsSiblings(testFile, [...helpersByFile.keys()], spec, preReadSiblings, false);
|
|
50
|
+
const self = await realpath(testFile);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const { file, content } of siblings) {
|
|
53
|
+
if (path.resolve(file) === self)
|
|
54
|
+
continue;
|
|
55
|
+
const localHelpers = localHelpersOf(content, spec);
|
|
56
|
+
// Comments blanked, strings kept — the same reading the helper's key was made
|
|
57
|
+
// from, so a commented-out click is not a step and the selectors compare.
|
|
58
|
+
const steps = actionStepsOf(blankComments(content, spec), spec.language);
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < steps.length) {
|
|
61
|
+
const match = sequences.find((s) => runMatches(steps, i, s, spec));
|
|
62
|
+
if (!match) {
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const first = steps[i];
|
|
67
|
+
const last = steps[i + match.steps.length - 1];
|
|
68
|
+
const owner = localHelperAt(localHelpers, first.line);
|
|
69
|
+
out.push({
|
|
70
|
+
kind: "sequence",
|
|
71
|
+
file,
|
|
72
|
+
line: first.line,
|
|
73
|
+
endLine: last.line,
|
|
74
|
+
actionKey: match.key,
|
|
75
|
+
helper: match.helper,
|
|
76
|
+
utilsFile: match.utilsFile,
|
|
77
|
+
...(owner ? { localHelper: owner.name } : {}),
|
|
78
|
+
});
|
|
79
|
+
i += match.steps.length;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** Whether the helper's sequence is the run of actions starting at `at`, with
|
|
85
|
+
* nothing between its actions that a helper body could not hold, and no wait in
|
|
86
|
+
* or right after the run that contradicts one of the helper's own. */
|
|
87
|
+
function runMatches(steps, at, helper, spec) {
|
|
88
|
+
const want = helper.steps;
|
|
89
|
+
if (at + want.length > steps.length)
|
|
90
|
+
return false;
|
|
91
|
+
for (let k = 0; k < want.length; k++) {
|
|
92
|
+
const step = steps[at + k];
|
|
93
|
+
if (step.key === undefined || step.key !== want[k])
|
|
94
|
+
return false;
|
|
95
|
+
if (k > 0) {
|
|
96
|
+
const between = transparentStatements(step.gapBefore, spec);
|
|
97
|
+
if (between === undefined)
|
|
98
|
+
return false;
|
|
99
|
+
if (contradictsWaits(between, helper.waits, spec))
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// The structural waits that follow the last action are the run's too: a wait the
|
|
104
|
+
// helper would perform, or its opposite.
|
|
105
|
+
const trailing = transparentStatements(steps[at + want.length - 1].gapAfter, spec, { prefix: true });
|
|
106
|
+
return !contradictsWaits(trailing ?? [], helper.waits, spec);
|
|
107
|
+
}
|
|
108
|
+
/** Whether one of `statements` asserts an element the helper waits on with a
|
|
109
|
+
* different matcher or negation. */
|
|
110
|
+
function contradictsWaits(statements, waits, spec) {
|
|
111
|
+
for (const s of statements) {
|
|
112
|
+
const wait = presenceWait(s.replace(/^await\s+/, ""), spec);
|
|
113
|
+
if (!wait)
|
|
114
|
+
continue;
|
|
115
|
+
const held = waits.get(wait[0]);
|
|
116
|
+
if (held !== undefined && held !== wait[1])
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The statements of `gap` that a shared browser helper may hold — a permitted
|
|
123
|
+
* structural wait, as the verify pass judges one (`HELPER_FAMILIES.browser`), or a
|
|
124
|
+
* `waitFor…` — or `undefined` when one is not, or when the gap closes a bracket it
|
|
125
|
+
* did not open. With `prefix`, the leading transparent statements up to the first
|
|
126
|
+
* that is not: the waits that follow a run, read until the run's own text ends.
|
|
127
|
+
* Statements are cut as statementSpans cuts them, on a string-blanked copy, so a
|
|
128
|
+
* `;` inside a literal is not a boundary and a Prettier-wrapped chain is one
|
|
129
|
+
* statement. Conservative: an unfamiliar statement ends the run.
|
|
130
|
+
*/
|
|
131
|
+
function transparentStatements(gap, spec, opts = {}) {
|
|
132
|
+
const structure = blankStringContents(gap);
|
|
133
|
+
if (!opts.prefix && leavesBlock(structure))
|
|
134
|
+
return undefined;
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const [from, to] of statementSpans(structure)) {
|
|
137
|
+
const text = gap.slice(from, to).trim();
|
|
138
|
+
// The `await` that precedes the next action's chain.
|
|
139
|
+
if (text === "await")
|
|
140
|
+
continue;
|
|
141
|
+
const body = text.replace(/^await\s+/, "");
|
|
142
|
+
const transparent = WAIT_STATEMENT_RE.test(blankStringContents(body)) ||
|
|
143
|
+
(spec.assertionRe.test(body) &&
|
|
144
|
+
HELPER_FAMILIES.browser.isAllowed(spec, body));
|
|
145
|
+
if (!transparent)
|
|
146
|
+
return opts.prefix ? out : undefined;
|
|
147
|
+
out.push(text);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/** A closing bracket the fragment did not open: the run leaves its block. */
|
|
152
|
+
function leavesBlock(structure) {
|
|
153
|
+
let depth = 0;
|
|
154
|
+
for (const ch of structure) {
|
|
155
|
+
if ("([{".includes(ch))
|
|
156
|
+
depth++;
|
|
157
|
+
else if (")]}".includes(ch) && --depth < 0)
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
/** `page.waitForTimeout(3000)`, `page.waitForLoadState()`, `page.getByTestId("x")
|
|
163
|
+
* .waitFor({ state: "visible" })` — a wait on the page or on a locator chain, as a
|
|
164
|
+
* statement of its own (not bound: `const p = page.waitForResponse(…)` is used
|
|
165
|
+
* later, and the run cannot carry it). */
|
|
166
|
+
const WAIT_STATEMENT_RE = /^(?:this\.)?page(?:\s*\.\s*[\w$]+\s*\([^;]*\))*\s*\.\s*(?:waitFor\w*|wait_for\w*)\s*\([^;]*\)\s*$/;
|
|
167
|
+
/** A step whose receiver is the keyboard or the mouse rather than a locator. */
|
|
168
|
+
function isElementlessStep(step) {
|
|
169
|
+
return /^\w+ (?:this\.)?page\.(?:keyboard|mouse)(?:\s|$)/.test(step);
|
|
170
|
+
}
|
|
171
|
+
/** Every keyed browser helper across the utils files, in file then body order, from
|
|
172
|
+
* the helpers the verify pass already parsed. The first helper to carry a key owns
|
|
173
|
+
* it; a second (the `duplicate-action` advisory's case) is not reported twice. */
|
|
174
|
+
function helperSequences(helpersByFile, spec) {
|
|
175
|
+
const out = [];
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
for (const [utilsFile, helpers] of helpersByFile)
|
|
178
|
+
for (const h of helpers) {
|
|
179
|
+
if (!h.actionKey || !h.actionSteps || seen.has(h.actionKey))
|
|
180
|
+
continue;
|
|
181
|
+
// A key of bare keyboard or mouse steps names no element: `press "Escape"`
|
|
182
|
+
// is what closes a dialog in one helper and a menu in the next, and every
|
|
183
|
+
// Escape in every sibling matched it (measured: fixture 22, the advisory
|
|
184
|
+
// named a menu close for a dialog-closing helper). One locator-bearing step
|
|
185
|
+
// is enough to identify the sequence.
|
|
186
|
+
if (h.actionKey.split("; ").every(isElementlessStep))
|
|
187
|
+
continue;
|
|
188
|
+
seen.add(h.actionKey);
|
|
189
|
+
out.push({
|
|
190
|
+
helper: h.name,
|
|
191
|
+
utilsFile,
|
|
192
|
+
key: h.actionKey,
|
|
193
|
+
steps: h.actionSteps,
|
|
194
|
+
waits: new Map(h.assertions
|
|
195
|
+
.map((a) => presenceWait(a.text, spec))
|
|
196
|
+
.filter((w) => w !== undefined)),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Longest first, so the run a longer sequence covers is not claimed by a shorter
|
|
200
|
+
// helper that shares its opening actions.
|
|
201
|
+
return out.sort((a, b) => b.steps.length - a.steps.length);
|
|
202
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
-
import { paramName, parseUtilsFile, signatureParams, } from "./parse.js";
|
|
2
|
+
import { localHelpersOf, paramName, parseUtilsFile, signatureParams, } from "./parse.js";
|
|
3
3
|
import { callText, generatedSiblings, REQUEST_CALL_RE, } from "./call-sites.js";
|
|
4
4
|
/** Words a fallback idiom spells that name no value: `x or {}`, `x ?? {}`,
|
|
5
5
|
* `x if x else None`. What remains after them is what the override actually sends. */
|
|
@@ -320,9 +320,7 @@ export async function findUnreachableBodies(testFile, utilsFiles, spec, preReadS
|
|
|
320
320
|
(await generatedSiblings(testFile, spec, { includeSelf: true }));
|
|
321
321
|
const out = [];
|
|
322
322
|
for (const { file, content } of siblings) {
|
|
323
|
-
for (const h of
|
|
324
|
-
if (/^test/i.test(h.name))
|
|
325
|
-
continue;
|
|
323
|
+
for (const h of localHelpersOf(content, spec)) {
|
|
326
324
|
if (!h.method || !h.normalizedPath)
|
|
327
325
|
continue;
|
|
328
326
|
const held = frozen.get(`${h.method} ${h.normalizedPath}`);
|
|
@@ -22,14 +22,13 @@ export declare function generatedSiblings(testFile: string, spec: UtilsLanguageS
|
|
|
22
22
|
}): Promise<SiblingFile[]>;
|
|
23
23
|
/** An SDK request call. Global: clone with `new RegExp(...)` before `matchAll`. */
|
|
24
24
|
export declare const REQUEST_CALL_RE: RegExp;
|
|
25
|
-
|
|
25
|
+
interface InlineSiteBase {
|
|
26
26
|
/** Absolute path of the sibling test. */
|
|
27
27
|
file: string;
|
|
28
|
-
/** 1-based line of the request call. */
|
|
28
|
+
/** 1-based line of the request call, or of the first action of the sequence. */
|
|
29
29
|
line: number;
|
|
30
|
-
method
|
|
31
|
-
|
|
32
|
-
/** The utils helper that wraps the same method+path. */
|
|
30
|
+
/** The utils helper that wraps the same method+path, or performs the same
|
|
31
|
+
* action sequence. */
|
|
33
32
|
helper: string;
|
|
34
33
|
/** Absolute path of the utils file that defines `helper` — with several modules in
|
|
35
34
|
* scope, the advisory names this one, not the list. */
|
|
@@ -39,6 +38,19 @@ export interface InlineCallSite {
|
|
|
39
38
|
* block-by-block. */
|
|
40
39
|
localHelper?: string;
|
|
41
40
|
}
|
|
41
|
+
/** An inline copy of a shared step: one request keyed by route (the API family), or
|
|
42
|
+
* a run of actions keyed by action sequence (the browser family). */
|
|
43
|
+
export type InlineCallSite = (InlineSiteBase & {
|
|
44
|
+
kind: "request";
|
|
45
|
+
method: string;
|
|
46
|
+
path: string;
|
|
47
|
+
}) | (InlineSiteBase & {
|
|
48
|
+
kind: "sequence";
|
|
49
|
+
/** The action key the sequence and the helper share (see actionKeyOf). */
|
|
50
|
+
actionKey: string;
|
|
51
|
+
/** 1-based line of the sequence's last action. */
|
|
52
|
+
endLine: number;
|
|
53
|
+
});
|
|
42
54
|
/**
|
|
43
55
|
* Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
|
|
44
56
|
* utils helper already wraps — the reuse the prompt asks for and the agent does not
|
|
@@ -54,7 +66,9 @@ export interface InlineCallSite {
|
|
|
54
66
|
* normalised path, nothing else — literals are what the call's arguments are for.
|
|
55
67
|
*
|
|
56
68
|
* Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
|
|
57
|
-
* flow owns, so it informs rather than gates. Never throws.
|
|
69
|
+
* flow owns, so it informs rather than gates. Never throws. The API family's scan;
|
|
70
|
+
* findSiblingInlineActionSites is the browser family's, and inlineCallSiteFamily
|
|
71
|
+
* decides which runs.
|
|
58
72
|
*/
|
|
59
73
|
export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<InlineCallSite[]>;
|
|
60
74
|
/** One operation written in two or more sibling generated specs that no shared
|
|
@@ -95,6 +109,10 @@ export interface UnextractedSite {
|
|
|
95
109
|
* different question. Advisory by design — consolidation is a judgement. Never throws.
|
|
96
110
|
*/
|
|
97
111
|
export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<UnextractedDuplicate[]>;
|
|
112
|
+
/** The generated siblings less the utils files themselves (a utils file carries no
|
|
113
|
+
* codegen marker, so this is belt and braces). Pre-read siblings are used as given —
|
|
114
|
+
* the caller's walk decides whether the spec itself is among them. */
|
|
115
|
+
export declare function nonUtilsSiblings(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings: SiblingFile[] | undefined, includeSelf: boolean): Promise<SiblingFile[]>;
|
|
98
116
|
/** Lines after a call scanned for its schema assertion — the generated shape puts it
|
|
99
117
|
* immediately after the status-code assertion. */
|
|
100
118
|
export declare const SCHEMA_SCAN_LINES = 12;
|
|
@@ -144,3 +162,4 @@ export declare function callShape(call: string): Set<string>;
|
|
|
144
162
|
export declare function shapeKeys(call: string): Set<string>;
|
|
145
163
|
/** Text of the parenthesised argument list starting at the `(` at `open`. */
|
|
146
164
|
export declare function callText(content: string, open: number): string;
|
|
165
|
+
export {};
|