@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,244 @@
1
+ import type { Finding } from '../core/types';
2
+ import { BaseAgent } from './base-agent';
3
+ import { ApiClient } from '../helpers/api-client';
4
+
5
+ const MAILHOG_API_URL = 'http://localhost:8025/api/v2/messages?limit=10';
6
+ const MAILHOG_HEALTH_URL = 'http://localhost:8025';
7
+
8
+ interface MailhogMessage {
9
+ ID: string;
10
+ Content: {
11
+ Headers: Record<string, string[]>;
12
+ Body: string;
13
+ };
14
+ Created: string;
15
+ }
16
+
17
+ interface MailhogResponse {
18
+ items: MailhogMessage[];
19
+ total: number;
20
+ count: number;
21
+ start: number;
22
+ }
23
+
24
+ async function isMailhogReachable(): Promise<boolean> {
25
+ try {
26
+ const response = await fetch(MAILHOG_HEALTH_URL, {
27
+ method: 'HEAD',
28
+ signal: AbortSignal.timeout(5_000),
29
+ });
30
+ return response.ok || response.status < 500;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ async function fetchMailhogMessages(): Promise<MailhogMessage[]> {
37
+ const response = await fetch(MAILHOG_API_URL, {
38
+ signal: AbortSignal.timeout(10_000),
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`Mailhog API returned ${response.status}`);
42
+ }
43
+ const data = (await response.json()) as MailhogResponse;
44
+ return data.items ?? [];
45
+ }
46
+
47
+ export class EmailNotificationVerifierAgent extends BaseAgent {
48
+ readonly agentId = 22;
49
+ readonly agentName = 'Email & Notification Verifier';
50
+
51
+ /** Set during preFlight so execute() knows whether Mailhog is available. */
52
+ private mailhogAvailable = false;
53
+
54
+ protected async preFlight(): Promise<void> {
55
+ this.mailhogAvailable = await isMailhogReachable();
56
+ // Advisory only — Mailhog is optional in non-local environments
57
+ }
58
+
59
+ protected async execute(): Promise<Finding[]> {
60
+ const findings: Finding[] = [];
61
+
62
+ const env =
63
+ this.config.environments['beta'] ??
64
+ this.config.environments['alpha'] ??
65
+ Object.values(this.config.environments)[0];
66
+
67
+ if (!env) {
68
+ findings.push({
69
+ id: `${this.agentId}-no-env`,
70
+ type: 'infra-issue',
71
+ severity: 'critical',
72
+ agentId: this.agentId,
73
+ module: 'email',
74
+ description: 'No environment configured — cannot run email/notification checks',
75
+ });
76
+ return findings;
77
+ }
78
+
79
+ // -----------------------------------------------------------------------
80
+ // Advisory finding when Mailhog is unavailable
81
+ // -----------------------------------------------------------------------
82
+ if (!this.mailhogAvailable) {
83
+ findings.push({
84
+ id: `${this.agentId}-mailhog-unavailable`,
85
+ type: 'infra-issue',
86
+ severity: 'low',
87
+ agentId: this.agentId,
88
+ module: 'email',
89
+ description:
90
+ 'Advisory: Mailhog is not reachable at localhost:8025. ' +
91
+ 'Email delivery verification will be skipped. ' +
92
+ 'Start Mailhog locally to enable full email testing.',
93
+ suggestedFix: 'Run `docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog` before the test suite.',
94
+ });
95
+ return findings;
96
+ }
97
+
98
+ // -----------------------------------------------------------------------
99
+ // Step 1: Trigger a password reset to generate an email
100
+ // -----------------------------------------------------------------------
101
+ const testEmail =
102
+ this.config.auth.credentials?.['admin']?.email ??
103
+ Object.values(this.config.auth.credentials ?? {})[0]?.email ??
104
+ 'test@example.com';
105
+
106
+ const apiClient = new ApiClient(env.baseUrl);
107
+ let passwordResetTriggered = false;
108
+
109
+ try {
110
+ const response = await apiClient.post('/api/auth/forgot-password', { email: testEmail });
111
+ if (response.status === 200 || response.status === 202 || response.status === 204) {
112
+ passwordResetTriggered = true;
113
+ } else {
114
+ findings.push({
115
+ id: `${this.agentId}-password-reset-failed`,
116
+ type: 'infra-issue',
117
+ severity: 'medium',
118
+ agentId: this.agentId,
119
+ module: 'email',
120
+ description:
121
+ `Password reset endpoint POST /api/auth/forgot-password returned ${response.status}. ` +
122
+ `Expected 200/202/204.`,
123
+ suggestedFix: 'Verify the forgot-password endpoint is implemented and returns a 2xx status.',
124
+ });
125
+ }
126
+ } catch (error) {
127
+ findings.push({
128
+ id: `${this.agentId}-password-reset-error`,
129
+ type: 'infra-issue',
130
+ severity: 'medium',
131
+ agentId: this.agentId,
132
+ module: 'email',
133
+ description: `Password reset trigger failed: ${error instanceof Error ? error.message : String(error)}`,
134
+ });
135
+ }
136
+
137
+ if (!passwordResetTriggered) {
138
+ return findings;
139
+ }
140
+
141
+ // -----------------------------------------------------------------------
142
+ // Step 2: Poll Mailhog for the delivered email (up to 5 seconds)
143
+ // -----------------------------------------------------------------------
144
+ let messages: MailhogMessage[] = [];
145
+ const backoffDelays = [200, 400, 800, 1600, 3200];
146
+
147
+ for (const delay of backoffDelays) {
148
+ try {
149
+ messages = await fetchMailhogMessages();
150
+ if (messages.length > 0) break;
151
+ } catch {
152
+ // Retry on transient errors
153
+ }
154
+ await new Promise<void>((resolve) => setTimeout(resolve, delay));
155
+ }
156
+
157
+ if (messages.length === 0) {
158
+ findings.push({
159
+ id: `${this.agentId}-email-not-delivered`,
160
+ type: 'code-bug-logic',
161
+ severity: 'high',
162
+ agentId: this.agentId,
163
+ module: 'email',
164
+ description:
165
+ `Email delivery failure: no messages received in Mailhog within 5 seconds of ` +
166
+ `triggering password reset for "${testEmail}".`,
167
+ suggestedFix:
168
+ 'Verify the mailer service is configured correctly and Mailhog SMTP port (1025) is reachable.',
169
+ });
170
+ return findings;
171
+ }
172
+
173
+ // -----------------------------------------------------------------------
174
+ // Step 3: Inspect email bodies for raw template variables like {{variable}}
175
+ // -----------------------------------------------------------------------
176
+ const templateVarPattern = /\{\{[^}]+\}\}/g;
177
+
178
+ for (const message of messages) {
179
+ const body = message.Content.Body;
180
+ const rawVars = body.match(templateVarPattern);
181
+
182
+ if (rawVars && rawVars.length > 0) {
183
+ findings.push({
184
+ id: `${this.agentId}-template-vars-${message.ID.slice(0, 8)}`,
185
+ type: 'code-bug-logic',
186
+ severity: 'high',
187
+ agentId: this.agentId,
188
+ module: 'email',
189
+ description:
190
+ `Email template variable(s) not substituted: found ${rawVars.join(', ')} in email body (message ID: ${message.ID}).`,
191
+ suggestedFix:
192
+ 'Check the email template renderer and ensure all variables are provided at send time.',
193
+ });
194
+ }
195
+ }
196
+
197
+ // -----------------------------------------------------------------------
198
+ // Step 4: Check that all links in the email body resolve (HTTP HEAD → 200)
199
+ // -----------------------------------------------------------------------
200
+ const urlPattern = /https?:\/\/[^\s"'<>)]+/g;
201
+
202
+ for (const message of messages) {
203
+ const body = message.Content.Body;
204
+ const urls = body.match(urlPattern) ?? [];
205
+ const uniqueUrls = [...new Set(urls)];
206
+
207
+ for (const url of uniqueUrls) {
208
+ try {
209
+ const response = await fetch(url, {
210
+ method: 'HEAD',
211
+ signal: AbortSignal.timeout(10_000),
212
+ redirect: 'follow',
213
+ });
214
+
215
+ if (response.status >= 400) {
216
+ findings.push({
217
+ id: `${this.agentId}-broken-link-${url.slice(0, 40).replace(/[^a-zA-Z0-9]/g, '-')}`,
218
+ type: 'code-bug-logic',
219
+ severity: 'medium',
220
+ agentId: this.agentId,
221
+ module: 'email',
222
+ description:
223
+ `Broken link in email body: HEAD ${url} returned ${response.status}.`,
224
+ suggestedFix: `Update the email template to use a valid URL for: ${url}`,
225
+ });
226
+ }
227
+ } catch (error) {
228
+ findings.push({
229
+ id: `${this.agentId}-link-error-${url.slice(0, 40).replace(/[^a-zA-Z0-9]/g, '-')}`,
230
+ type: 'infra-issue',
231
+ severity: 'low',
232
+ agentId: this.agentId,
233
+ module: 'email',
234
+ description:
235
+ `Could not verify link in email body: ${url}. ` +
236
+ `Error: ${error instanceof Error ? error.message : String(error)}`,
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ return findings;
243
+ }
244
+ }
@@ -0,0 +1,80 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { BaseAgent } from './base-agent';
4
+ import type { Finding } from '../core/types';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ export class MigrationTesterAgent extends BaseAgent {
9
+ readonly agentId = 23;
10
+ readonly agentName = 'Migration Tester';
11
+
12
+ private get projectRoot(): string {
13
+ return this.config.projectRoot ?? process.cwd();
14
+ }
15
+
16
+ protected async preFlight(): Promise<void> {
17
+ await execFileAsync('npx', ['prisma', '--version'], { shell: true, cwd: this.projectRoot });
18
+ }
19
+
20
+ protected async execute(): Promise<Finding[]> {
21
+ const findings: Finding[] = [];
22
+ const cwd = this.projectRoot;
23
+
24
+ try {
25
+ await execFileAsync('npx', ['prisma', 'migrate', 'status'], { shell: true, cwd });
26
+ } catch (error) {
27
+ const stderr = error instanceof Error && 'stderr' in error
28
+ ? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
29
+ : String(error);
30
+ // In alpha phase, DB connectivity issues are medium (dev env may lack DB)
31
+ // In beta/uat, they're critical (DB must be available)
32
+ const severity = this.phase === 'alpha' ? 'medium' as const : 'critical' as const;
33
+ findings.push({
34
+ id: `${this.agentId}-migrate-status`,
35
+ type: 'infra-issue',
36
+ severity,
37
+ agentId: this.agentId,
38
+ module: 'database',
39
+ description: `prisma migrate status failed: ${stderr}`,
40
+ });
41
+ return findings;
42
+ }
43
+
44
+ try {
45
+ await execFileAsync('npx', ['prisma', 'migrate', 'deploy'], { shell: true, cwd });
46
+ } catch (error) {
47
+ const stderr = error instanceof Error && 'stderr' in error
48
+ ? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
49
+ : String(error);
50
+ findings.push({
51
+ id: `${this.agentId}-migrate-deploy`,
52
+ type: 'infra-issue',
53
+ severity: 'critical',
54
+ agentId: this.agentId,
55
+ module: 'database',
56
+ description: `prisma migrate deploy failed: ${stderr}`,
57
+ });
58
+ return findings;
59
+ }
60
+
61
+ try {
62
+ await execFileAsync('npx', ['prisma', 'db', 'seed'], { shell: true, cwd });
63
+ } catch (error) {
64
+ const stderr = error instanceof Error && 'stderr' in error
65
+ ? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
66
+ : String(error);
67
+ findings.push({
68
+ id: `${this.agentId}-db-seed`,
69
+ type: 'infra-issue',
70
+ severity: 'critical',
71
+ agentId: this.agentId,
72
+ module: 'database',
73
+ description: `prisma db seed failed: ${stderr}`,
74
+ });
75
+ return findings;
76
+ }
77
+
78
+ return findings;
79
+ }
80
+ }
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { mkdtempSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { readFileSync } from 'node:fs';
6
+ import type { ValidatedConfig } from '../../core/config';
7
+
8
+ // Mock node:child_process before importing the agent
9
+ vi.mock('node:child_process', () => ({
10
+ execFile: vi.fn(),
11
+ }));
12
+
13
+ // Use real node:fs/promises so writeFile actually writes to the temp runDir
14
+
15
+ import { execFile } from 'node:child_process';
16
+ import { AnalystAgent } from '../01-analyst';
17
+
18
+ const mockedExecFile = vi.mocked(execFile);
19
+
20
+ function makeExecFileCallback(stdout: string, stderr = '', error: Error | null = null) {
21
+ return (...args: unknown[]) => {
22
+ // execFile may be called with or without an options object:
23
+ // execFile(file, args, cb) OR execFile(file, args, options, cb)
24
+ const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string; stderr: string }) => void;
25
+ cb(error, { stdout, stderr });
26
+ };
27
+ }
28
+
29
+ const stubConfig: ValidatedConfig = {
30
+ schemaVersion: 1,
31
+ name: 'test',
32
+ auth: { loginUrl: 'http://localhost' },
33
+ modules: [
34
+ { id: 'triage', route: '/triage', sidebarIcon: true },
35
+ { id: 'clients', route: '/clients', sidebarIcon: true },
36
+ { id: 'dashboard', route: '/dashboard', sidebarIcon: false },
37
+ ],
38
+ environments: {},
39
+ portals: [],
40
+ breakpoints: [],
41
+ integrations: [],
42
+ workers: [],
43
+ workflows: [],
44
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
45
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
46
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
47
+ agentClassification: { blocking: [], advisory: [] },
48
+ thresholds: {
49
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
50
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
51
+ apiResponseTime: 2000,
52
+ bundleSizeLimit: 500000,
53
+ maxConsoleErrors: 0,
54
+ },
55
+ disabledAgents: [],
56
+ disabledAgentSet: new Set<number>(),
57
+ };
58
+
59
+ function createAgent(runDir: string): AnalystAgent {
60
+ return new AnalystAgent(stubConfig, 'alpha', runDir);
61
+ }
62
+
63
+ describe('AnalystAgent', () => {
64
+ let runDir: string;
65
+
66
+ beforeEach(() => {
67
+ runDir = mkdtempSync(join(tmpdir(), 'analyst-test-'));
68
+ vi.resetAllMocks();
69
+ });
70
+
71
+ it('identifies correct modules from 2 changed files', async () => {
72
+ mockedExecFile
73
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never) // preFlight
74
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never) // detectDefaultBranch: git rev-parse --verify main
75
+ .mockImplementationOnce(makeExecFileCallback('src/pages/triage/TriagePage.tsx\nsrc/pages/clients/ClientList.tsx\n') as never); // git diff
76
+
77
+ const agent = createAgent(runDir);
78
+ const result = await agent.run();
79
+
80
+ expect(result.status).toBe('passed');
81
+ expect(result.findings).toHaveLength(0);
82
+
83
+ const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
84
+ const triageModule = testPlan.modules.find((m: { id: string }) => m.id === 'triage');
85
+ const clientsModule = testPlan.modules.find((m: { id: string }) => m.id === 'clients');
86
+ const dashboardModule = testPlan.modules.find((m: { id: string }) => m.id === 'dashboard');
87
+
88
+ expect(triageModule.risk).toBe('high');
89
+ expect(triageModule.changedFiles).toContain('src/pages/triage/TriagePage.tsx');
90
+
91
+ expect(clientsModule.risk).toBe('high');
92
+ expect(clientsModule.changedFiles).toContain('src/pages/clients/ClientList.tsx');
93
+
94
+ expect(dashboardModule.risk).toBe('low');
95
+ expect(dashboardModule.changedFiles).toHaveLength(0);
96
+ });
97
+
98
+ it('marks all modules critical when AuthContext is changed', async () => {
99
+ mockedExecFile
100
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
101
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never)
102
+ .mockImplementationOnce(makeExecFileCallback('src/context/AuthContext.tsx\n') as never);
103
+
104
+ const agent = createAgent(runDir);
105
+ const result = await agent.run();
106
+
107
+ expect(result.status).toBe('passed');
108
+
109
+ const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
110
+ for (const module of testPlan.modules) {
111
+ expect(module.risk).toBe('critical');
112
+ }
113
+ });
114
+
115
+ it('marks all modules critical when App.tsx is changed', async () => {
116
+ mockedExecFile
117
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
118
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never)
119
+ .mockImplementationOnce(makeExecFileCallback('src/App.tsx\n') as never);
120
+
121
+ const agent = createAgent(runDir);
122
+ await agent.run();
123
+
124
+ const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
125
+ for (const module of testPlan.modules) {
126
+ expect(module.risk).toBe('critical');
127
+ }
128
+ });
129
+
130
+ it('marks all modules critical when router file is changed', async () => {
131
+ mockedExecFile
132
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
133
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never)
134
+ .mockImplementationOnce(makeExecFileCallback('src/router/index.ts\n') as never);
135
+
136
+ const agent = createAgent(runDir);
137
+ await agent.run();
138
+
139
+ const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
140
+ for (const module of testPlan.modules) {
141
+ expect(module.risk).toBe('critical');
142
+ }
143
+ });
144
+
145
+ it('produces infra-issue finding when git fails', async () => {
146
+ const gitError = new Error('git: command not found');
147
+ const failImpl = makeExecFileCallback('', 'git: command not found', gitError);
148
+ mockedExecFile.mockImplementationOnce(failImpl as never);
149
+
150
+ const agent = createAgent(runDir);
151
+ const result = await agent.run();
152
+
153
+ expect(result.status).toBe('failed');
154
+ expect(result.findings).toHaveLength(1);
155
+ expect(result.findings[0].type).toBe('infra-issue');
156
+ expect(result.findings[0].severity).toBe('critical');
157
+ });
158
+
159
+ it('assigns low risk to modules with no changed files and no shared dependency changes', async () => {
160
+ mockedExecFile
161
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
162
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never)
163
+ .mockImplementationOnce(makeExecFileCallback('src/utils/formatDate.ts\n') as never);
164
+
165
+ const agent = createAgent(runDir);
166
+ await agent.run();
167
+
168
+ const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
169
+ for (const module of testPlan.modules) {
170
+ expect(module.risk).toBe('low');
171
+ }
172
+ });
173
+
174
+ it('writes test-plan.json to the run directory', async () => {
175
+ mockedExecFile
176
+ .mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
177
+ .mockImplementationOnce(makeExecFileCallback('main\n') as never)
178
+ .mockImplementationOnce(makeExecFileCallback('') as never);
179
+
180
+ const agent = createAgent(runDir);
181
+ await agent.run();
182
+
183
+ const testPlanPath = join(runDir, 'test-plan.json');
184
+ const testPlan = JSON.parse(readFileSync(testPlanPath, 'utf-8'));
185
+ expect(testPlan).toHaveProperty('modules');
186
+ expect(Array.isArray(testPlan.modules)).toBe(true);
187
+ });
188
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { ValidatedConfig } from '../../core/config';
3
+
4
+ vi.mock('node:fs', async (importOriginal) => {
5
+ const actual = await importOriginal<typeof import('node:fs')>();
6
+ return {
7
+ ...actual,
8
+ existsSync: vi.fn(),
9
+ };
10
+ });
11
+
12
+ vi.mock('../../helpers/seed-validator', () => ({
13
+ validateSeedCompleteness: vi.fn(),
14
+ }));
15
+
16
+ import { existsSync } from 'node:fs';
17
+ import { validateSeedCompleteness } from '../../helpers/seed-validator';
18
+ import { SeedArchitectAgent } from '../02-seed-architect';
19
+
20
+ const mockedExistsSync = vi.mocked(existsSync);
21
+ const mockedValidateSeedCompleteness = vi.mocked(validateSeedCompleteness);
22
+
23
+ const stubConfig: ValidatedConfig = {
24
+ schemaVersion: 1,
25
+ name: 'test',
26
+ auth: { loginUrl: 'http://localhost' },
27
+ modules: [
28
+ { id: 'triage', route: '/triage', sidebarIcon: true },
29
+ { id: 'clients', route: '/clients', sidebarIcon: true },
30
+ { id: 'dashboard', route: '/dashboard', sidebarIcon: false },
31
+ { id: 'reports', route: '/reports', sidebarIcon: true },
32
+ ],
33
+ environments: {},
34
+ portals: [],
35
+ breakpoints: [],
36
+ integrations: [],
37
+ workers: [],
38
+ workflows: [],
39
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
40
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
41
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
42
+ agentClassification: { blocking: [], advisory: [] },
43
+ thresholds: {
44
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
45
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
46
+ apiResponseTime: 2000,
47
+ bundleSizeLimit: 500000,
48
+ maxConsoleErrors: 0,
49
+ },
50
+ disabledAgents: [],
51
+ disabledAgentSet: new Set<number>(),
52
+ };
53
+
54
+ function createAgent(): SeedArchitectAgent {
55
+ return new SeedArchitectAgent(stubConfig, 'alpha', '/tmp/test-run');
56
+ }
57
+
58
+ describe('SeedArchitectAgent', () => {
59
+ beforeEach(() => {
60
+ vi.resetAllMocks();
61
+ });
62
+
63
+ it('has agentId 2 and agentName Seed Architect', () => {
64
+ const agent = createAgent();
65
+ expect(agent.agentId).toBe(2);
66
+ expect(agent.agentName).toBe('Seed Architect');
67
+ });
68
+
69
+ it('returns 3 medium findings when seed validator reports 3 missing modules', async () => {
70
+ mockedExistsSync.mockReturnValue(true);
71
+ mockedValidateSeedCompleteness.mockResolvedValue({
72
+ covered: ['triage'],
73
+ missing: ['clients', 'dashboard', 'reports'],
74
+ });
75
+
76
+ const agent = createAgent();
77
+ const result = await agent.run();
78
+
79
+ expect(result.status).toBe('passed'); // medium severity does not fail
80
+ expect(result.findings).toHaveLength(3);
81
+
82
+ for (const finding of result.findings) {
83
+ expect(finding.type).toBe('code-bug-logic');
84
+ expect(finding.severity).toBe('medium');
85
+ expect(finding.agentId).toBe(2);
86
+ }
87
+
88
+ const missingModules = result.findings.map(f => f.module);
89
+ expect(missingModules).toContain('clients');
90
+ expect(missingModules).toContain('dashboard');
91
+ expect(missingModules).toContain('reports');
92
+ });
93
+
94
+ it('returns no findings when all modules have seed coverage', async () => {
95
+ mockedExistsSync.mockReturnValue(true);
96
+ mockedValidateSeedCompleteness.mockResolvedValue({
97
+ covered: ['triage', 'clients', 'dashboard', 'reports'],
98
+ missing: [],
99
+ });
100
+
101
+ const agent = createAgent();
102
+ const result = await agent.run();
103
+
104
+ expect(result.status).toBe('passed');
105
+ expect(result.findings).toHaveLength(0);
106
+ });
107
+
108
+ it('returns warning finding when seed file does not exist', async () => {
109
+ mockedExistsSync.mockReturnValue(false);
110
+
111
+ const agent = createAgent();
112
+ const result = await agent.run();
113
+
114
+ expect(result.status).toBe('passed'); // medium severity does not fail
115
+ expect(result.findings).toHaveLength(1);
116
+ expect(result.findings[0].type).toBe('infra-issue');
117
+ expect(result.findings[0].severity).toBe('medium');
118
+ expect(result.findings[0].description).toContain('Seed file not found');
119
+ });
120
+
121
+ it('passes module IDs from config to seed validator', async () => {
122
+ mockedExistsSync.mockReturnValue(true);
123
+ mockedValidateSeedCompleteness.mockResolvedValue({
124
+ covered: ['triage', 'clients', 'dashboard', 'reports'],
125
+ missing: [],
126
+ });
127
+
128
+ const agent = createAgent();
129
+ await agent.run();
130
+
131
+ expect(mockedValidateSeedCompleteness).toHaveBeenCalledOnce();
132
+ const [, moduleIds] = mockedValidateSeedCompleteness.mock.calls[0];
133
+ expect(moduleIds).toContain('triage');
134
+ expect(moduleIds).toContain('clients');
135
+ expect(moduleIds).toContain('dashboard');
136
+ expect(moduleIds).toContain('reports');
137
+ });
138
+
139
+ it('includes suggestedFix in each finding for missing modules', async () => {
140
+ mockedExistsSync.mockReturnValue(true);
141
+ mockedValidateSeedCompleteness.mockResolvedValue({
142
+ covered: [],
143
+ missing: ['triage'],
144
+ });
145
+
146
+ const agent = createAgent();
147
+ const result = await agent.run();
148
+
149
+ expect(result.findings[0].suggestedFix).toBeTruthy();
150
+ expect(result.findings[0].suggestedFix).toContain('triage');
151
+ });
152
+ });