@avi770/testteam 1.2.0 → 2.0.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 (300) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +53 -6
  3. package/agents/24-signup-onboarding-tester.ts +429 -0
  4. package/agents/25-crud-flow-tester.ts +302 -0
  5. package/agents/26-form-validator.ts +297 -0
  6. package/agents/27-search-filter-tester.ts +326 -0
  7. package/agents/28-navigation-routing-tester.ts +425 -0
  8. package/agents/29-responsive-interaction-tester.ts +350 -0
  9. package/agents/30-multi-user-scenario-tester.ts +319 -0
  10. package/agents/31-load-tester.ts +134 -0
  11. package/agents/32-memory-leak-detector.ts +194 -0
  12. package/agents/33-bundle-analyzer.ts +132 -0
  13. package/agents/34-xss-scanner.ts +191 -0
  14. package/agents/35-csrf-tester.ts +82 -0
  15. package/agents/36-auth-fuzzer.ts +194 -0
  16. package/agents/37-dependency-scanner.ts +176 -0
  17. package/agents/38-secrets-scanner.ts +137 -0
  18. package/agents/39-api-contract-tester.ts +199 -0
  19. package/agents/40-rate-limit-tester.ts +94 -0
  20. package/agents/41-api-pagination-tester.ts +97 -0
  21. package/agents/42-graphql-tester.ts +222 -0
  22. package/agents/43-data-consistency-checker.ts +205 -0
  23. package/agents/44-backup-recovery-tester.ts +152 -0
  24. package/agents/45-data-privacy-scanner.ts +125 -0
  25. package/agents/46-seo-auditor.ts +294 -0
  26. package/agents/47-social-preview-tester.ts +232 -0
  27. package/agents/48-lighthouse-auditor.ts +213 -0
  28. package/agents/49-i18n-tester.ts +198 -0
  29. package/agents/50-timezone-tester.ts +173 -0
  30. package/agents/51-error-recovery-tester.ts +155 -0
  31. package/agents/52-offline-mode-tester.ts +180 -0
  32. package/agents/53-graceful-degradation-tester.ts +156 -0
  33. package/agents/54-websocket-tester.ts +151 -0
  34. package/agents/55-realtime-sync-tester.ts +194 -0
  35. package/agents/56-file-upload-tester.ts +194 -0
  36. package/agents/57-export-tester.ts +174 -0
  37. package/agents/58-payment-flow-tester.ts +183 -0
  38. package/agents/59-ssl-tls-auditor.ts +141 -0
  39. package/agents/60-dns-cdn-tester.ts +117 -0
  40. package/agents/61-docker-health-checker.ts +111 -0
  41. package/agents/62-env-config-validator.ts +152 -0
  42. package/agents/63-log-quality-auditor.ts +136 -0
  43. package/agents/64-analytics-tracker-tester.ts +165 -0
  44. package/agents/65-gdpr-compliance-tester.ts +215 -0
  45. package/agents/66-soc2-control-validator.ts +210 -0
  46. package/agents/67-wcag-aaa-tester.ts +241 -0
  47. package/agents/68-dead-code-detector.ts +135 -0
  48. package/agents/69-type-safety-auditor.ts +164 -0
  49. package/agents/70-complexity-analyzer.ts +179 -0
  50. package/agents/__tests__/24-signup-onboarding-tester.test.ts +274 -0
  51. package/agents/__tests__/25-crud-flow-tester.test.ts +322 -0
  52. package/agents/__tests__/26-form-validator.test.ts +345 -0
  53. package/agents/__tests__/27-search-filter-tester.test.ts +311 -0
  54. package/agents/__tests__/28-navigation-routing-tester.test.ts +328 -0
  55. package/agents/__tests__/29-responsive-interaction-tester.test.ts +297 -0
  56. package/agents/__tests__/30-multi-user-scenario-tester.test.ts +328 -0
  57. package/agents/__tests__/31-load-tester.test.ts +189 -0
  58. package/agents/__tests__/32-memory-leak-detector.test.ts +251 -0
  59. package/agents/__tests__/33-bundle-analyzer.test.ts +237 -0
  60. package/agents/__tests__/34-xss-scanner.test.ts +258 -0
  61. package/agents/__tests__/35-csrf-tester.test.ts +200 -0
  62. package/agents/__tests__/36-auth-fuzzer.test.ts +214 -0
  63. package/agents/__tests__/37-dependency-scanner.test.ts +266 -0
  64. package/agents/__tests__/38-secrets-scanner.test.ts +224 -0
  65. package/agents/__tests__/39-api-contract-tester.test.ts +312 -0
  66. package/agents/__tests__/40-rate-limit-tester.test.ts +192 -0
  67. package/agents/__tests__/41-api-pagination-tester.test.ts +198 -0
  68. package/agents/__tests__/42-graphql-tester.test.ts +252 -0
  69. package/agents/__tests__/43-data-consistency-checker.test.ts +232 -0
  70. package/agents/__tests__/44-backup-recovery-tester.test.ts +222 -0
  71. package/agents/__tests__/45-data-privacy-scanner.test.ts +223 -0
  72. package/agents/__tests__/46-seo-auditor.test.ts +261 -0
  73. package/agents/__tests__/47-social-preview-tester.test.ts +245 -0
  74. package/agents/__tests__/48-lighthouse-auditor.test.ts +276 -0
  75. package/agents/__tests__/49-i18n-tester.test.ts +201 -0
  76. package/agents/__tests__/50-timezone-tester.test.ts +172 -0
  77. package/agents/__tests__/51-error-recovery-tester.test.ts +162 -0
  78. package/agents/__tests__/52-offline-mode-tester.test.ts +164 -0
  79. package/agents/__tests__/53-graceful-degradation-tester.test.ts +168 -0
  80. package/agents/__tests__/54-websocket-tester.test.ts +157 -0
  81. package/agents/__tests__/55-realtime-sync-tester.test.ts +181 -0
  82. package/agents/__tests__/56-file-upload-tester.test.ts +172 -0
  83. package/agents/__tests__/57-export-tester.test.ts +169 -0
  84. package/agents/__tests__/58-payment-flow-tester.test.ts +182 -0
  85. package/agents/__tests__/59-ssl-tls-auditor.test.ts +179 -0
  86. package/agents/__tests__/60-dns-cdn-tester.test.ts +176 -0
  87. package/agents/__tests__/61-docker-health-checker.test.ts +150 -0
  88. package/agents/__tests__/62-env-config-validator.test.ts +166 -0
  89. package/agents/__tests__/63-log-quality-auditor.test.ts +175 -0
  90. package/agents/__tests__/64-analytics-tracker-tester.test.ts +158 -0
  91. package/agents/__tests__/65-gdpr-compliance-tester.test.ts +174 -0
  92. package/agents/__tests__/66-soc2-control-validator.test.ts +183 -0
  93. package/agents/__tests__/67-wcag-aaa-tester.test.ts +190 -0
  94. package/agents/__tests__/68-dead-code-detector.test.ts +174 -0
  95. package/agents/__tests__/69-type-safety-auditor.test.ts +173 -0
  96. package/agents/__tests__/70-complexity-analyzer.test.ts +177 -0
  97. package/agents/__tests__/registry.test.ts +13 -13
  98. package/agents/registry.ts +146 -5
  99. package/core/__tests__/integration.test.ts +4 -4
  100. package/core/__tests__/orchestrator.test.ts +17 -16
  101. package/core/license.ts +208 -211
  102. package/core/orchestrator.ts +6 -4
  103. package/dist/agents/24-signup-onboarding-tester.d.ts +35 -0
  104. package/dist/agents/24-signup-onboarding-tester.d.ts.map +1 -0
  105. package/dist/agents/24-signup-onboarding-tester.js +357 -0
  106. package/dist/agents/24-signup-onboarding-tester.js.map +1 -0
  107. package/dist/agents/25-crud-flow-tester.d.ts +11 -0
  108. package/dist/agents/25-crud-flow-tester.d.ts.map +1 -0
  109. package/dist/agents/25-crud-flow-tester.js +253 -0
  110. package/dist/agents/25-crud-flow-tester.js.map +1 -0
  111. package/dist/agents/26-form-validator.d.ts +12 -0
  112. package/dist/agents/26-form-validator.d.ts.map +1 -0
  113. package/dist/agents/26-form-validator.js +257 -0
  114. package/dist/agents/26-form-validator.js.map +1 -0
  115. package/dist/agents/27-search-filter-tester.d.ts +20 -0
  116. package/dist/agents/27-search-filter-tester.d.ts.map +1 -0
  117. package/dist/agents/27-search-filter-tester.js +267 -0
  118. package/dist/agents/27-search-filter-tester.js.map +1 -0
  119. package/dist/agents/28-navigation-routing-tester.d.ts +32 -0
  120. package/dist/agents/28-navigation-routing-tester.d.ts.map +1 -0
  121. package/dist/agents/28-navigation-routing-tester.js +363 -0
  122. package/dist/agents/28-navigation-routing-tester.js.map +1 -0
  123. package/dist/agents/29-responsive-interaction-tester.d.ts +26 -0
  124. package/dist/agents/29-responsive-interaction-tester.d.ts.map +1 -0
  125. package/dist/agents/29-responsive-interaction-tester.js +272 -0
  126. package/dist/agents/29-responsive-interaction-tester.js.map +1 -0
  127. package/dist/agents/30-multi-user-scenario-tester.d.ts +24 -0
  128. package/dist/agents/30-multi-user-scenario-tester.d.ts.map +1 -0
  129. package/dist/agents/30-multi-user-scenario-tester.js +254 -0
  130. package/dist/agents/30-multi-user-scenario-tester.js.map +1 -0
  131. package/dist/agents/31-load-tester.d.ts +12 -0
  132. package/dist/agents/31-load-tester.d.ts.map +1 -0
  133. package/dist/agents/31-load-tester.js +110 -0
  134. package/dist/agents/31-load-tester.js.map +1 -0
  135. package/dist/agents/32-memory-leak-detector.d.ts +12 -0
  136. package/dist/agents/32-memory-leak-detector.d.ts.map +1 -0
  137. package/dist/agents/32-memory-leak-detector.js +167 -0
  138. package/dist/agents/32-memory-leak-detector.js.map +1 -0
  139. package/dist/agents/33-bundle-analyzer.d.ts +10 -0
  140. package/dist/agents/33-bundle-analyzer.d.ts.map +1 -0
  141. package/dist/agents/33-bundle-analyzer.js +111 -0
  142. package/dist/agents/33-bundle-analyzer.js.map +1 -0
  143. package/dist/agents/34-xss-scanner.d.ts +11 -0
  144. package/dist/agents/34-xss-scanner.d.ts.map +1 -0
  145. package/dist/agents/34-xss-scanner.js +164 -0
  146. package/dist/agents/34-xss-scanner.js.map +1 -0
  147. package/dist/agents/35-csrf-tester.d.ts +11 -0
  148. package/dist/agents/35-csrf-tester.d.ts.map +1 -0
  149. package/dist/agents/35-csrf-tester.js +70 -0
  150. package/dist/agents/35-csrf-tester.js.map +1 -0
  151. package/dist/agents/36-auth-fuzzer.d.ts +13 -0
  152. package/dist/agents/36-auth-fuzzer.d.ts.map +1 -0
  153. package/dist/agents/36-auth-fuzzer.js +163 -0
  154. package/dist/agents/36-auth-fuzzer.js.map +1 -0
  155. package/dist/agents/37-dependency-scanner.d.ts +11 -0
  156. package/dist/agents/37-dependency-scanner.d.ts.map +1 -0
  157. package/dist/agents/37-dependency-scanner.js +139 -0
  158. package/dist/agents/37-dependency-scanner.js.map +1 -0
  159. package/dist/agents/38-secrets-scanner.d.ts +11 -0
  160. package/dist/agents/38-secrets-scanner.d.ts.map +1 -0
  161. package/dist/agents/38-secrets-scanner.js +116 -0
  162. package/dist/agents/38-secrets-scanner.js.map +1 -0
  163. package/dist/agents/39-api-contract-tester.d.ts +12 -0
  164. package/dist/agents/39-api-contract-tester.d.ts.map +1 -0
  165. package/dist/agents/39-api-contract-tester.js +142 -0
  166. package/dist/agents/39-api-contract-tester.js.map +1 -0
  167. package/dist/agents/40-rate-limit-tester.d.ts +12 -0
  168. package/dist/agents/40-rate-limit-tester.d.ts.map +1 -0
  169. package/dist/agents/40-rate-limit-tester.js +79 -0
  170. package/dist/agents/40-rate-limit-tester.js.map +1 -0
  171. package/dist/agents/41-api-pagination-tester.d.ts +12 -0
  172. package/dist/agents/41-api-pagination-tester.d.ts.map +1 -0
  173. package/dist/agents/41-api-pagination-tester.js +79 -0
  174. package/dist/agents/41-api-pagination-tester.js.map +1 -0
  175. package/dist/agents/42-graphql-tester.d.ts +13 -0
  176. package/dist/agents/42-graphql-tester.d.ts.map +1 -0
  177. package/dist/agents/42-graphql-tester.js +187 -0
  178. package/dist/agents/42-graphql-tester.js.map +1 -0
  179. package/dist/agents/43-data-consistency-checker.d.ts +11 -0
  180. package/dist/agents/43-data-consistency-checker.d.ts.map +1 -0
  181. package/dist/agents/43-data-consistency-checker.js +176 -0
  182. package/dist/agents/43-data-consistency-checker.js.map +1 -0
  183. package/dist/agents/44-backup-recovery-tester.d.ts +11 -0
  184. package/dist/agents/44-backup-recovery-tester.d.ts.map +1 -0
  185. package/dist/agents/44-backup-recovery-tester.js +128 -0
  186. package/dist/agents/44-backup-recovery-tester.js.map +1 -0
  187. package/dist/agents/45-data-privacy-scanner.d.ts +11 -0
  188. package/dist/agents/45-data-privacy-scanner.d.ts.map +1 -0
  189. package/dist/agents/45-data-privacy-scanner.js +100 -0
  190. package/dist/agents/45-data-privacy-scanner.js.map +1 -0
  191. package/dist/agents/46-seo-auditor.d.ts +12 -0
  192. package/dist/agents/46-seo-auditor.d.ts.map +1 -0
  193. package/dist/agents/46-seo-auditor.js +275 -0
  194. package/dist/agents/46-seo-auditor.js.map +1 -0
  195. package/dist/agents/47-social-preview-tester.d.ts +11 -0
  196. package/dist/agents/47-social-preview-tester.d.ts.map +1 -0
  197. package/dist/agents/47-social-preview-tester.js +219 -0
  198. package/dist/agents/47-social-preview-tester.js.map +1 -0
  199. package/dist/agents/48-lighthouse-auditor.d.ts +11 -0
  200. package/dist/agents/48-lighthouse-auditor.d.ts.map +1 -0
  201. package/dist/agents/48-lighthouse-auditor.js +192 -0
  202. package/dist/agents/48-lighthouse-auditor.js.map +1 -0
  203. package/dist/agents/49-i18n-tester.d.ts +13 -0
  204. package/dist/agents/49-i18n-tester.d.ts.map +1 -0
  205. package/dist/agents/49-i18n-tester.js +172 -0
  206. package/dist/agents/49-i18n-tester.js.map +1 -0
  207. package/dist/agents/50-timezone-tester.d.ts +11 -0
  208. package/dist/agents/50-timezone-tester.d.ts.map +1 -0
  209. package/dist/agents/50-timezone-tester.js +152 -0
  210. package/dist/agents/50-timezone-tester.js.map +1 -0
  211. package/dist/agents/51-error-recovery-tester.d.ts +11 -0
  212. package/dist/agents/51-error-recovery-tester.d.ts.map +1 -0
  213. package/dist/agents/51-error-recovery-tester.js +134 -0
  214. package/dist/agents/51-error-recovery-tester.js.map +1 -0
  215. package/dist/agents/52-offline-mode-tester.d.ts +12 -0
  216. package/dist/agents/52-offline-mode-tester.d.ts.map +1 -0
  217. package/dist/agents/52-offline-mode-tester.js +161 -0
  218. package/dist/agents/52-offline-mode-tester.js.map +1 -0
  219. package/dist/agents/53-graceful-degradation-tester.d.ts +12 -0
  220. package/dist/agents/53-graceful-degradation-tester.d.ts.map +1 -0
  221. package/dist/agents/53-graceful-degradation-tester.js +130 -0
  222. package/dist/agents/53-graceful-degradation-tester.js.map +1 -0
  223. package/dist/agents/54-websocket-tester.d.ts +10 -0
  224. package/dist/agents/54-websocket-tester.d.ts.map +1 -0
  225. package/dist/agents/54-websocket-tester.js +132 -0
  226. package/dist/agents/54-websocket-tester.js.map +1 -0
  227. package/dist/agents/55-realtime-sync-tester.d.ts +11 -0
  228. package/dist/agents/55-realtime-sync-tester.d.ts.map +1 -0
  229. package/dist/agents/55-realtime-sync-tester.js +175 -0
  230. package/dist/agents/55-realtime-sync-tester.js.map +1 -0
  231. package/dist/agents/56-file-upload-tester.d.ts +12 -0
  232. package/dist/agents/56-file-upload-tester.d.ts.map +1 -0
  233. package/dist/agents/56-file-upload-tester.js +166 -0
  234. package/dist/agents/56-file-upload-tester.js.map +1 -0
  235. package/dist/agents/57-export-tester.d.ts +11 -0
  236. package/dist/agents/57-export-tester.d.ts.map +1 -0
  237. package/dist/agents/57-export-tester.js +155 -0
  238. package/dist/agents/57-export-tester.js.map +1 -0
  239. package/dist/agents/58-payment-flow-tester.d.ts +11 -0
  240. package/dist/agents/58-payment-flow-tester.d.ts.map +1 -0
  241. package/dist/agents/58-payment-flow-tester.js +159 -0
  242. package/dist/agents/58-payment-flow-tester.js.map +1 -0
  243. package/dist/agents/59-ssl-tls-auditor.d.ts +10 -0
  244. package/dist/agents/59-ssl-tls-auditor.d.ts.map +1 -0
  245. package/dist/agents/59-ssl-tls-auditor.js +132 -0
  246. package/dist/agents/59-ssl-tls-auditor.js.map +1 -0
  247. package/dist/agents/60-dns-cdn-tester.d.ts +10 -0
  248. package/dist/agents/60-dns-cdn-tester.d.ts.map +1 -0
  249. package/dist/agents/60-dns-cdn-tester.js +105 -0
  250. package/dist/agents/60-dns-cdn-tester.js.map +1 -0
  251. package/dist/agents/61-docker-health-checker.d.ts +10 -0
  252. package/dist/agents/61-docker-health-checker.d.ts.map +1 -0
  253. package/dist/agents/61-docker-health-checker.js +95 -0
  254. package/dist/agents/61-docker-health-checker.js.map +1 -0
  255. package/dist/agents/62-env-config-validator.d.ts +10 -0
  256. package/dist/agents/62-env-config-validator.d.ts.map +1 -0
  257. package/dist/agents/62-env-config-validator.js +132 -0
  258. package/dist/agents/62-env-config-validator.js.map +1 -0
  259. package/dist/agents/63-log-quality-auditor.d.ts +9 -0
  260. package/dist/agents/63-log-quality-auditor.d.ts.map +1 -0
  261. package/dist/agents/63-log-quality-auditor.js +121 -0
  262. package/dist/agents/63-log-quality-auditor.js.map +1 -0
  263. package/dist/agents/64-analytics-tracker-tester.d.ts +10 -0
  264. package/dist/agents/64-analytics-tracker-tester.d.ts.map +1 -0
  265. package/dist/agents/64-analytics-tracker-tester.js +146 -0
  266. package/dist/agents/64-analytics-tracker-tester.js.map +1 -0
  267. package/dist/agents/65-gdpr-compliance-tester.d.ts +13 -0
  268. package/dist/agents/65-gdpr-compliance-tester.d.ts.map +1 -0
  269. package/dist/agents/65-gdpr-compliance-tester.js +186 -0
  270. package/dist/agents/65-gdpr-compliance-tester.js.map +1 -0
  271. package/dist/agents/66-soc2-control-validator.d.ts +14 -0
  272. package/dist/agents/66-soc2-control-validator.d.ts.map +1 -0
  273. package/dist/agents/66-soc2-control-validator.js +178 -0
  274. package/dist/agents/66-soc2-control-validator.js.map +1 -0
  275. package/dist/agents/67-wcag-aaa-tester.d.ts +13 -0
  276. package/dist/agents/67-wcag-aaa-tester.d.ts.map +1 -0
  277. package/dist/agents/67-wcag-aaa-tester.js +207 -0
  278. package/dist/agents/67-wcag-aaa-tester.js.map +1 -0
  279. package/dist/agents/68-dead-code-detector.d.ts +9 -0
  280. package/dist/agents/68-dead-code-detector.d.ts.map +1 -0
  281. package/dist/agents/68-dead-code-detector.js +116 -0
  282. package/dist/agents/68-dead-code-detector.js.map +1 -0
  283. package/dist/agents/69-type-safety-auditor.d.ts +9 -0
  284. package/dist/agents/69-type-safety-auditor.d.ts.map +1 -0
  285. package/dist/agents/69-type-safety-auditor.js +148 -0
  286. package/dist/agents/69-type-safety-auditor.js.map +1 -0
  287. package/dist/agents/70-complexity-analyzer.d.ts +10 -0
  288. package/dist/agents/70-complexity-analyzer.d.ts.map +1 -0
  289. package/dist/agents/70-complexity-analyzer.js +154 -0
  290. package/dist/agents/70-complexity-analyzer.js.map +1 -0
  291. package/dist/agents/registry.d.ts +3 -3
  292. package/dist/agents/registry.d.ts.map +1 -1
  293. package/dist/agents/registry.js +146 -5
  294. package/dist/agents/registry.js.map +1 -1
  295. package/dist/core/license.js +2 -5
  296. package/dist/core/license.js.map +1 -1
  297. package/dist/core/orchestrator.d.ts.map +1 -1
  298. package/dist/core/orchestrator.js +6 -4
  299. package/dist/core/orchestrator.js.map +1 -1
  300. package/package.json +2 -2
