@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,790 @@
1
+ import { execFile } from 'child_process';
2
+ import path from 'path';
3
+ import type { Browser, Page } from 'playwright';
4
+ import type { Finding } from '../core/types';
5
+ import { BaseAgent } from './base-agent';
6
+ import { login, navigateToSection, waitForSectionLoad, switchPortal } from '../helpers/navigation';
7
+ import { captureScreenshot } from '../helpers/screenshot';
8
+ import { discoverInteractiveElements } from '../helpers/element-discovery';
9
+ import { fillForm } from '../helpers/form-filler';
10
+ import { openModal, closeModal } from '../helpers/modal-handler';
11
+ import { ApiClient } from '../helpers/api-client';
12
+ import { resolveEnvironment, isUrlReachable } from '../helpers/env-resolver';
13
+
14
+ function execFileAsync(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
15
+ return new Promise((resolve, reject) => {
16
+ execFile(file, args, { shell: true }, (error, stdout, stderr) => {
17
+ if (error) {
18
+ reject(error);
19
+ return;
20
+ }
21
+ resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
22
+ });
23
+ });
24
+ }
25
+
26
+ export class BrowserCrawlerAgent extends BaseAgent {
27
+ readonly agentId = 5;
28
+ readonly agentName = 'Browser Crawler';
29
+
30
+ protected async preFlight(): Promise<void> {
31
+ // Verify playwright is available
32
+ await execFileAsync('npx', ['playwright', '--version']);
33
+
34
+ // Verify base URL is reachable (with fallback)
35
+ const { env } = await resolveEnvironment(this.config, this.phase);
36
+
37
+ const reachable = await isUrlReachable(env.baseUrl);
38
+ if (!reachable) {
39
+ throw new Error(`Base URL is not reachable: ${env.baseUrl}`);
40
+ }
41
+ }
42
+
43
+ protected async execute(): Promise<Finding[]> {
44
+ if (this.phase === 'alpha') {
45
+ return this.executeSmoke();
46
+ }
47
+ if (this.phase === 'beta') {
48
+ return this.executeBeta();
49
+ }
50
+ if (this.phase === 'uat') {
51
+ return this.executeUat();
52
+ }
53
+ if (this.phase === 'release') {
54
+ return this.executeRelease();
55
+ }
56
+ throw new Error(`Phase "${this.phase}" is not implemented in BrowserCrawlerAgent yet`);
57
+ }
58
+
59
+ private async executeSmoke(): Promise<Finding[]> {
60
+ const findings: Finding[] = [];
61
+
62
+ const env = this.config.environments['alpha'] ?? Object.values(this.config.environments)[0];
63
+ if (!env) {
64
+ findings.push({
65
+ id: '5-no-env',
66
+ type: 'infra-issue',
67
+ severity: 'critical',
68
+ agentId: this.agentId,
69
+ module: 'browser-crawler',
70
+ description: 'No environment configured — cannot determine base URL',
71
+ });
72
+ return findings;
73
+ }
74
+
75
+ const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
76
+ const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${env.baseUrl}${rawLoginUrl}`;
77
+ const credentials = this.config.auth.credentials?.['admin'] ??
78
+ Object.values(this.config.auth.credentials ?? {})[0];
79
+
80
+ if (!credentials) {
81
+ findings.push({
82
+ id: '5-no-credentials',
83
+ type: 'infra-issue',
84
+ severity: 'critical',
85
+ agentId: this.agentId,
86
+ module: 'browser-crawler',
87
+ description: 'No credentials configured in auth.credentials — cannot log in',
88
+ });
89
+ return findings;
90
+ }
91
+
92
+ // Dynamic import keeps playwright optional at module load time
93
+ const { chromium } = await import('playwright');
94
+
95
+ let browser: Browser | null = null;
96
+ let page: Page | null = null;
97
+
98
+ try {
99
+ browser = await chromium.launch({ headless: true });
100
+ page = await browser.newPage();
101
+
102
+ try {
103
+ await login(page, credentials, loginUrl);
104
+ } catch (loginError) {
105
+ // In alpha, login failure is expected (API may not be running)
106
+ const loginSeverity = this.phase === 'alpha' ? 'medium' as const : 'critical' as const;
107
+ findings.push({
108
+ id: '5-login-failed',
109
+ type: 'infra-issue',
110
+ severity: loginSeverity,
111
+ agentId: this.agentId,
112
+ module: 'auth',
113
+ description: `Login failed (will crawl anonymously): ${loginError instanceof Error ? loginError.message.split('\n')[0] : String(loginError)}`,
114
+ });
115
+ // Navigate to baseUrl so anonymous crawling can proceed
116
+ await page.goto(env.baseUrl, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
117
+ }
118
+
119
+ for (const module of this.config.modules) {
120
+ const consoleErrors: string[] = [];
121
+ const networkFailures: string[] = [];
122
+
123
+ page.on('console', (msg) => {
124
+ if (msg.type() === 'error') {
125
+ consoleErrors.push(msg.text());
126
+ }
127
+ });
128
+
129
+ page.on('response', (response) => {
130
+ const status = response.status();
131
+ if (status >= 400) {
132
+ networkFailures.push(`${status} ${response.url()}`);
133
+ }
134
+ });
135
+
136
+ try {
137
+ await navigateToSection(page, module.id, this.config);
138
+ await waitForSectionLoad(page);
139
+ } catch (navErr) {
140
+ findings.push({
141
+ id: `5-nav-${module.id}`,
142
+ type: 'infra-issue',
143
+ severity: 'high',
144
+ agentId: this.agentId,
145
+ module: module.id,
146
+ description: `Navigation to module "${module.id}" failed: ${navErr instanceof Error ? navErr.message : String(navErr)}`,
147
+ });
148
+ continue;
149
+ }
150
+
151
+ const screenshotPath = await captureScreenshot(
152
+ page,
153
+ `smoke-${module.id}`,
154
+ this.runDir,
155
+ );
156
+
157
+ this.addEvidence({
158
+ type: 'screenshot',
159
+ path: screenshotPath,
160
+ description: `Smoke screenshot for module "${module.id}"`,
161
+ });
162
+
163
+ for (const errorMsg of consoleErrors) {
164
+ findings.push({
165
+ id: `5-console-${module.id}-${errorMsg.slice(0, 40).replace(/\s+/g, '-')}`,
166
+ type: 'code-bug-logic',
167
+ severity: 'high',
168
+ agentId: this.agentId,
169
+ module: module.id,
170
+ description: `Console error in module "${module.id}": ${errorMsg}`,
171
+ screenshot: screenshotPath,
172
+ consoleError: errorMsg,
173
+ });
174
+ }
175
+
176
+ for (const failure of networkFailures) {
177
+ findings.push({
178
+ id: `5-network-${module.id}-${failure.slice(0, 40).replace(/\s+/g, '-')}`,
179
+ type: 'infra-issue',
180
+ severity: 'high',
181
+ agentId: this.agentId,
182
+ module: module.id,
183
+ description: `Network failure in module "${module.id}": ${failure}`,
184
+ screenshot: screenshotPath,
185
+ });
186
+ }
187
+ }
188
+ } finally {
189
+ if (page) await page.close().catch(() => undefined);
190
+ if (browser) await browser.close().catch(() => undefined);
191
+ }
192
+
193
+ return findings;
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Release mode — lightweight post-deployment smoke test (no browser needed)
198
+ // ---------------------------------------------------------------------------
199
+
200
+ private async executeRelease(): Promise<Finding[]> {
201
+ const findings: Finding[] = [];
202
+
203
+ const env =
204
+ this.config.environments['release'] ??
205
+ this.config.environments['uat'] ??
206
+ this.config.environments['beta'] ??
207
+ this.config.environments['alpha'] ??
208
+ Object.values(this.config.environments)[0];
209
+
210
+ if (!env) {
211
+ findings.push({
212
+ id: '5-release-no-env',
213
+ type: 'infra-issue',
214
+ severity: 'critical',
215
+ agentId: this.agentId,
216
+ module: 'browser-crawler',
217
+ description: 'No environment configured — cannot determine base URL for release verification',
218
+ });
219
+ return findings;
220
+ }
221
+
222
+ // 1. Verify the production base URL is reachable and returns expected HTML
223
+ let baseHtml: string | null = null;
224
+ try {
225
+ const response = await fetch(env.baseUrl, { signal: AbortSignal.timeout(15_000) });
226
+ if (!response.ok) {
227
+ findings.push({
228
+ id: '5-release-base-status',
229
+ type: 'infra-issue',
230
+ severity: 'critical',
231
+ agentId: this.agentId,
232
+ module: 'browser-crawler',
233
+ description: `Production base URL returned HTTP ${response.status}: ${env.baseUrl}`,
234
+ });
235
+ } else {
236
+ baseHtml = await response.text();
237
+ }
238
+ } catch (err) {
239
+ findings.push({
240
+ id: '5-release-base-unreachable',
241
+ type: 'infra-issue',
242
+ severity: 'critical',
243
+ agentId: this.agentId,
244
+ module: 'browser-crawler',
245
+ description: `Production base URL is unreachable: ${env.baseUrl} — ${err instanceof Error ? err.message : String(err)}`,
246
+ });
247
+ return findings;
248
+ }
249
+
250
+ // 2. Validate HTML markers (title tag or meta charset) if we got a body
251
+ if (baseHtml !== null) {
252
+ const hasTitle = /<title[^>]*>.+<\/title>/i.test(baseHtml);
253
+ const hasMetaCharset = /<meta[^>]*charset/i.test(baseHtml);
254
+ const hasHtmlTag = /<html/i.test(baseHtml);
255
+
256
+ if (!hasTitle && !hasMetaCharset && !hasHtmlTag) {
257
+ findings.push({
258
+ id: '5-release-base-html-markers',
259
+ type: 'infra-issue',
260
+ severity: 'high',
261
+ agentId: this.agentId,
262
+ module: 'browser-crawler',
263
+ description: `Production base URL HTML is missing expected markers (no <title>, <meta charset>, or <html> tag): ${env.baseUrl}`,
264
+ });
265
+ }
266
+ }
267
+
268
+ // 3. Check each module route returns a non-error status code
269
+ for (const module of this.config.modules) {
270
+ const routeUrl = module.route.startsWith('http')
271
+ ? module.route
272
+ : `${env.baseUrl}${module.route}`;
273
+
274
+ try {
275
+ const routeResponse = await fetch(routeUrl, {
276
+ method: 'GET',
277
+ redirect: 'follow',
278
+ signal: AbortSignal.timeout(10_000),
279
+ });
280
+
281
+ if (routeResponse.status >= 400) {
282
+ findings.push({
283
+ id: `5-release-route-${module.id}`,
284
+ type: 'infra-issue',
285
+ severity: routeResponse.status >= 500 ? 'critical' : 'high',
286
+ agentId: this.agentId,
287
+ module: module.id,
288
+ description: `[Release] Route "${module.route}" returned HTTP ${routeResponse.status}: ${routeUrl}`,
289
+ });
290
+ }
291
+ } catch (routeErr) {
292
+ findings.push({
293
+ id: `5-release-route-${module.id}`,
294
+ type: 'infra-issue',
295
+ severity: 'critical',
296
+ agentId: this.agentId,
297
+ module: module.id,
298
+ description: `[Release] Route "${module.route}" is unreachable: ${routeUrl} — ${routeErr instanceof Error ? routeErr.message : String(routeErr)}`,
299
+ });
300
+ }
301
+ }
302
+
303
+ return findings;
304
+ }
305
+
306
+ private async executeBeta(): Promise<Finding[]> {
307
+ const findings: Finding[] = [];
308
+
309
+ const { env, fallback } = await resolveEnvironment(this.config, 'beta');
310
+
311
+ if (!env) {
312
+ findings.push({
313
+ id: '5-beta-no-env',
314
+ type: 'infra-issue',
315
+ severity: 'critical',
316
+ agentId: this.agentId,
317
+ module: 'browser-crawler',
318
+ description: 'No environment configured — cannot determine base URL for beta mode',
319
+ });
320
+ return findings;
321
+ }
322
+
323
+ const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
324
+ const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${env.baseUrl}${rawLoginUrl}`;
325
+ const credentials = this.config.auth.credentials?.['admin'] ??
326
+ Object.values(this.config.auth.credentials ?? {})[0];
327
+
328
+ if (!credentials) {
329
+ findings.push({
330
+ id: '5-beta-no-credentials',
331
+ type: 'infra-issue',
332
+ severity: 'critical',
333
+ agentId: this.agentId,
334
+ module: 'browser-crawler',
335
+ description: 'No credentials configured in auth.credentials — cannot log in for beta mode',
336
+ });
337
+ return findings;
338
+ }
339
+
340
+ const { chromium } = await import('playwright');
341
+
342
+ let browser: Browser | null = null;
343
+ let page: Page | null = null;
344
+ try {
345
+ browser = await chromium.launch({ headless: true });
346
+ page = await browser.newPage();
347
+
348
+ // Beta: single desktop breakpoint for performance — full breakpoint matrix is UAT-only
349
+ const breakpoints = [1280];
350
+
351
+ for (const breakpoint of breakpoints) {
352
+ await page.setViewportSize({ width: breakpoint, height: 800 });
353
+
354
+ // Bookkeeper portal pass
355
+ try {
356
+ await login(page, credentials, loginUrl);
357
+ } catch (loginError) {
358
+ const loginSeverity = fallback ? 'medium' as const : 'critical' as const;
359
+ findings.push({
360
+ id: '5-beta-login-failed',
361
+ type: 'infra-issue',
362
+ severity: loginSeverity,
363
+ agentId: this.agentId,
364
+ module: 'auth',
365
+ description: `Login failed (will crawl anonymously): ${loginError instanceof Error ? loginError.message.split('\n')[0] : String(loginError)}`,
366
+ });
367
+ // Navigate to baseUrl so anonymous crawling can proceed
368
+ await page.goto(env.baseUrl, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
369
+ }
370
+ // Beta: limit interaction pass to first 5 modules for performance
371
+ const originalModules = this.config.modules;
372
+ const betaModules = this.config.modules.slice(0, 5);
373
+ this.config.modules = betaModules;
374
+
375
+ const bookeeperFindings = await this.runInteractionPass(page, findings, `bp${breakpoint}`);
376
+ findings.push(...bookeeperFindings);
377
+
378
+ // Client portal pass — only if portals are configured with a queryParam
379
+ const clientPortals = this.config.portals.filter(p => p.queryParam !== null);
380
+ for (const portal of clientPortals) {
381
+ await switchPortal(page, portal.queryParam);
382
+ const portalFindings = await this.runInteractionPass(
383
+ page,
384
+ findings,
385
+ `portal-${portal.name}-bp${breakpoint}`,
386
+ );
387
+ findings.push(...portalFindings);
388
+ }
389
+
390
+ // Restore full module list
391
+ this.config.modules = originalModules;
392
+ }
393
+ } finally {
394
+ if (page) await page.close().catch(() => undefined);
395
+ if (browser) await browser.close().catch(() => undefined);
396
+ }
397
+
398
+ return findings;
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------
402
+ // UAT mode — exhaustive testing with snapshot/restore for destructive actions
403
+ // ---------------------------------------------------------------------------
404
+
405
+ private async executeUat(): Promise<Finding[]> {
406
+ const findings: Finding[] = [];
407
+
408
+ const { env, fallback } = await resolveEnvironment(this.config, 'uat');
409
+
410
+ if (!env) {
411
+ findings.push({
412
+ id: '5-uat-no-env',
413
+ type: 'infra-issue',
414
+ severity: 'critical',
415
+ agentId: this.agentId,
416
+ module: 'browser-crawler',
417
+ description: 'No environment configured — cannot determine base URL for UAT mode',
418
+ });
419
+ return findings;
420
+ }
421
+
422
+ // UAT credentials take priority; fall back to admin
423
+ const credentials =
424
+ this.config.auth.credentials?.['uat'] ??
425
+ this.config.auth.credentials?.['admin'] ??
426
+ Object.values(this.config.auth.credentials ?? {})[0];
427
+
428
+ if (!credentials) {
429
+ findings.push({
430
+ id: '5-uat-no-credentials',
431
+ type: 'infra-issue',
432
+ severity: 'critical',
433
+ agentId: this.agentId,
434
+ module: 'browser-crawler',
435
+ description: 'No UAT credentials configured in auth.credentials — cannot log in for UAT mode',
436
+ });
437
+ return findings;
438
+ }
439
+
440
+ const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
441
+ const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${env.baseUrl}${rawLoginUrl}`;
442
+
443
+ const { chromium } = await import('playwright');
444
+
445
+ let browser: Browser | null = null;
446
+ let page: Page | null = null;
447
+
448
+ // UAT: single pass at default 1280 breakpoint — exhaustive, not multi-breakpoint
449
+ try {
450
+ browser = await chromium.launch({ headless: true });
451
+ page = await browser.newPage();
452
+ await page.setViewportSize({ width: 1280, height: 800 });
453
+
454
+ try {
455
+ await login(page, credentials, loginUrl);
456
+ } catch (loginError) {
457
+ const loginSeverity = fallback ? 'medium' as const : 'critical' as const;
458
+ findings.push({
459
+ id: '5-uat-login-failed',
460
+ type: 'infra-issue',
461
+ severity: loginSeverity,
462
+ agentId: this.agentId,
463
+ module: 'auth',
464
+ description: `Login failed (will crawl anonymously): ${loginError instanceof Error ? loginError.message.split('\n')[0] : String(loginError)}`,
465
+ });
466
+ // Navigate to baseUrl so anonymous crawling can proceed
467
+ await page.goto(env.baseUrl, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
468
+ }
469
+
470
+ // Record video evidence path (Playwright video recording is configured via context;
471
+ // we record an evidence item pointing to where the video will be saved)
472
+ const videoPath = path.join(this.runDir, 'uat-session.webm');
473
+ this.addEvidence({
474
+ type: 'video',
475
+ path: videoPath,
476
+ description: 'UAT session recording — full exhaustive browser crawl',
477
+ });
478
+
479
+ // Non-destructive pass (same as beta runInteractionPass)
480
+ const nonDestructiveFindings = await this.runInteractionPass(page, findings, 'uat');
481
+ findings.push(...nonDestructiveFindings);
482
+
483
+ // Edge-case pass: test each module with special-character and boundary inputs
484
+ const edgeCaseFindings = await this.runUatEdgeCases(page, env.baseUrl);
485
+ findings.push(...edgeCaseFindings);
486
+
487
+ // Destructive action pass with snapshot/restore
488
+ const apiClient = await ApiClient.createAuthenticated(env.baseUrl, credentials).catch(
489
+ () => null,
490
+ );
491
+ const destructiveFindings = await this.runUatDestructivePass(page, apiClient);
492
+ findings.push(...destructiveFindings);
493
+ } finally {
494
+ if (page) await page.close().catch(() => undefined);
495
+ if (browser) await browser.close().catch(() => undefined);
496
+ }
497
+
498
+ return findings;
499
+ }
500
+
501
+ /**
502
+ * UAT edge-case pass: visit each module and attempt inputs with empty strings,
503
+ * unicode characters, emoji, and max-length strings to probe boundary handling.
504
+ */
505
+ private async runUatEdgeCases(page: Page, baseUrl: string): Promise<Finding[]> {
506
+ const findings: Finding[] = [];
507
+
508
+ const edgeCaseInputs = [
509
+ '', // empty field
510
+ '日本語テキスト', // unicode (Japanese)
511
+ '🎉🔥💡', // emoji
512
+ 'a'.repeat(1_000), // max-length (1 k chars — boundary test without DoS risk)
513
+ ];
514
+
515
+ for (const module of this.config.modules) {
516
+ const consoleErrors: string[] = [];
517
+ const consoleListener = (msg: { type(): string; text(): string }): void => {
518
+ if (msg.type() === 'error') consoleErrors.push(msg.text());
519
+ };
520
+ page.on('console', consoleListener);
521
+
522
+ try {
523
+ await navigateToSection(page, module.id, this.config);
524
+ await waitForSectionLoad(page);
525
+ } catch {
526
+ page.removeListener('console', consoleListener);
527
+ continue; // navigation failure already captured in non-destructive pass
528
+ }
529
+
530
+ for (const edgeValue of edgeCaseInputs) {
531
+ try {
532
+ await fillForm(page, 'form', { value: edgeValue });
533
+ } catch {
534
+ // form fill is best-effort
535
+ }
536
+ }
537
+
538
+ page.removeListener('console', consoleListener);
539
+
540
+ for (const errorMsg of consoleErrors) {
541
+ findings.push({
542
+ id: `5-uat-edge-${module.id}-${errorMsg.slice(0, 40).replace(/\s+/g, '-')}`,
543
+ type: 'code-bug-logic',
544
+ severity: 'medium',
545
+ agentId: this.agentId,
546
+ module: module.id,
547
+ description: `[UAT edge-case] Console error in module "${module.id}": ${errorMsg}`,
548
+ consoleError: errorMsg,
549
+ });
550
+ }
551
+
552
+ const screenshotPath = await captureScreenshot(
553
+ page,
554
+ `uat-edge-${module.id}`,
555
+ this.runDir,
556
+ );
557
+ this.addEvidence({
558
+ type: 'screenshot',
559
+ path: screenshotPath,
560
+ description: `[UAT edge-case] Screenshot after boundary inputs for module "${module.id}"`,
561
+ });
562
+
563
+ void baseUrl; // used only to satisfy linter — env already applied via navigateToSection
564
+ }
565
+
566
+ return findings;
567
+ }
568
+
569
+ /**
570
+ * UAT destructive pass: for each destructive element discovered, snapshot the entity
571
+ * state via API, execute the action, verify outcome, then restore via API.
572
+ * If restore fails → log critical finding and stop further destructive tests.
573
+ */
574
+ private async runUatDestructivePass(
575
+ page: Page,
576
+ apiClient: ApiClient | null,
577
+ ): Promise<Finding[]> {
578
+ const findings: Finding[] = [];
579
+ let stopDestructive = false;
580
+
581
+ for (const module of this.config.modules) {
582
+ if (stopDestructive) break;
583
+
584
+ try {
585
+ await navigateToSection(page, module.id, this.config);
586
+ await waitForSectionLoad(page);
587
+ } catch {
588
+ continue;
589
+ }
590
+
591
+ const elements = await discoverInteractiveElements(page);
592
+ const destructiveElements = elements.filter(e => e.isDestructive);
593
+
594
+ for (const element of destructiveElements) {
595
+ if (stopDestructive) break;
596
+
597
+ // 1. Snapshot entity state via API before destructive action
598
+ let snapshot: unknown = null;
599
+ if (apiClient) {
600
+ try {
601
+ const snapshotRes = await apiClient.get(`/api/${module.id}`);
602
+ snapshot = snapshotRes.body;
603
+ } catch {
604
+ snapshot = null;
605
+ }
606
+ }
607
+
608
+ this.addEvidence({
609
+ type: 'snapshot',
610
+ path: path.join(this.runDir, `uat-snapshot-${module.id}-${element.selector.replace(/[^a-z0-9]/gi, '-')}.json`),
611
+ description: `[UAT destructive] Pre-action snapshot for "${element.text ?? element.selector}" in module "${module.id}"`,
612
+ });
613
+
614
+ // 2. Execute destructive action
615
+ let actionError: string | undefined;
616
+ try {
617
+ if (element.type === 'button' || element.type === 'link') {
618
+ const modalOpened = await openModal(page, element.selector);
619
+ if (modalOpened) {
620
+ // Close modal instead of confirming — safer in UAT
621
+ await closeModal(page, 'escape');
622
+ }
623
+ }
624
+ } catch (err) {
625
+ actionError = err instanceof Error ? err.message : String(err);
626
+ }
627
+
628
+ const screenshotPath = await captureScreenshot(
629
+ page,
630
+ `uat-destructive-${module.id}-${element.selector.slice(0, 30).replace(/[^a-z0-9]/gi, '-')}`,
631
+ this.runDir,
632
+ );
633
+ this.addEvidence({
634
+ type: 'screenshot',
635
+ path: screenshotPath,
636
+ description: `[UAT destructive] Post-action screenshot for "${element.text ?? element.selector}"`,
637
+ });
638
+
639
+ if (actionError) {
640
+ findings.push({
641
+ id: `5-uat-destructive-${module.id}-${element.selector.slice(0, 30).replace(/[^a-z0-9]/gi, '-')}`,
642
+ type: 'test-bug',
643
+ severity: 'medium',
644
+ agentId: this.agentId,
645
+ module: module.id,
646
+ description: `[UAT destructive] Action failed for "${element.text ?? element.selector}": ${actionError}`,
647
+ screenshot: screenshotPath,
648
+ });
649
+ }
650
+
651
+ // 3. Restore entity via API using snapshot
652
+ if (apiClient && snapshot !== null) {
653
+ try {
654
+ await apiClient.put(`/api/${module.id}/restore`, { snapshot });
655
+ } catch (restoreErr) {
656
+ const restoreMsg = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
657
+ findings.push({
658
+ id: `5-uat-restore-failed-${module.id}`,
659
+ type: 'infra-issue',
660
+ severity: 'critical',
661
+ agentId: this.agentId,
662
+ module: module.id,
663
+ description: `[UAT destructive] Restore failed for module "${module.id}" after destructive action — stopping destructive tests. Error: ${restoreMsg}`,
664
+ });
665
+ stopDestructive = true;
666
+ }
667
+ }
668
+ }
669
+ }
670
+
671
+ return findings;
672
+ }
673
+
674
+ /**
675
+ * For each module: navigate, discover interactive elements, interact with
676
+ * non-destructive ones, fill + submit forms, open/close modals.
677
+ * Returns findings discovered in this pass; does NOT push to the outer array directly.
678
+ */
679
+ private async runInteractionPass(
680
+ page: Page,
681
+ _existingFindings: Finding[],
682
+ passLabel: string,
683
+ ): Promise<Finding[]> {
684
+ const findings: Finding[] = [];
685
+
686
+ for (const module of this.config.modules) {
687
+ // Register console listener BEFORE navigation so errors during page load are captured
688
+ const consoleErrors: string[] = [];
689
+ const consoleListener = (msg: { type(): string; text(): string }): void => {
690
+ if (msg.type() === 'error') {
691
+ consoleErrors.push(msg.text());
692
+ }
693
+ };
694
+ page.on('console', consoleListener);
695
+
696
+ try {
697
+ await Promise.race([
698
+ navigateToSection(page, module.id, this.config),
699
+ new Promise<never>((_, reject) =>
700
+ setTimeout(() => reject(new Error('Navigation timed out after 5s')), 5_000),
701
+ ),
702
+ ]);
703
+ await waitForSectionLoad(page);
704
+ } catch (navErr) {
705
+ page.removeListener('console', consoleListener);
706
+ const isTimeout = navErr instanceof Error && navErr.message.includes('timed out after 5s');
707
+ findings.push({
708
+ id: `5-beta-nav-${passLabel}-${module.id}`,
709
+ type: 'infra-issue',
710
+ severity: isTimeout ? 'medium' : 'high',
711
+ agentId: this.agentId,
712
+ module: module.id,
713
+ description: isTimeout
714
+ ? `[Beta] Slow page: navigation to module "${module.id}" exceeded 5s timeout`
715
+ : `[Beta] Navigation to module "${module.id}" failed: ${navErr instanceof Error ? navErr.message : String(navErr)}`,
716
+ });
717
+ continue;
718
+ }
719
+
720
+ const screenshotBefore = await captureScreenshot(
721
+ page,
722
+ `beta-${passLabel}-${module.id}-before`,
723
+ this.runDir,
724
+ );
725
+ this.addEvidence({
726
+ type: 'screenshot',
727
+ path: screenshotBefore,
728
+ description: `[Beta] Before-interaction screenshot for module "${module.id}" (${passLabel})`,
729
+ });
730
+
731
+ const elements = await discoverInteractiveElements(page);
732
+
733
+ for (const element of elements) {
734
+ if (element.isDestructive) {
735
+ // Skip destructive elements — never click delete/remove/archive/etc.
736
+ continue;
737
+ }
738
+
739
+ if (element.type === 'button' || element.type === 'link' ||
740
+ element.type === 'tab' || element.type === 'menuitem') {
741
+ try {
742
+ // Check for modal-opening elements first
743
+ const modalOpened = await openModal(page, element.selector);
744
+ if (modalOpened) {
745
+ await closeModal(page, 'escape');
746
+ } else {
747
+ await page.waitForTimeout(1000);
748
+ }
749
+ } catch {
750
+ // Element may have been detached; continue
751
+ }
752
+ } else if (element.type === 'input' || element.type === 'textarea' || element.type === 'select') {
753
+ try {
754
+ await fillForm(page, 'form', undefined);
755
+ } catch {
756
+ // Form fill is best-effort
757
+ }
758
+ }
759
+ }
760
+
761
+ // Flush accumulated console errors
762
+ page.removeListener('console', consoleListener);
763
+
764
+ for (const errorMsg of consoleErrors) {
765
+ findings.push({
766
+ id: `5-beta-console-${passLabel}-${module.id}-${errorMsg.slice(0, 40).replace(/\s+/g, '-')}`,
767
+ type: 'code-bug-logic',
768
+ severity: 'high',
769
+ agentId: this.agentId,
770
+ module: module.id,
771
+ description: `[Beta] Console error in module "${module.id}" (${passLabel}): ${errorMsg}`,
772
+ consoleError: errorMsg,
773
+ });
774
+ }
775
+
776
+ const screenshotAfter = await captureScreenshot(
777
+ page,
778
+ `beta-${passLabel}-${module.id}-after`,
779
+ this.runDir,
780
+ );
781
+ this.addEvidence({
782
+ type: 'screenshot',
783
+ path: screenshotAfter,
784
+ description: `[Beta] After-interaction screenshot for module "${module.id}" (${passLabel})`,
785
+ });
786
+ }
787
+
788
+ return findings;
789
+ }
790
+ }