@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
@@ -1,11 +1,11 @@
1
1
  import * as fsp from "fs/promises";
2
2
  import * as path from "path";
3
- import { readFile, stat, access, constants as fsConstants } from "fs/promises";
4
- import { realpathSync } from "fs";
3
+ import { readFile, stat } from "fs/promises";
5
4
  import { execFile } from "child_process";
6
5
  import { promisify } from "util";
7
6
  import { logger } from "../../utils/logger.js";
8
- import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
7
+ import { resolveRunStatePath, } from "../../utils/AnalysisStateManager.js";
8
+ import { isAbsentFromHead } from "../../utils/utils-verify/head.js";
9
9
  import { verifyReuse, } from "../../utils/pom-verify/index.js";
10
10
  import { buildSkipped, findUnparsableKeptInline, flaggedFrom, mergeFlagged, verificationFrom, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./reuse-outcome.js";
11
11
  import { verifyUtils, realpath, utilsSpecFor, countAssertions, hasPageErrorGuard, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
@@ -18,6 +18,11 @@ const execFileAsync = promisify(execFile);
18
18
  import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
19
19
  import { parsePomCatalog, findMissedReuse, } from "../../utils/pom-catalog-parse.js";
20
20
  import { ProgrammingLanguage } from "../../types/TestTypes.js";
21
+ import { canonPath, freshRecord, owns, readHandOff, readReuseRecord, readRunState, runStateWritable, samePath, updateReuseOutcome, } from "./reuse-record-store.js";
22
+ import { combineRederivations, pomFirstHandOff } from "./pom-pass-state.js";
23
+ /** The canonical path comparison the records use — declared in the store, re-exported
24
+ * here for the readers that always imported it from this module. */
25
+ export { samePath, canonPath } from "./reuse-record-store.js";
21
26
  /**
22
27
  * Persistence for the server-derived POM-reuse outcome, keyed by test-file
23
28
  * basename (what `newTestsCreated[].fileName` carries, so
@@ -28,52 +33,6 @@ import { ProgrammingLanguage } from "../../types/TestTypes.js";
28
33
  * file degrades to "no reuse summary in the report" — the same outcome as running
29
34
  * outside a Testbot run, where there is no run state to record into.
30
35
  */
31
- /**
32
- * Read-modify-write one spec's outcome. `patch` receives the entry recorded so
33
- * far, so a caller that needs prior state (the verify pass, which accumulates
34
- * flagged members) uses the same path as one that does not.
35
- *
36
- * Fields the patch omits are preserved: the prompt-generation call knows the
37
- * candidate count, each verify pass knows the counts and verdict.
38
- */
39
- async function updateReuseOutcome(testFile, patch, explicitStateFile) {
40
- const stateFile = resolveRunStatePath(explicitStateFile);
41
- if (!stateFile)
42
- return false;
43
- try {
44
- const manager = StateManager.fromStatePath(stateFile);
45
- const data = await manager.readData();
46
- if (!data)
47
- return false;
48
- const key = path.basename(testFile);
49
- const existing = data.reuseOutcomes?.[key] ?? {};
50
- await manager.appendData({
51
- ...data,
52
- reuseOutcomes: {
53
- ...data.reuseOutcomes,
54
- [key]: { ...existing, ...patch(existing) },
55
- },
56
- });
57
- return true;
58
- }
59
- catch (err) {
60
- logger.warning("Could not record POM-reuse outcome — report will omit the reuse summary", {
61
- testFile,
62
- error: String(err),
63
- });
64
- return false;
65
- }
66
- }
67
- /** Read one spec's record, or `undefined` when there is no run state, no record, or the
68
- * state file cannot be read. Shares `updateReuseOutcome`'s resolve-read-basename plumbing
69
- * so the two cannot disagree about which key a spec maps to. */
70
- async function readReuseRecord(testFile, explicitStateFile) {
71
- const stateFile = resolveRunStatePath(explicitStateFile);
72
- if (!stateFile)
73
- return undefined;
74
- const data = await StateManager.fromStatePath(stateFile).readData();
75
- return data?.reuseOutcomes?.[path.basename(testFile)];
76
- }
77
36
  /**
78
37
  * Record the tier-1 candidate count STEP 1 detected, plus the identity needed to
79
38
  * re-derive the counts from the delivered spec later.
@@ -221,17 +180,6 @@ export async function recordUtilsReuseIssued(testFile, language, framework, test
221
180
  },
222
181
  }), explicitStateFile);
223
182
  }
224
- /** Every field cleared — the patch spread onto a record owned by another spec. */
225
- function freshRecord() {
226
- return {
227
- candidatesDetected: undefined,
228
- verification: undefined,
229
- flagged: undefined,
230
- language: undefined,
231
- utils: undefined,
232
- modularized: undefined,
233
- };
234
- }
235
183
  /** A verify pass that threw. Recorded so the debt FAILS OPEN — a verifier that cannot
236
184
  * run must not leave a debt nothing can pay — and cleared by the next successful pass. */
237
185
  export async function recordUtilsVerifyError(testFile, explicitStateFile) {
@@ -239,20 +187,6 @@ export async function recordUtilsVerifyError(testFile, explicitStateFile) {
239
187
  ? { utils: { ...existing.utils, verifyError: true } }
240
188
  : {}, explicitStateFile);
241
189
  }
242
- /** Whether `record` describes `testFile` (or nobody yet). Basename-keyed run state
243
- * means two specs can share one record; only the owner's history carries over. */
244
- function owns(record, testFile) {
245
- return !record.testFilePath || samePath(record.testFilePath, testFile);
246
- }
247
- /** Canonical comparison: a record written through a symlinked path (macOS /var →
248
- * /private/var) must still be recognised as this spec's when checked by realpath. */
249
- export function samePath(a, b) {
250
- return canon(a) === canon(b);
251
- }
252
- /** Exported for retrofit-state: the same canonicalisation the records use. */
253
- export function canonPath(p) {
254
- return canon(p);
255
- }
256
190
  /**
257
191
  * The label a report row shows for a set of utils files: each file's path relative
258
192
  * to its repository root, posix-separated, joined with `, `.
@@ -273,7 +207,7 @@ function repoRelativeLabel(file) {
273
207
  return path.basename(file);
274
208
  // Both sides canonical: git reports the REAL root, and a path through a symlink
275
209
  // (macOS /tmp → /private/tmp) would otherwise relativise to `../../…`.
276
- const rel = path.relative(canon(root), canon(file));
210
+ const rel = path.relative(canonPath(root), canonPath(file));
277
211
  // Outside the root means a leading `..` SEGMENT — not any name starting with two
278
212
  // dots (`..generated/SkyrampUtils.py` is a valid in-repository path).
279
213
  const outside = rel === ".." || rel.startsWith(`..${path.sep}`);
@@ -281,14 +215,6 @@ function repoRelativeLabel(file) {
281
215
  return path.basename(file);
282
216
  return rel.split(path.sep).join("/");
283
217
  }
284
- function canon(p) {
285
- try {
286
- return realpathSync(p);
287
- }
288
- catch {
289
- return path.resolve(p);
290
- }
291
- }
292
218
  /** Verdict of one utils verify pass. The verdict alone is stored — counts and the
293
219
  * file list are re-derived from the delivered files at report time. */
294
220
  export async function recordUtilsVerifyOutcome(testFile, r, language, framework, testType, explicitStateFile,
@@ -333,20 +259,6 @@ inHouseDeclared) {
333
259
  };
334
260
  }, explicitStateFile);
