@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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.
Files changed (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -0,0 +1,325 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { verifyReuse } from "./verify.js";
6
+ const OO = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "oo-style");
7
+ const AF = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "af-style");
8
+ /** Materializes a hermetic mkdtemp project (so `findRoot`/`globPomFiles` resolve deterministically
9
+ * against just this tree, not the real monorepo checkout) and returns its root + a cleanup fn. */
10
+ function makeProject(files) {
11
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pom-verify-"));
12
+ for (const [rel, content] of Object.entries(files)) {
13
+ const full = path.join(root, rel);
14
+ fs.mkdirSync(path.dirname(full), { recursive: true });
15
+ fs.writeFileSync(full, content);
16
+ }
17
+ return { root, cleanup: () => fs.rmSync(root, { recursive: true, force: true }) };
18
+ }
19
+ const read = (dir, name) => fs.readFileSync(path.join(dir, name), "utf8");
20
+ describe("verifyReuse -- oo-style (page-manager convention)", () => {
21
+ it("passes a clean spec: real method via pm binding -> ok:true, checkedCalls>0", async () => {
22
+ const { root, cleanup } = makeProject({
23
+ "package.json": `{"type":"module"}`,
24
+ "pages/page-manager.js": read(OO, "page-manager.js"),
25
+ "pages/loginPage.js": read(OO, "loginPage.js"),
26
+ "pages/widgetsPage.js": read(OO, "widgetsPage.js"),
27
+ "pages/widgetHelpers.js": read(OO, "widgetHelpers.js"),
28
+ "clean.spec.js": `
29
+ import { PageManager } from './pages/page-manager.js';
30
+ async function run() {
31
+ const pm = new PageManager(page);
32
+ await pm.loginPage.signInAs(user);
33
+ }
34
+ await run();
35
+ `,
36
+ });
37
+ try {
38
+ const result = await verifyReuse(path.join(root, "clean.spec.js"), "javascript");
39
+ expect(result.ok).toBe(true);
40
+ expect(result.violations).toEqual([]);
41
+ expect(result.checkedCalls).toBeGreaterThan(0);
42
+ expect(result.parseError).toBeUndefined();
43
+ }
44
+ finally {
45
+ cleanup();
46
+ }
47
+ });
48
+ it("flags a hallucinated member with the real member list", async () => {
49
+ const { root, cleanup } = makeProject({
50
+ "package.json": `{"type":"module"}`,
51
+ "pages/widgetsPage.js": read(OO, "widgetsPage.js"),
52
+ "pages/widgetHelpers.js": read(OO, "widgetHelpers.js"),
53
+ "violation.spec.js": `
54
+ import { WidgetsPage } from './pages/widgetsPage.js';
55
+ async function run() {
56
+ const widgets = new WidgetsPage(page);
57
+ await widgets.dragWidgetFake();
58
+ }
59
+ await run();
60
+ `,
61
+ });
62
+ try {
63
+ const result = await verifyReuse(path.join(root, "violation.spec.js"), "javascript");
64
+ expect(result.violations).toEqual([
65
+ {
66
+ binding: "widgets",
67
+ member: "dragWidgetFake",
68
+ searched: [path.join(root, "pages", "widgetsPage.js"), path.join(root, "pages", "widgetHelpers.js")],
69
+ availableMembers: expect.arrayContaining(["addBtn"]),
70
+ },
71
+ ]);
72
+ expect(result.ok).toBe(false);
73
+ }
74
+ finally {
75
+ cleanup();
76
+ }
77
+ });
78
+ });
79
+ describe("verifyReuse -- oo-style (CJS page-manager convention)", () => {
80
+ it("engages on a CJS PageManager (no `export` on the class, module.exports, a default import, an extensionless require) -- real call passes, checkedCalls>0", async () => {
81
+ const { root, cleanup } = makeProject({
82
+ "package.json": `{}`,
83
+ "pages/page-manager.js": read(OO, "page-manager-cjs.js"),
84
+ "pages/loginPage.js": read(OO, "loginPage.js"),
85
+ "pages/widgetsPage.js": read(OO, "widgetsPage.js"),
86
+ "pages/widgetHelpers.js": read(OO, "widgetHelpers.js"),
87
+ "cjs-clean.spec.js": `
88
+ const PageManager = require('./pages/page-manager.js');
89
+ async function run() {
90
+ const pm = new PageManager(page);
91
+ await pm.loginPage.signInAs(user);
92
+ }
93
+ run();
94
+ `,
95
+ });
96
+ try {
97
+ const result = await verifyReuse(path.join(root, "cjs-clean.spec.js"), "javascript");
98
+ expect(result.ok).toBe(true);
99
+ expect(result.violations).toEqual([]);
100
+ expect(result.checkedCalls).toBeGreaterThan(0);
101
+ expect(result.parseError).toBeUndefined();
102
+ }
103
+ finally {
104
+ cleanup();
105
+ }
106
+ });
107
+ it("flags a hallucinated method on a real pm member as a violation, with the real member list", async () => {
108
+ const { root, cleanup } = makeProject({
109
+ "package.json": `{}`,
110
+ "pages/page-manager.js": read(OO, "page-manager-cjs.js"),
111
+ "pages/loginPage.js": read(OO, "loginPage.js"),
112
+ "pages/widgetsPage.js": read(OO, "widgetsPage.js"),
113
+ "pages/widgetHelpers.js": read(OO, "widgetHelpers.js"),
114
+ "cjs-violation.spec.js": `
115
+ const PageManager = require('./pages/page-manager.js');
116
+ async function run() {
117
+ const pm = new PageManager(page);
118
+ await pm.widgetsPage.dragWidgetFake();
119
+ }
120
+ run();
121
+ `,
122
+ });
123
+ try {
124
+ const result = await verifyReuse(path.join(root, "cjs-violation.spec.js"), "javascript");
125
+ expect(result.violations).toEqual([
126
+ {
127
+ binding: "pm.widgetsPage",
128
+ member: "dragWidgetFake",
129
+ searched: [path.join(root, "pages", "widgetsPage.js"), path.join(root, "pages", "widgetHelpers.js")],
130
+ availableMembers: expect.arrayContaining(["addBtn"]),
131
+ },
132
+ ]);
133
+ expect(result.ok).toBe(false);
134
+ }
135
+ finally {
136
+ cleanup();
137
+ }
138
+ });
139
+ });
140
+ describe("verifyReuse -- af-style (tsconfig path-alias imports)", () => {
141
+ it("engages via a `@pageobjects/*` tsconfig alias -- real member passes, checkedCalls>0", async () => {
142
+ const { root, cleanup } = makeProject({
143
+ "tsconfig.json": read(AF, "alias.tsconfig.json"),
144
+ "src/pageobjects/asset-list-page.ts": read(AF, "pageobjects/asset-list-page.ts"),
145
+ "src/tests/alias.spec.ts": `
146
+ import { AssetListPage } from '@pageobjects/asset-list-page';
147
+ const list = new AssetListPage(page);
148
+ await list.clickSearch();
149
+ `,
150
+ });
151
+ try {
152
+ const result = await verifyReuse(path.join(root, "src", "tests", "alias.spec.ts"), "typescript");
153
+ expect(result.ok).toBe(true);
154
+ expect(result.violations).toEqual([]);
155
+ expect(result.checkedCalls).toBeGreaterThan(0);
156
+ }
157
+ finally {
158
+ cleanup();
159
+ }
160
+ });
161
+ it("flags a hallucinated method reached via the alias import as a violation", async () => {
162
+ const { root, cleanup } = makeProject({
163
+ "tsconfig.json": read(AF, "alias.tsconfig.json"),
164
+ "src/pageobjects/asset-list-page.ts": read(AF, "pageobjects/asset-list-page.ts"),
165
+ "src/tests/alias-violation.spec.ts": `
166
+ import { AssetListPage } from '@pageobjects/asset-list-page';
167
+ const list = new AssetListPage(page);
168
+ await list.clickSearchFake();
169
+ `,
170
+ });
171
+ try {
172
+ const result = await verifyReuse(path.join(root, "src", "tests", "alias-violation.spec.ts"), "typescript");
173
+ expect(result.violations).toEqual([
174
+ {
175
+ binding: "list",
176
+ member: "clickSearchFake",
177
+ searched: [path.join(root, "src", "pageobjects", "asset-list-page.ts")],
178
+ availableMembers: expect.arrayContaining(["clickSearch"]),
179
+ },
180
+ ]);
181
+ expect(result.ok).toBe(false);
182
+ }
183
+ finally {
184
+ cleanup();
185
+ }
186
+ });
187
+ });
188
+ describe("verifyReuse -- af-style (direct instantiation)", () => {
189
+ it("routes deep chains and unknown bindings to unverifiable, never violations", async () => {
190
+ const { root, cleanup } = makeProject({
191
+ "components/asset-list.page.ts": read(AF, "asset-list.page.ts"),
192
+ "components/workflow-footer.page.ts": read(AF, "workflow-footer.page.ts"),
193
+ "deep.spec.ts": `
194
+ import { AssetListPage } from './components/asset-list.page.js';
195
+ import { Unknown } from './components/unknown.js';
196
+ const list = new AssetListPage(page);
197
+ const thing = new Unknown();
198
+ await list.footer.okBtn.click();
199
+ `,
200
+ });
201
+ try {
202
+ const result = await verifyReuse(path.join(root, "deep.spec.ts"), "typescript");
203
+ expect(result.violations).toEqual([]);
204
+ expect(result.unverifiable).toEqual(expect.arrayContaining(["list.footer.okBtn.click", "thing"]));
205
+ expect(result.ok).toBe(true);
206
+ }
207
+ finally {
208
+ cleanup();
209
+ }
210
+ });
211
+ it("verifies a direct-instantiation class's own member against the class file", async () => {
212
+ const { root, cleanup } = makeProject({
213
+ "components/report.iframe.page.ts": read(AF, "report.iframe.page.ts"),
214
+ "iframe.spec.ts": `
215
+ import { ReportIframePage } from './components/report.iframe.page.js';
216
+ const report = new ReportIframePage(frame);
217
+ expect(report.exportBtn).toBeVisible();
218
+ `,
219
+ });
220
+ try {
221
+ const result = await verifyReuse(path.join(root, "iframe.spec.ts"), "typescript");
222
+ expect(result.violations).toEqual([]);
223
+ expect(result.ok).toBe(true);
224
+ expect(result.checkedCalls).toBeGreaterThan(0);
225
+ }
226
+ finally {
227
+ cleanup();
228
+ }
229
+ });
230
+ });
231
+ describe("verifyReuse -- free-function bindings", () => {
232
+ it("verifies a real free-function call (empty chain) against its source file -- ok:true, checkedCalls>0", async () => {
233
+ const { root, cleanup } = makeProject({
234
+ "package.json": `{"type":"module"}`,
235
+ "pages/commonActions.js": read(OO, "commonActions.js"),
236
+ "clean-fn.spec.js": `
237
+ import { openNavFlyout } from './pages/commonActions.js';
238
+ async function run() {
239
+ await openNavFlyout(page);
240
+ }
241
+ await run();
242
+ `,
243
+ });
244
+ try {
245
+ const result = await verifyReuse(path.join(root, "clean-fn.spec.js"), "javascript");
246
+ expect(result.violations).toEqual([]);
247
+ expect(result.ok).toBe(true);
248
+ expect(result.checkedCalls).toBeGreaterThan(0);
249
+ }
250
+ finally {
251
+ cleanup();
252
+ }
253
+ });
254
+ it("flags a hallucinated free-function call instead of crashing on the empty chain", async () => {
255
+ const { root, cleanup } = makeProject({
256
+ "package.json": `{"type":"module"}`,
257
+ "pages/commonActions.js": read(OO, "commonActions.js"),
258
+ "violation-fn.spec.js": `
259
+ import { fakeHelper } from './pages/commonActions.js';
260
+ async function run() {
261
+ await fakeHelper(page);
262
+ }
263
+ await run();
264
+ `,
265
+ });
266
+ try {
267
+ const result = await verifyReuse(path.join(root, "violation-fn.spec.js"), "javascript");
268
+ expect(result.violations).toEqual([
269
+ {
270
+ binding: "fakeHelper",
271
+ member: "fakeHelper",
272
+ searched: [path.join(root, "pages", "commonActions.js")],
273
+ availableMembers: expect.any(Array),
274
+ },
275
+ ]);
276
+ expect(result.ok).toBe(false);
277
+ }
278
+ finally {
279
+ cleanup();
280
+ }
281
+ });
282
+ });
283
+ describe("verifyReuse -- tsconfig paths with non-string values", () => {
284
+ it("skips a non-string paths target instead of throwing -- verification still runs", async () => {
285
+ const { root, cleanup } = makeProject({
286
+ "package.json": `{"type":"module"}`,
287
+ "tsconfig.json": `{"compilerOptions":{"paths":{"@x/*":[42]}}}`,
288
+ "pages/commonActions.js": read(OO, "commonActions.js"),
289
+ "clean-fn.spec.js": `
290
+ import { openNavFlyout } from './pages/commonActions.js';
291
+ async function run() {
292
+ await openNavFlyout(page);
293
+ }
294
+ await run();
295
+ `,
296
+ });
297
+ try {
298
+ const result = await verifyReuse(path.join(root, "clean-fn.spec.js"), "javascript");
299
+ expect(result.parseError).toBeUndefined();
300
+ expect(result.violations).toEqual([]);
301
+ expect(result.ok).toBe(true);
302
+ expect(result.checkedCalls).toBeGreaterThan(0);
303
+ }
304
+ finally {
305
+ cleanup();
306
+ }
307
+ });
308
+ });
309
+ describe("verifyReuse -- parse errors", () => {
310
+ it("reports a parse error for broken JS output", async () => {
311
+ const { root, cleanup } = makeProject({
312
+ "package.json": `{"type":"module"}`,
313
+ "broken.spec.js": `function broken( {\n`,
314
+ });
315
+ try {
316
+ const result = await verifyReuse(path.join(root, "broken.spec.js"), "javascript");
317
+ expect(result.ok).toBe(false);
318
+ expect(result.parseError).toBeDefined();
319
+ expect(result.parseError.length).toBeLessThanOrEqual(500);
320
+ }
321
+ finally {
322
+ cleanup();
323
+ }
324
+ });
325
+ });
@@ -0,0 +1,61 @@
1
+ import { DriftAction } from "../types/TestAnalysis.js";
2
+ /**
3
+ * Enumerate every path that differs from HEAD in the working tree — staged,
4
+ * unstaged, or untracked — as repo-relative paths, via `git status --porcelain`.
5
+ * A file the agent physically created or edited always shows up here (even if it
6
+ * was never `git add`ed), so a claim with no matching entry is one where no file
7
+ * work actually happened.
8
+ *
9
+ * Uses the NUL-delimited (`-z`) format: paths are emitted verbatim (no quoting,
10
+ * no backslash-escaping) and separated by NUL, so filenames containing spaces,
11
+ * leading/trailing whitespace, or even newlines are parsed exactly. In this
12
+ * format a rename/copy record spans two NUL fields — the destination path first,
13
+ * then the original — so we take the destination and skip the following field.
14
+ * `--untracked-files=all` lists each untracked file individually (otherwise git
15
+ * collapses a brand-new directory to "dir/", which would defeat path matching).
16
+ */
17
+ export declare function listChangedFiles(repoRoot: string): Promise<string[]>;
18
+ export interface UnbackedClaimsInput {
19
+ /** Absolute path of the primary repo checkout the report pertains to. */
20
+ repoRoot: string;
21
+ /** Repo-relative paths that have a working-tree change (from listChangedFiles). */
22
+ changedFiles: string[];
23
+ /** LLM-supplied created-test claims. */
24
+ newTests: {
25
+ testId?: string;
26
+ fileName?: string;
27
+ repository?: string;
28
+ }[];
29
+ /** Maintenance verdicts (server-derived from skyramp_actions). */
30
+ verdicts: {
31
+ action: DriftAction;
32
+ testFilePath: string;
33
+ }[];
34
+ /** owner/repo of the primary repo — used to exempt cross-repo newTest claims
35
+ * whose files live in a different checkout. */
36
+ primaryRepository?: string;
37
+ }
38
+ /**
39
+ * Cross-check report claims against files that actually changed in the working
40
+ * tree, returning a human-readable description for each claim NOT backed by a
41
+ * real change. An empty result means every claim is backed.
42
+ *
43
+ * The delivery path can only ship what the agent actually edited (SKYR-3883), so
44
+ * a report must never claim file work the tree doesn't reflect. This is the
45
+ * source-side guard: fail `skyramp_submit_report` loudly so the agent makes the
46
+ * edit (or drops the claim) rather than hallucinating it into the report.
47
+ *
48
+ * Only UPDATE verdicts are gated: an UPDATE is a targeted in-place edit that
49
+ * reliably implies a working-tree change. Exemptions:
50
+ * - REGENERATE verdicts — on an EXTERNAL (user-authored Playwright/Cypress/RTL)
51
+ * test these are report-only (Skyramp can't auto-regenerate them; the developer
52
+ * acts manually), so no working-tree change is expected; and even an internal
53
+ * regeneration can produce byte-identical content (no diff). Gating them would
54
+ * falsely reject legitimate reports.
55
+ * - DELETE verdicts — applied deterministically by the delivery step, not
56
+ * necessarily reflected in the tree at report time.
57
+ * - IGNORE / VERIFY verdicts — assert that NO edit was needed.
58
+ * - Claims for files outside this checkout (cross-repo `repository`, or an
59
+ * absolute path resolving outside `repoRoot`) — a different checkout owns them.
60
+ */
61
+ export declare function findUnbackedClaims(input: UnbackedClaimsInput): string[];
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+ import { execFile } from "child_process";
3
+ import { promisify } from "util";
4
+ import { testFileMatches } from "./utils.js";
5
+ import { DriftAction } from "../types/TestAnalysis.js";
6
+ const execFileAsync = promisify(execFile);
7
+ /**
8
+ * Enumerate every path that differs from HEAD in the working tree — staged,
9
+ * unstaged, or untracked — as repo-relative paths, via `git status --porcelain`.
10
+ * A file the agent physically created or edited always shows up here (even if it
11
+ * was never `git add`ed), so a claim with no matching entry is one where no file
12
+ * work actually happened.
13
+ *
14
+ * Uses the NUL-delimited (`-z`) format: paths are emitted verbatim (no quoting,
15
+ * no backslash-escaping) and separated by NUL, so filenames containing spaces,
16
+ * leading/trailing whitespace, or even newlines are parsed exactly. In this
17
+ * format a rename/copy record spans two NUL fields — the destination path first,
18
+ * then the original — so we take the destination and skip the following field.
19
+ * `--untracked-files=all` lists each untracked file individually (otherwise git
20
+ * collapses a brand-new directory to "dir/", which would defeat path matching).
21
+ */
22
+ export async function listChangedFiles(repoRoot) {
23
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot });
24
+ const files = [];
25
+ const tokens = stdout.split("\0");
26
+ for (let i = 0; i < tokens.length; i++) {
27
+ const entry = tokens[i];
28
+ // Each record is "XY <path>"; a trailing empty token follows the final NUL.
29
+ if (entry.length < 4)
30
+ continue;
31
+ const status = entry.slice(0, 2);
32
+ const p = entry.slice(3);
33
+ // Rename (R) / copy (C): the NEXT NUL field is the original path — consume
34
+ // and skip it; the destination path (this token) is the changed file.
35
+ if (/[RC]/.test(status))
36
+ i++;
37
+ if (p)
38
+ files.push(p);
39
+ }
40
+ return files;
41
+ }
42
+ /**
43
+ * Cross-check report claims against files that actually changed in the working
44
+ * tree, returning a human-readable description for each claim NOT backed by a
45
+ * real change. An empty result means every claim is backed.
46
+ *
47
+ * The delivery path can only ship what the agent actually edited (SKYR-3883), so
48
+ * a report must never claim file work the tree doesn't reflect. This is the
49
+ * source-side guard: fail `skyramp_submit_report` loudly so the agent makes the
50
+ * edit (or drops the claim) rather than hallucinating it into the report.
51
+ *
52
+ * Only UPDATE verdicts are gated: an UPDATE is a targeted in-place edit that
53
+ * reliably implies a working-tree change. Exemptions:
54
+ * - REGENERATE verdicts — on an EXTERNAL (user-authored Playwright/Cypress/RTL)
55
+ * test these are report-only (Skyramp can't auto-regenerate them; the developer
56
+ * acts manually), so no working-tree change is expected; and even an internal
57
+ * regeneration can produce byte-identical content (no diff). Gating them would
58
+ * falsely reject legitimate reports.
59
+ * - DELETE verdicts — applied deterministically by the delivery step, not
60
+ * necessarily reflected in the tree at report time.
61
+ * - IGNORE / VERIFY verdicts — assert that NO edit was needed.
62
+ * - Claims for files outside this checkout (cross-repo `repository`, or an
63
+ * absolute path resolving outside `repoRoot`) — a different checkout owns them.
64
+ */
65
+ export function findUnbackedClaims(input) {
66
+ const { repoRoot, changedFiles, newTests, verdicts, primaryRepository } = input;
67
+ const unbacked = [];
68
+ const isBacked = (claimedPath) => {
69
+ let candidate = claimedPath;
70
+ if (path.isAbsolute(claimedPath)) {
71
+ const rel = path.relative(repoRoot, claimedPath);
72
+ // Outside this checkout — not verifiable here; treat as backed (exempt).
73
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
74
+ return true;
75
+ candidate = rel;
76
+ }
77
+ return changedFiles.some((c) => testFileMatches(c, candidate));
78
+ };
79
+ for (const t of newTests) {
80
+ if (!t.fileName)
81
+ continue;
82
+ // Cross-repo finding — its file lives in a different checkout, not this tree.
83
+ // Exempt any claim explicitly attributed to a repo that isn't the primary.
84
+ // When the primary repo is unknown (primaryRepository undefined) we can't
85
+ // confirm the file belongs to this checkout, so any explicitly-attributed
86
+ // claim is exempted rather than risk a false rejection.
87
+ if (t.repository && t.repository !== primaryRepository)
88
+ continue;
89
+ if (!isBacked(t.fileName)) {
90
+ unbacked.push(`newTestsCreated "${t.testId ?? t.fileName}" claims file "${t.fileName}", but no matching file was created or modified in the working tree`);
91
+ }
92
+ }
93
+ for (const v of verdicts) {
94
+ // Only UPDATE (a targeted in-place edit) is required to have a working-tree
95
+ // change. REGENERATE is exempt: external report-only regenerations make no
96
+ // change, and internal ones may regenerate to identical content.
97
+ if (v.action !== DriftAction.Update)
98
+ continue;
99
+ if (!isBacked(v.testFilePath)) {
100
+ unbacked.push(`testMaintenance ${v.action} claims an edit to "${path.basename(v.testFilePath)}", but that file has no working-tree change`);
101
+ }
102
+ }
103
+ return unbacked;
104
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { execFileSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ import { DriftAction } from "../types/TestAnalysis.js";
7
+ import { findUnbackedClaims, listChangedFiles } from "./reportVerification.js";
8
+ const REPO = "/repo";
9
+ describe("findUnbackedClaims", () => {
10
+ it("returns [] when every claim is backed by a working-tree change", () => {
11
+ const unbacked = findUnbackedClaims({
12
+ repoRoot: REPO,
13
+ changedFiles: [
14
+ "tests/skyramp/products_contract_test.py",
15
+ "dcim/tests/test_filtersets.py",
16
+ ],
17
+ newTests: [
18
+ {
19
+ testId: "contract-get-products",
20
+ fileName: "products_contract_test.py",
21
+ },
22
+ ],
23
+ verdicts: [
24
+ {
25
+ action: DriftAction.Update,
26
+ testFilePath: "/repo/dcim/tests/test_filtersets.py",
27
+ },
28
+ ],
29
+ });
30
+ expect(unbacked).toEqual([]);
31
+ });
32
+ it("flags a newTestsCreated claim whose file was never created (hallucination)", () => {
33
+ const unbacked = findUnbackedClaims({
34
+ repoRoot: REPO,
35
+ changedFiles: ["tests/skyramp/products_contract_test.py"],
36
+ newTests: [
37
+ {
38
+ testId: "integration-phantom",
39
+ fileName: "phantom_integration_test.py",
40
+ },
41
+ ],
42
+ verdicts: [],
43
+ });
44
+ expect(unbacked).toHaveLength(1);
45
+ expect(unbacked[0]).toContain("phantom_integration_test.py");
46
+ });
47
+ it("flags an UPDATE verdict whose file has no working-tree change (the SKYR-3883 case)", () => {
48
+ const unbacked = findUnbackedClaims({
49
+ repoRoot: REPO,
50
+ changedFiles: ["tests/skyramp/site_filter_integration_test.py"],
51
+ newTests: [],
52
+ verdicts: [
53
+ {
54
+ action: DriftAction.Update,
55
+ testFilePath: "/repo/dcim/tests/test_filtersets.py",
56
+ },
57
+ ],
58
+ });
59
+ expect(unbacked).toHaveLength(1);
60
+ expect(unbacked[0]).toContain("test_filtersets.py");
61
+ });
62
+ it("never flags REGENERATE/DELETE/IGNORE/VERIFY verdicts even when absent from the tree", () => {
63
+ // REGENERATE is exempt: external (user-authored) regenerations are report-only
64
+ // with no working-tree change, and internal ones may regenerate identically.
65
+ const unbacked = findUnbackedClaims({
66
+ repoRoot: REPO,
67
+ changedFiles: [],
68
+ newTests: [],
69
+ verdicts: [
70
+ {
71
+ action: DriftAction.Regenerate,
72
+ testFilePath: "/repo/frontend/tests/cart_pom.spec.ts",
73
+ },
74
+ { action: DriftAction.Delete, testFilePath: "/repo/tests/gone.py" },
75
+ { action: DriftAction.Ignore, testFilePath: "/repo/tests/kept.py" },
76
+ { action: DriftAction.Verify, testFilePath: "/repo/tests/checked.py" },
77
+ ],
78
+ });
79
+ expect(unbacked).toEqual([]);
80
+ });
81
+ it("exempts a cross-repo newTest claim (repository differs from the primary)", () => {
82
+ const unbacked = findUnbackedClaims({
83
+ repoRoot: REPO,
84
+ changedFiles: [],
85
+ newTests: [
86
+ {
87
+ testId: "contract-related",
88
+ fileName: "related_contract_test.py",
89
+ repository: "acme/related-repo",
90
+ },
91
+ ],
92
+ verdicts: [],
93
+ primaryRepository: "acme/primary-repo",
94
+ });
95
+ expect(unbacked).toEqual([]);
96
+ });
97
+ it("exempts a cross-repo newTest claim even when the primary repo is unknown", () => {
98
+ // primaryRepository omitted: we can't confirm the file belongs to this
99
+ // checkout, so an explicitly-attributed claim must not be validated here.
100
+ const unbacked = findUnbackedClaims({
101
+ repoRoot: REPO,
102
+ changedFiles: [],
103
+ newTests: [
104
+ {
105
+ testId: "contract-related",
106
+ fileName: "related_contract_test.py",
107
+ repository: "acme/related-repo",
108
+ },
109
+ ],
110
+ verdicts: [],
111
+ });
112
+ expect(unbacked).toEqual([]);
113
+ });
114
+ it("exempts a verdict whose absolute path resolves outside the checkout", () => {
115
+ const unbacked = findUnbackedClaims({
116
+ repoRoot: REPO,
117
+ changedFiles: [],
118
+ newTests: [],
119
+ verdicts: [
120
+ {
121
+ action: DriftAction.Update,
122
+ testFilePath: "/somewhere/else/outside_test.py",
123
+ },
124
+ ],
125
+ });
126
+ expect(unbacked).toEqual([]);
127
+ });
128
+ it("matches a nested newTest fileName by basename against the changed path", () => {
129
+ const backed = findUnbackedClaims({
130
+ repoRoot: REPO,
131
+ changedFiles: ["tests/skyramp/deep/nested_contract_test.py"],
132
+ newTests: [{ testId: "c", fileName: "nested_contract_test.py" }],
133
+ verdicts: [],
134
+ });
135
+ expect(backed).toEqual([]);
136
+ });
137
+ });
138
+ describe("listChangedFiles", () => {
139
+ it("lists staged, unstaged, and untracked paths as repo-relative strings", async () => {
140
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), "rv-repo-"));
141
+ const git = (args) => execFileSync("git", args, { cwd: repo, stdio: "pipe" });
142
+ git(["init", "-q"]);
143
+ git(["config", "user.email", "t@t.dev"]);
144
+ git(["config", "user.name", "t"]);
145
+ // committed baseline
146
+ fs.mkdirSync(path.join(repo, "dcim/tests"), { recursive: true });
147
+ fs.writeFileSync(path.join(repo, "dcim/tests/test_filtersets.py"), "orig\n");
148
+ git(["add", "-A"]);
149
+ git(["commit", "-q", "-m", "base"]);
150
+ // modify a tracked file (unstaged) + add an untracked file
151
+ fs.writeFileSync(path.join(repo, "dcim/tests/test_filtersets.py"), "edited\n");
152
+ fs.mkdirSync(path.join(repo, "tests/skyramp"), { recursive: true });
153
+ fs.writeFileSync(path.join(repo, "tests/skyramp/new_contract_test.py"), "new\n");
154
+ const changed = await listChangedFiles(repo);
155
+ expect(changed).toContain("dcim/tests/test_filtersets.py");
156
+ expect(changed).toContain("tests/skyramp/new_contract_test.py");
157
+ });
158
+ it("attributes a renamed file to its destination path and skips the original", async () => {
159
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), "rv-repo-"));
160
+ const git = (args) => execFileSync("git", args, { cwd: repo, stdio: "pipe" });
161
+ git(["init", "-q"]);
162
+ git(["config", "user.email", "t@t.dev"]);
163
+ git(["config", "user.name", "t"]);
164
+ fs.mkdirSync(path.join(repo, "dcim/tests"), { recursive: true });
165
+ fs.writeFileSync(path.join(repo, "dcim/tests/old_test.py"), "def test_x(): pass\n");
166
+ git(["add", "-A"]);
167
+ git(["commit", "-q", "-m", "base"]);
168
+ // Staged rename (identical content → git detects it as R100).
169
+ git(["mv", "dcim/tests/old_test.py", "dcim/tests/new_test.py"]);
170
+ const changed = await listChangedFiles(repo);
171
+ expect(changed).toContain("dcim/tests/new_test.py");
172
+ expect(changed).not.toContain("dcim/tests/old_test.py");
173
+ });
174
+ it("preserves a path containing spaces (NUL-delimited, no quoting)", async () => {
175
+ const repo = fs.mkdtempSync(path.join(os.tmpdir(), "rv-repo-"));
176
+ const git = (args) => execFileSync("git", args, { cwd: repo, stdio: "pipe" });
177
+ git(["init", "-q"]);
178
+ git(["config", "user.email", "t@t.dev"]);
179
+ git(["config", "user.name", "t"]);
180
+ fs.mkdirSync(path.join(repo, "tests/skyramp"), { recursive: true });
181
+ fs.writeFileSync(path.join(repo, "tests/skyramp/my new test.py"), "new\n");
182
+ const changed = await listChangedFiles(repo);
183
+ expect(changed).toContain("tests/skyramp/my new test.py");
184
+ });
185
+ });