@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,619 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { ValidatedConfig } from '../../core/config';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Stub config
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const stubConfig: ValidatedConfig = {
9
+ schemaVersion: 1,
10
+ name: 'test',
11
+ auth: {
12
+ loginUrl: 'http://localhost/login',
13
+ credentials: { admin: { email: 'admin@test.com', password: 'secret' } },
14
+ },
15
+ modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
16
+ environments: {
17
+ alpha: { baseUrl: 'http://localhost:3000', seed: false },
18
+ },
19
+ portals: [],
20
+ breakpoints: [],
21
+ integrations: [],
22
+ workers: [],
23
+ workflows: [],
24
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
25
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
26
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
27
+ agentClassification: { blocking: [], advisory: [] },
28
+ thresholds: {
29
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
30
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
31
+ apiResponseTime: 2000,
32
+ bundleSizeLimit: 500000,
33
+ maxConsoleErrors: 0,
34
+ },
35
+ disabledAgents: [],
36
+ disabledAgentSet: new Set<number>(),
37
+ };
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Mock node:fs/promises (readdir + readFile)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ const readdirMock = vi.fn();
44
+ const readFileMock = vi.fn();
45
+
46
+ vi.mock('node:fs/promises', () => ({
47
+ readdir: (...args: unknown[]) => readdirMock(...args),
48
+ readFile: (...args: unknown[]) => readFileMock(...args),
49
+ }));
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Mock node:path (so join returns predictable values in tests)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ vi.mock('node:path', async (importOriginal) => {
56
+ const original = await importOriginal<typeof import('node:path')>();
57
+ return { ...original };
58
+ });
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Mock ApiClient
62
+ // ---------------------------------------------------------------------------
63
+
64
+ const getMock = vi.fn();
65
+ const postMock = vi.fn();
66
+ const putMock = vi.fn();
67
+ const deleteMock = vi.fn();
68
+ const createAuthenticatedMock = vi.fn();
69
+
70
+ vi.mock('../../helpers/api-client', () => ({
71
+ ApiClient: class MockApiClient {
72
+ static createAuthenticated = (...args: unknown[]) => createAuthenticatedMock(...args);
73
+ get = (...args: unknown[]) => getMock(...args);
74
+ post = (...args: unknown[]) => postMock(...args);
75
+ put = (...args: unknown[]) => putMock(...args);
76
+ delete = (...args: unknown[]) => deleteMock(...args);
77
+ },
78
+ }));
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Mock fetch (preFlight URL check)
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const fetchMock = vi.fn();
85
+ vi.stubGlobal('fetch', fetchMock);
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Dynamic import AFTER mocks
89
+ // ---------------------------------------------------------------------------
90
+
91
+ const { ApiExerciserAgent } = await import('../06-api-exerciser');
92
+
93
+ function createAgent(): InstanceType<typeof ApiExerciserAgent> {
94
+ return new ApiExerciserAgent(stubConfig, 'beta', '/tmp/test-run');
95
+ }
96
+
97
+ function makeOkResponse(status = 200, durationMs = 50): object {
98
+ return { status, body: {}, headers: {}, durationMs };
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Tests
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe('ApiExerciserAgent', () => {
106
+ beforeEach(() => {
107
+ readdirMock.mockReset();
108
+ readFileMock.mockReset();
109
+ getMock.mockReset();
110
+ postMock.mockReset();
111
+ putMock.mockReset();
112
+ deleteMock.mockReset();
113
+ createAuthenticatedMock.mockReset();
114
+ fetchMock.mockReset();
115
+
116
+ // Default: preFlight succeeds
117
+ fetchMock.mockResolvedValue({ ok: true, status: 200 });
118
+
119
+ // Default: no route files
120
+ readdirMock.mockResolvedValue([]);
121
+
122
+ // Default: authenticated client creation succeeds
123
+ createAuthenticatedMock.mockResolvedValue({
124
+ get: getMock,
125
+ post: postMock,
126
+ put: putMock,
127
+ delete: deleteMock,
128
+ });
129
+
130
+ // Default: all API calls return 200 quickly
131
+ getMock.mockResolvedValue(makeOkResponse(200));
132
+ postMock.mockResolvedValue(makeOkResponse(200));
133
+ putMock.mockResolvedValue(makeOkResponse(200));
134
+ deleteMock.mockResolvedValue(makeOkResponse(200));
135
+ });
136
+
137
+ // -------------------------------------------------------------------------
138
+ // Identity
139
+ // -------------------------------------------------------------------------
140
+
141
+ it('has agentId 6 and agentName "API Exerciser"', () => {
142
+ const agent = createAgent();
143
+ expect(agent.agentId).toBe(6);
144
+ expect(agent.agentName).toBe('API Exerciser');
145
+ });
146
+
147
+ // -------------------------------------------------------------------------
148
+ // preFlight
149
+ // -------------------------------------------------------------------------
150
+
151
+ describe('preFlight()', () => {
152
+ it('fails when base URL is not reachable', async () => {
153
+ fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
154
+
155
+ const agent = createAgent();
156
+ const result = await agent.run();
157
+
158
+ expect(result.status).toBe('failed');
159
+ expect(result.findings[0].description).toMatch(/not reachable/i);
160
+ });
161
+
162
+ it('passes when base URL returns 200', async () => {
163
+ const agent = createAgent();
164
+ const result = await agent.run();
165
+
166
+ expect(result.findings.some(f => f.description.match(/not reachable/i))).toBe(false);
167
+ });
168
+ });
169
+
170
+ // -------------------------------------------------------------------------
171
+ // Endpoint discovery
172
+ // -------------------------------------------------------------------------
173
+
174
+ describe('endpoint discovery', () => {
175
+ it('discovers GET routes from router.get() patterns in route files', () => {
176
+ const agent = createAgent();
177
+ const content = `
178
+ router.get('/clients', authenticate, getClients);
179
+ router.get('/clients/:id', authenticate, getClient);
180
+ `;
181
+ const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
182
+
183
+ expect(endpoints.filter(e => e.method === 'GET')).toHaveLength(2);
184
+ });
185
+
186
+ it('discovers POST routes from router.post() patterns', () => {
187
+ const agent = createAgent();
188
+ const content = `router.post('/clients', authenticate, createClient);`;
189
+ const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
190
+
191
+ expect(endpoints.find(e => e.method === 'POST' && e.path === '/clients')).toBeDefined();
192
+ });
193
+
194
+ it('discovers PUT routes from router.put() patterns', () => {
195
+ const agent = createAgent();
196
+ const content = `router.put('/clients/:id', authenticate, updateClient);`;
197
+ const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
198
+
199
+ expect(endpoints.find(e => e.method === 'PUT')).toBeDefined();
200
+ });
201
+
202
+ it('discovers DELETE routes from router.delete() patterns', () => {
203
+ const agent = createAgent();
204
+ const content = `router.delete('/clients/:id', authenticate, deleteClient);`;
205
+ const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
206
+
207
+ expect(endpoints.find(e => e.method === 'DELETE')).toBeDefined();
208
+ });
209
+
210
+ it('discovers NestJS @Get decorator patterns', () => {
211
+ const agent = createAgent();
212
+ const content = `@Get('/invoices') findAll() {}`;
213
+ const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
214
+
215
+ expect(endpoints.find(e => e.method === 'GET' && e.path === '/invoices')).toBeDefined();
216
+ });
217
+
218
+ it('discovers NestJS @Post decorator patterns', () => {
219
+ const agent = createAgent();
220
+ const content = `@Post('/invoices') create() {}`;
221
+ const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
222
+
223
+ expect(endpoints.find(e => e.method === 'POST' && e.path === '/invoices')).toBeDefined();
224
+ });
225
+
226
+ it('skips .test.ts files when scanning the routes directory', async () => {
227
+ readdirMock.mockResolvedValue(['clients.routes.ts', 'clients.routes.test.ts']);
228
+ readFileMock.mockResolvedValue(`router.get('/clients', h);`);
229
+
230
+ const agent = createAgent();
231
+ await agent.run();
232
+
233
+ // readFile should only be called once (for the non-test file)
234
+ expect(readFileMock).toHaveBeenCalledTimes(1);
235
+ });
236
+
237
+ it('returns empty findings when routes directory does not exist', async () => {
238
+ readdirMock.mockRejectedValue(new Error('ENOENT: no such file or directory'));
239
+
240
+ const agent = createAgent();
241
+ const result = await agent.run();
242
+
243
+ expect(result.status).toBe('passed');
244
+ expect(result.findings).toHaveLength(0);
245
+ });
246
+ });
247
+
248
+ // -------------------------------------------------------------------------
249
+ // Auth check
250
+ // -------------------------------------------------------------------------
251
+
252
+ describe('auth check', () => {
253
+ it('creates a security finding when endpoint does not return 401 for anonymous request', async () => {
254
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
255
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
256
+
257
+ // Authenticated client returns 200; anonymous client also returns 200 (missing auth guard)
258
+ const authedInstance = {
259
+ get: vi.fn().mockResolvedValue(makeOkResponse(200)),
260
+ post: postMock,
261
+ put: putMock,
262
+ delete: deleteMock,
263
+ };
264
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
265
+ getMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200
266
+
267
+ const agent = createAgent();
268
+ const result = await agent.run();
269
+
270
+ const authFinding = result.findings.find(f =>
271
+ f.type === 'code-bug-security' &&
272
+ f.description.includes('did not return 401'),
273
+ );
274
+ expect(authFinding).toBeDefined();
275
+ expect(authFinding!.severity).toBe('high');
276
+ });
277
+
278
+ it('does not create an auth finding when endpoint returns 401 for anonymous request', async () => {
279
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
280
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
281
+
282
+ const authedInstance = {
283
+ get: vi.fn().mockResolvedValue(makeOkResponse(200)),
284
+ post: postMock,
285
+ put: putMock,
286
+ delete: deleteMock,
287
+ };
288
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
289
+ getMock.mockResolvedValue(makeOkResponse(401)); // anon gets 401
290
+
291
+ const agent = createAgent();
292
+ const result = await agent.run();
293
+
294
+ const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
295
+ expect(authFindings).toHaveLength(0);
296
+ });
297
+
298
+ it('skips auth check for /auth routes', async () => {
299
+ readdirMock.mockResolvedValue(['auth.routes.ts']);
300
+ readFileMock.mockResolvedValue(`router.post('/auth/login', loginHandler);`);
301
+
302
+ const authedInstance = {
303
+ get: getMock,
304
+ post: vi.fn().mockResolvedValue(makeOkResponse(200)),
305
+ put: putMock,
306
+ delete: deleteMock,
307
+ };
308
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
309
+ postMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200, but it's an auth route
310
+
311
+ const agent = createAgent();
312
+ const result = await agent.run();
313
+
314
+ const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
315
+ expect(authFindings).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ // -------------------------------------------------------------------------
320
+ // Response time
321
+ // -------------------------------------------------------------------------
322
+
323
+ describe('response time', () => {
324
+ it('creates a medium finding when response time exceeds 500ms', async () => {
325
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
326
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
327
+
328
+ const authedInstance = {
329
+ get: vi.fn().mockResolvedValue(makeOkResponse(200, 600)), // 600ms > 500ms threshold
330
+ post: postMock,
331
+ put: putMock,
332
+ delete: deleteMock,
333
+ };
334
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
335
+ getMock.mockResolvedValue(makeOkResponse(401));
336
+
337
+ const agent = createAgent();
338
+ const result = await agent.run();
339
+
340
+ const slowFinding = result.findings.find(f =>
341
+ f.description.includes('600ms'),
342
+ );
343
+ expect(slowFinding).toBeDefined();
344
+ expect(slowFinding!.severity).toBe('medium');
345
+ });
346
+ });
347
+
348
+ // -------------------------------------------------------------------------
349
+ // Validation check
350
+ // -------------------------------------------------------------------------
351
+
352
+ describe('validation check', () => {
353
+ it('creates a finding when POST with empty body returns 200 instead of 400', async () => {
354
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
355
+ readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
356
+
357
+ const authedInstance = {
358
+ get: getMock,
359
+ post: vi.fn().mockResolvedValue(makeOkResponse(200)),
360
+ put: putMock,
361
+ delete: deleteMock,
362
+ };
363
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
364
+ // Anon POST also returns 200 (no validation)
365
+ postMock.mockResolvedValue(makeOkResponse(200));
366
+
367
+ const agent = createAgent();
368
+ const result = await agent.run();
369
+
370
+ const validationFinding = result.findings.find(f =>
371
+ f.description.includes('empty body'),
372
+ );
373
+ expect(validationFinding).toBeDefined();
374
+ expect(validationFinding!.severity).toBe('medium');
375
+ });
376
+
377
+ it('does not create a validation finding when POST with empty body returns 400', async () => {
378
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
379
+ readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
380
+
381
+ const authedInstance = {
382
+ get: getMock,
383
+ post: vi.fn().mockResolvedValue(makeOkResponse(200)),
384
+ put: putMock,
385
+ delete: deleteMock,
386
+ };
387
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
388
+ postMock.mockResolvedValue(makeOkResponse(400));
389
+
390
+ const agent = createAgent();
391
+ const result = await agent.run();
392
+
393
+ const validationFindings = result.findings.filter(f =>
394
+ f.description.includes('empty body'),
395
+ );
396
+ expect(validationFindings).toHaveLength(0);
397
+ });
398
+
399
+ it('does not create a validation finding when POST returns 401 (auth guard first)', async () => {
400
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
401
+ readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
402
+
403
+ const authedInstance = {
404
+ get: getMock,
405
+ post: vi.fn().mockResolvedValue(makeOkResponse(200)),
406
+ put: putMock,
407
+ delete: deleteMock,
408
+ };
409
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
410
+ postMock.mockResolvedValue(makeOkResponse(401));
411
+
412
+ const agent = createAgent();
413
+ const result = await agent.run();
414
+
415
+ const validationFindings = result.findings.filter(f =>
416
+ f.description.includes('empty body'),
417
+ );
418
+ expect(validationFindings).toHaveLength(0);
419
+ });
420
+ });
421
+
422
+ // -------------------------------------------------------------------------
423
+ // Happy path: error responses
424
+ // -------------------------------------------------------------------------
425
+
426
+ describe('happy path', () => {
427
+ it('creates a finding when authenticated GET returns 500', async () => {
428
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
429
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
430
+
431
+ const authedInstance = {
432
+ get: vi.fn().mockResolvedValue(makeOkResponse(500)),
433
+ post: postMock,
434
+ put: putMock,
435
+ delete: deleteMock,
436
+ };
437
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
438
+ getMock.mockResolvedValue(makeOkResponse(401));
439
+
440
+ const agent = createAgent();
441
+ const result = await agent.run();
442
+
443
+ const happyFinding = result.findings.find(f => f.id.includes('happy'));
444
+ expect(happyFinding).toBeDefined();
445
+ expect(happyFinding!.severity).toBe('high');
446
+ });
447
+
448
+ it('does not create a finding for 401 or 404 in happy path', async () => {
449
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
450
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
451
+
452
+ const authedInstance = {
453
+ get: vi.fn().mockResolvedValue(makeOkResponse(404)),
454
+ post: postMock,
455
+ put: putMock,
456
+ delete: deleteMock,
457
+ };
458
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
459
+ getMock.mockResolvedValue(makeOkResponse(401));
460
+
461
+ const agent = createAgent();
462
+ const result = await agent.run();
463
+
464
+ const happyFindings = result.findings.filter(f => f.id.includes('happy'));
465
+ expect(happyFindings).toHaveLength(0);
466
+ });
467
+ });
468
+
469
+ // -------------------------------------------------------------------------
470
+ // No environment
471
+ // -------------------------------------------------------------------------
472
+
473
+ describe('no environment', () => {
474
+ it('creates a critical finding when no environment is configured', async () => {
475
+ const noEnvConfig: ValidatedConfig = {
476
+ ...stubConfig,
477
+ environments: {},
478
+ };
479
+ fetchMock.mockRejectedValue(new Error('No env'));
480
+
481
+ const agent = new ApiExerciserAgent(noEnvConfig, 'beta', '/tmp/test-run');
482
+ const result = await agent.run();
483
+
484
+ expect(result.status).toBe('failed');
485
+ });
486
+ });
487
+
488
+ // -------------------------------------------------------------------------
489
+ // Auth client creation failure
490
+ // -------------------------------------------------------------------------
491
+
492
+ describe('auth client creation failure', () => {
493
+ it('still runs auth checks with anonymous client when createAuthenticated fails', async () => {
494
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
495
+ readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
496
+
497
+ createAuthenticatedMock.mockRejectedValue(new Error('Auth failed'));
498
+ getMock.mockResolvedValue(makeOkResponse(401));
499
+
500
+ const agent = createAgent();
501
+ const result = await agent.run();
502
+
503
+ // Auth check still runs (anonymous client returns 401 — no finding)
504
+ expect(getMock).toHaveBeenCalled();
505
+ });
506
+ });
507
+
508
+ // -------------------------------------------------------------------------
509
+ // PUT validation check
510
+ // -------------------------------------------------------------------------
511
+
512
+ describe('PUT validation check', () => {
513
+ it('runs validation check for PUT endpoints with empty body', async () => {
514
+ readdirMock.mockResolvedValue(['clients.routes.ts']);
515
+ readFileMock.mockResolvedValue(`router.put('/clients/:id', authenticate, updateClient);`);
516
+
517
+ const authedInstance = {
518
+ get: getMock,
519
+ post: postMock,
520
+ put: vi.fn().mockResolvedValue(makeOkResponse(200)),
521
+ delete: deleteMock,
522
+ };
523
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
524
+ putMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200
525
+
526
+ const agent = createAgent();
527
+ const result = await agent.run();
528
+
529
+ const validationFinding = result.findings.find(f =>
530
+ f.id.includes('validation') && f.description.includes('empty body'),
531
+ );
532
+ expect(validationFinding).toBeDefined();
533
+ });
534
+ });
535
+
536
+ // -------------------------------------------------------------------------
537
+ // NestJS @Put/@Delete decorators
538
+ // -------------------------------------------------------------------------
539
+
540
+ describe('NestJS decorator patterns', () => {
541
+ it('discovers NestJS @Put decorator patterns', () => {
542
+ const agent = createAgent();
543
+ const content = `@Put('/invoices/:id') update() {}`;
544
+ const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
545
+ expect(endpoints.find(e => e.method === 'PUT')).toBeDefined();
546
+ });
547
+
548
+ it('discovers NestJS @Delete decorator patterns', () => {
549
+ const agent = createAgent();
550
+ const content = `@Delete('/invoices/:id') remove() {}`;
551
+ const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
552
+ expect(endpoints.find(e => e.method === 'DELETE')).toBeDefined();
553
+ });
554
+
555
+ it('handles empty path in NestJS decorators', () => {
556
+ const agent = createAgent();
557
+ const content = `@Get('') findAll() {}`;
558
+ const endpoints = agent.parseRouteFile(content, 'root.controller.ts');
559
+ expect(endpoints.find(e => e.method === 'GET' && e.path === '/')).toBeDefined();
560
+ });
561
+ });
562
+
563
+ // -------------------------------------------------------------------------
564
+ // Path normalization
565
+ // -------------------------------------------------------------------------
566
+
567
+ describe('path normalization', () => {
568
+ it('replaces :param path parameters with placeholder', () => {
569
+ const agent = createAgent();
570
+ const content = `router.get('/clients/:clientId/invoices/:invoiceId', h);`;
571
+ const endpoints = agent.parseRouteFile(content, 'routes.ts');
572
+ // Verify normalization works through the happy path
573
+ expect(endpoints[0].path).toBe('/clients/:clientId/invoices/:invoiceId');
574
+ });
575
+ });
576
+
577
+ // -------------------------------------------------------------------------
578
+ // readFile failure
579
+ // -------------------------------------------------------------------------
580
+
581
+ describe('readFile failure', () => {
582
+ it('skips route files that fail to read', async () => {
583
+ readdirMock.mockResolvedValue(['broken.routes.ts']);
584
+ readFileMock.mockRejectedValue(new Error('Permission denied'));
585
+
586
+ const agent = createAgent();
587
+ const result = await agent.run();
588
+
589
+ // Should complete without findings
590
+ expect(result.status).toBe('passed');
591
+ });
592
+ });
593
+
594
+ // -------------------------------------------------------------------------
595
+ // Public route /health skip
596
+ // -------------------------------------------------------------------------
597
+
598
+ describe('public routes', () => {
599
+ it('skips auth check finding for /health endpoints', async () => {
600
+ readdirMock.mockResolvedValue(['health.routes.ts']);
601
+ readFileMock.mockResolvedValue(`router.get('/health/status', healthHandler);`);
602
+
603
+ const authedInstance = {
604
+ get: vi.fn().mockResolvedValue(makeOkResponse(200)),
605
+ post: postMock,
606
+ put: putMock,
607
+ delete: deleteMock,
608
+ };
609
+ createAuthenticatedMock.mockResolvedValue(authedInstance);
610
+ getMock.mockResolvedValue(makeOkResponse(200));
611
+
612
+ const agent = createAgent();
613
+ const result = await agent.run();
614
+
615
+ const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
616
+ expect(authFindings).toHaveLength(0);
617
+ });
618
+ });
619
+ });