335
261
  }
336
- /** Whether the run state file can be written — the precondition for any debt this
337
- * module holds, since only a write clears it. */
338
- async function runStateWritable(explicitStateFile) {
339
- const stateFile = resolveRunStatePath(explicitStateFile);
340
- if (!stateFile)
341
- return false;
342
- try {
343
- await access(stateFile, fsConstants.W_OK);
344
- return true;
345
- }
346
- catch {
347
- return false;
348
- }
349
- }
350
262
  /** `skyramp_modularization` was called for this spec. Ownership on the write, like
351
263
  * the utils debt: a same-basename spec elsewhere does not inherit the mark. */
352
264
  export async function recordModularized(testFile, explicitStateFile) {
@@ -399,14 +311,11 @@ export async function utilsBaselineFor(testFile, explicitStateFile) {
399
311
  */
400
312
  export async function pendingModularization(testFile, explicitStateFile) {
401
313
  try {
402
- const stateFile = resolveRunStatePath(explicitStateFile);
403
- if (!stateFile)
404
- return undefined;
405
- const data = await StateManager.fromStatePath(stateFile).readData();
406
- const handOff = data?.reuseHandOffs?.[await realpath(testFile)];
407
- if (!handOff)
314
+ const read = await readHandOff(testFile, explicitStateFile);
315
+ if (!read?.handOff)
408
316
  return undefined;
409
- const record = data?.reuseOutcomes?.[path.basename(testFile)];
317
+ const { data, handOff } = read;
318
+ const record = data.reuseOutcomes?.[path.basename(testFile)];
410
319
  if (record && !owns(record, testFile))
411
320
  return undefined;
412
321
  if (record?.modularized)
@@ -506,14 +415,11 @@ export async function recordReuseHandOff(files, testType, language, framework, e
506
415
  // nothing on the reuse side records for it — so no hand-off is owed either.
507
416
  if (!utilsSpecFor(language))
508
417
  return;
509
- const stateFile = resolveRunStatePath(explicitStateFile);
510
- if (!stateFile)
511
- return;
512
418
  try {
513
- const manager = StateManager.fromStatePath(stateFile);
514
- const data = await manager.readData();
515
- if (!data)
419
+ const state = await readRunState(explicitStateFile);
420
+ if (!state)
516
421
  return;
422
+ const { manager, data } = state;
517
423
  // A REGENERATE marks its file as maintenance before the generation call it
518
424
  // prescribes records the same file as generation. The rewrite is still the
519
425
  // maintenance of a file at HEAD, so the origin is never demoted.
@@ -567,9 +473,7 @@ async function isNewInRepo(file) {
567
473
  // Only "absent from HEAD" (or no HEAD yet) means new. Anything else — git missing,
568
474
  // a corrupt object store, a permission error — is git not answering, and the
569
475
  // documented answer to that is false: never block, never accuse, on a malfunction.
570
- const e = err;
571
- const text = `${e.stderr ?? ""} ${String(err)}`;
572
- if (/does not exist|Not a valid object name|bad revision|exists on disk, but not in/i.test(text))
476
+ if (isAbsentFromHead(err))
573
477
  return true;
574
478
  logger.warning("Could not determine whether a file is new in the repository — treating it as pre-existing", {
575
479
  file,
@@ -596,17 +500,14 @@ async function isNewInRepo(file) {
596
500
  */
597
501
  export async function pendingReuseChain(testFile, explicitStateFile, testType) {
598
502
  try {
599
- const stateFile = resolveRunStatePath(explicitStateFile);
600
- if (!stateFile)
601
- return undefined;
602
- const data = await StateManager.fromStatePath(stateFile).readData();
603
503
  // Keyed by the GENERATED FILE: generation snapshots its outputDir before and after
604
504
  // the codegen call and records exactly the files it wrote, so a test generated
605
505
  // without codeReuse into the same directory owes nothing, and neither does a
606
506
  // pre-existing test beside it.
607
- const handOff = data?.reuseHandOffs?.[await realpath(testFile)];
608
- if (!handOff)
507
+ const read = await readHandOff(testFile, explicitStateFile);
508
+ if (!read?.handOff)
609
509
  return undefined;
510
+ const { data, handOff } = read;
610
511
  // A caller that knows the file's type and disagrees with the hand-off is exempt;
611
512
  // an unknown type still owes (the hand-off is authoritative for this file).
612
513
  if (testType && testType !== handOff.testType)
@@ -619,7 +520,7 @@ export async function pendingReuseChain(testFile, explicitStateFile, testType) {
619
520
  // and execute.
620
521
  if (record && !owns(record, testFile))
621
522
  return undefined;
622
- if (record && reuseRan(record))
523
+ if (record && reuseRan(record, pomFirstHandOff(handOff)))
623
524
  return undefined;
624
525
  const maintained = handOff.source === "maintenance";
625
526
  if (!maintained && !(await isNewInRepo(testFile)))
@@ -674,7 +575,7 @@ handOffs) {
674
575
  if (record) {
675
576
  if (!owns(record, file))
676
577
  continue; // foreign basename — fail open for this file
677
- if (reuseRan(record))
578
+ if (reuseRan(record, pomFirstHandOff(h)))
678
579
  return undefined;
679
580
  }
680
581
  try {
@@ -693,8 +594,12 @@ handOffs) {
693
594
  }
694
595
  /** Whether a record shows the reuse tool ran for its spec: the utils path issued its
695
596
  * instructions, or the POM path recorded detection/verification. A modularization mark
696
- * alone, or a verify verdict recorded without a prior discovery, is NOT reuse. */
697
- function reuseRan(record) {
597
+ * alone, or a verify verdict recorded without a prior discovery, is NOT reuse. When the
598
+ * page-object pass ran first (`pomFirst`), its verdict is only the first half of the
599
+ * chain: the pair it owes is paid by the utils path's instructions alone. */
600
+ function reuseRan(record, pomFirst) {
601
+ if (pomFirst)
602
+ return record.utils?.issued === true;
698
603
  return (record.utils?.issued === true ||
699
604
  record.candidatesDetected !== undefined ||
700
605
  record.verification !== undefined);
@@ -826,6 +731,13 @@ async function rederiveHelperOutcome(record, carriedAllows) {
826
731
  ...(r.inlineCallSites.length > 0
827
732
  ? { siblingInlineCallSites: r.inlineCallSites.length }
828
733
  : {}),
734
+ // Helpers nothing imports — the code a deleted test leaves behind for the
735
+ // customer to maintain. Reported, never removed and never gated: the caller
736
+ // list behind it is a lower bound, and removing a helper that is still called
737
+ // is far worse than leaving a dead one. When the walk could not read every
738
+ // file beside the module it reports NO orphan and says so, rather than let an
739
+ // unreadable directory read as "nothing imports this".
740
+ ...orphanFields(r),
829
741
  verification: r.ok
830
742
  ? HelperVerificationOutcome.Passed
831
743
  : HelperVerificationOutcome.Failed,
@@ -836,6 +748,22 @@ async function rederiveHelperOutcome(record, carriedAllows) {
836
748
  blocking,
837
749
  };
838
750
  }
751
+ /** The orphan fields of a report row: the helpers nothing imports, or the statement
752
+ * that the walk could not tell. Exactly one of the two, and neither when the walk ran
753
+ * and found none. Exported for its tests — the derivation is the whole behaviour. */
754
+ export function orphanFields(r) {
755
+ if (!r.importersComplete)
756
+ return { orphanScanIncomplete: true };
757
+ const orphans = r.singleImporters.filter((h) => h.importers.length === 0);
758
+ if (orphans.length === 0)
759
+ return {};
760
+ return {
761
+ orphanedHelpers: orphans.map((h) => ({
762
+ utilsFile: utilsFileLabel([h.file]),
763
+ helper: h.helper,
764
+ })),
765
+ };
766
+ }
839
767
  /**
840
768
  * The reason behind a failed shared-helper verdict, one entry per blocking kind in
841
769
  * the order the verifier found them. The verify pass had these in hand and dropped
@@ -948,18 +876,36 @@ export async function deriveReuseFromDelivered(testFile, language) {
948
876
  };
949
877
  }
950
878
  export async function rederiveReuse(record, carriedAllows) {
951
- const { candidatesDetected, verification, flagged, testFilePath, language } = record;
952
- // A SkyrampUtils-path record has nothing for the POM verifier to measure; its
953
- // outcome is re-derived from the utils file instead.
879
+ // A SkyrampUtils-path record is re-derived from the utils file. With both reuse
880
+ // settings on, the same spec also went through the page-object pass first, and its
881
+ // row carries both halves: the page-object fields and `helpers`/`inHouse`.
954
882
  if (record.utils) {
883
+ let helper;
955
884
  try {
956
- return await rederiveHelperOutcome(record, carriedAllows);
885
+ helper = await rederiveHelperOutcome(record, carriedAllows);
957
886
  }
958
887
  catch (err) {
959
- logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: testFilePath, error: String(err) });
960
- return { outcome: undefined };
888
+ logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: record.testFilePath, error: String(err) });
889
+ helper = { outcome: undefined };
961
890
  }
891
+ if (!record.pomPassFinished)
892
+ return helper;
893
+ return combineRederivations(await rederivePomOutcome(record, carriedAllows, true), helper, (carriedAllows ?? []).some((c) => c.keptInline !== undefined), record.testFilePath
894
+ ? utilsVerifyCall(record, record.testFilePath)
895
+ : undefined);
962
896
  }
897
+ return rederivePomOutcome(record, carriedAllows);
898
+ }
899
+ /** The page-object half of a row, re-derived from the delivered spec. `composed`
900
+ * says the spec also took the SkyrampUtils flow after this pass: the report-time
901
+ * gates then split candidates by owner exactly as the live ones did. */
902
+ async function rederivePomOutcome(record, carriedAllows, composed = false) {
903
+ const { candidatesDetected, verification, flagged, testFilePath, language } = record;
904
+ // No page-object layer was found: that detection-time fact is the whole half. A
905
+ // composed spec whose SkyrampUtils flow wrote its identity must not be measured
906
+ // into page-object counts it never had.
907
+ if (composed && verification === ReuseVerificationOutcome.SkippedNoPom)
908
+ return { outcome: { candidatesDetected, verification } };
963
909
  // No spec identity was ever recorded, so there is nothing to measure — only the
964
910
  // detection-time facts, which no spec change can invalidate. Reached by the
965
911
  // no-POM-layer path; a run that detected candidates always records the identity
@@ -971,7 +917,7 @@ export async function rederiveReuse(record, carriedAllows) {
971
917
  const specContent = await readFile(testFilePath, "utf8");
972
918
  const carriedKeptInline = (carriedAllows ?? []).filter((c) => c.keptInline !== undefined);
973
919
  const classification = { covered: new Set(), ran: false };
974
- const blocking = await pomBlockingVerdict(testFilePath, language, r, carriedKeptInline.map((c) => c.keptInline), classification);
920
+ const blocking = await pomBlockingVerdict(testFilePath, language, r, carriedKeptInline.map((c) => c.keptInline), classification, composed);
975
921
  // Diagnose the file the report is actually built from. A marker the zero-reuse
976
922
  // gate accepts but this cannot parse satisfies the escape hatch while its
977
923
  // decline never reaches the report — silent signal loss is the failure class
@@ -1071,7 +1017,7 @@ async function pomBlockingVerdict(testFile, language, r, carriedKeptInline,
1071
1017
  /** Filled with what the zero-reuse gate did — see `zeroReuseGateFailure`. `ran`
1072
1018
  * stays false on every path that returns before the candidate walk, including the
1073
1019
  * unknown language and the infra-gate precedence below. */
1074
- classification) {
1020
+ classification, composed) {
1075
1021
  // The record's language was written from the reuse tool's validated `language`
1076
1022
  // param, but the record type keeps it a string: a value outside the enum means a
1077
1023
  // check that cannot run, and a check that cannot run does not block.
@@ -1084,6 +1030,7 @@ classification) {
1084
1030
  testFile,
1085
1031
  language: language,
1086
1032
  framework: "playwright",
1033
+ composed,
1087
1034
  };
1088
1035
  let infra;
1089
1036
  let zero;
@@ -36,7 +36,7 @@ export function formatUtilsVerifyText(r, ctx = {}) {
36
36
  return ((unchecked.length > 0
37
37
  ? `VERIFICATION PASSED — no Skyramp utils file; this test imports ${unchecked.length} module${unchecked.length === 1 ? "" : "s"} the pass read and does not check: ${unchecked
38
38
  .map((f) => path.basename(f))
39
- .join(", ")} — the repository's own, tracked without a Skyramp header. The in-house resolve and additive checks did not run: ${r.inHouseChecksSkipped ?? "they run for a UI test with page-object reuse off"}. Continue.`
39
+ .join(", ")} — the repository's own, tracked without a Skyramp header. The in-house resolve and additive checks did not run: ${r.inHouseChecksSkipped ?? "they run for a UI test in TypeScript or JavaScript with utils reuse on"}. Continue.`
40
40
  : "VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify.") +
41
41
  tail +
42
42
  unextractedDuplicatesAdvisory(r));
@@ -286,11 +286,11 @@ function singleImporterAdvisory(r) {
286
286
  const single = r.singleImporters;
287
287
  if (single.length === 0 || r.helpers === 0)
288
288
  return "";
289
- const dead = single.filter((h) => h.importers === 0);
289
+ const dead = single.filter((h) => h.importers.length === 0);
290
290
  const lines = ["", ""];
291
291
  if (dead.length > 0)
292
292
  lines.push(`ADVISORY — ${dead.length} helper${dead.length === 1 ? " is" : "s are"} imported by NOTHING, not even the test that wrote ${dead.length === 1 ? "it" : "them"} (does not block):`, ...dead.map((h) => `- ${show(h.file, r)}:${h.line} · ${h.helper}`), "A dead helper is always actionable: call it from the test it belongs to, or delete it.");
293
- const once = single.filter((h) => h.importers === 1);
293
+ const once = single.filter((h) => h.importers.length === 1);
294
294
  if (once.length === 0)
295
295
  return lines.join("\n");
296
296
  lines.push(`ADVISORY — ${once.length} of ${r.helpers} shared helper${r.helpers === 1 ? "" : "s"} ${once.length === 1 ? "is" : "are"} imported by exactly one test (does not block):`, ...once.map((h) => `- ${show(h.file, r)}:${h.line} · ${h.helper}`), "Other delivered tests exist and do not call these. Inline a helper back into its test only if it wraps a step specific to this scenario's narrative that no other test would perform as-is; a request wrapper or a cross-test action (sign-in, open a form, apply a filter) stays shared.");
@@ -14,6 +14,10 @@ import { languageSchema } from "../../types/TestTypes.js";
14
14
  export type VerifyTarget = Pick<z.infer<typeof languageSchema>, "language" | "framework"> & {
15
15
  testFile: string;
16
16
  testType?: string;
17
+ /** Both reuse settings on for this target (the route's `composed`): the in-house
18
+ * path owns free-function modules, so only a class module is a candidate this
19
+ * pass skipped. Stated by the caller, which resolved the route; never inferred. */
20
+ composed: boolean;
17
21
  };
18
22
  /** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
19
23
  export declare function formatVerifyReport(r: VerifyResult): string;
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { logger } from "../../utils/logger.js";
4
4
  import { isPomAwareTarget } from "../../utils/reuseRouting.js";
5
+ import { pageObjectCandidates } from "../../utils/pom-scope/ownership.js";
5
6
  import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
6
7
  import { stripComments, stripStrings } from "../../utils/pom-scope/strip.js";
7
8
  import { escapeRegExp } from "../../utils/regex.js";
@@ -174,10 +175,16 @@ classification = {
174
175
  return undefined;
175
176
  try {
176
177
  const specContent = await readFile(params.testFile, "utf8");
177
- const { tier1, diagnostics } = await selectScopedPoms({
178
+ const scoped = await selectScopedPoms({
178
179
  testFile: params.testFile,
179
180
  specContent,
180
181
  });
182
+ const { diagnostics } = scoped;
183
+ // Both settings on: a free-function module is the in-house path's, so it is not
184
+ // a candidate this pass skipped — the same split discovery applied.
185
+ const tier1 = params.composed
186
+ ? await pageObjectCandidates(scoped.tier1)
187
+ : scoped.tier1;
181
188
  // Only fire on candidates found via conventional POM dirs (globs) — the selector-grep
182
189
  // discovery fallback also matches ordinary in-repo frontend source (app components
183
190
  // legitimately contain the spec's test-ids by construction), which is not evidence of
@@ -399,14 +399,14 @@ const CHAINING_SECTION = CONSUMER_MODE_ENABLED
399
399
  - \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`skipProvisionParents\`.`;
400
400
  export function registerContractTestTool(server) {
401
401
  server.registerTool(TOOL_NAME, {
402
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
402
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
403
403
  1. The endpoint URL and HTTP method being tested
404
- 2. Whether the endpoint is a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.) — if YES, decide: do I have the request body to provision the parent, or should I use skipProvisionParents?
405
- 3. Assertion planning — answer all four:
406
- a. Echo-back fields: which fields in the response are returned unchanged from the request body? (e.g. name, email, status) — these must be asserted with the exact sent value, not just not-null
407
- b. Computed fields: are there any numeric fields derived from other values? (e.g. total_amount, discount_amount) — these must be asserted with the formula, never a hardcoded number
408
- c. Test data isolation: does this test create its own resources via parentRequestData, or does it depend on pre-existing data? All IDs must come from provisioned resources, never hardcoded.
409
- d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, revisit them now — did the recommendation phase flag any boundary conditions, invalid-input behaviors, or business logic bugs? Each flagged risk should map to a targeted assertion in this test. A test that doesn't target a known risk is low-value.
404
+ 2. For a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.), use the request body to provision the parent, or use skipProvisionParents when it cannot be provisioned.
405
+ 3. Assertion requirements:
406
+ a. Echo-back fields: assert response fields returned unchanged from the request body (e.g. name, email, status) with the exact sent value, not just not-null.
407
+ b. Computed fields: assert numeric fields derived from other values (e.g. total_amount, discount_amount) with the formula, never a hardcoded number.
408
+ c. Test data isolation: create the test's own resources via parentRequestData. All IDs must come from provisioned resources, never hardcoded.
409
+ d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
410
410
  4. Each required parameter and what value it will take, with source (workspace config / diff / schema / user input)
411
411
  NEVER use a hardcoded ID (UUID or integer) as a path parameter value. If a real resource ID is needed and cannot be provisioned, use skipProvisionParents instead.
412
412
 
@@ -89,15 +89,15 @@ ${enhanceAssertionsStep("integration", "generated test file")}
89
89
  const TOOL_NAME = TOOL_INTEGRATION_TEST_GENERATION;
90
90
  export function registerIntegrationTestTool(server) {
91
91
  server.registerTool(TOOL_NAME, {
92
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
92
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
93
93
  1. The endpoint URL(s) and HTTP method(s) involved in this multi-step workflow
94
- 2. Why an integration test (multi-step workflow validation) is the right choice for this intent
95
- 3. Assertion planning — answer all four for each step:
96
- a. Echo-back fields: which response fields are returned unchanged from the request body? List them by name — these get exact-value assertions, not just not-null checks
97
- b. Computed fields: are there derived numeric fields (e.g. total_amount = price × quantity)? Name the formula — these get formula-based assertions, never hardcoded numbers
98
- c. Chaining: which IDs in step N+1 come from step N's response? Name the JSON path (e.g. POST /orders response → order_id → GET /orders/{order_id}). Every ID must be dynamically chained — no hardcoded IDs anywhere in the test
99
- d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints, revisit them now — did the recommendation phase flag any boundary conditions, invalid-input behaviors, or business logic bugs? Each flagged risk should map to a targeted assertion in this test. A test that doesn't target a known risk is low-value.
100
- 4. Test data isolation: does this test create all its own resources from scratch, or does it depend on pre-existing database state? All prerequisite resources must be created within the test setup.
94
+ 2. The requested test exercises a multi-step workflow.
95
+ 3. Assertion requirements for each step:
96
+ a. Echo-back fields: assert response fields returned unchanged from the request body with the exact sent value, not just not-null.
97
+ b. Computed fields: assert derived numeric fields (e.g. total_amount = price × quantity) with the formula, never hardcoded numbers.
98
+ c. Chaining: use the relevant JSON path in step N's response for IDs in step N+1 (e.g. POST /orders response → order_id → GET /orders/{order_id}). Every ID must be dynamically chained — no hardcoded IDs anywhere in the test.
99
+ d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
100
+ 4. Test data isolation: create all prerequisite resources within the test setup, without depending on pre-existing database state.
101
101
  If any required parameter cannot be determined without guessing, STOP and ask the user before calling the tool.
102
102
 
103
103
  ---
@@ -101,10 +101,10 @@ export const uiTestSchema = {
101
101
  };
102
102
  export function registerUITestTool(server) {
103
103
  server.registerTool(TOOL_NAME, {
104
- description: `${getPersonaPrefix()}Before calling this tool, you MUST output a <thinking> block that covers:
104
+ description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs against the Playwright trace, codebase, and filesystem:
105
105
  1. The user-facing flow(s) captured in the Playwright trace (pages visited, actions taken)
106
- 2. Why a UI test (Playwright-based interaction replay) is the right choice for this intent
107
- 3. Which assertions this test should validate (page content, element state, navigation results)
106
+ 2. The intended behavior is covered by the recorded Playwright interactions.
107
+ 3. The expected page content, element state, or navigation results the test must assert
108
108
  4. The absolute path to the trace zip and the output directory, with source confirmation
109
109
  If the trace zip path cannot be confirmed as an absolute path to an existing file, STOP and re-export before calling this tool.
110
110