@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.
Files changed (119) hide show
  1. package/build/execution/wrapperConfig.d.ts +4 -0
  2. package/build/execution/wrapperConfig.js +14 -2
  3. package/build/prompts/code-reuse.d.ts +5 -1
  4. package/build/prompts/code-reuse.js +16 -10
  5. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  6. package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
  7. package/build/prompts/pom-aware-code-reuse.js +38 -6
  8. package/build/prompts/reuse-hand-off.d.ts +4 -0
  9. package/build/prompts/reuse-hand-off.js +10 -5
  10. package/build/prompts/shared-helper-policy.d.ts +5 -0
  11. package/build/prompts/shared-helper-policy.js +5 -0
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
  14. package/build/prompts/testbot/testbot-prompts.js +33 -16
  15. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  16. package/build/recommendation/fakeVerifyContext.js +16 -0
  17. package/build/recommendation/runVerifiers.js +2 -0
  18. package/build/recommendation/types.d.ts +12 -0
  19. package/build/recommendation/verifierContracts.d.ts +24 -2
  20. package/build/recommendation/verifierContracts.js +32 -4
  21. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  22. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  23. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  24. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  25. package/build/services/TestGenerationService.js +4 -0
  26. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  27. package/build/tools/code-refactor/caller-gate.js +174 -0
  28. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  29. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  30. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  32. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  33. package/build/tools/code-refactor/helper-callers.js +85 -0
  34. package/build/tools/code-refactor/modularizationTool.js +10 -0
  35. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  36. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  37. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  38. package/build/tools/code-refactor/retrofit-state.js +11 -12
  39. package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
  40. package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
  41. package/build/tools/code-refactor/reuse-record-store.js +126 -0
  42. package/build/tools/code-refactor/reuse-state.d.ts +18 -6
  43. package/build/tools/code-refactor/reuse-state.js +78 -131
  44. package/build/tools/code-refactor/utils-verify-gates.js +3 -3
  45. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  46. package/build/tools/code-refactor/verify-gates.js +8 -1
  47. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  48. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  49. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  50. package/build/tools/submitReportTool.js +91 -29
  51. package/build/tools/test-management/actionsTool.js +7 -0
  52. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  53. package/build/tools/test-management/registerTestPlanTool.js +70 -22
  54. package/build/types/RepositoryAnalysis.d.ts +20 -20
  55. package/build/types/ReuseOutcome.d.ts +96 -0
  56. package/build/types/TestExecution.d.ts +11 -0
  57. package/build/types/TestExecution.js +19 -0
  58. package/build/types/index.d.ts +1 -1
  59. package/build/utils/branchDiff.d.ts +1 -1
  60. package/build/utils/branchDiff.js +1 -1
  61. package/build/utils/changedRuns.d.ts +13 -0
  62. package/build/utils/changedRuns.js +56 -0
  63. package/build/utils/featureFlags.d.ts +8 -10
  64. package/build/utils/featureFlags.js +23 -21
  65. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  66. package/build/utils/normalizeSkyrampImports.js +24 -17
  67. package/build/utils/pom-scope/ownership.d.ts +19 -0
  68. package/build/utils/pom-scope/ownership.js +31 -0
  69. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  70. package/build/utils/pom-scope/pom-files.js +8 -0
  71. package/build/utils/pom-scope/scoring.js +13 -1
  72. package/build/utils/pom-scope/strip.d.ts +10 -0
  73. package/build/utils/pom-scope/strip.js +9 -0
  74. package/build/utils/pom-verify/verify.js +13 -2
  75. package/build/utils/reuseRouting.d.ts +30 -2
  76. package/build/utils/reuseRouting.js +36 -15
  77. package/build/utils/testDependencyPolicy.js +4 -16
  78. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  79. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  80. package/build/utils/utils-verify/body-reach.js +4 -1
  81. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  82. package/build/utils/utils-verify/call-sites.js +12 -6
  83. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  84. package/build/utils/utils-verify/changed-helpers.js +91 -0
  85. package/build/utils/utils-verify/head.d.ts +48 -0
  86. package/build/utils/utils-verify/head.js +115 -0
  87. package/build/utils/utils-verify/importers.d.ts +59 -10
  88. package/build/utils/utils-verify/importers.js +121 -31
  89. package/build/utils/utils-verify/in-house.js +26 -31
  90. package/build/utils/utils-verify/index.d.ts +3 -0
  91. package/build/utils/utils-verify/index.js +3 -0
  92. package/build/utils/utils-verify/language-spec.js +10 -2
  93. package/build/utils/utils-verify/locate.d.ts +3 -0
  94. package/build/utils/utils-verify/locate.js +6 -6
  95. package/build/utils/utils-verify/parse.d.ts +13 -1
  96. package/build/utils/utils-verify/parse.js +122 -49
  97. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  98. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  99. package/build/utils/utils-verify/stage.js +4 -16
  100. package/build/utils/utils-verify/verify.d.ts +9 -3
  101. package/build/utils/utils-verify/verify.js +26 -12
  102. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  103. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  104. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  105. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
  106. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
  110. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  111. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  112. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  115. package/node_modules/playwright/package.json +1 -1
  116. package/package.json +2 -2
  117. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  118. package/plugin/prompts/plan-tests.md +2 -2
  119. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -131,6 +131,79 @@ export interface RetrofitOutcome {
131
131
  file: string;
132
132
  execution?: TestExecutionStatus;
133
133
  }