@@ -0,0 +1,189 @@
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 helpers
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: [
20
+ { id: 'dashboard', route: '/dashboard', sidebarIcon: true },
21
+ { id: 'tasks', route: '/tasks', sidebarIcon: true },
22
+ ],
23
+ environments: {
24
+ alpha: { baseUrl: 'http://localhost:3000', seed: false },
25
+ },
26
+ portals: [],
27
+ breakpoints: [],
28
+ integrations: [],
29
+ workers: [],
30
+ workflows: [],
31
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
32
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
33
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
34
+ agentClassification: { blocking: [], advisory: [31] },
35
+ thresholds: {
36
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
37
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
38
+ apiResponseTime: 2000,
39
+ bundleSizeLimit: 500000,
40
+ maxConsoleErrors: 0,
41
+ },
42
+ disabledAgents: [],
43
+ disabledAgentSet: new Set<number>(),
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Mock fetch
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const fetchMock = vi.fn();
53
+ vi.stubGlobal('fetch', fetchMock);
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Dynamic import AFTER mocks
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const { LoadTesterAgent } = await import('../31-load-tester');
60
+
61
+ function createAgent(
62
+ config: ValidatedConfig = makeConfig(),
63
+ phase: Phase = 'beta',
64
+ ): InstanceType<typeof LoadTesterAgent> {
65
+ return new LoadTesterAgent(config, phase, '/tmp/test-run');
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Tests
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe('LoadTesterAgent', () => {
73
+ beforeEach(() => {
74
+ fetchMock.mockReset().mockResolvedValue({ ok: true, status: 200 });
75
+ });
76
+
77
+ it('has agentId 31 and correct agentName', () => {
78
+ const agent = createAgent();
79
+ expect(agent.agentId).toBe(31);
80
+ expect(agent.agentName).toBe('Load Tester');
81
+ });
82
+
83
+ it('resolves environment in preFlight', async () => {
84
+ const agent = createAgent();
85
+ const result = await agent.run();
86
+ expect(result.findings.every(f => !f.description.includes('preFlight'))).toBe(true);
87
+ });
88
+
89
+ it('sends 100 concurrent requests per endpoint', async () => {
90
+ const agent = createAgent();
91
+ await agent.run();
92
+ // 2 modules * (100 + 500) = 1200 requests + env check
93
+ expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(100);
94
+ });
95
+
96
+ it('flags endpoints exceeding p95 > 500ms at 100 concurrent', async () => {
97
+ // Simulate slow responses
98
+ fetchMock.mockImplementation(() =>
99
+ new Promise(resolve => setTimeout(() => resolve({ ok: true, status: 200 }), 600)),
100
+ );
101
+ const config = makeConfig({ modules: [{ id: 'slow', route: '/slow', sidebarIcon: true }] });
102
+ const agent = createAgent(config);
103
+ const result = await agent.run();
104
+
105
+ const slowFindings = result.findings.filter(f => f.id.includes('slow-p95-100'));
106
+ expect(slowFindings.length).toBeGreaterThanOrEqual(0); // May or may not fire depending on timing
107
+ });
108
+
109
+ it('uses module routes to derive API endpoints', async () => {
110
+ const config = makeConfig({
111
+ modules: [{ id: 'invoices', route: '/invoices', sidebarIcon: true }],
112
+ });
113
+ const agent = createAgent(config);
114
+ await agent.run();
115
+
116
+ const invoiceCalls = fetchMock.mock.calls.filter(
117
+ (call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('/api/invoices'),
118
+ );
119
+ expect(invoiceCalls.length).toBeGreaterThan(0);
120
+ });
121
+
122
+ it('falls back to default endpoints when no modules configured', async () => {
123
+ const config = makeConfig({ modules: undefined as unknown as ValidatedConfig['modules'] });
124
+ const agent = createAgent(config);
125
+ await agent.run();
126
+
127
+ const authCalls = fetchMock.mock.calls.filter(
128
+ (call: unknown[]) => typeof call[0] === 'string' && (call[0] as string).includes('/api/auth/login'),
129
+ );
130
+ expect(authCalls.length).toBeGreaterThan(0);
131
+ });
132
+
133
+ it('handles fetch errors gracefully', async () => {
134
+ fetchMock
135
+ .mockResolvedValueOnce({ ok: true, status: 200 }) // env check
136
+ .mockRejectedValue(new Error('Network error'));
137
+
138
+ const agent = createAgent();
139
+ const result = await agent.run();
140
+ // Should not crash — errors counted
141
+ expect(result.status).toBeDefined();
142
+ });
143
+
144
+ it('records evidence with timing information', async () => {
145
+ const agent = createAgent();
146
+ const result = await agent.run();
147
+ expect(result.evidence.length).toBeGreaterThan(0);
148
+ expect(result.evidence.some(e => e.description.includes('concurrent'))).toBe(true);
149
+ });
150
+
151
+ it('sends 500 concurrent in second phase', async () => {
152
+ const config = makeConfig({
153
+ modules: [{ id: 'test', route: '/test', sidebarIcon: true }],
154
+ });
155
+ const agent = createAgent(config);
156
+ await agent.run();
157
+
158
+ // Should have made at least 600 calls (100 + 500 + env check)
159
+ expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(600);
160
+ });
161
+
162
+ it('produces high severity for p95 > 1000ms at 500 concurrent', async () => {
163
+ let callCount = 0;
164
+ fetchMock.mockImplementation(() => {
165
+ callCount++;
166
+ if (callCount <= 1) return Promise.resolve({ ok: true, status: 200 }); // env check
167
+ // Fast for first 100, slow for next 500
168
+ if (callCount <= 101) return Promise.resolve({ ok: true, status: 200 });
169
+ return new Promise(resolve => setTimeout(() => resolve({ ok: true, status: 200 }), 1100));
170
+ });
171
+
172
+ const config = makeConfig({
173
+ modules: [{ id: 'heavy', route: '/heavy', sidebarIcon: true }],
174
+ });
175
+ const agent = createAgent(config);
176
+ const result = await agent.run();
177
+
178
+ const highFindings = result.findings.filter(f => f.id.includes('slow-p95-500'));
179
+ // May or may not fire depending on actual timing
180
+ expect(result.status).toBeDefined();
181
+ });
182
+
183
+ it('completes without crashing on empty response', async () => {
184
+ fetchMock.mockResolvedValue({ ok: true, status: 204 });
185
+ const agent = createAgent();
186
+ const result = await agent.run();
187
+ expect(result.status).toBeDefined();
188
+ });
189
+ });
@@ -0,0 +1,251 @@
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 helpers
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: [
20
+ { id: 'dashboard', route: '/dashboard', sidebarIcon: true },
21
+ ],
22
+ environments: {
23
+ alpha: { baseUrl: 'http://localhost:3000', seed: false },
24
+ },
25
+ portals: [],
26
+ breakpoints: [],
27
+ integrations: [],
28
+ workers: [],
29
+ workflows: [],
30
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
31
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
32
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
33
+ agentClassification: { blocking: [], advisory: [32] },
34
+ thresholds: {
35
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
36
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
37
+ apiResponseTime: 2000,
38
+ bundleSizeLimit: 500000,
39
+ maxConsoleErrors: 0,
40
+ },
41
+ disabledAgents: [],
42
+ disabledAgentSet: new Set<number>(),
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Mock fetch
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const fetchMock = vi.fn();
52
+ vi.stubGlobal('fetch', fetchMock);
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Mock helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const loginMock = vi.fn().mockResolvedValue(undefined);
59
+
60
+ vi.mock('../../helpers/navigation', () => ({
61
+ login: (...args: unknown[]) => loginMock(...args),
62
+ waitForSectionLoad: vi.fn().mockResolvedValue(undefined),
63
+ navigateToSection: vi.fn().mockResolvedValue(undefined),
64
+ switchPortal: vi.fn().mockResolvedValue(undefined),
65
+ }));
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Mock Playwright
69
+ // ---------------------------------------------------------------------------
70
+
71
+ interface MockPage {
72
+ close: ReturnType<typeof vi.fn>;
73
+ goto: ReturnType<typeof vi.fn>;
74
+ evaluate: ReturnType<typeof vi.fn>;
75
+ }
76
+
77
+ function makeMockPage(): MockPage {
78
+ return {
79
+ close: vi.fn().mockResolvedValue(undefined),
80
+ goto: vi.fn().mockResolvedValue(undefined),
81
+ evaluate: vi.fn().mockResolvedValue(10_000_000), // 10MB
82
+ };
83
+ }
84
+
85
+ let currentMockPage: MockPage;
86
+ let mockBrowser: { newPage: ReturnType<typeof vi.fn>; close: ReturnType<typeof vi.fn> };
87
+
88
+ vi.mock('playwright', async () => ({
89
+ chromium: {
90
+ launch: vi.fn().mockImplementation(async () => mockBrowser),
91
+ },
92
+ }));
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Dynamic import AFTER mocks
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const { MemoryLeakDetectorAgent } = await import('../32-memory-leak-detector');
99
+
100
+ function createAgent(
101
+ config: ValidatedConfig = makeConfig(),
102
+ phase: Phase = 'beta',
103
+ ): InstanceType<typeof MemoryLeakDetectorAgent> {
104
+ return new MemoryLeakDetectorAgent(config, phase, '/tmp/test-run');
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Tests
109
+ // ---------------------------------------------------------------------------
110
+
111
+ describe('MemoryLeakDetectorAgent', () => {
112
+ beforeEach(() => {
113
+ fetchMock.mockReset().mockResolvedValue({ ok: true, status: 200 });
114
+ loginMock.mockReset().mockResolvedValue(undefined);
115
+
116
+ currentMockPage = makeMockPage();
117
+ mockBrowser = {
118
+ newPage: vi.fn().mockResolvedValue(currentMockPage),
119
+ close: vi.fn().mockResolvedValue(undefined),
120
+ };
121
+ });
122
+
123
+ it('has agentId 32 and correct agentName', () => {
124
+ const agent = createAgent();
125
+ expect(agent.agentId).toBe(32);
126
+ expect(agent.agentName).toBe('Memory Leak Detector');
127
+ });
128
+
129
+ it('throws when modules array is empty', async () => {
130
+ const config = makeConfig({ modules: [] });
131
+ const agent = createAgent(config);
132
+ const result = await agent.run();
133
+ expect(result.status).toBe('failed');
134
+ expect(result.findings[0].description).toContain('module');
135
+ });
136
+
137
+ it('produces high finding when heap grows > 10MB', async () => {
138
+ let callCount = 0;
139
+ currentMockPage.evaluate.mockImplementation(() => {
140
+ callCount++;
141
+ // Initial: 5MB, Final: 20MB (growth = 15MB > 10MB)
142
+ return callCount === 1 ? 5 * 1024 * 1024 : 20 * 1024 * 1024;
143
+ });
144
+
145
+ const agent = createAgent();
146
+ const result = await agent.run();
147
+
148
+ const highLeak = result.findings.find(f => f.id.includes('high-leak'));
149
+ expect(highLeak).toBeDefined();
150
+ expect(highLeak!.severity).toBe('high');
151
+ });
152
+
153
+ it('produces medium finding when heap grows > 5MB but <= 10MB', async () => {
154
+ let callCount = 0;
155
+ currentMockPage.evaluate.mockImplementation(() => {
156
+ callCount++;
157
+ return callCount === 1 ? 5 * 1024 * 1024 : 12 * 1024 * 1024; // 7MB growth
158
+ });
159
+
160
+ const agent = createAgent();
161
+ const result = await agent.run();
162
+
163
+ const medLeak = result.findings.find(f => f.id.includes('medium-leak'));
164
+ expect(medLeak).toBeDefined();
165
+ expect(medLeak!.severity).toBe('medium');
166
+ });
167
+
168
+ it('produces no leak finding when growth is under threshold', async () => {
169
+ let callCount = 0;
170
+ currentMockPage.evaluate.mockImplementation(() => {
171
+ callCount++;
172
+ return callCount === 1 ? 5 * 1024 * 1024 : 6 * 1024 * 1024; // 1MB growth
173
+ });
174
+
175
+ const agent = createAgent();
176
+ const result = await agent.run();
177
+
178
+ const leakFindings = result.findings.filter(f => f.id.includes('leak'));
179
+ expect(leakFindings.length).toBe(0);
180
+ });
181
+
182
+ it('produces low finding when performance.memory API is unavailable', async () => {
183
+ currentMockPage.evaluate.mockResolvedValue(null);
184
+
185
+ const agent = createAgent();
186
+ const result = await agent.run();
187
+
188
+ const noApi = result.findings.find(f => f.id.includes('no-memory-api'));
189
+ expect(noApi).toBeDefined();
190
+ expect(noApi!.severity).toBe('low');
191
+ });
192
+
193
+ it('produces medium finding when login fails', async () => {
194
+ loginMock.mockRejectedValue(new Error('Invalid credentials'));
195
+
196
+ const agent = createAgent();
197
+ const result = await agent.run();
198
+
199
+ const loginFinding = result.findings.find(f => f.id.includes('login-failed'));
200
+ expect(loginFinding).toBeDefined();
201
+ expect(loginFinding!.severity).toBe('medium');
202
+ });
203
+
204
+ it('produces medium finding when Playwright crashes', async () => {
205
+ mockBrowser.newPage.mockRejectedValue(new Error('Browser crashed'));
206
+
207
+ const agent = createAgent();
208
+ const result = await agent.run();
209
+
210
+ const pwFinding = result.findings.find(f => f.id.includes('playwright-failure'));
211
+ expect(pwFinding).toBeDefined();
212
+ expect(pwFinding!.severity).toBe('medium');
213
+ });
214
+
215
+ it('closes browser even when an error occurs', async () => {
216
+ mockBrowser.newPage.mockRejectedValue(new Error('Launch error'));
217
+
218
+ const agent = createAgent();
219
+ await agent.run();
220
+
221
+ expect(mockBrowser.close).toHaveBeenCalled();
222
+ });
223
+
224
+ it('produces medium finding when navigation fails', async () => {
225
+ currentMockPage.goto.mockRejectedValue(new Error('Navigation failed'));
226
+
227
+ const agent = createAgent();
228
+ const result = await agent.run();
229
+
230
+ const navFinding = result.findings.find(f => f.id.includes('nav-failed'));
231
+ expect(navFinding).toBeDefined();
232
+ expect(navFinding!.severity).toBe('medium');
233
+ });
234
+
235
+ it('tests at most 5 modules', async () => {
236
+ const manyModules = Array.from({ length: 8 }, (_, i) => ({
237
+ id: `mod-${i}`, route: `/mod-${i}`, sidebarIcon: true,
238
+ }));
239
+ const config = makeConfig({ modules: manyModules });
240
+ const agent = createAgent(config);
241
+ await agent.run();
242
+
243
+ // Should navigate to at most 5 modules (initial + iterations)
244
+ // Each module: 1 initial + 2*10 iteration navigations = 21
245
+ // 5 modules * 21 = 105 max goto calls
246
+ // The key assertion is that not all 8 modules were tested
247
+ const gotoUrls = currentMockPage.goto.mock.calls.map((c: unknown[]) => c[0] as string);
248
+ const uniqueModules = new Set(gotoUrls.filter((u: string) => u.includes('/mod-')).map((u: string) => u.split('/mod-')[1]?.split('/')[0]));
249
+ expect(uniqueModules.size).toBeLessThanOrEqual(5);
250
+ });
251
+ });
@@ -0,0 +1,237 @@
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 helpers
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
+ },
17
+ modules: [],
18
+ environments: {
19
+ alpha: { baseUrl: 'http://localhost:3000', seed: false },
20
+ },
21
+ portals: [],
22
+ breakpoints: [],
23
+ integrations: [],
24
+ workers: [],
25
+ workflows: [],
26
+ accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
27
+ tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
28
+ costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
29
+ agentClassification: { blocking: [], advisory: [33] },
30
+ thresholds: {
31
+ unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
32
+ lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
33
+ apiResponseTime: 2000,
34
+ bundleSizeLimit: 500000,
35
+ maxConsoleErrors: 0,
36
+ },
37
+ disabledAgents: [],
38
+ disabledAgentSet: new Set<number>(),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Mock fs
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const mockStat = vi.fn();
48
+ const mockReaddir = vi.fn();
49
+
50
+ vi.mock('node:fs/promises', () => ({
51
+ stat: (...args: unknown[]) => mockStat(...args),
52
+ readdir: (...args: unknown[]) => mockReaddir(...args),
53
+ }));
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Dynamic import AFTER mocks
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const { BundleAnalyzerAgent } = await import('../33-bundle-analyzer');
60
+
61
+ function createAgent(
62
+ config: ValidatedConfig = makeConfig(),
63
+ phase: Phase = 'beta',
64
+ ): InstanceType<typeof BundleAnalyzerAgent> {
65
+ return new BundleAnalyzerAgent(config, phase, '/tmp/test-run');
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Tests
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe('BundleAnalyzerAgent', () => {
73
+ beforeEach(() => {
74
+ mockStat.mockReset();
75
+ mockReaddir.mockReset();
76
+ });
77
+
78
+ it('has agentId 33 and correct agentName', () => {
79
+ const agent = createAgent();
80
+ expect(agent.agentId).toBe(33);
81
+ expect(agent.agentName).toBe('Bundle Analyzer');
82
+ });
83
+
84
+ it('produces low finding when no assets directory exists', async () => {
85
+ mockStat.mockRejectedValue(new Error('ENOENT'));
86
+
87
+ const agent = createAgent();
88
+ const result = await agent.run();
89
+
90
+ const noDir = result.findings.find(f => f.id.includes('no-assets-dir'));
91
+ expect(noDir).toBeDefined();
92
+ expect(noDir!.severity).toBe('low');
93
+ });
94
+
95
+ it('flags JS chunks larger than 250KB', async () => {
96
+ mockStat.mockImplementation((path: string) => {
97
+ if (path.includes('assets') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
98
+ return Promise.resolve({ isDirectory: () => false, size: 300 * 1024 }); // 300KB
99
+ });
100
+ mockReaddir.mockResolvedValue([
101
+ { name: 'main.js', isFile: () => true },
102
+ ]);
103
+
104
+ const agent = createAgent();
105
+ const result = await agent.run();
106
+
107
+ const largeChunk = result.findings.find(f => f.id.includes('large-chunk'));
108
+ expect(largeChunk).toBeDefined();
109
+ expect(largeChunk!.severity).toBe('medium');
110
+ });
111
+
112
+ it('does not flag JS chunks under 250KB', async () => {
113
+ mockStat.mockImplementation((path: string) => {
114
+ if (path.includes('assets') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
115
+ return Promise.resolve({ isDirectory: () => false, size: 100 * 1024 }); // 100KB
116
+ });
117
+ mockReaddir.mockResolvedValue([
118
+ { name: 'vendor.js', isFile: () => true },
119
+ ]);
120
+
121
+ const agent = createAgent();
122
+ const result = await agent.run();
123
+
124
+ const largeChunk = result.findings.find(f => f.id.includes('large-chunk'));
125
+ expect(largeChunk).toBeUndefined();
126
+ });
127
+
128
+ it('flags source maps in production build', async () => {
129
+ mockStat.mockImplementation((path: string) => {
130
+ if (path.includes('assets') && !path.includes('.js') && !path.includes('.map')) return Promise.resolve({ isDirectory: () => true, size: 0 });
131
+ return Promise.resolve({ isDirectory: () => false, size: 50 * 1024 });
132
+ });
133
+ mockReaddir.mockResolvedValue([
134
+ { name: 'main.js', isFile: () => true },
135
+ { name: 'main.js.map', isFile: () => true },
136
+ ]);
137
+
138
+ const agent = createAgent();
139
+ const result = await agent.run();
140
+
141
+ const mapFinding = result.findings.find(f => f.id.includes('source-maps-in-production'));
142
+ expect(mapFinding).toBeDefined();
143
+ expect(mapFinding!.severity).toBe('medium');
144
+ });
145
+
146
+ it('does not flag when no source maps present', async () => {
147
+ mockStat.mockImplementation((path: string) => {
148
+ if (path.includes('assets') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
149
+ return Promise.resolve({ isDirectory: () => false, size: 50 * 1024 });
150
+ });
151
+ mockReaddir.mockResolvedValue([
152
+ { name: 'app.js', isFile: () => true },
153
+ ]);
154
+
155
+ const agent = createAgent();
156
+ const result = await agent.run();
157
+
158
+ const mapFinding = result.findings.find(f => f.id.includes('source-maps'));
159
+ expect(mapFinding).toBeUndefined();
160
+ });
161
+
162
+ it('records evidence with total bundle size', async () => {
163
+ mockStat.mockImplementation((path: string) => {
164
+ if (path.includes('assets') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
165
+ return Promise.resolve({ isDirectory: () => false, size: 200 * 1024 });
166
+ });
167
+ mockReaddir.mockResolvedValue([
168
+ { name: 'chunk1.js', isFile: () => true },
169
+ { name: 'chunk2.js', isFile: () => true },
170
+ ]);
171
+
172
+ const agent = createAgent();
173
+ const result = await agent.run();
174
+
175
+ expect(result.evidence.length).toBeGreaterThan(0);
176
+ expect(result.evidence[0].description).toContain('Total bundle size');
177
+ });
178
+
179
+ it('handles scan errors gracefully', async () => {
180
+ mockStat.mockImplementation((path: string) => {
181
+ if (path.includes('assets') && !path.includes('.')) return Promise.resolve({ isDirectory: () => true, size: 0 });
182
+ throw new Error('Permission denied');
183
+ });
184
+ mockReaddir.mockRejectedValue(new Error('EACCES'));
185
+
186
+ const agent = createAgent();
187
+ const result = await agent.run();
188
+
189
+ const scanError = result.findings.find(f => f.id.includes('scan-error'));
190
+ expect(scanError).toBeDefined();
191
+ expect(scanError!.severity).toBe('medium');
192
+ });
193
+
194
+ it('tries dist/assets first, then out/assets', async () => {
195
+ mockStat.mockImplementation((path: string) => {
196
+ if (path.includes('dist')) return Promise.reject(new Error('ENOENT'));
197
+ if (path.includes('out') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
198
+ return Promise.resolve({ isDirectory: () => false, size: 50 * 1024 });
199
+ });
200
+ mockReaddir.mockResolvedValue([
201
+ { name: 'app.js', isFile: () => true },
202
+ ]);
203
+
204
+ const agent = createAgent();
205
+ const result = await agent.run();
206
+
207
+ const noDir = result.findings.find(f => f.id.includes('no-assets-dir'));
208
+ expect(noDir).toBeUndefined();
209
+ });
210
+
211
+ it('uses projectRoot from config when available', async () => {
212
+ const config = makeConfig({ projectRoot: '/custom/project' });
213
+ mockStat.mockRejectedValue(new Error('ENOENT'));
214
+
215
+ const agent = createAgent(config);
216
+ await agent.run();
217
+
218
+ expect(mockStat).toHaveBeenCalledWith(expect.stringContaining('custom'));
219
+ });
220
+
221
+ it('skips non-file entries in directory listing', async () => {
222
+ mockStat.mockImplementation((path: string) => {
223
+ if (path.includes('assets') && !path.includes('.js')) return Promise.resolve({ isDirectory: () => true, size: 0 });
224
+ return Promise.resolve({ isDirectory: () => false, size: 50 * 1024 });
225
+ });
226
+ mockReaddir.mockResolvedValue([
227
+ { name: 'subdir', isFile: () => false },
228
+ { name: 'app.js', isFile: () => true },
229
+ ]);
230
+
231
+ const agent = createAgent();
232
+ const result = await agent.run();
233
+
234
+ // Should only have processed app.js, not subdir
235
+ expect(result.status).toBeDefined();
236
+ });
237
+ });