@avi770/testteam 1.2.0

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 (325) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/agents/01-analyst.ts +100 -0
  5. package/agents/02-seed-architect.ts +59 -0
  6. package/agents/03-test-generator.ts +191 -0
  7. package/agents/04-unit-runner.ts +160 -0
  8. package/agents/05-browser-crawler.ts +790 -0
  9. package/agents/06-api-exerciser.ts +311 -0
  10. package/agents/07-security-scout.ts +188 -0
  11. package/agents/08-a11y-guardian.ts +212 -0
  12. package/agents/09-healer.ts +228 -0
  13. package/agents/10-reporter.ts +266 -0
  14. package/agents/11-fixer.ts +253 -0
  15. package/agents/12-ux-inspector.ts +444 -0
  16. package/agents/13-performance-profiler.ts +271 -0
  17. package/agents/14-data-integrity-auditor.ts +417 -0
  18. package/agents/15-regression-sentinel.ts +307 -0
  19. package/agents/16-chaos-agent.ts +228 -0
  20. package/agents/17-documentation-validator.ts +266 -0
  21. package/agents/18-integration-watchdog.ts +178 -0
  22. package/agents/19-tenant-isolation-auditor.ts +199 -0
  23. package/agents/20-workflow-completion-tester.ts +203 -0
  24. package/agents/21-state-session-tester.ts +262 -0
  25. package/agents/22-email-notification-verifier.ts +244 -0
  26. package/agents/23-migration-tester.ts +80 -0
  27. package/agents/__tests__/01-analyst.test.ts +188 -0
  28. package/agents/__tests__/02-seed-architect.test.ts +152 -0
  29. package/agents/__tests__/03-test-generator-full.test.ts +321 -0
  30. package/agents/__tests__/03-test-generator.test.ts +318 -0
  31. package/agents/__tests__/04-unit-runner.test.ts +320 -0
  32. package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
  33. package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
  34. package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
  35. package/agents/__tests__/05-browser-crawler.test.ts +518 -0
  36. package/agents/__tests__/06-api-exerciser.test.ts +619 -0
  37. package/agents/__tests__/07-security-scout.test.ts +382 -0
  38. package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
  39. package/agents/__tests__/09-healer.test.ts +384 -0
  40. package/agents/__tests__/10-reporter.test.ts +366 -0
  41. package/agents/__tests__/11-fixer.test.ts +406 -0
  42. package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
  43. package/agents/__tests__/12-ux-inspector.test.ts +443 -0
  44. package/agents/__tests__/13-performance-profiler.test.ts +411 -0
  45. package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
  46. package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
  47. package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
  48. package/agents/__tests__/16-chaos-agent.test.ts +427 -0
  49. package/agents/__tests__/17-documentation-validator.test.ts +402 -0
  50. package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
  51. package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
  52. package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
  53. package/agents/__tests__/21-state-session-tester.test.ts +374 -0
  54. package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
  55. package/agents/__tests__/23-migration-tester.test.ts +145 -0
  56. package/agents/__tests__/base-agent.test.ts +188 -0
  57. package/agents/__tests__/registry.test.ts +218 -0
  58. package/agents/base-agent.ts +77 -0
  59. package/agents/registry.ts +136 -0
  60. package/baselines/api-schemas/.gitkeep +0 -0
  61. package/baselines/performance/.gitkeep +0 -0
  62. package/baselines/screenshots/.gitkeep +0 -0
  63. package/bin/testteam.js +10 -0
  64. package/core/__tests__/ci-output.test.ts +430 -0
  65. package/core/__tests__/cli.test.ts +387 -0
  66. package/core/__tests__/config.test.ts +78 -0
  67. package/core/__tests__/cost-tracker.test.ts +158 -0
  68. package/core/__tests__/evidence.test.ts +265 -0
  69. package/core/__tests__/fix-loop.test.ts +210 -0
  70. package/core/__tests__/health-check.test.ts +44 -0
  71. package/core/__tests__/init.test.ts +609 -0
  72. package/core/__tests__/integration.test.ts +204 -0
  73. package/core/__tests__/license-gen.test.ts +227 -0
  74. package/core/__tests__/license.test.ts +326 -0
  75. package/core/__tests__/multi-browser.test.ts +278 -0
  76. package/core/__tests__/orchestrator.test.ts +519 -0
  77. package/core/__tests__/phase-gate.test.ts +43 -0
  78. package/core/__tests__/report-html.test.ts +398 -0
  79. package/core/__tests__/report-upload.test.ts +325 -0
  80. package/core/__tests__/run-counter.test.ts +234 -0
  81. package/core/ci-output.ts +240 -0
  82. package/core/cli.ts +232 -0
  83. package/core/config.ts +178 -0
  84. package/core/cost-tracker.ts +59 -0
  85. package/core/evidence.ts +132 -0
  86. package/core/fix-loop.ts +85 -0
  87. package/core/health-check.ts +54 -0
  88. package/core/init.ts +546 -0
  89. package/core/license-gen.ts +212 -0
  90. package/core/license.ts +211 -0
  91. package/core/messages.ts +67 -0
  92. package/core/multi-browser.ts +136 -0
  93. package/core/orchestrator.ts +354 -0
  94. package/core/phase-gate.ts +55 -0
  95. package/core/report-html.ts +657 -0
  96. package/core/report-upload.ts +188 -0
  97. package/core/run-counter.ts +175 -0
  98. package/core/types.ts +57 -0
  99. package/dist/agents/01-analyst.d.ts +11 -0
  100. package/dist/agents/01-analyst.d.ts.map +1 -0
  101. package/dist/agents/01-analyst.js +75 -0
  102. package/dist/agents/01-analyst.js.map +1 -0
  103. package/dist/agents/02-seed-architect.d.ts +11 -0
  104. package/dist/agents/02-seed-architect.d.ts.map +1 -0
  105. package/dist/agents/02-seed-architect.js +51 -0
  106. package/dist/agents/02-seed-architect.js.map +1 -0
  107. package/dist/agents/03-test-generator.d.ts +9 -0
  108. package/dist/agents/03-test-generator.d.ts.map +1 -0
  109. package/dist/agents/03-test-generator.js +167 -0
  110. package/dist/agents/03-test-generator.js.map +1 -0
  111. package/dist/agents/04-unit-runner.d.ts +9 -0
  112. package/dist/agents/04-unit-runner.d.ts.map +1 -0
  113. package/dist/agents/04-unit-runner.js +113 -0
  114. package/dist/agents/04-unit-runner.js.map +1 -0
  115. package/dist/agents/05-browser-crawler.d.ts +30 -0
  116. package/dist/agents/05-browser-crawler.d.ts.map +1 -0
  117. package/dist/agents/05-browser-crawler.js +685 -0
  118. package/dist/agents/05-browser-crawler.js.map +1 -0
  119. package/dist/agents/06-api-exerciser.d.ts +23 -0
  120. package/dist/agents/06-api-exerciser.d.ts.map +1 -0
  121. package/dist/agents/06-api-exerciser.js +253 -0
  122. package/dist/agents/06-api-exerciser.js.map +1 -0
  123. package/dist/agents/07-security-scout.d.ts +11 -0
  124. package/dist/agents/07-security-scout.d.ts.map +1 -0
  125. package/dist/agents/07-security-scout.js +142 -0
  126. package/dist/agents/07-security-scout.js.map +1 -0
  127. package/dist/agents/08-a11y-guardian.d.ts +13 -0
  128. package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
  129. package/dist/agents/08-a11y-guardian.js +176 -0
  130. package/dist/agents/08-a11y-guardian.js.map +1 -0
  131. package/dist/agents/09-healer.d.ts +33 -0
  132. package/dist/agents/09-healer.d.ts.map +1 -0
  133. package/dist/agents/09-healer.js +167 -0
  134. package/dist/agents/09-healer.js.map +1 -0
  135. package/dist/agents/10-reporter.d.ts +26 -0
  136. package/dist/agents/10-reporter.d.ts.map +1 -0
  137. package/dist/agents/10-reporter.js +215 -0
  138. package/dist/agents/10-reporter.js.map +1 -0
  139. package/dist/agents/11-fixer.d.ts +26 -0
  140. package/dist/agents/11-fixer.d.ts.map +1 -0
  141. package/dist/agents/11-fixer.js +195 -0
  142. package/dist/agents/11-fixer.js.map +1 -0
  143. package/dist/agents/12-ux-inspector.d.ts +15 -0
  144. package/dist/agents/12-ux-inspector.d.ts.map +1 -0
  145. package/dist/agents/12-ux-inspector.js +364 -0
  146. package/dist/agents/12-ux-inspector.js.map +1 -0
  147. package/dist/agents/13-performance-profiler.d.ts +13 -0
  148. package/dist/agents/13-performance-profiler.d.ts.map +1 -0
  149. package/dist/agents/13-performance-profiler.js +216 -0
  150. package/dist/agents/13-performance-profiler.js.map +1 -0
  151. package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
  152. package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
  153. package/dist/agents/14-data-integrity-auditor.js +356 -0
  154. package/dist/agents/14-data-integrity-auditor.js.map +1 -0
  155. package/dist/agents/15-regression-sentinel.d.ts +25 -0
  156. package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
  157. package/dist/agents/15-regression-sentinel.js +251 -0
  158. package/dist/agents/15-regression-sentinel.js.map +1 -0
  159. package/dist/agents/16-chaos-agent.d.ts +9 -0
  160. package/dist/agents/16-chaos-agent.d.ts.map +1 -0
  161. package/dist/agents/16-chaos-agent.js +207 -0
  162. package/dist/agents/16-chaos-agent.js.map +1 -0
  163. package/dist/agents/17-documentation-validator.d.ts +31 -0
  164. package/dist/agents/17-documentation-validator.d.ts.map +1 -0
  165. package/dist/agents/17-documentation-validator.js +246 -0
  166. package/dist/agents/17-documentation-validator.js.map +1 -0
  167. package/dist/agents/18-integration-watchdog.d.ts +10 -0
  168. package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
  169. package/dist/agents/18-integration-watchdog.js +138 -0
  170. package/dist/agents/18-integration-watchdog.js.map +1 -0
  171. package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
  172. package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
  173. package/dist/agents/19-tenant-isolation-auditor.js +166 -0
  174. package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
  175. package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
  176. package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
  177. package/dist/agents/20-workflow-completion-tester.js +159 -0
  178. package/dist/agents/20-workflow-completion-tester.js.map +1 -0
  179. package/dist/agents/21-state-session-tester.d.ts +10 -0
  180. package/dist/agents/21-state-session-tester.d.ts.map +1 -0
  181. package/dist/agents/21-state-session-tester.js +233 -0
  182. package/dist/agents/21-state-session-tester.js.map +1 -0
  183. package/dist/agents/22-email-notification-verifier.d.ts +11 -0
  184. package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
  185. package/dist/agents/22-email-notification-verifier.js +199 -0
  186. package/dist/agents/22-email-notification-verifier.js.map +1 -0
  187. package/dist/agents/23-migration-tester.d.ts +10 -0
  188. package/dist/agents/23-migration-tester.d.ts.map +1 -0
  189. package/dist/agents/23-migration-tester.js +74 -0
  190. package/dist/agents/23-migration-tester.js.map +1 -0
  191. package/dist/agents/base-agent.d.ts +19 -0
  192. package/dist/agents/base-agent.d.ts.map +1 -0
  193. package/dist/agents/base-agent.js +67 -0
  194. package/dist/agents/base-agent.js.map +1 -0
  195. package/dist/agents/registry.d.ts +29 -0
  196. package/dist/agents/registry.d.ts.map +1 -0
  197. package/dist/agents/registry.js +117 -0
  198. package/dist/agents/registry.js.map +1 -0
  199. package/dist/core/ci-output.d.ts +35 -0
  200. package/dist/core/ci-output.d.ts.map +1 -0
  201. package/dist/core/ci-output.js +193 -0
  202. package/dist/core/ci-output.js.map +1 -0
  203. package/dist/core/cli.d.ts +11 -0
  204. package/dist/core/cli.d.ts.map +1 -0
  205. package/dist/core/cli.js +197 -0
  206. package/dist/core/cli.js.map +1 -0
  207. package/dist/core/config.d.ts +111 -0
  208. package/dist/core/config.d.ts.map +1 -0
  209. package/dist/core/config.js +42 -0
  210. package/dist/core/config.js.map +1 -0
  211. package/dist/core/cost-tracker.d.ts +22 -0
  212. package/dist/core/cost-tracker.d.ts.map +1 -0
  213. package/dist/core/cost-tracker.js +41 -0
  214. package/dist/core/cost-tracker.js.map +1 -0
  215. package/dist/core/evidence.d.ts +28 -0
  216. package/dist/core/evidence.d.ts.map +1 -0
  217. package/dist/core/evidence.js +95 -0
  218. package/dist/core/evidence.js.map +1 -0
  219. package/dist/core/fix-loop.d.ts +29 -0
  220. package/dist/core/fix-loop.d.ts.map +1 -0
  221. package/dist/core/fix-loop.js +70 -0
  222. package/dist/core/fix-loop.js.map +1 -0
  223. package/dist/core/health-check.d.ts +21 -0
  224. package/dist/core/health-check.d.ts.map +1 -0
  225. package/dist/core/health-check.js +26 -0
  226. package/dist/core/health-check.js.map +1 -0
  227. package/dist/core/init.d.ts +2 -0
  228. package/dist/core/init.d.ts.map +1 -0
  229. package/dist/core/init.js +435 -0
  230. package/dist/core/init.js.map +1 -0
  231. package/dist/core/license-gen.d.ts +12 -0
  232. package/dist/core/license-gen.d.ts.map +1 -0
  233. package/dist/core/license-gen.js +169 -0
  234. package/dist/core/license-gen.js.map +1 -0
  235. package/dist/core/license.d.ts +33 -0
  236. package/dist/core/license.d.ts.map +1 -0
  237. package/dist/core/license.js +170 -0
  238. package/dist/core/license.js.map +1 -0
  239. package/dist/core/messages.d.ts +10 -0
  240. package/dist/core/messages.d.ts.map +1 -0
  241. package/dist/core/messages.js +47 -0
  242. package/dist/core/messages.js.map +1 -0
  243. package/dist/core/multi-browser.d.ts +36 -0
  244. package/dist/core/multi-browser.d.ts.map +1 -0
  245. package/dist/core/multi-browser.js +88 -0
  246. package/dist/core/multi-browser.js.map +1 -0
  247. package/dist/core/orchestrator.d.ts +48 -0
  248. package/dist/core/orchestrator.d.ts.map +1 -0
  249. package/dist/core/orchestrator.js +291 -0
  250. package/dist/core/orchestrator.js.map +1 -0
  251. package/dist/core/phase-gate.d.ts +4 -0
  252. package/dist/core/phase-gate.d.ts.map +1 -0
  253. package/dist/core/phase-gate.js +39 -0
  254. package/dist/core/phase-gate.js.map +1 -0
  255. package/dist/core/report-html.d.ts +9 -0
  256. package/dist/core/report-html.d.ts.map +1 -0
  257. package/dist/core/report-html.js +617 -0
  258. package/dist/core/report-html.js.map +1 -0
  259. package/dist/core/report-upload.d.ts +16 -0
  260. package/dist/core/report-upload.d.ts.map +1 -0
  261. package/dist/core/report-upload.js +124 -0
  262. package/dist/core/report-upload.js.map +1 -0
  263. package/dist/core/run-counter.d.ts +40 -0
  264. package/dist/core/run-counter.d.ts.map +1 -0
  265. package/dist/core/run-counter.js +120 -0
  266. package/dist/core/run-counter.js.map +1 -0
  267. package/dist/core/types.d.ts +53 -0
  268. package/dist/core/types.d.ts.map +1 -0
  269. package/dist/core/types.js +2 -0
  270. package/dist/core/types.js.map +1 -0
  271. package/dist/helpers/api-client.d.ts +30 -0
  272. package/dist/helpers/api-client.d.ts.map +1 -0
  273. package/dist/helpers/api-client.js +77 -0
  274. package/dist/helpers/api-client.js.map +1 -0
  275. package/dist/helpers/element-discovery.d.ts +18 -0
  276. package/dist/helpers/element-discovery.d.ts.map +1 -0
  277. package/dist/helpers/element-discovery.js +82 -0
  278. package/dist/helpers/element-discovery.js.map +1 -0
  279. package/dist/helpers/env-resolver.d.ts +29 -0
  280. package/dist/helpers/env-resolver.d.ts.map +1 -0
  281. package/dist/helpers/env-resolver.js +51 -0
  282. package/dist/helpers/env-resolver.js.map +1 -0
  283. package/dist/helpers/form-filler.d.ts +13 -0
  284. package/dist/helpers/form-filler.d.ts.map +1 -0
  285. package/dist/helpers/form-filler.js +98 -0
  286. package/dist/helpers/form-filler.js.map +1 -0
  287. package/dist/helpers/modal-handler.d.ts +16 -0
  288. package/dist/helpers/modal-handler.d.ts.map +1 -0
  289. package/dist/helpers/modal-handler.js +95 -0
  290. package/dist/helpers/modal-handler.js.map +1 -0
  291. package/dist/helpers/navigation.d.ts +37 -0
  292. package/dist/helpers/navigation.d.ts.map +1 -0
  293. package/dist/helpers/navigation.js +83 -0
  294. package/dist/helpers/navigation.js.map +1 -0
  295. package/dist/helpers/quality-gate.d.ts +17 -0
  296. package/dist/helpers/quality-gate.d.ts.map +1 -0
  297. package/dist/helpers/quality-gate.js +144 -0
  298. package/dist/helpers/quality-gate.js.map +1 -0
  299. package/dist/helpers/screenshot.d.ts +24 -0
  300. package/dist/helpers/screenshot.d.ts.map +1 -0
  301. package/dist/helpers/screenshot.js +76 -0
  302. package/dist/helpers/screenshot.js.map +1 -0
  303. package/dist/helpers/seed-validator.d.ts +15 -0
  304. package/dist/helpers/seed-validator.d.ts.map +1 -0
  305. package/dist/helpers/seed-validator.js +53 -0
  306. package/dist/helpers/seed-validator.js.map +1 -0
  307. package/helpers/__tests__/api-client.test.ts +199 -0
  308. package/helpers/__tests__/element-discovery.test.ts +202 -0
  309. package/helpers/__tests__/form-filler-extended.test.ts +212 -0
  310. package/helpers/__tests__/form-filler.test.ts +99 -0
  311. package/helpers/__tests__/modal-handler.test.ts +152 -0
  312. package/helpers/__tests__/navigation.test.ts +214 -0
  313. package/helpers/__tests__/quality-gate.test.ts +117 -0
  314. package/helpers/__tests__/screenshot.test.ts +139 -0
  315. package/helpers/__tests__/seed-validator.test.ts +114 -0
  316. package/helpers/api-client.ts +111 -0
  317. package/helpers/element-discovery.ts +105 -0
  318. package/helpers/env-resolver.ts +69 -0
  319. package/helpers/form-filler.ts +126 -0
  320. package/helpers/modal-handler.ts +108 -0
  321. package/helpers/navigation.ts +100 -0
  322. package/helpers/quality-gate.ts +180 -0
  323. package/helpers/screenshot.ts +111 -0
  324. package/helpers/seed-validator.ts +70 -0
  325. package/package.json +88 -0