134
+ /** What a run did to a helper that already existed in the repository. A helper the
135
+ * module gained is not here: this run created it, and it has no incumbent callers
136
+ * whose behaviour it could change. */
137
+ export type HelperChangeKind =
138
+ /** The parameter list differs — the change a caller's own diff can be read
139
+ * against, and the one an added argument or a re-defaulted argument lands in
140
+ * (a helper re-defaulted to a rejection status is the recorded case). */
141
+ "signature"
142
+ /** The parameter list is byte-identical and the body is not. The dangerous case
143
+ * for a browser helper: a selector change alters what every caller does and
144
+ * appears in no calling test's diff. */
145
+ | "body"
146
+ /** The incumbent defined it and the module no longer does. Every caller's import
147
+ * now names something that is not there. */
148
+ | "removed";
149
+ /** A helper this run changed in a module that already existed in the repository,
150
+ * with the tests that call it.
151
+ *
152
+ * A shared helper's change reaches every caller at once — that is the feature's
153
+ * value and its danger. A caller the run did not touch is not delivered, so nothing
154
+ * executed it, and a break in one reached the customer's own continuous integration
155
+ * instead of this report. A browser helper is the worse case: its selector can
156
+ * change with no signature change at all, and the change appears in no calling
157
+ * test's diff. */
158
+ export interface ChangedHelperEntry {
159
+ /** REPOSITORY-RELATIVE path (posix) of the module that defines it — per helper,
160
+ * as {@link OrphanedHelper} names its: a delivery can carry several modules, and
161
+ * the same helper name in two of them is two entries a reader must tell apart. */
162
+ utilsFile: string;
163
+ helper: string;
164
+ change: HelperChangeKind;
165
+ /** The Skyramp-generated tests that call it, by BASENAME, each with the execution
166
+ * recorded against the module as it stands.
167
+ *
168
+ * SCOPED TO THE MODULE'S OWN DIRECTORY, which is as far as the caller walk reads.
169
+ * A generated test that imports the module from another directory is not here, so
170
+ * this is a lower bound: read it as "the callers found beside the module", never
171
+ * as "every test that calls it". The gate acts only on what is listed, so a caller
172
+ * it cannot see costs a report line and an unrun test — never a false block, which
173
+ * is the direction this feature chooses everywhere.
174
+ *
175
+ * ABSENT means even that much could not be established — never "no test calls
176
+ * it". An empty array is the positive statement that the walk ran and found
177
+ * none beside the module. A caller with no `execution` and no `deliveredByThisRun`
178
+ * was not run: the run could not execute it (a native suite, or a test the prescan
179
+ * never inventoried), which is disclosed rather than blocked on. */
180
+ callers?: ChangedHelperCaller[];
181
+ }
182
+ export interface ChangedHelperCaller {
183
+ /** BASENAME of the calling test. */
184
+ file: string;
185
+ execution?: TestExecutionStatus;
186
+ /** This run delivered or edited the caller, so its own row in this report carries
187
+ * its execution and its own checks judge it. Read an absent `execution` as "not
188
+ * run" only when this is absent too. */
189
+ deliveredByThisRun?: true;
190
+ }
191
+ /** A helper in a shared module that NO Skyramp-generated test imports.
192
+ *
193
+ * A run can delete a test, and a helper that test was the last importer of stays in
194
+ * the module. The customer then maintains code nothing calls, and nothing in the
195
+ * pull request says so — the agent was told, and the reviewer was not.
196
+ *
197
+ * REPORTED, never removed, and never gated. Removing a helper that is still called
198
+ * is far worse than leaving a dead one, and the caller list this is read from is a
199
+ * lower bound scoped to the module's directory. A reader decides; the run does not. */
200
+ export interface OrphanedHelper {
201
+ /** REPOSITORY-RELATIVE path (posix) of the module that defines it. Per helper, not
202
+ * per row: a delivery can carry several modules, and a reader needs the file to
203
+ * open. */
204
+ utilsFile: string;
205
+ helper: string;
206
+ }
134
207
  /** Shared-helper reuse for an API test, re-derived from the delivered files at report
135
208
  * time — nothing here is read back from what the agent or the verify pass recorded.
136
209
  * Absent altogether when no utils file exists for the test (the same omission rule
@@ -159,6 +232,16 @@ export interface HelperReuseOutcome {
159
232
  * wraps — reuse that was available and not taken. Omitted at zero. Informational,
160
233
  * like {@link ReuseOutcome.missedReuse}. */
161
234
  siblingInlineCallSites?: number;
235
+ /** Helpers in `utilsFile` that no Skyramp-generated test imports — see
236
+ * {@link OrphanedHelper}. Omitted when there are none, and omitted (with
237
+ * `orphanScanIncomplete` set) when the walk could not establish the importers at
238
+ * all. Informational: nothing gates on it. */
239
+ orphanedHelpers?: OrphanedHelper[];
240
+ /** Present — and only ever `true` — when the importer walk could not read every
241
+ * file beside the module, so no helper is reported as orphaned. Absent means the
242
+ * walk ran, so an absent `orphanedHelpers` then means none were found rather than
243
+ * that nothing was looked for. */
244
+ orphanScanIncomplete?: true;
162
245
  /** Whether the delivered utils file holds the shared-helper invariants: one helper
163
246
  * per method+path, status-code-only assertions, method+resource names — with
164
247
  * documented declines (`reuse-verify: allow …`) counted as holding. */
