@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,441 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { ValidatedConfig } from '../../core/config';
3
+ import type { Phase } from '../../core/types';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Stub config
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const stubConfig: ValidatedConfig = {
10
+ schemaVersion: 1,
11
+ name: 'test',
12
+ auth: {
13
+ loginUrl: 'http://localhost/login',
14
+ credentials: { admin: { email: 'admin@test.com', password: 'secret' } },
15
+ },
16
+ modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
17
+ environments: {
18
+ beta: { baseUrl: 'http://localhost:3000', seed: false },
19
+ },
20
+ portals: [],
21
+ breakpoints: [],
22
+ integrations: [],
23
+ workers: [],
24
+ workflows: [],
25
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
26
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
27
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
28
+ agentClassification: { blocking: [], advisory: [] },
29
+ thresholds: {
30
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
31
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
32
+ apiResponseTime: 2000,
33
+ bundleSizeLimit: 500000,
34
+ maxConsoleErrors: 0,
35
+ },
36
+ disabledAgents: [],
37
+ disabledAgentSet: new Set<number>(),
38
+ };
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Mock global fetch (Mailhog health, Mailhog API, link resolution)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const fetchMock = vi.fn();
45
+ vi.stubGlobal('fetch', fetchMock);
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Mock ApiClient (password reset)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const apiClientPostMock = vi.fn();
52
+
53
+ vi.mock('../../helpers/api-client', () => ({
54
+ ApiClient: class MockApiClient {
55
+ post(...args: unknown[]) {
56
+ return apiClientPostMock(...args);
57
+ }
58
+ },
59
+ }));
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers for building Mailhog message objects
63
+ // ---------------------------------------------------------------------------
64
+
65
+ interface MailhogMessage {
66
+ ID: string;
67
+ Content: { Headers: Record<string, string[]>; Body: string };
68
+ Created: string;
69
+ }
70
+
71
+ function makeMailhogMessage(body: string, id = 'msg-001'): MailhogMessage {
72
+ return {
73
+ ID: id,
74
+ Content: {
75
+ Headers: {
76
+ Subject: ['Password Reset'],
77
+ To: ['admin@test.com'],
78
+ },
79
+ Body: body,
80
+ },
81
+ Created: new Date().toISOString(),
82
+ };
83
+ }
84
+
85
+ function mailhogApiResponse(messages: MailhogMessage[]) {
86
+ return {
87
+ ok: true,
88
+ status: 200,
89
+ json: async () => ({
90
+ items: messages,
91
+ total: messages.length,
92
+ count: messages.length,
93
+ start: 0,
94
+ }),
95
+ };
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Import after mocks
100
+ // ---------------------------------------------------------------------------
101
+
102
+ const { EmailNotificationVerifierAgent } = await import('../22-email-notification-verifier');
103
+
104
+ function createAgent(phase: Phase = 'beta'): InstanceType<typeof EmailNotificationVerifierAgent> {
105
+ return new EmailNotificationVerifierAgent(stubConfig, phase, '/tmp/test-run');
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Tests
110
+ // ---------------------------------------------------------------------------
111
+
112
+ describe('EmailNotificationVerifierAgent', () => {
113
+ beforeEach(() => {
114
+ vi.resetAllMocks();
115
+ // Default: password reset API returns success
116
+ apiClientPostMock.mockResolvedValue({ status: 200, body: {} });
117
+ });
118
+
119
+ it('has agentId 22 and agentName "Email & Notification Verifier"', () => {
120
+ const agent = createAgent();
121
+ expect(agent.agentId).toBe(22);
122
+ expect(agent.agentName).toBe('Email & Notification Verifier');
123
+ });
124
+
125
+ // -------------------------------------------------------------------------
126
+ // preFlight() — Mailhog availability (advisory, never throws)
127
+ // -------------------------------------------------------------------------
128
+
129
+ describe('preFlight() — Mailhog advisory', () => {
130
+ it('does not throw when Mailhog is unreachable (advisory only)', async () => {
131
+ fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
132
+
133
+ const agent = createAgent();
134
+ // run() must not throw even when Mailhog is down
135
+ const result = await agent.run();
136
+
137
+ // Should complete with a low advisory finding
138
+ expect(result.status).toBe('passed'); // low severity does not fail
139
+ const advisoryFinding = result.findings.find((f) => f.id.includes('mailhog-unavailable'));
140
+ expect(advisoryFinding).toBeDefined();
141
+ expect(advisoryFinding!.severity).toBe('low');
142
+ });
143
+ });
144
+
145
+ // -------------------------------------------------------------------------
146
+ // execute() — Mailhog unavailable: returns advisory and stops
147
+ // -------------------------------------------------------------------------
148
+
149
+ describe('execute() when Mailhog is unavailable', () => {
150
+ it('returns advisory finding and does not call password reset endpoint', async () => {
151
+ fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
152
+
153
+ const agent = createAgent();
154
+ await agent.run();
155
+
156
+ expect(apiClientPostMock).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ // -------------------------------------------------------------------------
161
+ // execute() — password reset triggering
162
+ // -------------------------------------------------------------------------
163
+
164
+ describe('execute() — password reset', () => {
165
+ it('creates a medium finding when password reset returns non-2xx', async () => {
166
+ // Mailhog reachable
167
+ fetchMock
168
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // HEAD to Mailhog health
169
+ .mockResolvedValue(mailhogApiResponse([])); // subsequent calls
170
+
171
+ apiClientPostMock.mockResolvedValue({ status: 500, body: { error: 'Internal error' } });
172
+
173
+ const agent = createAgent();
174
+ const result = await agent.run();
175
+
176
+ const resetFinding = result.findings.find((f) => f.id.includes('password-reset'));
177
+ expect(resetFinding).toBeDefined();
178
+ expect(resetFinding!.severity).toBe('medium');
179
+ });
180
+ });
181
+
182
+ // -------------------------------------------------------------------------
183
+ // execute() — email delivery
184
+ // -------------------------------------------------------------------------
185
+
186
+ describe('execute() — email delivery', () => {
187
+ it('creates a high finding when no email arrives in Mailhog within timeout', async () => {
188
+ // Mailhog reachable but no messages
189
+ fetchMock
190
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // HEAD to Mailhog health
191
+ .mockResolvedValue(mailhogApiResponse([])); // always empty
192
+
193
+ const agent = createAgent();
194
+ const result = await agent.run();
195
+
196
+ const deliveryFinding = result.findings.find((f) => f.id.includes('email-not-delivered'));
197
+ expect(deliveryFinding).toBeDefined();
198
+ expect(deliveryFinding!.severity).toBe('high');
199
+ });
200
+
201
+ it('creates no delivery finding when email arrives in Mailhog', async () => {
202
+ const msg = makeMailhogMessage(
203
+ 'Click here to reset: http://localhost:3000/reset?token=abc123',
204
+ );
205
+
206
+ fetchMock
207
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // HEAD to Mailhog health
208
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // first poll — has messages
209
+ .mockResolvedValue({ ok: true, status: 200 }); // link HEAD check
210
+
211
+ const agent = createAgent();
212
+ const result = await agent.run();
213
+
214
+ const deliveryFinding = result.findings.find((f) => f.id.includes('email-not-delivered'));
215
+ expect(deliveryFinding).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ // -------------------------------------------------------------------------
220
+ // execute() — template variable detection
221
+ // -------------------------------------------------------------------------
222
+
223
+ describe('execute() — template variable detection', () => {
224
+ it('creates a high finding when email body contains raw template variables', async () => {
225
+ const msg = makeMailhogMessage(
226
+ 'Hello {{firstName}}, reset your password: http://localhost:3000/reset?token={{resetToken}}',
227
+ );
228
+
229
+ fetchMock
230
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
231
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll returns message
232
+ .mockResolvedValue({ ok: true, status: 200 }); // link resolution
233
+
234
+ const agent = createAgent();
235
+ const result = await agent.run();
236
+
237
+ const templateFinding = result.findings.find((f) => f.id.includes('template-vars'));
238
+ expect(templateFinding).toBeDefined();
239
+ expect(templateFinding!.severity).toBe('high');
240
+ expect(templateFinding!.description).toContain('{{firstName}}');
241
+ });
242
+
243
+ it('creates no finding for an email body with no template variables', async () => {
244
+ const msg = makeMailhogMessage(
245
+ 'Hello Admin, click http://localhost:3000/reset?token=abc to reset your password.',
246
+ );
247
+
248
+ fetchMock
249
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
250
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll
251
+ .mockResolvedValue({ ok: true, status: 200 }); // link resolution
252
+
253
+ const agent = createAgent();
254
+ const result = await agent.run();
255
+
256
+ const templateFindings = result.findings.filter((f) => f.id.includes('template-vars'));
257
+ expect(templateFindings).toHaveLength(0);
258
+ });
259
+ });
260
+
261
+ // -------------------------------------------------------------------------
262
+ // execute() — broken link detection
263
+ // -------------------------------------------------------------------------
264
+
265
+ describe('execute() — broken link detection', () => {
266
+ it('creates a medium finding when a link in the email body returns 404', async () => {
267
+ const msg = makeMailhogMessage(
268
+ 'Reset here: http://localhost:3000/reset?token=abc123',
269
+ );
270
+
271
+ fetchMock
272
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
273
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll
274
+ .mockResolvedValue({ ok: false, status: 404 }); // HEAD link check → 404
275
+
276
+ const agent = createAgent();
277
+ const result = await agent.run();
278
+
279
+ const brokenLinkFinding = result.findings.find((f) => f.id.includes('broken-link'));
280
+ expect(brokenLinkFinding).toBeDefined();
281
+ expect(brokenLinkFinding!.severity).toBe('medium');
282
+ });
283
+
284
+ it('creates no broken-link finding when all links in the email body resolve to 200', async () => {
285
+ const msg = makeMailhogMessage(
286
+ 'Reset here: http://localhost:3000/reset?token=abc123',
287
+ );
288
+
289
+ fetchMock
290
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
291
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll
292
+ .mockResolvedValue({ ok: true, status: 200 }); // HEAD link check → 200
293
+
294
+ const agent = createAgent();
295
+ const result = await agent.run();
296
+
297
+ const brokenLinkFindings = result.findings.filter((f) => f.id.includes('broken-link'));
298
+ expect(brokenLinkFindings).toHaveLength(0);
299
+ });
300
+ });
301
+
302
+ // -------------------------------------------------------------------------
303
+ // Missing environment
304
+ // -------------------------------------------------------------------------
305
+
306
+ describe('execute() — no environment', () => {
307
+ it('creates a critical finding when no environment is configured', async () => {
308
+ const noEnvConfig: ValidatedConfig = {
309
+ ...stubConfig,
310
+ environments: {},
311
+ };
312
+ // Mailhog unavailable
313
+ fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
314
+
315
+ const agent = new EmailNotificationVerifierAgent(noEnvConfig, 'beta', '/tmp/test-run');
316
+ const result = await agent.run();
317
+
318
+ const finding = result.findings.find(f => f.id.includes('no-env'));
319
+ expect(finding).toBeDefined();
320
+ expect(finding!.severity).toBe('critical');
321
+ });
322
+ });
323
+
324
+ // -------------------------------------------------------------------------
325
+ // Password reset trigger error (fetch throws)
326
+ // -------------------------------------------------------------------------
327
+
328
+ describe('execute() — password reset error', () => {
329
+ it('creates a medium finding when password reset API throws', async () => {
330
+ fetchMock
331
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
332
+ .mockResolvedValue(mailhogApiResponse([]));
333
+
334
+ apiClientPostMock.mockRejectedValue(new Error('Network error'));
335
+
336
+ const agent = createAgent();
337
+ const result = await agent.run();
338
+
339
+ const errorFinding = result.findings.find(f => f.id.includes('password-reset-error'));
340
+ expect(errorFinding).toBeDefined();
341
+ expect(errorFinding!.severity).toBe('medium');
342
+ });
343
+ });
344
+
345
+ // -------------------------------------------------------------------------
346
+ // Link resolution error (fetch throws for link)
347
+ // -------------------------------------------------------------------------
348
+
349
+ describe('execute() — link resolution error', () => {
350
+ it('creates a low finding when link verification fetch throws', async () => {
351
+ const msg = makeMailhogMessage(
352
+ 'Visit http://localhost:3000/reset?token=xyz to reset.',
353
+ );
354
+
355
+ fetchMock
356
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
357
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll
358
+ .mockRejectedValue(new Error('DNS resolution failed')); // link check
359
+
360
+ const agent = createAgent();
361
+ const result = await agent.run();
362
+
363
+ const linkErrorFinding = result.findings.find(f => f.id.includes('link-error'));
364
+ expect(linkErrorFinding).toBeDefined();
365
+ expect(linkErrorFinding!.severity).toBe('low');
366
+ });
367
+ });
368
+
369
+ // -------------------------------------------------------------------------
370
+ // Email body with no URLs
371
+ // -------------------------------------------------------------------------
372
+
373
+ describe('execute() — email with no URLs', () => {
374
+ it('creates no broken-link findings when email body has no URLs', async () => {
375
+ const msg = makeMailhogMessage('Your password has been reset. No links here.');
376
+
377
+ fetchMock
378
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
379
+ .mockResolvedValueOnce(mailhogApiResponse([msg])); // poll
380
+
381
+ const agent = createAgent();
382
+ const result = await agent.run();
383
+
384
+ const linkFindings = result.findings.filter(f =>
385
+ f.id.includes('broken-link') || f.id.includes('link-error'),
386
+ );
387
+ expect(linkFindings).toHaveLength(0);
388
+ });
389
+ });
390
+
391
+ // -------------------------------------------------------------------------
392
+ // Password reset returns 202/204
393
+ // -------------------------------------------------------------------------
394
+
395
+ describe('execute() — accepted status codes', () => {
396
+ it('proceeds when password reset returns 202', async () => {
397
+ const msg = makeMailhogMessage('Reset here: http://localhost:3000/reset');
398
+
399
+ fetchMock
400
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
401
+ .mockResolvedValueOnce(mailhogApiResponse([msg])) // poll
402
+ .mockResolvedValue({ ok: true, status: 200 }); // link check
403
+
404
+ apiClientPostMock.mockResolvedValue({ status: 202, body: {} });
405
+
406
+ const agent = createAgent();
407
+ const result = await agent.run();
408
+
409
+ const resetFinding = result.findings.find(f => f.id.includes('password-reset-failed'));
410
+ expect(resetFinding).toBeUndefined();
411
+ });
412
+ });
413
+
414
+ // -------------------------------------------------------------------------
415
+ // Mailhog poll retry
416
+ // -------------------------------------------------------------------------
417
+
418
+ describe('execute() — Mailhog poll retry', () => {
419
+ it('retries polling when first attempts return empty and eventually succeeds', async () => {
420
+ const msg = makeMailhogMessage('Reset at http://localhost:3000/reset');
421
+
422
+ let pollCount = 0;
423
+ fetchMock
424
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // Mailhog health
425
+ .mockImplementation(async (url: string) => {
426
+ if (url.includes('messages')) {
427
+ pollCount++;
428
+ if (pollCount < 3) return mailhogApiResponse([]);
429
+ return mailhogApiResponse([msg]);
430
+ }
431
+ return { ok: true, status: 200 };
432
+ });
433
+
434
+ const agent = createAgent();
435
+ const result = await agent.run();
436
+
437
+ const deliveryFinding = result.findings.find(f => f.id.includes('email-not-delivered'));
438
+ expect(deliveryFinding).toBeUndefined();
439
+ });
440
+ });
441
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { ValidatedConfig } from '../../core/config';
3
+
4
+ vi.mock('node:child_process', () => ({
5
+ execFile: vi.fn(),
6
+ }));
7
+
8
+ import { execFile } from 'node:child_process';
9
+ import { MigrationTesterAgent } from '../23-migration-tester';
10
+
11
+ const mockedExecFile = vi.mocked(execFile);
12
+
13
+ type ExecFileCallback = (err: Error | null, stdout: string, stderr: string) => void;
14
+
15
+ function makeExecFileSuccess(stdout = '') {
16
+ return (...args: unknown[]) => {
17
+ // execFile may be called with or without an options object:
18
+ // execFile(file, args, cb) OR execFile(file, args, options, cb)
19
+ const cb = args[args.length - 1] as ExecFileCallback;
20
+ cb(null, stdout, '');
21
+ };
22
+ }
23
+
24
+ function makeExecFileFailure(stderr: string) {
25
+ const error = Object.assign(new Error('Command failed'), { stderr });
26
+ return (...args: unknown[]) => {
27
+ const cb = args[args.length - 1] as ExecFileCallback;
28
+ cb(error, '', stderr);
29
+ };
30
+ }
31
+
32
+ const stubConfig: ValidatedConfig = {
33
+ schemaVersion: 1,
34
+ name: 'test',
35
+ auth: { loginUrl: 'http://localhost' },
36
+ modules: [{ id: 'mod', route: '/mod', sidebarIcon: true }],
37
+ environments: {},
38
+ portals: [],
39
+ breakpoints: [],
40
+ integrations: [],
41
+ workers: [],
42
+ workflows: [],
43
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
44
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
45
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
46
+ agentClassification: { blocking: [], advisory: [] },
47
+ thresholds: {
48
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
49
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
50
+ apiResponseTime: 2000,
51
+ bundleSizeLimit: 500000,
52
+ maxConsoleErrors: 0,
53
+ },
54
+ disabledAgents: [],
55
+ disabledAgentSet: new Set<number>(),
56
+ };
57
+
58
+ function createAgent(): MigrationTesterAgent {
59
+ return new MigrationTesterAgent(stubConfig, 'alpha', '/tmp/test-run');
60
+ }
61
+
62
+ describe('MigrationTesterAgent', () => {
63
+ beforeEach(() => {
64
+ vi.resetAllMocks();
65
+ });
66
+
67
+ it('has agentId 23 and agentName Migration Tester', () => {
68
+ const agent = createAgent();
69
+ expect(agent.agentId).toBe(23);
70
+ expect(agent.agentName).toBe('Migration Tester');
71
+ });
72
+
73
+ it('returns no findings when all migration steps succeed', async () => {
74
+ // preFlight: prisma --version, then 3 execute steps
75
+ mockedExecFile
76
+ .mockImplementationOnce(makeExecFileSuccess('prisma: 5.0.0') as never)
77
+ .mockImplementationOnce(makeExecFileSuccess('All migrations have been applied.') as never)
78
+ .mockImplementationOnce(makeExecFileSuccess('Migrations applied successfully.') as never)
79
+ .mockImplementationOnce(makeExecFileSuccess('Seeded database successfully.') as never);
80
+
81
+ const agent = createAgent();
82
+ const result = await agent.run();
83
+
84
+ expect(result.status).toBe('passed');
85
+ expect(result.findings).toHaveLength(0);
86
+ });
87
+
88
+ it('returns medium infra-issue finding when migrate status fails in alpha', async () => {
89
+ mockedExecFile
90
+ .mockImplementationOnce(makeExecFileSuccess('prisma: 5.0.0') as never)
91
+ .mockImplementationOnce(makeExecFileFailure('Error: P1001: Can\'t reach database server') as never);
92
+
93
+ const agent = createAgent();
94
+ const result = await agent.run();
95
+
96
+ expect(result.status).toBe('passed'); // medium severity doesn't trigger failure
97
+ expect(result.findings).toHaveLength(1);
98
+ expect(result.findings[0].type).toBe('infra-issue');
99
+ expect(result.findings[0].severity).toBe('medium'); // alpha phase → medium
100
+ expect(result.findings[0].description).toContain('migrate status failed');
101
+ });
102
+
103
+ it('returns critical infra-issue finding when migrate deploy fails', async () => {
104
+ mockedExecFile
105
+ .mockImplementationOnce(makeExecFileSuccess('prisma: 5.0.0') as never)
106
+ .mockImplementationOnce(makeExecFileSuccess('') as never)
107
+ .mockImplementationOnce(makeExecFileFailure('Error: Migration failed to apply') as never);
108
+
109
+ const agent = createAgent();
110
+ const result = await agent.run();
111
+
112
+ expect(result.status).toBe('failed');
113
+ expect(result.findings).toHaveLength(1);
114
+ expect(result.findings[0].type).toBe('infra-issue');
115
+ expect(result.findings[0].severity).toBe('critical');
116
+ expect(result.findings[0].description).toContain('migrate deploy failed');
117
+ });
118
+
119
+ it('returns critical infra-issue finding when db seed fails', async () => {
120
+ mockedExecFile
121
+ .mockImplementationOnce(makeExecFileSuccess('prisma: 5.0.0') as never)
122
+ .mockImplementationOnce(makeExecFileSuccess('') as never)
123
+ .mockImplementationOnce(makeExecFileSuccess('') as never)
124
+ .mockImplementationOnce(makeExecFileFailure('Error: Seed script exited with code 1') as never);
125
+
126
+ const agent = createAgent();
127
+ const result = await agent.run();
128
+
129
+ expect(result.status).toBe('failed');
130
+ expect(result.findings).toHaveLength(1);
131
+ expect(result.findings[0].type).toBe('infra-issue');
132
+ expect(result.findings[0].severity).toBe('critical');
133
+ expect(result.findings[0].description).toContain('db seed failed');
134
+ });
135
+
136
+ it('returns infra-issue finding when preFlight (prisma --version) fails', async () => {
137
+ mockedExecFile.mockImplementationOnce(makeExecFileFailure('npx: command not found') as never);
138
+
139
+ const agent = createAgent();
140
+ const result = await agent.run();
141
+
142
+ expect(result.status).toBe('failed');
143
+ expect(result.findings[0].type).toBe('infra-issue');
144
+ });
145
+ });