@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,402 @@
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
+ function makeConfig(overrides: Partial<ValidatedConfig> = {}): ValidatedConfig {
10
+ return {
11
+ schemaVersion: 1,
12
+ name: 'test',
13
+ auth: {
14
+ loginUrl: 'http://localhost/login',
15
+ credentials: {
16
+ admin: { email: 'admin@test.com', password: 'secret' },
17
+ },
18
+ },
19
+ modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
20
+ environments: {
21
+ alpha: { baseUrl: 'http://localhost:3000', seed: false },
22
+ },
23
+ portals: [],
24
+ breakpoints: [],
25
+ integrations: [],
26
+ workers: [],
27
+ workflows: [],
28
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
29
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
30
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
31
+ agentClassification: { blocking: [], advisory: [] },
32
+ thresholds: {
33
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
34
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
35
+ apiResponseTime: 2000,
36
+ bundleSizeLimit: 500000,
37
+ maxConsoleErrors: 0,
38
+ },
39
+ disabledAgents: [],
40
+ disabledAgentSet: new Set<number>(),
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Mock node:fs/promises — we fully control what the agent "sees" on disk
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const statMock = vi.fn();
50
+ const readdirMock = vi.fn();
51
+ const readFileMock = vi.fn();
52
+
53
+ vi.mock('node:fs/promises', async (importOriginal) => {
54
+ const actual = await importOriginal<typeof import('node:fs/promises')>();
55
+ return {
56
+ ...actual,
57
+ stat: (...args: unknown[]) => statMock(...args),
58
+ readdir: (...args: unknown[]) => readdirMock(...args),
59
+ readFile: (...args: unknown[]) => readFileMock(...args),
60
+ };
61
+ });
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Mock fetch (URL health checks)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const fetchMock = vi.fn();
68
+ vi.stubGlobal('fetch', fetchMock);
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Dynamic import AFTER mocks are hoisted
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const { DocumentationValidatorAgent } = await import('../17-documentation-validator');
75
+
76
+ function createAgent(
77
+ config: ValidatedConfig = makeConfig(),
78
+ phase: Phase = 'uat',
79
+ ): InstanceType<typeof DocumentationValidatorAgent> {
80
+ return new DocumentationValidatorAgent(config, phase, '/tmp/test-run');
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Helpers
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /** Configure fs mock so the agent finds a README.md in project root. */
88
+ function setupBasicFs(readmeContent: string = '# Hello', docsExists = false): void {
89
+ statMock.mockImplementation((p: string) => {
90
+ if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
91
+ if (p.endsWith('docs')) {
92
+ if (docsExists) return Promise.resolve({ isDirectory: () => true });
93
+ return Promise.reject(new Error('ENOENT'));
94
+ }
95
+ return Promise.reject(new Error('ENOENT'));
96
+ });
97
+
98
+ readdirMock.mockImplementation((dir: string) => {
99
+ if (!dir.toString().includes('docs')) return Promise.resolve(['README.md']);
100
+ return Promise.resolve([]);
101
+ });
102
+
103
+ readFileMock.mockImplementation((filePath: string) => {
104
+ if (String(filePath).endsWith('README.md')) return Promise.resolve(readmeContent);
105
+ return Promise.resolve('');
106
+ });
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Tests
111
+ // ---------------------------------------------------------------------------
112
+
113
+ describe('DocumentationValidatorAgent', () => {
114
+ beforeEach(() => {
115
+ vi.resetAllMocks();
116
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
117
+ });
118
+
119
+ // -------------------------------------------------------------------------
120
+ // Identity
121
+ // -------------------------------------------------------------------------
122
+
123
+ it('has agentId 17 and agentName "Documentation Validator"', () => {
124
+ const agent = createAgent();
125
+ expect(agent.agentId).toBe(17);
126
+ expect(agent.agentName).toBe('Documentation Validator');
127
+ });
128
+
129
+ // -------------------------------------------------------------------------
130
+ // preFlight()
131
+ // -------------------------------------------------------------------------
132
+
133
+ describe('preFlight()', () => {
134
+ it('throws when neither README.md nor docs/ directory exists', async () => {
135
+ statMock.mockRejectedValue(new Error('ENOENT'));
136
+
137
+ const agent = createAgent();
138
+ const result = await agent.run();
139
+
140
+ expect(result.status).toBe('failed');
141
+ expect(result.findings[0].description).toContain('README.md');
142
+ });
143
+
144
+ it('passes when README.md exists', async () => {
145
+ setupBasicFs();
146
+
147
+ const agent = createAgent();
148
+ const result = await agent.run();
149
+
150
+ expect(result.findings.some(f => f.description.includes('README.md'))).toBe(false);
151
+ });
152
+
153
+ it('passes when only docs/ directory exists (no README.md)', async () => {
154
+ statMock.mockImplementation((p: string) => {
155
+ if (p.endsWith('docs')) return Promise.resolve({ isDirectory: () => true });
156
+ return Promise.reject(new Error('ENOENT'));
157
+ });
158
+ readdirMock.mockResolvedValue([]);
159
+
160
+ const agent = createAgent();
161
+ const result = await agent.run();
162
+
163
+ expect(result.status).toBe('passed');
164
+ });
165
+ });
166
+
167
+ // -------------------------------------------------------------------------
168
+ // URL extraction from markdown
169
+ // -------------------------------------------------------------------------
170
+
171
+ describe('URL extraction', () => {
172
+ it('extracts URLs from markdown link syntax', async () => {
173
+ setupBasicFs('See [docs](https://example.com/docs) for details.');
174
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
175
+
176
+ const agent = createAgent();
177
+ await agent.run();
178
+
179
+ const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
180
+ const urls = fetchCalls.map(([url]) => url);
181
+ expect(urls).toContain('https://example.com/docs');
182
+ });
183
+
184
+ it('extracts bare URLs from markdown content', async () => {
185
+ setupBasicFs('Visit https://bare-url.example.com/page for info.');
186
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
187
+
188
+ const agent = createAgent();
189
+ await agent.run();
190
+
191
+ const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
192
+ const urls = fetchCalls.map(([url]) => url);
193
+ expect(urls.some((u) => u.includes('bare-url.example.com'))).toBe(true);
194
+ });
195
+
196
+ it('does not duplicate URLs that appear in both link syntax and bare form', async () => {
197
+ setupBasicFs(
198
+ 'See [link](https://example.com/dup) and also https://example.com/dup here.',
199
+ );
200
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
201
+
202
+ const agent = createAgent();
203
+ await agent.run();
204
+
205
+ const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
206
+ const dupUrls = fetchCalls.filter(
207
+ ([url]) => url === 'https://example.com/dup',
208
+ );
209
+ expect(dupUrls.length).toBe(1);
210
+ });
211
+ });
212
+
213
+ // -------------------------------------------------------------------------
214
+ // Broken link → finding
215
+ // -------------------------------------------------------------------------
216
+
217
+ describe('broken link detection', () => {
218
+ it('creates a finding for a 404 URL', async () => {
219
+ setupBasicFs('[broken](https://dead.example.com/gone)');
220
+ fetchMock.mockResolvedValue({ ok: false, status: 404 });
221
+
222
+ const agent = createAgent();
223
+ const result = await agent.run();
224
+
225
+ const brokenFinding = result.findings.find(
226
+ f => f.description.includes('https://dead.example.com/gone'),
227
+ );
228
+ expect(brokenFinding).toBeDefined();
229
+ expect(brokenFinding!.severity).toBe('low');
230
+ });
231
+
232
+ it('creates a finding for a 5xx URL', async () => {
233
+ setupBasicFs('[server-error](https://error.example.com/500)');
234
+ fetchMock.mockResolvedValue({ ok: false, status: 500 });
235
+
236
+ const agent = createAgent();
237
+ const result = await agent.run();
238
+
239
+ const brokenFinding = result.findings.find(
240
+ f => f.description.includes('https://error.example.com/500'),
241
+ );
242
+ expect(brokenFinding).toBeDefined();
243
+ expect(brokenFinding!.severity).toBe('low');
244
+ });
245
+
246
+ it('creates a finding for an unreachable URL (network error)', async () => {
247
+ setupBasicFs('[unreachable](https://unreachable.example.com/)');
248
+ fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
249
+
250
+ const agent = createAgent();
251
+ const result = await agent.run();
252
+
253
+ const brokenFinding = result.findings.find(
254
+ f => f.description.includes('https://unreachable.example.com/'),
255
+ );
256
+ expect(brokenFinding).toBeDefined();
257
+ });
258
+
259
+ it('does not create findings for valid links (200)', async () => {
260
+ setupBasicFs('[ok](https://ok.example.com/)');
261
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
262
+
263
+ const agent = createAgent();
264
+ const result = await agent.run();
265
+
266
+ const linkFindings = result.findings.filter(
267
+ f => f.description.includes('https://ok.example.com/'),
268
+ );
269
+ expect(linkFindings).toHaveLength(0);
270
+ });
271
+
272
+ it('does not create findings for redirect responses (301/302)', async () => {
273
+ setupBasicFs('[redirect](https://redirect.example.com/)');
274
+ fetchMock.mockResolvedValue({ ok: false, status: 301 });
275
+
276
+ const agent = createAgent();
277
+ const result = await agent.run();
278
+
279
+ const linkFindings = result.findings.filter(
280
+ f => f.description.includes('redirect.example.com'),
281
+ );
282
+ expect(linkFindings).toHaveLength(0);
283
+ });
284
+
285
+ it('sets finding.file to the markdown file path', async () => {
286
+ setupBasicFs('[broken](https://broken2.example.com/)');
287
+ fetchMock.mockResolvedValue({ ok: false, status: 404 });
288
+
289
+ const agent = createAgent();
290
+ const result = await agent.run();
291
+
292
+ const brokenFinding = result.findings.find(
293
+ f => f.description.includes('https://broken2.example.com/'),
294
+ );
295
+ expect(brokenFinding!.file).toBeDefined();
296
+ expect(brokenFinding!.file).toContain('README.md');
297
+ });
298
+ });
299
+
300
+ // -------------------------------------------------------------------------
301
+ // Bare catch-log pattern
302
+ // -------------------------------------------------------------------------
303
+
304
+ describe('bare catch log detection', () => {
305
+ it('creates a finding when src/ contains a catch block that only logs error.message', async () => {
306
+ setupBasicFs();
307
+
308
+ // Override stat and readdir to include a fake src/ directory
309
+ statMock.mockImplementation((p: string) => {
310
+ if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
311
+ if (p.endsWith('docs')) return Promise.reject(new Error('ENOENT'));
312
+ if (p.endsWith('src')) return Promise.resolve({ isDirectory: () => true });
313
+ if (p.endsWith('service.ts')) return Promise.resolve({ isDirectory: () => false });
314
+ return Promise.reject(new Error('ENOENT'));
315
+ });
316
+
317
+ readdirMock.mockImplementation((dir: string) => {
318
+ if (String(dir).endsWith('src')) return Promise.resolve(['service.ts']);
319
+ return Promise.resolve(['README.md']);
320
+ });
321
+
322
+ readFileMock.mockImplementation((filePath: string) => {
323
+ if (String(filePath).endsWith('README.md')) return Promise.resolve('# Docs');
324
+ if (String(filePath).endsWith('service.ts')) {
325
+ return Promise.resolve(`
326
+ async function doSomething() {
327
+ try {
328
+ await risky();
329
+ } catch (err) {
330
+ console.error(err.message);
331
+ }
332
+ }
333
+ `);
334
+ }
335
+ return Promise.resolve('');
336
+ });
337
+
338
+ const agent = createAgent();
339
+ const result = await agent.run();
340
+
341
+ const catchFinding = result.findings.find(
342
+ f => f.description.includes('catch') || f.type === 'code-bug-logic',
343
+ );
344
+ expect(catchFinding).toBeDefined();
345
+ expect(catchFinding!.severity).toBe('low');
346
+ });
347
+
348
+ it('does not create a bare-catch finding when no such pattern exists in source', async () => {
349
+ setupBasicFs();
350
+
351
+ statMock.mockImplementation((p: string) => {
352
+ if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
353
+ if (p.endsWith('docs')) return Promise.reject(new Error('ENOENT'));
354
+ if (p.endsWith('src')) return Promise.resolve({ isDirectory: () => true });
355
+ if (p.endsWith('clean-service.ts')) return Promise.resolve({ isDirectory: () => false });
356
+ return Promise.reject(new Error('ENOENT'));
357
+ });
358
+
359
+ readdirMock.mockImplementation((dir: string) => {
360
+ if (String(dir).endsWith('src')) return Promise.resolve(['clean-service.ts']);
361
+ return Promise.resolve(['README.md']);
362
+ });
363
+
364
+ readFileMock.mockImplementation((filePath: string) => {
365
+ if (String(filePath).endsWith('README.md')) return Promise.resolve('# Docs');
366
+ if (String(filePath).endsWith('clean-service.ts')) {
367
+ return Promise.resolve(`
368
+ async function doSomething() {
369
+ try {
370
+ await risky();
371
+ } catch (err) {
372
+ throw new BadRequestException('Something went wrong — please try again');
373
+ }
374
+ }
375
+ `);
376
+ }
377
+ return Promise.resolve('');
378
+ });
379
+
380
+ const agent = createAgent();
381
+ const result = await agent.run();
382
+
383
+ const catchFindings = result.findings.filter(f => f.type === 'code-bug-logic');
384
+ expect(catchFindings).toHaveLength(0);
385
+ });
386
+ });
387
+
388
+ // -------------------------------------------------------------------------
389
+ // No findings when everything is valid
390
+ // -------------------------------------------------------------------------
391
+
392
+ it('returns passed status with no findings for clean docs and valid links', async () => {
393
+ setupBasicFs('# README\n\nThis project has no links.');
394
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
395
+
396
+ const agent = createAgent();
397
+ const result = await agent.run();
398
+
399
+ expect(result.status).toBe('passed');
400
+ expect(result.findings).toHaveLength(0);
401
+ });
402
+ });
@@ -0,0 +1,263 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { ValidatedConfig } from '../../core/config';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Stub config
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function makeConfig(overrides: Partial<ValidatedConfig> = {}): ValidatedConfig {
9
+ return {
10
+ schemaVersion: 1,
11
+ name: 'test',
12
+ auth: { loginUrl: 'http://localhost/login' },
13
+ modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
14
+ environments: { alpha: { baseUrl: 'http://localhost:3000', seed: false } },
15
+ portals: [],
16
+ breakpoints: [],
17
+ integrations: [{ name: 'xero' }],
18
+ workers: [],
19
+ workflows: [],
20
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
21
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
22
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
23
+ agentClassification: { blocking: [], advisory: [] },
24
+ thresholds: {
25
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
26
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
27
+ apiResponseTime: 2000,
28
+ bundleSizeLimit: 500000,
29
+ maxConsoleErrors: 0,
30
+ },
31
+ disabledAgents: [],
32
+ disabledAgentSet: new Set<number>(),
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Mock node:fs and node:child_process
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const existsSyncMock = vi.fn();
42
+ vi.mock('node:fs', () => ({
43
+ existsSync: (...args: unknown[]) => existsSyncMock(...args),
44
+ }));
45
+
46
+ type ExecFileCallback = (err: Error | null, stdout: string, stderr: string) => void;
47
+
48
+ const execFileMock = vi.fn();
49
+ vi.mock('node:child_process', () => ({
50
+ execFile: (...args: unknown[]) => execFileMock(...args),
51
+ }));
52
+
53
+ /** Helper: make execFile resolve with success */
54
+ function setupExecSuccess(stdout = ''): void {
55
+ execFileMock.mockImplementation(
56
+ (_file: string, _args: string[], _opts: unknown, cb: ExecFileCallback) => {
57
+ cb(null, stdout, '');
58
+ },
59
+ );
60
+ }
61
+
62
+ /** Helper: make execFile reject with a failure */
63
+ function setupExecFailure(stderr: string): void {
64
+ const error = Object.assign(new Error('Command failed'), { stderr, stdout: '' });
65
+ execFileMock.mockImplementation(
66
+ (_file: string, _args: string[], _opts: unknown, cb: ExecFileCallback) => {
67
+ cb(error, '', stderr);
68
+ },
69
+ );
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Import after mocks
74
+ // ---------------------------------------------------------------------------
75
+
76
+ const { IntegrationWatchdogAgent } = await import('../18-integration-watchdog');
77
+
78
+ function createAgent(config: ValidatedConfig = makeConfig()): InstanceType<typeof IntegrationWatchdogAgent> {
79
+ return new IntegrationWatchdogAgent(config, 'beta', '/tmp/test-run');
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Tests
84
+ // ---------------------------------------------------------------------------
85
+
86
+ describe('IntegrationWatchdogAgent', () => {
87
+ beforeEach(() => {
88
+ vi.resetAllMocks();
89
+ // Default: workers dir exists
90
+ existsSyncMock.mockReturnValue(true);
91
+ // Default: vitest runs succeed
92
+ setupExecSuccess('All tests passed');
93
+ });
94
+
95
+ it('has agentId 18 and agentName "Integration Watchdog"', () => {
96
+ const agent = createAgent();
97
+ expect(agent.agentId).toBe(18);
98
+ expect(agent.agentName).toBe('Integration Watchdog');
99
+ });
100
+
101
+ // -------------------------------------------------------------------------
102
+ // preFlight()
103
+ // -------------------------------------------------------------------------
104
+
105
+ describe('preFlight()', () => {
106
+ it('throws when config.integrations is empty', async () => {
107
+ const config = makeConfig({ integrations: [] });
108
+ const agent = createAgent(config);
109
+ const result = await agent.run();
110
+
111
+ expect(result.status).toBe('failed');
112
+ expect(result.findings[0].description).toContain('at least one integration');
113
+ });
114
+
115
+ it('throws when worker directories do not exist', async () => {
116
+ existsSyncMock.mockReturnValue(false);
117
+ const agent = createAgent();
118
+ const result = await agent.run();
119
+
120
+ expect(result.status).toBe('failed');
121
+ expect(result.findings[0].description).toContain('Worker directory not found');
122
+ });
123
+
124
+ it('passes when integrations are configured and worker dir exists', async () => {
125
+ existsSyncMock.mockReturnValue(true);
126
+ const agent = createAgent();
127
+ const result = await agent.run();
128
+
129
+ expect(result.findings.some((f) => f.description.includes('at least one integration'))).toBe(false);
130
+ });
131
+ });
132
+
133
+ // -------------------------------------------------------------------------
134
+ // execute() — worker checks
135
+ // -------------------------------------------------------------------------
136
+
137
+ describe('execute() — worker source file checks', () => {
138
+ it('creates a medium finding when a worker source file is missing', async () => {
139
+ const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
140
+
141
+ // workers dir exists, but worker source file and vitest config do not
142
+ existsSyncMock.mockImplementation((p: string) => {
143
+ const n = p.replace(/\\/g, '/');
144
+ // apps/workers dir must exist for preFlight
145
+ if (n.endsWith('apps/workers')) return true;
146
+ // Everything else (source files, vitest config) does not exist
147
+ return false;
148
+ });
149
+
150
+ const agent = createAgent(config);
151
+ const result = await agent.run();
152
+
153
+ const workerFinding = result.findings.find((f) => f.id.includes('worker-missing'));
154
+ expect(workerFinding).toBeDefined();
155
+ expect(workerFinding!.severity).toBe('medium');
156
+ });
157
+
158
+ it('creates a medium finding when a worker has no test file', async () => {
159
+ const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
160
+
161
+ existsSyncMock.mockImplementation((p: string) => {
162
+ const n = p.replace(/\\/g, '/');
163
+ // apps/workers dir exists for preFlight
164
+ if (n.endsWith('apps/workers')) return true;
165
+ // vitest config does NOT exist — avoids running tests in execute()
166
+ if (n.endsWith('vitest.config.ts')) return false;
167
+ // worker source file exists, test files do not
168
+ if (n.includes('sync-xero') && !n.includes('.test.') && !n.includes('.spec.')) return true;
169
+ return false;
170
+ });
171
+
172
+ const agent = createAgent(config);
173
+ const result = await agent.run();
174
+
175
+ const testFinding = result.findings.find((f) => f.id.includes('worker-no-tests'));
176
+ expect(testFinding).toBeDefined();
177
+ expect(testFinding!.severity).toBe('medium');
178
+ });
179
+
180
+ it('creates no worker finding when source and test file both exist', async () => {
181
+ const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
182
+
183
+ // All paths exist — source, tests, and vitest config
184
+ existsSyncMock.mockReturnValue(true);
185
+ setupExecSuccess('All tests passed');
186
+
187
+ const agent = createAgent(config);
188
+ const result = await agent.run();
189
+
190
+ const workerFindings = result.findings.filter(
191
+ (f) => f.id.includes('worker-missing') || f.id.includes('worker-no-tests'),
192
+ );
193
+ expect(workerFindings).toHaveLength(0);
194
+ });
195
+ });
196
+
197
+ // -------------------------------------------------------------------------
198
+ // execute() — worker test runner
199
+ // -------------------------------------------------------------------------
200
+
201
+ describe('execute() — worker test runner', () => {
202
+ it('creates a high finding when worker tests fail', async () => {
203
+ const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
204
+
205
+ existsSyncMock.mockReturnValue(true);
206
+ setupExecFailure('FAIL sync-xero.test.ts');
207
+
208
+ const agent = createAgent(config);
209
+ const result = await agent.run();
210
+
211
+ const testFailFinding = result.findings.find((f) => f.id.includes('worker-tests-failed'));
212
+ expect(testFailFinding).toBeDefined();
213
+ expect(testFailFinding!.severity).toBe('high');
214
+ });
215
+
216
+ it('creates no finding when all worker tests pass', async () => {
217
+ const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
218
+
219
+ existsSyncMock.mockReturnValue(true);
220
+ setupExecSuccess('All tests passed');
221
+
222
+ const agent = createAgent(config);
223
+ const result = await agent.run();
224
+
225
+ const testFailFinding = result.findings.find((f) => f.id.includes('worker-tests-failed'));
226
+ expect(testFailFinding).toBeUndefined();
227
+ });
228
+ });
229
+
230
+ // -------------------------------------------------------------------------
231
+ // execute() — integration sandbox checks
232
+ // -------------------------------------------------------------------------
233
+
234
+ describe('execute() — integration sandbox checks', () => {
235
+ it('creates a low advisory finding when no sandbox config exists for an integration', async () => {
236
+ // Workers dir exists but integration sandbox config files do not
237
+ existsSyncMock.mockImplementation((p: string) => {
238
+ const n = p.replace(/\\/g, '/');
239
+ if (n.endsWith('apps/workers')) return true;
240
+ if (n.includes('xero')) return false;
241
+ return true;
242
+ });
243
+
244
+ const agent = createAgent();
245
+ const result = await agent.run();
246
+
247
+ const sandboxFinding = result.findings.find((f) => f.id.includes('integration-no-sandbox'));
248
+ expect(sandboxFinding).toBeDefined();
249
+ expect(sandboxFinding!.severity).toBe('low');
250
+ });
251
+
252
+ it('creates no sandbox finding when sandbox config exists', async () => {
253
+ // All paths exist — sandbox config found
254
+ existsSyncMock.mockReturnValue(true);
255
+
256
+ const agent = createAgent();
257
+ const result = await agent.run();
258
+
259
+ const sandboxFinding = result.findings.find((f) => f.id.includes('integration-no-sandbox'));
260
+ expect(sandboxFinding).toBeUndefined();
261
+ });
262
+ });
263
+ });