@@ -296,6 +379,19 @@ export interface ReuseOutcome {
296
379
  * not the module, and is most telling when no module exists at all — the case
297
380
  * where `helpers` is rightly absent. Omitted at zero. Informational. */
298
381
  unextractedDuplicateSites?: number;
382
+ /** Helpers that already existed in the repository and that this run CHANGED, each
383
+ * with the tests that call it. Omitted when the run changed no incumbent helper —
384
+ * which is every first run, where the module is created rather than edited. A
385
+ * helper an earlier run created and this one changed appears here as changed,
386
+ * never as created.
387
+ *
388
+ * Top level, not on the helpers row, for the same reason as
389
+ * {@link unextractedDuplicateSites}: it describes what happened to a module, and
390
+ * it is most telling when NO module is there to describe — a run that deleted or
391
+ * renamed the shared module removes every helper at once and leaves `helpers`
392
+ * rightly absent, which is exactly when a reader most needs to be told. See
393
+ * {@link ChangedHelperEntry}. */
394
+ changedHelpers?: ChangedHelperEntry[];
299
395
  verification?: ReuseVerificationOutcome;
300
396
  /** Why `verification` is `failed` on the page-object path, on the same terms as
301
397
  * {@link HelperReuseOutcome.verificationFailures}: present only when the
@@ -14,6 +14,17 @@ export declare enum TestExecutionStatus {
14
14
  /** Execution was expected (UPDATE/REGENERATE) but no result was recorded. */
15
15
  Unknown = "Unknown"
16
16
  }
17
+ /** Whether a status is a RESULT: the test ran and pass, fail or error is what it
18
+ * did. `Skipped` is a decision not to execute and `Unknown` is an execution that was
19
+ * expected and recorded nothing — neither may satisfy a demand for a run, or a run
20
+ * that never touched the file would clear the debt for it. */
21
+ export declare function isResult(status: TestExecutionStatus): boolean;
22
+ /** The status of `record` when it is a result that ran the bytes written at
23
+ * `sinceMs` or later, and `undefined` otherwise. THE freshness rule, for every gate
24
+ * that asks whether a file has been run as it now stands: an execution older than
25
+ * the last write to what it exercised ran bytes that no longer exist. Which write —
26
+ * the file's own, or a module's under it — is the caller's to say. */
27
+ export declare function freshExecution(record: Pick<TestExecutionData, "status" | "executedAt"> | undefined, sinceMs: number): TestExecutionStatus | undefined;
17
28
  export interface TestExecutionData {
18
29
  status: TestExecutionStatus;
19
30
  executedAt: string;
@@ -14,3 +14,22 @@ export var TestExecutionStatus;
14
14
  /** Execution was expected (UPDATE/REGENERATE) but no result was recorded. */
15
15
  TestExecutionStatus["Unknown"] = "Unknown";
16
16
  })(TestExecutionStatus || (TestExecutionStatus = {}));
17
+ /** Whether a status is a RESULT: the test ran and pass, fail or error is what it
18
+ * did. `Skipped` is a decision not to execute and `Unknown` is an execution that was
19
+ * expected and recorded nothing — neither may satisfy a demand for a run, or a run
20
+ * that never touched the file would clear the debt for it. */
21
+ export function isResult(status) {
22
+ return (status === TestExecutionStatus.Pass ||
23
+ status === TestExecutionStatus.Fail ||
24
+ status === TestExecutionStatus.Error);
25
+ }
26
+ /** The status of `record` when it is a result that ran the bytes written at
27
+ * `sinceMs` or later, and `undefined` otherwise. THE freshness rule, for every gate
28
+ * that asks whether a file has been run as it now stands: an execution older than
29
+ * the last write to what it exercised ran bytes that no longer exist. Which write —
30
+ * the file's own, or a module's under it — is the caller's to say. */
31
+ export function freshExecution(record, sinceMs) {
32
+ if (!record || !isResult(record.status))
33
+ return undefined;
34
+ return Date.parse(record.executedAt) >= sinceMs ? record.status : undefined;
35
+ }
@@ -4,5 +4,5 @@ export { TestType, HttpMethod } from "./TestTypes.js";
4
4
  export type { TestbotReport } from "./TestbotReport.js";
5
5
  export { IssueFoundCategory } from "./TestbotReport.js";
6
6
  export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
7
- export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, RetrofitOutcome, } from "./ReuseOutcome.js";
7
+ export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, RetrofitOutcome, ChangedHelperEntry, ChangedHelperCaller, OrphanedHelper, } from "./ReuseOutcome.js";
8
8
  export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