@@ -0,0 +1,657 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { join } from 'path';
3
+ import type { ValidatedConfig } from '../../core/config';
4
+ import type { Phase } from '../../core/types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Stub config
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const stubConfig = {
11
+ schemaVersion: 1,
12
+ name: 'test',
13
+ auth: {
14
+ loginUrl: 'http://localhost/login',
15
+ credentials: { admin: { email: 'admin@test.com', password: 'secret' } },
16
+ },
17
+ modules: [
18
+ { id: 'dashboard', route: '/dashboard', sidebarIcon: true },
19
+ { id: 'clients', route: '/clients', sidebarIcon: true },
20
+ ],
21
+ environments: {
22
+ beta: { baseUrl: 'http://localhost:3000', seed: false },
23
+ },
24
+ portals: [],
25
+ breakpoints: [],
26
+ integrations: [],
27
+ workers: [],
28
+ workflows: [],
29
+ accuracy: { enabled: true, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
30
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
31
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' as const },
32
+ agentClassification: { blocking: [], advisory: [] },
33
+ thresholds: {
34
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
35
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
36
+ apiResponseTime: 2000,
37
+ bundleSizeLimit: 500000,
38
+ maxConsoleErrors: 0,
39
+ },
40
+ disabledAgents: [],
41
+ disabledAgentSet: new Set<number>(),
42
+ } satisfies ValidatedConfig;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Mock node:fs/promises
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const statMock = vi.fn();
49
+ const readdirMock = vi.fn();
50
+ const mkdirMock = vi.fn();
51
+ const readFileMock = vi.fn();
52
+ const writeFileMock = vi.fn();
53
+
54
+ vi.mock('node:fs/promises', async (importOriginal) => {
55
+ const actual = await importOriginal<typeof import('node:fs/promises')>();
56
+ return {
57
+ ...actual,
58
+ stat: (...args: unknown[]) => statMock(...args),
59
+ readdir: (...args: unknown[]) => readdirMock(...args),
60
+ mkdir: (...args: unknown[]) => mkdirMock(...args),
61
+ readFile: (...args: unknown[]) => readFileMock(...args),
62
+ writeFile: (...args: unknown[]) => writeFileMock(...args),
63
+ copyFile: vi.fn().mockResolvedValue(undefined),
64
+ };
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Mock screenshot helper
69
+ // ---------------------------------------------------------------------------
70
+
71
+ const compareScreenshotsMock = vi.fn();
72
+ const updateBaselineMock = vi.fn();
73
+
74
+ vi.mock('../../helpers/screenshot', () => ({
75
+ compareScreenshots: (...args: unknown[]) => compareScreenshotsMock(...args),
76
+ updateBaseline: (...args: unknown[]) => updateBaselineMock(...args),
77
+ captureScreenshot: vi.fn().mockResolvedValue('/tmp/test-run/screenshot.png'),
78
+ }));
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Dynamic import AFTER mocks
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const { RegressionSentinelAgent } = await import('../15-regression-sentinel');
85
+
86
+ // Compute paths the same way the agent does so they match on all platforms
87
+ const RUN_DIR = join('/tmp', 'test-runs', 'run-001');
88
+ const BASELINES_DIR = join('/tmp', 'test-runs', 'baselines');
89
+ const PARENT_DIR = join('/tmp', 'test-runs');
90
+
91
+ function createAgent(phase: Phase = 'beta'): InstanceType<typeof RegressionSentinelAgent> {
92
+ return new RegressionSentinelAgent(stubConfig, phase, RUN_DIR);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Tests
97
+ // ---------------------------------------------------------------------------
98
+
99
+ describe('RegressionSentinelAgent', () => {
100
+ beforeEach(() => {
101
+ statMock.mockReset();
102
+ readdirMock.mockReset();
103
+ mkdirMock.mockReset();
104
+ readFileMock.mockReset();
105
+ writeFileMock.mockReset();
106
+ compareScreenshotsMock.mockReset();
107
+ updateBaselineMock.mockReset().mockResolvedValue(join(BASELINES_DIR, 'screenshot.png'));
108
+
109
+ // Default: parent dir exists, run dir exists, baselines dir does NOT yet exist (first run)
110
+ statMock.mockImplementation((p: string) => {
111
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
112
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
113
+ if (p === BASELINES_DIR) return Promise.reject(new Error('ENOENT'));
114
+ return Promise.reject(new Error('ENOENT'));
115
+ });
116
+
117
+ readdirMock.mockImplementation((dir: string) => {
118
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
119
+ return Promise.resolve([]);
120
+ });
121
+
122
+ mkdirMock.mockResolvedValue(undefined);
123
+ writeFileMock.mockResolvedValue(undefined);
124
+ });
125
+
126
+ it('has agentId 15 and agentName "Regression Sentinel"', () => {
127
+ const agent = createAgent();
128
+ expect(agent.agentId).toBe(15);
129
+ expect(agent.agentName).toBe('Regression Sentinel');
130
+ });
131
+
132
+ // -------------------------------------------------------------------------
133
+ // preFlight()
134
+ // -------------------------------------------------------------------------
135
+
136
+ describe('preFlight()', () => {
137
+ it('throws when parent directory does not exist', async () => {
138
+ statMock.mockImplementation((p: string) => {
139
+ if (p === PARENT_DIR) return Promise.reject(new Error('ENOENT')); // parent missing
140
+ return Promise.reject(new Error('ENOENT'));
141
+ });
142
+
143
+ const agent = createAgent();
144
+ const result = await agent.run();
145
+
146
+ expect(result.status).toBe('failed');
147
+ expect(result.findings[0].description).toContain('Parent directory does not exist');
148
+ });
149
+
150
+ it('passes when parent directory exists (baselines may not yet exist)', async () => {
151
+ const agent = createAgent();
152
+ const result = await agent.run();
153
+
154
+ expect(result.findings.some((f) => f.description.includes('Parent directory'))).toBe(false);
155
+ });
156
+ });
157
+
158
+ // -------------------------------------------------------------------------
159
+ // First-run baseline creation
160
+ // -------------------------------------------------------------------------
161
+
162
+ describe('first-run baseline creation', () => {
163
+ it('creates baselines directory and copies all screenshots on first run', async () => {
164
+ const agent = createAgent();
165
+ await agent.run();
166
+
167
+ expect(mkdirMock).toHaveBeenCalledWith(BASELINES_DIR, { recursive: true });
168
+ expect(updateBaselineMock).toHaveBeenCalledTimes(2); // one per screenshot
169
+ });
170
+
171
+ it('returns no findings on first run', async () => {
172
+ const agent = createAgent();
173
+ const result = await agent.run();
174
+
175
+ expect(result.findings).toHaveLength(0);
176
+ });
177
+
178
+ it('returns passed status on first run', async () => {
179
+ const agent = createAgent();
180
+ const result = await agent.run();
181
+
182
+ expect(result.status).toBe('passed');
183
+ });
184
+
185
+ it('adds evidence for each baseline created on first run', async () => {
186
+ updateBaselineMock.mockImplementation(async (_src: string, dir: string) => {
187
+ return `${dir}/baseline.png`;
188
+ });
189
+
190
+ const agent = createAgent();
191
+ const result = await agent.run();
192
+
193
+ const screenshotEvidence = result.evidence.filter((e) => e.type === 'screenshot');
194
+ expect(screenshotEvidence.length).toBeGreaterThanOrEqual(2);
195
+ });
196
+ });
197
+
198
+ // -------------------------------------------------------------------------
199
+ // First-run: baselines dir exists but contains only .gitkeep
200
+ // -------------------------------------------------------------------------
201
+
202
+ describe('first-run when baselines dir contains only .gitkeep', () => {
203
+ beforeEach(() => {
204
+ // Baselines dir EXISTS (committed as empty placeholder) but has only .gitkeep
205
+ statMock.mockImplementation((p: string) => {
206
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
207
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
208
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true }); // dir exists...
209
+ return Promise.reject(new Error('ENOENT'));
210
+ });
211
+
212
+ // ...but only contains .gitkeep
213
+ readdirMock.mockImplementation((dir: string) => {
214
+ if (dir === BASELINES_DIR) return Promise.resolve(['.gitkeep']);
215
+ return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
216
+ });
217
+ });
218
+
219
+ it('treats .gitkeep-only baselines dir as first run and creates baselines', async () => {
220
+ const agent = createAgent();
221
+ await agent.run();
222
+
223
+ expect(mkdirMock).toHaveBeenCalledWith(BASELINES_DIR, { recursive: true });
224
+ expect(updateBaselineMock).toHaveBeenCalledTimes(2);
225
+ });
226
+
227
+ it('returns no findings when baselines dir has only .gitkeep', async () => {
228
+ const agent = createAgent();
229
+ const result = await agent.run();
230
+
231
+ expect(result.findings).toHaveLength(0);
232
+ });
233
+
234
+ it('returns passed status when baselines dir has only .gitkeep', async () => {
235
+ const agent = createAgent();
236
+ const result = await agent.run();
237
+
238
+ expect(result.status).toBe('passed');
239
+ });
240
+
241
+ it('logs the first-run message to console', async () => {
242
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
243
+
244
+ const agent = createAgent();
245
+ await agent.run();
246
+
247
+ expect(consoleSpy).toHaveBeenCalledWith(
248
+ expect.stringContaining('No baselines found — creating initial baselines from this run'),
249
+ );
250
+
251
+ consoleSpy.mockRestore();
252
+ });
253
+ });
254
+
255
+ // -------------------------------------------------------------------------
256
+ // Second-run: baselines dir has real files (not .gitkeep-only) → compare
257
+ // -------------------------------------------------------------------------
258
+
259
+ describe('second-run baseline comparison (baselines exist beyond .gitkeep)', () => {
260
+ beforeEach(() => {
261
+ statMock.mockImplementation((p: string) => {
262
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
263
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
264
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
265
+ if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
266
+ return Promise.reject(new Error('ENOENT'));
267
+ });
268
+
269
+ // Baselines dir has real PNG baselines — NOT treated as first run
270
+ readdirMock.mockImplementation((dir: string) => {
271
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
272
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
273
+ return Promise.resolve([]);
274
+ });
275
+
276
+ compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
277
+ });
278
+
279
+ it('does NOT create new baselines when real baseline files exist', async () => {
280
+ const agent = createAgent();
281
+ await agent.run();
282
+
283
+ expect(mkdirMock).not.toHaveBeenCalled();
284
+ expect(updateBaselineMock).not.toHaveBeenCalled();
285
+ });
286
+
287
+ it('compares screenshots against existing baselines', async () => {
288
+ const agent = createAgent();
289
+ await agent.run();
290
+
291
+ expect(compareScreenshotsMock).toHaveBeenCalledTimes(2);
292
+ });
293
+ });
294
+
295
+ // -------------------------------------------------------------------------
296
+ // Subsequent run: matching screenshots → no findings
297
+ // -------------------------------------------------------------------------
298
+
299
+ describe('matching screenshots → no findings', () => {
300
+ beforeEach(() => {
301
+ // Baselines now exist
302
+ statMock.mockImplementation((p: string) => {
303
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
304
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
305
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
306
+ if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
307
+ return Promise.reject(new Error('ENOENT'));
308
+ });
309
+
310
+ // Baselines dir has real PNG files — NOT treated as first run
311
+ readdirMock.mockImplementation((dir: string) => {
312
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
313
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
314
+ return Promise.resolve([]);
315
+ });
316
+
317
+ compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
318
+ });
319
+
320
+ it('returns no findings when all screenshots match baselines', async () => {
321
+ const agent = createAgent();
322
+ const result = await agent.run();
323
+
324
+ const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
325
+ expect(regressionFindings).toHaveLength(0);
326
+ });
327
+
328
+ it('returns passed status when all screenshots match', async () => {
329
+ const agent = createAgent();
330
+ const result = await agent.run();
331
+
332
+ expect(result.status).toBe('passed');
333
+ });
334
+ });
335
+
336
+ // -------------------------------------------------------------------------
337
+ // Subsequent run: different screenshots → findings
338
+ // -------------------------------------------------------------------------
339
+
340
+ describe('visual regression detection', () => {
341
+ beforeEach(() => {
342
+ statMock.mockImplementation((p: string) => {
343
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
344
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
345
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
346
+ if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
347
+ return Promise.reject(new Error('ENOENT'));
348
+ });
349
+
350
+ // Baselines dir has real PNG files — NOT treated as first run
351
+ readdirMock.mockImplementation((dir: string) => {
352
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
353
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
354
+ return Promise.resolve([]);
355
+ });
356
+ });
357
+
358
+ it('creates a medium-severity finding when screenshot diff exceeds threshold', async () => {
359
+ compareScreenshotsMock.mockResolvedValue({
360
+ match: false,
361
+ diffPercentage: 5.3,
362
+ diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
363
+ });
364
+
365
+ const agent = createAgent();
366
+ const result = await agent.run();
367
+
368
+ const regressionFinding = result.findings.find(
369
+ (f) => f.id.includes('visual-regression') && f.id.includes('dashboard'),
370
+ );
371
+ expect(regressionFinding).toBeDefined();
372
+ expect(regressionFinding!.severity).toBe('medium');
373
+ expect(regressionFinding!.type).toBe('code-bug-cosmetic');
374
+ });
375
+
376
+ it('includes the diff percentage in the finding description', async () => {
377
+ compareScreenshotsMock.mockResolvedValue({
378
+ match: false,
379
+ diffPercentage: 2.75,
380
+ diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
381
+ });
382
+
383
+ const agent = createAgent();
384
+ const result = await agent.run();
385
+
386
+ const regressionFinding = result.findings.find((f) => f.id.includes('visual-regression'));
387
+ expect(regressionFinding!.description).toContain('2.75');
388
+ });
389
+
390
+ it('adds diff image as evidence for each regression finding', async () => {
391
+ compareScreenshotsMock.mockResolvedValue({
392
+ match: false,
393
+ diffPercentage: 3.0,
394
+ diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
395
+ });
396
+
397
+ const agent = createAgent();
398
+ const result = await agent.run();
399
+
400
+ const diffEvidence = result.evidence.filter((e) =>
401
+ e.path.includes('_diff.png'),
402
+ );
403
+ expect(diffEvidence.length).toBeGreaterThan(0);
404
+ });
405
+
406
+ it('creates a finding for each differing screenshot', async () => {
407
+ compareScreenshotsMock.mockResolvedValue({
408
+ match: false,
409
+ diffPercentage: 1.5,
410
+ });
411
+
412
+ const agent = createAgent();
413
+ const result = await agent.run();
414
+
415
+ const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
416
+ expect(regressionFindings).toHaveLength(2); // one per screenshot
417
+ });
418
+
419
+ it('does NOT create findings for screenshots that match', async () => {
420
+ // First screenshot matches, second does not
421
+ compareScreenshotsMock
422
+ .mockResolvedValueOnce({ match: true, diffPercentage: 0 })
423
+ .mockResolvedValueOnce({ match: false, diffPercentage: 2.0 });
424
+
425
+ const agent = createAgent();
426
+ const result = await agent.run();
427
+
428
+ const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
429
+ expect(regressionFindings).toHaveLength(1);
430
+ });
431
+ });
432
+
433
+ // -------------------------------------------------------------------------
434
+ // API schema comparison
435
+ // -------------------------------------------------------------------------
436
+
437
+ describe('API schema comparison', () => {
438
+ beforeEach(() => {
439
+ statMock.mockImplementation((p: string) => {
440
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
441
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
442
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
443
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
444
+ return Promise.reject(new Error('ENOENT'));
445
+ });
446
+
447
+ // Baselines dir has real PNG files — NOT treated as first run
448
+ readdirMock.mockImplementation((dir: string) => {
449
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
450
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
451
+ return Promise.resolve([]);
452
+ });
453
+
454
+ // All screenshots match
455
+ compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
456
+ });
457
+
458
+ it('creates a schema baseline when current snapshot exists but no baseline schema', async () => {
459
+ statMock.mockImplementation((p: string) => {
460
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
461
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
462
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
463
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
464
+ if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true }); // current schema exists
465
+ if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.reject(new Error('ENOENT')); // baseline does not
466
+ return Promise.reject(new Error('ENOENT'));
467
+ });
468
+
469
+ readFileMock.mockResolvedValue(JSON.stringify({ '/api/clients': { id: 'string' } }));
470
+
471
+ const agent = createAgent();
472
+ await agent.run();
473
+
474
+ expect(writeFileMock).toHaveBeenCalledWith(
475
+ join(BASELINES_DIR, 'api-schema-snapshot.json'),
476
+ expect.anything(),
477
+ );
478
+ });
479
+
480
+ it('creates a finding when an endpoint is removed from schema', async () => {
481
+ statMock.mockImplementation((p: string) => {
482
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
483
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
484
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
485
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
486
+ if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
487
+ if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
488
+ return Promise.reject(new Error('ENOENT'));
489
+ });
490
+
491
+ const currentSchema = { '/api/clients': { id: 'string' } };
492
+ const baselineSchema = { '/api/clients': { id: 'string' }, '/api/invoices': { id: 'string' } }; // invoices removed!
493
+
494
+ readFileMock
495
+ .mockResolvedValueOnce(JSON.stringify(currentSchema))
496
+ .mockResolvedValueOnce(JSON.stringify(baselineSchema));
497
+
498
+ const agent = createAgent();
499
+ const result = await agent.run();
500
+
501
+ const schemaFinding = result.findings.find((f) => f.id.includes('schema-removed'));
502
+ expect(schemaFinding).toBeDefined();
503
+ expect(schemaFinding!.severity).toBe('high');
504
+ expect(schemaFinding!.description).toContain('/api/invoices');
505
+ });
506
+
507
+ it('creates a finding when an endpoint schema changes', async () => {
508
+ statMock.mockImplementation((p: string) => {
509
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
510
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
511
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
512
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
513
+ if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
514
+ if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
515
+ return Promise.reject(new Error('ENOENT'));
516
+ });
517
+
518
+ const currentSchema = { '/api/clients': { id: 'string', name: 'string', status: 'string' } }; // added status
519
+ const baselineSchema = { '/api/clients': { id: 'string', name: 'string' } };
520
+
521
+ readFileMock
522
+ .mockResolvedValueOnce(JSON.stringify(currentSchema))
523
+ .mockResolvedValueOnce(JSON.stringify(baselineSchema));
524
+
525
+ const agent = createAgent();
526
+ const result = await agent.run();
527
+
528
+ const schemaFinding = result.findings.find((f) => f.id.includes('schema-changed'));
529
+ expect(schemaFinding).toBeDefined();
530
+ expect(schemaFinding!.severity).toBe('medium');
531
+ });
532
+
533
+ it('creates a finding when schema JSON is malformed', async () => {
534
+ statMock.mockImplementation((p: string) => {
535
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
536
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
537
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
538
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
539
+ if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
540
+ if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
541
+ return Promise.reject(new Error('ENOENT'));
542
+ });
543
+
544
+ readFileMock.mockResolvedValue('not valid json');
545
+
546
+ const agent = createAgent();
547
+ const result = await agent.run();
548
+
549
+ const parseFinding = result.findings.find(f => f.id.includes('schema-parse-error'));
550
+ expect(parseFinding).toBeDefined();
551
+ expect(parseFinding!.severity).toBe('low');
552
+ });
553
+
554
+ it('creates a new baseline for a screenshot with no prior baseline', async () => {
555
+ statMock.mockImplementation((p: string) => {
556
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
557
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
558
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
559
+ // baseline for dashboard exists, but NOT for clients
560
+ if (p === join(BASELINES_DIR, 'smoke-dashboard.png')) return Promise.resolve({ isFile: () => true });
561
+ if (p === join(BASELINES_DIR, 'smoke-clients.png')) return Promise.reject(new Error('ENOENT'));
562
+ return Promise.reject(new Error('ENOENT'));
563
+ });
564
+
565
+ readdirMock.mockImplementation((dir: string) => {
566
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png']);
567
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
568
+ return Promise.resolve([]);
569
+ });
570
+
571
+ compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
572
+
573
+ const agent = createAgent();
574
+ await agent.run();
575
+
576
+ // updateBaseline called for the new screenshot (clients)
577
+ expect(updateBaselineMock).toHaveBeenCalledTimes(1);
578
+ });
579
+
580
+ it('creates a finding when compareScreenshots throws', async () => {
581
+ statMock.mockImplementation((p: string) => {
582
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
583
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
584
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
585
+ if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
586
+ return Promise.reject(new Error('ENOENT'));
587
+ });
588
+
589
+ readdirMock.mockImplementation((dir: string) => {
590
+ if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png']);
591
+ if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png']);
592
+ return Promise.resolve([]);
593
+ });
594
+
595
+ compareScreenshotsMock.mockRejectedValue(new Error('Image decode error'));
596
+
597
+ const agent = createAgent();
598
+ const result = await agent.run();
599
+
600
+ const compareErrorFinding = result.findings.find(f => f.id.includes('compare-error'));
601
+ expect(compareErrorFinding).toBeDefined();
602
+ expect(compareErrorFinding!.severity).toBe('medium');
603
+ });
604
+
605
+ it('creates a critical finding when run directory does not exist', async () => {
606
+ statMock.mockImplementation((p: string) => {
607
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
608
+ if (p === RUN_DIR) return Promise.reject(new Error('ENOENT'));
609
+ return Promise.reject(new Error('ENOENT'));
610
+ });
611
+
612
+ const agent = createAgent();
613
+ const result = await agent.run();
614
+
615
+ const noRunDirFinding = result.findings.find(f => f.id === '15-no-run-dir');
616
+ expect(noRunDirFinding).toBeDefined();
617
+ expect(noRunDirFinding!.severity).toBe('critical');
618
+ });
619
+
620
+ it('handles readdir failure for run directory gracefully', async () => {
621
+ statMock.mockImplementation((p: string) => {
622
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
623
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
624
+ return Promise.reject(new Error('ENOENT'));
625
+ });
626
+
627
+ readdirMock.mockRejectedValue(new Error('Permission denied'));
628
+
629
+ const agent = createAgent();
630
+ const result = await agent.run();
631
+
632
+ // Should still complete (empty screenshot list, first run)
633
+ expect(result.status).toBe('passed');
634
+ });
635
+
636
+ it('returns no schema findings when schemas are identical', async () => {
637
+ statMock.mockImplementation((p: string) => {
638
+ if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
639
+ if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
640
+ if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
641
+ if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
642
+ if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
643
+ if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
644
+ return Promise.reject(new Error('ENOENT'));
645
+ });
646
+
647
+ const schema = JSON.stringify({ '/api/clients': { id: 'string' } });
648
+ readFileMock.mockResolvedValue(schema);
649
+
650
+ const agent = createAgent();
651
+ const result = await agent.run();
652
+
653
+ const schemaFindings = result.findings.filter((f) => f.id.includes('schema'));
654
+ expect(schemaFindings).toHaveLength(0);
655
+ });
656
+ });
657
+ });