@@ -72,5 +72,5 @@ export declare function changedFileNames(diff: BranchDiffData): ChangedFileName[
72
72
  * compares two commits through the three-dot form, which is the CI shape. */
73
73
  export declare function computeBranchDiff(repositoryPath: string, providedBaseBranch?: string, includeUncommitted?: boolean,
74
74
  /** Names only: no diff text is computed, and `diffContent`/`diffStat` come back
75
- * empty. The plan checks read which files changed, never what changed in them. */
75
+ * empty. */
76
76
  namesOnly?: boolean): Promise<BranchDiffData>;
@@ -349,7 +349,7 @@ export function changedFileNames(diff) {
349
349
  * compares two commits through the three-dot form, which is the CI shape. */
350
350
  export async function computeBranchDiff(repositoryPath, providedBaseBranch, includeUncommitted = false,
351
351
  /** Names only: no diff text is computed, and `diffContent`/`diffStat` come back
352
- * empty. The plan checks read which files changed, never what changed in them. */
352
+ * empty. */
353
353
  namesOnly = false) {
354
354
  const git = simpleGit(repositoryPath);
355
355
  const isRepo = await git.checkIsRepo();
@@ -0,0 +1,13 @@
1
+ /** One run of consecutive changed lines in a file: lines removed, added, or both. */
2
+ export interface ChangedRun {
3
+ file: string;
4
+ /** First and last line of the run in the new file. A run that only removes
5
+ * lines has none of its own, so both name the line that now follows it. */
6
+ start: number;
7
+ end: number;
8
+ /** The run's first changed line as the diff shows it, for the evidence. */
9
+ firstLine: string;
10
+ }
11
+ /** Every run of changed lines, file by file, in the new file's numbering. A run
12
+ * whose changed lines are all blank changes nothing a test can observe. */
13
+ export declare function collectChangedRuns(rawDiff: string): ChangedRun[];
@@ -0,0 +1,56 @@
1
+ import { diffBlocksByPath } from "./branchDiff.js";
2
+ /** Every run of changed lines, file by file, in the new file's numbering. A run
3
+ * whose changed lines are all blank changes nothing a test can observe. */
4
+ export function collectChangedRuns(rawDiff) {
5
+ const runs = [];
6
+ for (const [file, block] of diffBlocksByPath(rawDiff)) {
7
+ let inHunk = false;
8
+ let newLine = 0;
9
+ let open;
10
+ const close = () => {
11
+ if (open && open.lines.some((text) => text.slice(1).trim() !== "")) {
12
+ runs.push({
13
+ file,
14
+ start: open.start,
15
+ end: open.added > 0 ? open.start + open.added - 1 : open.start,
16
+ firstLine: open.lines.find((text) => text.slice(1).trim() !== "") ?? "",
17
+ });
18
+ }
19
+ open = undefined;
20
+ };
21
+ for (const text of block.split("\n")) {
22
+ // A path printed twice holds its header twice, and the second one is no hunk.
23
+ if (text.startsWith("diff ")) {
24
+ close();
25
+ inHunk = false;
26
+ continue;
27
+ }
28
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(text);
29
+ if (hunk) {
30
+ close();
31
+ inHunk = true;
32
+ newLine = Number(hunk[1]);
33
+ continue;
34
+ }
35
+ // Only the file header spells `---`/`+++`; inside a hunk they are content.
36
+ if (!inHunk)
37
+ continue;
38
+ if (text.startsWith("-") || text.startsWith("+")) {
39
+ open ??= { start: newLine, added: 0, lines: [] };
40
+ open.lines.push(text);
41
+ if (text.startsWith("+")) {
42
+ open.added++;
43
+ newLine++;
44
+ }
45
+ }
46
+ else if (!text.startsWith("\\")) {
47
+ // `` belongs to the line before it.
48
+ close();
49
+ if (text.startsWith(" "))
50
+ newLine++;
51
+ }
52
+ }
53
+ close();
54
+ }
55
+ return runs;
56
+ }
@@ -4,8 +4,9 @@
4
4
  * Each helper in this module reads a `SKYRAMP_FEATURE_*` env var and returns
5
5
  * a boolean. These helpers treat a flag as ON only when the env var is
6
6
  * exactly `"1"`; anything else (unset, empty, "0", "true", "false", etc.) is
7
- * treated as OFF. The utils-reuse flag is additionally ON for the orgs in
8
- * `UTILS_REUSE_DEFAULT_ORGS` regardless of the env vars.
7
+ * treated as OFF. The POM-reuse and utils-reuse flags are additionally ON for
8
+ * the orgs in `POM_REUSE_DEFAULT_ORGS` and `UTILS_REUSE_DEFAULT_ORGS`
9
+ * regardless of the env vars.
9
10
  */
10
11
  /**
11
12
  * Gates BOTH consumer-side contract tests AND the SDK's "default mode"
@@ -54,15 +55,12 @@ export declare function isLocalDevEnabled(): boolean;
54
55
  export declare function isOneClickEnabled(): boolean;
55
56
  /**
56
57
  * Gates the POM-aware path of `skyramp_reuse_code`
57
- * (SKYRAMP_FEATURE_POM_REUSE=1).
58
+ * (SKYRAMP_FEATURE_POM_REUSE=1, or a `POM_REUSE_DEFAULT_ORGS` org).
58
59
  *
59
- * Deliberately NOT defaulted on for `UTILS_REUSE_DEFAULT_ORGS`: for UI tests
60
- * the POM path supersedes the modularize-first flow (see
61
- * `isModularizeFirstTarget` in utils/reuseRouting.ts), so an org-level POM
62
- * default would stop those orgs' UI tests from ever seeding a SkyrampUtils
63
- * file — the very thing the utils org default exists to provide (SKYR-4290).
64
- * And an explicit-"0"-override escape hatch cannot work either, because
65
- * testbot normalizes an unset flag to a literal "0" before forwarding.
60
+ * With both flags on, a UI test takes the POM pass first and the
61
+ * modularize-first flow over what it left inline (see `isPomFirstTarget` in
62
+ * utils/reuseRouting.ts), so a `POM_REUSE_DEFAULT_ORGS` org that also has the
63
+ * utils default still seeds a SkyrampUtils file for its UI tests.
66
64
  *
67
65
  * When OFF (default), TypeScript/JavaScript + Playwright targets are treated
68
66
  * like every other target: `skyramp_reuse_code` returns the SkyrampUtils
@@ -4,8 +4,9 @@
4
4
  * Each helper in this module reads a `SKYRAMP_FEATURE_*` env var and returns
5
5
  * a boolean. These helpers treat a flag as ON only when the env var is
6
6
  * exactly `"1"`; anything else (unset, empty, "0", "true", "false", etc.) is
7
- * treated as OFF. The utils-reuse flag is additionally ON for the orgs in
8
- * `UTILS_REUSE_DEFAULT_ORGS` regardless of the env vars.
7
+ * treated as OFF. The POM-reuse and utils-reuse flags are additionally ON for
8
+ * the orgs in `POM_REUSE_DEFAULT_ORGS` and `UTILS_REUSE_DEFAULT_ORGS`
9
+ * regardless of the env vars.
9
10
  */
10
11
  /**
11
12
  * Gates BOTH consumer-side contract tests AND the SDK's "default mode"
@@ -62,15 +63,12 @@ export function isOneClickEnabled() {
62
63
  }
63
64
  /**
64
65
  * Gates the POM-aware path of `skyramp_reuse_code`
65
- * (SKYRAMP_FEATURE_POM_REUSE=1).
66
+ * (SKYRAMP_FEATURE_POM_REUSE=1, or a `POM_REUSE_DEFAULT_ORGS` org).
66
67
  *
67
- * Deliberately NOT defaulted on for `UTILS_REUSE_DEFAULT_ORGS`: for UI tests
68
- * the POM path supersedes the modularize-first flow (see
69
- * `isModularizeFirstTarget` in utils/reuseRouting.ts), so an org-level POM
70
- * default would stop those orgs' UI tests from ever seeding a SkyrampUtils
71
- * file — the very thing the utils org default exists to provide (SKYR-4290).
72
- * And an explicit-"0"-override escape hatch cannot work either, because
73
- * testbot normalizes an unset flag to a literal "0" before forwarding.
68
+ * With both flags on, a UI test takes the POM pass first and the
69
+ * modularize-first flow over what it left inline (see `isPomFirstTarget` in
70
+ * utils/reuseRouting.ts), so a `POM_REUSE_DEFAULT_ORGS` org that also has the
71
+ * utils default still seeds a SkyrampUtils file for its UI tests.
74
72
  *
75
73
  * When OFF (default), TypeScript/JavaScript + Playwright targets are treated
76
74
  * like every other target: `skyramp_reuse_code` returns the SkyrampUtils
@@ -83,7 +81,8 @@ export function isOneClickEnabled() {
83
81
  * the verification gates apply.
84
82
  */
85
83
  export function isPomReuseEnabled() {
86
- return process.env.SKYRAMP_FEATURE_POM_REUSE === "1";
84
+ return (process.env.SKYRAMP_FEATURE_POM_REUSE === "1" ||
85
+ isDefaultOrg(POM_REUSE_DEFAULT_ORGS));
87
86
  }
88
87
  /**
89
88
  * Gates SkyrampUtils code reuse for integration tests on the testbot path, and
@@ -107,27 +106,30 @@ export function isPomReuseEnabled() {
107
106
  * yields the single reuse pass it did before the feature existed.
108
107
  */
109
108
  export function isUtilsReuseEnabled() {
110
- return (process.env.SKYRAMP_FEATURE_UTILS_REUSE === "1" || isUtilsReuseDefaultOrg());
109
+ return (process.env.SKYRAMP_FEATURE_UTILS_REUSE === "1" ||
110
+ isDefaultOrg(UTILS_REUSE_DEFAULT_ORGS));
111
111
  }
112
112
  /**
113
- * GitHub orgs where SkyrampUtils code reuse is on by default, without
114
- * requiring the SKYRAMP_FEATURE_UTILS_REUSE env var. Compared
115
- * case-insensitively (GitHub owner handles are case-insensitive, but
116
- * GITHUB_REPOSITORY_OWNER preserves display casing). Because this ORs with
117
- * the env check, these orgs cannot turn the feature off by setting the env
118
- * var to "0". POM reuse has no org default — see `isPomReuseEnabled`.
113
+ * GitHub orgs where a reuse feature is on by default, without requiring its
114
+ * SKYRAMP_FEATURE_* env var. Compared case-insensitively (GitHub owner handles
115
+ * are case-insensitive, but GITHUB_REPOSITORY_OWNER preserves display casing).
116
+ * Because these OR with the env check, these orgs cannot turn the feature off
117
+ * by setting the env var to "0" and testbot normalizes an unset flag to a
118
+ * literal "0" anyway.
119
119
  *
120
- * "Skyramp-HandsOn" is the Wipro / IntelliAssure pilot org (SKYR-4286).
120
+ * "Skyramp-HandsOn" is the Wipro / IntelliAssure pilot org (SKYR-4286 for
121
+ * utils reuse, SKYR-4514 for POM reuse).
121
122
  */
122
123
  const UTILS_REUSE_DEFAULT_ORGS = ["skyramp-handson"];
123
- function isUtilsReuseDefaultOrg() {
124
+ const POM_REUSE_DEFAULT_ORGS = ["skyramp-handson"];
125
+ function isDefaultOrg(orgs) {
124
126
  // GITHUB_REPOSITORY_OWNER is set on GitHub Actions runners; fall back to
125
127
  // the owner half of GITHUB_REPOSITORY ("owner/repo") for environments that
126
128
  // only set the latter.
127
129
  const owner = process.env.GITHUB_REPOSITORY_OWNER ??
128
130
  process.env.GITHUB_REPOSITORY?.split("/")[0] ??
129
131
  "";
130
- return UTILS_REUSE_DEFAULT_ORGS.includes(owner.toLowerCase());
132
+ return orgs.includes(owner.toLowerCase());
131
133
  }
132
134
  /**
133
135
  * Returns true when the calling agent has the Skyramp Agent Skills plugin
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Ensure `expect` is imported from `@skyramp/skyramp` rather than `@playwright/test`.
3
3
  * - Strips `expect` from any `@playwright/test` import (keeps other identifiers).
4
- * - If the file uses `expect(...)`, ensures an `@skyramp/skyramp` import carries `expect`.
4
+ * - If the file uses `expect(...)`, ensures its `@skyramp/skyramp` import carries `expect`.
5
+ * A file with no `@skyramp/skyramp` import is not a Skyramp test (a repository's
6
+ * own spec), so it is left alone, as is a file that takes `expect` from another
7
+ * module.
5
8
  * Returns the original string if no rewrite was needed.
6
9
  */
7
10
  export declare function normalizeSkyrampExpectImport(source: string): string;
@@ -3,7 +3,19 @@ import { logger } from "./logger.js";
3
3
  const PLAYWRIGHT_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@playwright\/test['"]\s*;?[ \t]*\r?\n?/g;
4
4
  const SKYRAMP_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]@skyramp\/skyramp['"]\s*;?/;
5
5
  const EXPECT_USAGE_RE = /\bexpect\s*\(/;
6
- const LAST_IMPORT_RE = /^import\s[^;]*;[ \t]*$/gm;
6
+ const NAMED_IMPORT_RE = /import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
7
+ /** True when a module other than @playwright/test or @skyramp/skyramp imports
8
+ * `expect` (e.g. a repository's own harness); moving it would break that file. */
9
+ function expectFromOtherModule(source) {
10
+ for (const [, raw, from] of source.matchAll(NAMED_IMPORT_RE)) {
11
+ if (from === "@playwright/test" || from === "@skyramp/skyramp")
12
+ continue;
13
+ const names = splitImports(raw).map((item) => item.split(/\s+as\s+/).pop());
14
+ if (names.includes("expect"))
15
+ return true;
16
+ }
17
+ return false;
18
+ }
7
19
  function splitImports(raw) {
8
20
  return raw
9
21
  .split(",")
@@ -13,10 +25,16 @@ function splitImports(raw) {
13
25
  /**
14
26
  * Ensure `expect` is imported from `@skyramp/skyramp` rather than `@playwright/test`.
15
27
  * - Strips `expect` from any `@playwright/test` import (keeps other identifiers).
16
- * - If the file uses `expect(...)`, ensures an `@skyramp/skyramp` import carries `expect`.
28
+ * - If the file uses `expect(...)`, ensures its `@skyramp/skyramp` import carries `expect`.
29
+ * A file with no `@skyramp/skyramp` import is not a Skyramp test (a repository's
30
+ * own spec), so it is left alone, as is a file that takes `expect` from another
31
+ * module.
17
32
  * Returns the original string if no rewrite was needed.
18
33
  */
19
34
  export function normalizeSkyrampExpectImport(source) {
35
+ if (!SKYRAMP_IMPORT_RE.test(source) || expectFromOtherModule(source)) {
36
+ return source;
37
+ }
20
38
  let out = source;
21
39
  out = out.replace(PLAYWRIGHT_IMPORT_RE, (match, raw) => {
22
40
  const items = splitImports(raw);
@@ -31,22 +49,11 @@ export function normalizeSkyrampExpectImport(source) {
31
49
  if (!EXPECT_USAGE_RE.test(out))
32
50
  return out;
33
51
  const skyrampMatch = out.match(SKYRAMP_IMPORT_RE);
34
- if (skyrampMatch) {
35
- const items = splitImports(skyrampMatch[1]);
36
- if (items.includes("expect"))
37
- return out;
38
- items.push("expect");
39
- out = out.replace(SKYRAMP_IMPORT_RE, `import { ${items.join(", ")} } from '@skyramp/skyramp';`);
52
+ const items = splitImports(skyrampMatch[1]);
53
+ if (items.includes("expect"))
40
54
  return out;
41
- }
42
- const newImport = `import { expect } from '@skyramp/skyramp';`;
43
- const imports = [...out.matchAll(LAST_IMPORT_RE)];
44
- const last = imports[imports.length - 1];
45
- if (last && last.index !== undefined) {
46
- const insertAt = last.index + last[0].length;
47
- return out.slice(0, insertAt) + "\n" + newImport + out.slice(insertAt);
48
- }
49
- return newImport + "\n" + out;
55
+ items.push("expect");
56
+ return out.replace(SKYRAMP_IMPORT_RE, `import { ${items.join(", ")} } from '@skyramp/skyramp';`);
50
57
  }
51
58
  /**
52
59
  * Normalize the `expect` import in a file on disk. No-op if the file is missing,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Which reuse path owns a repository module when both reuse settings are on: the
3
+ * page-object path owns a module that declares a class, the in-house helper path
4
+ * (utils-verify/in-house.ts) owns one that does not. ONE predicate, read by both
5
+ * sides, so a module is never claimed by both and never by neither.
6
+ *
7
+ * Decided by shape because the in-house parser cannot read a class — it reports
8
+ * one as unverifiable — while page-object detection also globs free-function
9
+ * helper libraries (`support/`, `testUtils`). Without this split both paths would
10
+ * work on the same free-function module in one run.
11
+ */
12
+ export declare function declaresClass(content: string): boolean;
13
+ /** The file's working-copy content decides; an unreadable file is not a page
14
+ * object (fails toward the in-house path, whose additive gate reads HEAD). */
15
+ export declare function isPageObjectModule(file: string): Promise<boolean>;
16
+ /** Keep only the candidates the page-object path owns. */
17
+ export declare function pageObjectCandidates<T extends {
18
+ file: string;
19
+ }>(candidates: T[]): Promise<T[]>;
@@ -0,0 +1,31 @@
1
+ import { readFile } from "fs/promises";
2
+ import { stripCommentsAndStrings } from "./strip.js";
3
+ /**
4
+ * Which reuse path owns a repository module when both reuse settings are on: the
5
+ * page-object path owns a module that declares a class, the in-house helper path
6
+ * (utils-verify/in-house.ts) owns one that does not. ONE predicate, read by both
7
+ * sides, so a module is never claimed by both and never by neither.
8
+ *
9
+ * Decided by shape because the in-house parser cannot read a class — it reports
10
+ * one as unverifiable — while page-object detection also globs free-function
11
+ * helper libraries (`support/`, `testUtils`). Without this split both paths would
12
+ * work on the same free-function module in one run.
13
+ */
14
+ export function declaresClass(content) {
15
+ return /(?:^|[\s;}])class\s+[A-Za-z_$][\w$]*/.test(stripCommentsAndStrings(content));
16
+ }
17
+ /** The file's working-copy content decides; an unreadable file is not a page
18
+ * object (fails toward the in-house path, whose additive gate reads HEAD). */
19
+ export async function isPageObjectModule(file) {
20
+ try {
21
+ return declaresClass(await readFile(file, "utf8"));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /** Keep only the candidates the page-object path owns. */
28
+ export async function pageObjectCandidates(candidates) {
29
+ const owned = await Promise.all(candidates.map((c) => isPageObjectModule(c.file)));
30
+ return candidates.filter((_, i) => owned[i]);
31
+ }
@@ -3,6 +3,14 @@
3
3
  * targets them.
4
4
  */
5
5
  export declare const SPEC_BASENAME: RegExp;
6
+ /**
7
+ * Skyramp's own shared helper module is never a page object, wherever it lives — a
8
+ * customer layout such as `tests/lib/skyrampUtils.ts` matches the test-tree helper
9
+ * glob above. These globs only LIST files, and the whole-repository fallback lists
10
+ * every source file, so the module is not recognised here: the two readers that
11
+ * open candidates anyway — `scorePoms` for discovery, `verifyReuse` for the files a
12
+ * spec binds — drop a file whose head carries the shared-module header.
13
+ */
6
14
  export declare function globPomFiles(root: string): Promise<string[]>;
7
15
  /**
8
16
  * Spec files anywhere under `root`, by basename rather than by directory.
@@ -34,6 +34,14 @@ const BUILD_IGNORE = [
34
34
  /** Test trees a POM is not expected to live in. Not skipped when the tests themselves are the subject. */
35
35
  const TEST_TREE_IGNORE = ["**/cypress/**", "**/__tests__/**"];
36
36
  const ALL_SOURCE_IGNORE = [...BUILD_IGNORE, ...TEST_TREE_IGNORE];
37
+ /**
38
+ * Skyramp's own shared helper module is never a page object, wherever it lives — a
39
+ * customer layout such as `tests/lib/skyrampUtils.ts` matches the test-tree helper
40
+ * glob above. These globs only LIST files, and the whole-repository fallback lists
41
+ * every source file, so the module is not recognised here: the two readers that
42
+ * open candidates anyway — `scorePoms` for discovery, `verifyReuse` for the files a
43
+ * spec binds — drop a file whose head carries the shared-module header.
44
+ */
37
45
  export async function globPomFiles(root) {
38
46
  const matches = await fg(POM_GLOBS, {
39
47
  cwd: root,
@@ -1,4 +1,6 @@
1
1
  import { readFile } from "fs/promises";
2
+ import { hasUtilsHeaderContent } from "../utils-verify/locate.js";
3
+ import { logger } from "../logger.js";
2
4
  export const RARE_DF = 5;
3
5
  /**
4
6
  * IDF-style scoring: each selector contributes 1/df to every file containing it
@@ -18,7 +20,17 @@ export async function scorePoms(selectors, files) {
18
20
  const contents = new Map();
19
21
  await Promise.all(files.map(async (f) => {
20
22
  try {
21
- contents.set(f, (await readFile(f, "utf8")).replace(/\\/g, ""));
23
+ // The whole file: the scoring loop below counts a selector wherever it
24
+ // appears in the content, so a partial read would change the scores.
25
+ const text = await readFile(f, "utf8");
26
+ // Skyramp's own shared module is never a page-object candidate, wherever the
27
+ // globs found it (see pom-files.ts). Judged on the first lines of the content
28
+ // the scorer holds anyway, so the check adds no read.
29
+ if (hasUtilsHeaderContent(text)) {
30
+ logger.info("Page-object scoring skipped a Skyramp shared module the globs listed", { file: f });
31
+ return;
32
+ }
33
+ contents.set(f, text.replace(/\\/g, ""));
22
34
  }
23
35
  catch {
24
36
  /* unreadable file: skip */
@@ -1,3 +1,8 @@
1
+ /** Half-open `[start, end)` offsets of a region in the source. */
2
+ export interface SourceSpan {
3
+ start: number;
4
+ end: number;
5
+ }
1
6
  /** Every line- and block-comment span in `src`, in source order. Exported for callers
2
7
  * that must decide whether a `//` OPENS a comment rather than sits inside one: a `//`
3
8
  * on its own line inside `/* … *\/` opens nothing, and deleting that line can take the
@@ -11,6 +16,11 @@ export declare function commentSpans(src: string): {
11
16
  * (newlines preserved) so remaining code characters keep their original offset. */
12
17
  export declare function stripComments(src: string): string;
13
18
  /** Blanks string/template literal contents only, preserving comments as-is. */
19
+ /** The string literals in TS/JS source, quotes INCLUDED, in order. The same scan the
20
+ * blanking helpers use — exported so a caller that must treat literal contents
21
+ * differently from code (rather than blank them) has one definition of where a
22
+ * literal starts and ends, not a second scanner of its own. */
23
+ export declare function stringSpans(src: string): SourceSpan[];
14
24
  export declare function stripStrings(src: string): string;
15
25
  /** Blanks both comments and string/template literals -- for scanners (like call-chain matching)
16
26
  * that never need to read quoted text and want prose in either kind fully inert. */
@@ -317,6 +317,15 @@ export function stripComments(src) {
317
317
  return blankSpans(src, classifySpans(src).filter((s) => s.kind === "comment"));
318
318
  }
319
319
  /** Blanks string/template literal contents only, preserving comments as-is. */
320
+ /** The string literals in TS/JS source, quotes INCLUDED, in order. The same scan the
321
+ * blanking helpers use — exported so a caller that must treat literal contents
322
+ * differently from code (rather than blank them) has one definition of where a
323
+ * literal starts and ends, not a second scanner of its own. */
324
+ export function stringSpans(src) {
325
+ return classifySpans(src)
326
+ .filter((s) => s.kind === "string")
327
+ .map(({ start, end }) => ({ start, end }));
328
+ }
320
329
  export function stripStrings(src) {
321
330
  return blankSpans(src, classifySpans(src).filter((s) => s.kind === "string"));
322
331
  }
@@ -4,7 +4,8 @@ import * as path from "path";
4
4
  import { promisify } from "util";
5
5
  import { findRoot } from "../pom-scope/index.js";
6
6
  import { globPomFiles, globAllSourceFiles } from "../pom-scope/pom-files.js";
7
- import { extractPomBindings } from "./bindings.js";
7
+ import { extractPomBindings, primaryFile } from "./bindings.js";
8
+ import { hasUtilsHeader } from "../utils-verify/locate.js";
8
9
  import { extractPomCalls } from "./calls.js";
9
10
  import { resolveMemberSource } from "./resolve.js";
10
11
  import { stripComments, stripCommentsAndStrings } from "../pom-scope/strip.js";
@@ -128,12 +129,22 @@ export async function verifyReuse(testFile, language) {
128
129
  pomFiles = await globAllSourceFiles(root);
129
130
  const pageManagerFile = pomFiles.find((f) => PAGE_MANAGER_RE.test(path.basename(f)));
130
131
  const aliasMap = await loadAliasMap(root);
131
- const { bindings, unrecognized } = await extractPomBindings(spec, {
132
+ const extracted = await extractPomBindings(spec, {
132
133
  specDir: path.dirname(testFile),
133
134
  pomFiles,
134
135
  pageManagerFile,
135
136
  aliasMap,
136
137
  });
138
+ // Skyramp's own shared module can sit inside a page-object glob (`tests/lib/`), and
139
+ // a call to one of its helpers must not count as page-object reuse. The header is
140
+ // read on the few files the spec binds, never across the glob: the whole-repository
141
+ // fallback above lists every source file. Dropping the binding is the whole change:
142
+ // `unrecognized` fills only from a `new` of an unresolved class, which a dropped
143
+ // binding never was, so nothing needs a second extraction.
144
+ const bound = [...new Set(extracted.bindings.map(primaryFile))];
145
+ const ours = new Set((await Promise.all(bound.map(async (f) => ((await hasUtilsHeader(f)) ? f : undefined)))).filter((f) => f !== undefined));
146
+ const { unrecognized } = extracted;
147
+ const bindings = extracted.bindings.filter((b) => !ours.has(primaryFile(b)));
137
148
  const calls = extractPomCalls(spec, bindings);
138
149
  const bindingsByName = new Map(bindings.map((b) => [b.name, b]));
139
150
  const violations = [];