@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,657 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { RunState, AgentResult, Finding, FindingSeverity, Phase, PhaseGateResult } from './types';
4
+
5
+ /** Overall verdict derived from gate results. */
6
+ type Verdict = 'PASS' | 'CONDITIONAL' | 'FAIL';
7
+
8
+ /** Determine the overall verdict from phase gate results. */
9
+ function determineVerdict(gateResults: PhaseGateResult[]): Verdict {
10
+ const hasBlockingFailures = gateResults.some(
11
+ (g) => g.failedBlockingAgents.length > 0,
12
+ );
13
+ if (hasBlockingFailures) return 'FAIL';
14
+
15
+ const hasAdvisoryFailures = gateResults.some((g) =>
16
+ g.advisoryResults.some((r) => r.status === 'failed'),
17
+ );
18
+ if (hasAdvisoryFailures) return 'CONDITIONAL';
19
+
20
+ return 'PASS';
21
+ }
22
+
23
+ /** Format milliseconds into a human-readable duration string. */
24
+ function formatDuration(ms: number): string {
25
+ if (ms < 1000) return `${ms}ms`;
26
+ const seconds = Math.floor(ms / 1000);
27
+ if (seconds < 60) return `${seconds}s`;
28
+ const minutes = Math.floor(seconds / 60);
29
+ const remainingSeconds = seconds % 60;
30
+ return `${minutes}m ${remainingSeconds}s`;
31
+ }
32
+
33
+ /** Compute total duration from all agent results. */
34
+ function computeTotalDuration(agentResults: Map<number, AgentResult>): number {
35
+ let total = 0;
36
+ for (const result of agentResults.values()) {
37
+ total += result.durationMs;
38
+ }
39
+ return total;
40
+ }
41
+
42
+ /** Escape HTML special characters. */
43
+ function escapeHtml(text: string): string {
44
+ return text
45
+ .replace(/&/g, '&amp;')
46
+ .replace(/</g, '&lt;')
47
+ .replace(/>/g, '&gt;')
48
+ .replace(/"/g, '&quot;')
49
+ .replace(/'/g, '&#039;');
50
+ }
51
+
52
+ /** Map verdict to CSS class suffix. */
53
+ function verdictClass(verdict: Verdict): string {
54
+ switch (verdict) {
55
+ case 'PASS': return 'pass';
56
+ case 'CONDITIONAL': return 'warn';
57
+ case 'FAIL': return 'fail';
58
+ }
59
+ }
60
+
61
+ /** Map agent status to CSS class suffix. */
62
+ function statusClass(status: AgentResult['status']): string {
63
+ switch (status) {
64
+ case 'passed': return 'pass';
65
+ case 'failed': return 'fail';
66
+ case 'skipped': return 'skip';
67
+ case 'running': return 'warn';
68
+ case 'pending': return 'skip';
69
+ }
70
+ }
71
+
72
+ /** Map severity to CSS class suffix. */
73
+ function severityClass(severity: FindingSeverity): string {
74
+ switch (severity) {
75
+ case 'critical': return 'fail';
76
+ case 'high': return 'fail';
77
+ case 'medium': return 'warn';
78
+ case 'low': return 'pass';
79
+ case 'info': return 'info';
80
+ }
81
+ }
82
+
83
+ /** Map phase gate passed boolean to CSS class suffix. */
84
+ function phaseClass(gate: PhaseGateResult | undefined): string {
85
+ if (!gate) return 'skip';
86
+ if (!gate.passed) return 'fail';
87
+ const hasAdvisoryFail = gate.advisoryResults.some((r) => r.status === 'failed');
88
+ if (hasAdvisoryFail) return 'warn';
89
+ return 'pass';
90
+ }
91
+
92
+ /** Ordered severity levels for grouping findings. */
93
+ const SEVERITY_ORDER: readonly FindingSeverity[] = ['critical', 'high', 'medium', 'low', 'info'] as const;
94
+
95
+ /** The four phases in pipeline order. */
96
+ const PHASE_ORDER: readonly Phase[] = ['alpha', 'beta', 'uat', 'release'] as const;
97
+
98
+ /** Determine the agent tier label based on agentId convention. */
99
+ function agentTier(agentId: number): string {
100
+ if (agentId <= 3) return 'core';
101
+ if (agentId <= 9) return 'specialist';
102
+ if (agentId <= 15) return 'advanced';
103
+ return 'extended';
104
+ }
105
+
106
+ function buildCss(): string {
107
+ return `
108
+ :root {
109
+ --bg: #1a1b26;
110
+ --bg-surface: #24283b;
111
+ --bg-surface2: #2f3349;
112
+ --text: #c0caf5;
113
+ --text-muted: #565f89;
114
+ --border: #3b4261;
115
+ --pass: #9ece6a;
116
+ --warn: #e0af68;
117
+ --fail: #f7768e;
118
+ --info: #7aa2f7;
119
+ --skip: #565f89;
120
+ }
121
+
122
+ * { margin: 0; padding: 0; box-sizing: border-box; }
123
+
124
+ body {
125
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
126
+ background: var(--bg);
127
+ color: var(--text);
128
+ line-height: 1.6;
129
+ padding: 2rem;
130
+ max-width: 1200px;
131
+ margin: 0 auto;
132
+ }
133
+
134
+ h1, h2, h3 { font-weight: 600; }
135
+ h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
136
+ h2 { font-size: 1.3rem; margin-bottom: 1rem; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
137
+
138
+ .header {
139
+ background: var(--bg-surface);
140
+ border-radius: 8px;
141
+ padding: 1.5rem 2rem;
142
+ margin-bottom: 2rem;
143
+ border: 1px solid var(--border);
144
+ }
145
+
146
+ .header-meta {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
149
+ gap: 0.75rem;
150
+ margin-top: 1rem;
151
+ }
152
+
153
+ .header-meta dt { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
154
+ .header-meta dd { font-size: 1rem; font-weight: 500; }
155
+
156
+ .badge {
157
+ display: inline-block;
158
+ padding: 0.15rem 0.6rem;
159
+ border-radius: 4px;
160
+ font-size: 0.8rem;
161
+ font-weight: 600;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.03em;
164
+ cursor: default;
165
+ }
166
+ .badge-pass { background: color-mix(in srgb, var(--pass) 20%, transparent); color: var(--pass); }
167
+ .badge-warn { background: color-mix(in srgb, var(--warn) 20%, transparent); color: var(--warn); }
168
+ .badge-fail { background: color-mix(in srgb, var(--fail) 20%, transparent); color: var(--fail); }
169
+ .badge-skip { background: color-mix(in srgb, var(--skip) 20%, transparent); color: var(--skip); }
170
+ .badge-info { background: color-mix(in srgb, var(--info) 20%, transparent); color: var(--info); }
171
+
172
+ /* Severity filter badges */
173
+ .filter-badge { cursor: pointer; user-select: none; }
174
+ .filter-badge:hover { opacity: 0.8; }
175
+ .filter-badge.active { outline: 2px solid currentColor; outline-offset: 2px; }
176
+
177
+ .section {
178
+ background: var(--bg-surface);
179
+ border-radius: 8px;
180
+ padding: 1.5rem 2rem;
181
+ margin-bottom: 2rem;
182
+ border: 1px solid var(--border);
183
+ }
184
+
185
+ /* Phase gate pipeline */
186
+ .pipeline {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ gap: 0;
191
+ flex-wrap: wrap;
192
+ margin-top: 1rem;
193
+ }
194
+
195
+ .phase-node {
196
+ display: flex;
197
+ flex-direction: column;
198
+ align-items: center;
199
+ gap: 0.5rem;
200
+ }
201
+
202
+ .phase-circle {
203
+ width: 48px;
204
+ height: 48px;
205
+ border-radius: 50%;
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ font-weight: 700;
210
+ font-size: 0.7rem;
211
+ text-transform: uppercase;
212
+ }
213
+ .phase-circle-pass { background: var(--pass); color: var(--bg); }
214
+ .phase-circle-warn { background: var(--warn); color: var(--bg); }
215
+ .phase-circle-fail { background: var(--fail); color: var(--bg); }
216
+ .phase-circle-skip { background: var(--bg-surface2); color: var(--text-muted); border: 2px dashed var(--border); }
217
+
218
+ .phase-label {
219
+ font-size: 0.75rem;
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.05em;
222
+ color: var(--text-muted);
223
+ }
224
+
225
+ .phase-arrow {
226
+ width: 40px;
227
+ height: 2px;
228
+ background: var(--border);
229
+ margin: 0 0.25rem;
230
+ margin-bottom: 1.5rem;
231
+ }
232
+
233
+ /* Table */
234
+ table {
235
+ width: 100%;
236
+ border-collapse: collapse;
237
+ font-size: 0.9rem;
238
+ }
239
+ thead th {
240
+ text-align: left;
241
+ padding: 0.75rem;
242
+ border-bottom: 2px solid var(--border);
243
+ font-size: 0.75rem;
244
+ text-transform: uppercase;
245
+ letter-spacing: 0.05em;
246
+ color: var(--text-muted);
247
+ }
248
+ tbody td {
249
+ padding: 0.6rem 0.75rem;
250
+ border-bottom: 1px solid var(--border);
251
+ vertical-align: top;
252
+ }
253
+ tbody tr:hover { background: var(--bg-surface2); }
254
+
255
+ /* Collapsible sections — pure CSS */
256
+ .collapsible > input[type="checkbox"] { display: none; }
257
+ .collapsible > label {
258
+ display: block;
259
+ cursor: pointer;
260
+ user-select: none;
261
+ padding: 0.5rem 0;
262
+ font-weight: 600;
263
+ }
264
+ .collapsible > label::before {
265
+ content: '\\25B6';
266
+ display: inline-block;
267
+ margin-right: 0.5rem;
268
+ font-size: 0.7rem;
269
+ transition: transform 0.15s;
270
+ }
271
+ .collapsible > input:checked + label::before {
272
+ transform: rotate(90deg);
273
+ }
274
+ .collapsible > .content {
275
+ max-height: 0;
276
+ overflow: hidden;
277
+ transition: max-height 0.2s ease-out;
278
+ }
279
+ .collapsible > input:checked ~ .content {
280
+ max-height: 5000px;
281
+ }
282
+
283
+ /* Agent detail rows */
284
+ .agent-detail {
285
+ background: var(--bg);
286
+ border-radius: 4px;
287
+ padding: 0.75rem;
288
+ margin: 0.5rem 0;
289
+ font-size: 0.85rem;
290
+ }
291
+ .agent-detail p { margin-bottom: 0.25rem; }
292
+
293
+ /* Findings */
294
+ .finding-card {
295
+ background: var(--bg);
296
+ border-radius: 6px;
297
+ padding: 1rem;
298
+ margin-bottom: 0.75rem;
299
+ border-left: 3px solid var(--border);
300
+ }
301
+ .finding-card-critical { border-left-color: var(--fail); }
302
+ .finding-card-high { border-left-color: var(--fail); }
303
+ .finding-card-medium { border-left-color: var(--warn); }
304
+ .finding-card-low { border-left-color: var(--pass); }
305
+ .finding-card-info { border-left-color: var(--info); }
306
+
307
+ .finding-header {
308
+ display: flex;
309
+ align-items: center;
310
+ gap: 0.5rem;
311
+ margin-bottom: 0.5rem;
312
+ }
313
+ .finding-desc { font-size: 0.9rem; margin-bottom: 0.25rem; }
314
+ .finding-meta { font-size: 0.8rem; color: var(--text-muted); }
315
+
316
+ .filter-bar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
317
+
318
+ /* Print styles */
319
+ @media print {
320
+ body { background: #fff; color: #222; padding: 1rem; }
321
+ .section, .header { background: #f9f9f9; border-color: #ddd; }
322
+ .badge-pass { background: #d4edda; color: #155724; }
323
+ .badge-warn { background: #fff3cd; color: #856404; }
324
+ .badge-fail { background: #f8d7da; color: #721c24; }
325
+ .badge-info { background: #d1ecf1; color: #0c5460; }
326
+ .collapsible > .content { max-height: none !important; }
327
+ }
328
+
329
+ .empty-state {
330
+ text-align: center;
331
+ padding: 2rem;
332
+ color: var(--text-muted);
333
+ font-style: italic;
334
+ }
335
+ `;
336
+ }
337
+
338
+ function buildJs(): string {
339
+ return `
340
+ document.addEventListener('DOMContentLoaded', function() {
341
+ var filterBadges = document.querySelectorAll('.filter-badge');
342
+ var findingCards = document.querySelectorAll('.finding-card');
343
+ var activeFilters = new Set();
344
+
345
+ filterBadges.forEach(function(badge) {
346
+ badge.addEventListener('click', function() {
347
+ var severity = this.getAttribute('data-severity');
348
+ if (!severity) return;
349
+
350
+ if (activeFilters.has(severity)) {
351
+ activeFilters.delete(severity);
352
+ this.classList.remove('active');
353
+ } else {
354
+ activeFilters.add(severity);
355
+ this.classList.add('active');
356
+ }
357
+
358
+ findingCards.forEach(function(card) {
359
+ var cardSev = card.getAttribute('data-severity');
360
+ if (activeFilters.size === 0) {
361
+ card.style.display = '';
362
+ } else {
363
+ card.style.display = activeFilters.has(cardSev) ? '' : 'none';
364
+ }
365
+ });
366
+ });
367
+ });
368
+
369
+ // Expandable agent rows
370
+ var expandBtns = document.querySelectorAll('.expand-agent');
371
+ expandBtns.forEach(function(btn) {
372
+ btn.addEventListener('click', function() {
373
+ var targetId = this.getAttribute('data-target');
374
+ if (!targetId) return;
375
+ var detail = document.getElementById(targetId);
376
+ if (!detail) return;
377
+ var isHidden = detail.style.display === 'none' || detail.style.display === '';
378
+ detail.style.display = isHidden ? 'table-row' : 'none';
379
+ this.textContent = isHidden ? '\\u25BC' : '\\u25B6';
380
+ });
381
+ });
382
+ });
383
+ `;
384
+ }
385
+
386
+ /** Build the phase gate pipeline visualization. */
387
+ function buildPipelineHtml(gateResults: PhaseGateResult[]): string {
388
+ const gateMap = new Map<Phase, PhaseGateResult>();
389
+ for (const gate of gateResults) {
390
+ gateMap.set(gate.phase, gate);
391
+ }
392
+
393
+ const nodes: string[] = [];
394
+ for (let i = 0; i < PHASE_ORDER.length; i++) {
395
+ const phase = PHASE_ORDER[i];
396
+ const gate = gateMap.get(phase);
397
+ const cls = phaseClass(gate);
398
+ const icon = cls === 'pass' ? '&#10003;' : cls === 'fail' ? '&#10007;' : cls === 'warn' ? '!' : '&#8212;';
399
+
400
+ nodes.push(`
401
+ <div class="phase-node">
402
+ <div class="phase-circle phase-circle-${cls}">${icon}</div>
403
+ <span class="phase-label">${escapeHtml(phase)}</span>
404
+ </div>
405
+ `);
406
+
407
+ if (i < PHASE_ORDER.length - 1) {
408
+ nodes.push('<div class="phase-arrow"></div>');
409
+ }
410
+ }
411
+
412
+ return `<div class="pipeline">${nodes.join('')}</div>`;
413
+ }
414
+
415
+ /** Build the agent results table. */
416
+ function buildAgentTableHtml(agentResults: Map<number, AgentResult>): string {
417
+ const sorted = Array.from(agentResults.values()).sort((a, b) => a.agentId - b.agentId);
418
+
419
+ if (sorted.length === 0) {
420
+ return '<p class="empty-state">No agent results recorded.</p>';
421
+ }
422
+
423
+ const rows: string[] = [];
424
+ for (const result of sorted) {
425
+ const cls = statusClass(result.status);
426
+ const detailId = `agent-detail-${result.agentId}`;
427
+ rows.push(`
428
+ <tr>
429
+ <td><button class="expand-agent badge badge-info" data-target="${detailId}" style="cursor:pointer;border:none;min-width:1.5rem;">&#9654;</button></td>
430
+ <td>${result.agentId}</td>
431
+ <td>${escapeHtml(result.agentName)}</td>
432
+ <td>${escapeHtml(result.phase)}</td>
433
+ <td><span class="badge badge-${cls}">${escapeHtml(result.status)}</span></td>
434
+ <td>${formatDuration(result.durationMs)}</td>
435
+ <td>${result.findings.length}</td>
436
+ <td>${agentTier(result.agentId)}</td>
437
+ </tr>
438
+ <tr id="${detailId}" style="display:none;">
439
+ <td colspan="8">
440
+ <div class="agent-detail">
441
+ <p><strong>Summary:</strong> ${escapeHtml(result.summary)}</p>
442
+ ${result.findings.length > 0
443
+ ? `<p><strong>Findings:</strong></p>
444
+ <ul>${result.findings.map((f) => `<li><span class="badge badge-${severityClass(f.severity)}">${escapeHtml(f.severity)}</span> ${escapeHtml(f.module)}: ${escapeHtml(f.description)}</li>`).join('')}</ul>`
445
+ : '<p>No findings.</p>'}
446
+ </div>
447
+ </td>
448
+ </tr>
449
+ `);
450
+ }
451
+
452
+ return `
453
+ <table>
454
+ <thead>
455
+ <tr>
456
+ <th></th>
457
+ <th>#</th>
458
+ <th>Name</th>
459
+ <th>Phase</th>
460
+ <th>Status</th>
461
+ <th>Duration</th>
462
+ <th>Findings</th>
463
+ <th>Tier</th>
464
+ </tr>
465
+ </thead>
466
+ <tbody>
467
+ ${rows.join('')}
468
+ </tbody>
469
+ </table>
470
+ `;
471
+ }
472
+
473
+ /** Build the findings section grouped by severity. */
474
+ function buildFindingsHtml(agentResults: Map<number, AgentResult>): string {
475
+ const allFindings: Array<Finding & { agentName: string }> = [];
476
+ for (const result of agentResults.values()) {
477
+ for (const finding of result.findings) {
478
+ allFindings.push({ ...finding, agentName: result.agentName });
479
+ }
480
+ }
481
+
482
+ if (allFindings.length === 0) {
483
+ return '<p class="empty-state">No findings — all clear.</p>';
484
+ }
485
+
486
+ // Filter bar
487
+ const severityCounts = new Map<FindingSeverity, number>();
488
+ for (const f of allFindings) {
489
+ severityCounts.set(f.severity, (severityCounts.get(f.severity) ?? 0) + 1);
490
+ }
491
+
492
+ const filterBar = `<div class="filter-bar">${
493
+ SEVERITY_ORDER.map((sev) => {
494
+ const count = severityCounts.get(sev) ?? 0;
495
+ if (count === 0) return '';
496
+ return `<span class="badge badge-${severityClass(sev)} filter-badge" data-severity="${sev}">${sev} (${count})</span>`;
497
+ }).join('')
498
+ }</div>`;
499
+
500
+ // Group by severity
501
+ const groups: string[] = [];
502
+ for (const severity of SEVERITY_ORDER) {
503
+ const group = allFindings.filter((f) => f.severity === severity);
504
+ if (group.length === 0) continue;
505
+
506
+ const cards = group.map((f) => {
507
+ const location = f.file
508
+ ? `${escapeHtml(f.file)}${f.line ? `:${f.line}` : ''}`
509
+ : '';
510
+ const evidenceLink = f.screenshot
511
+ ? `<span class="finding-meta">Evidence: ${escapeHtml(f.screenshot)}</span>`
512
+ : '';
513
+
514
+ return `
515
+ <div class="finding-card finding-card-${severity}" data-severity="${severity}">
516
+ <div class="finding-header">
517
+ <span class="badge badge-${severityClass(severity)}">${escapeHtml(severity)}</span>
518
+ <span style="font-weight:600;">${escapeHtml(f.module)}</span>
519
+ <span class="finding-meta">Agent: ${escapeHtml(f.agentName)} (#${f.agentId})</span>
520
+ </div>
521
+ <p class="finding-desc">${escapeHtml(f.description)}</p>
522
+ ${location ? `<p class="finding-meta">Location: ${location}</p>` : ''}
523
+ ${evidenceLink}
524
+ </div>
525
+ `;
526
+ }).join('');
527
+
528
+ const sectionId = `findings-${severity}`;
529
+ groups.push(`
530
+ <div class="collapsible">
531
+ <input type="checkbox" id="${sectionId}" checked>
532
+ <label for="${sectionId}">${severity.toUpperCase()} (${group.length})</label>
533
+ <div class="content">${cards}</div>
534
+ </div>
535
+ `);
536
+ }
537
+
538
+ return filterBar + groups.join('');
539
+ }
540
+
541
+ /** Build an optional coverage summary section. */
542
+ function buildCoverageHtml(agentResults: Map<number, AgentResult>): string {
543
+ // Extract coverage data from known specialist agents if present
544
+ const coverageAgent = agentResults.get(4); // Coverage agent
545
+ const securityAgent = agentResults.get(7); // Security agent
546
+ const a11yAgent = agentResults.get(8); // Accessibility agent
547
+
548
+ const items: string[] = [];
549
+
550
+ if (coverageAgent) {
551
+ items.push(`
552
+ <div>
553
+ <dt>Unit Coverage</dt>
554
+ <dd>${escapeHtml(coverageAgent.summary)}</dd>
555
+ </div>
556
+ `);
557
+ }
558
+
559
+ if (securityAgent) {
560
+ items.push(`
561
+ <div>
562
+ <dt>Security</dt>
563
+ <dd><span class="badge badge-${statusClass(securityAgent.status)}">${escapeHtml(securityAgent.status)}</span> ${escapeHtml(securityAgent.summary)}</dd>
564
+ </div>
565
+ `);
566
+ }
567
+
568
+ if (a11yAgent) {
569
+ items.push(`
570
+ <div>
571
+ <dt>Accessibility</dt>
572
+ <dd><span class="badge badge-${statusClass(a11yAgent.status)}">${escapeHtml(a11yAgent.status)}</span> ${escapeHtml(a11yAgent.summary)}</dd>
573
+ </div>
574
+ `);
575
+ }
576
+
577
+ if (items.length === 0) {
578
+ return '<p class="empty-state">No coverage data available.</p>';
579
+ }
580
+
581
+ return `<dl class="header-meta">${items.join('')}</dl>`;
582
+ }
583
+
584
+ /**
585
+ * Generates a self-contained HTML report from test run results.
586
+ *
587
+ * The output file contains all CSS and JS inline — no external dependencies.
588
+ * It can be opened directly from the filesystem in any modern browser.
589
+ */
590
+ export function generateHTMLReport(state: RunState, outputPath: string): void {
591
+ const verdict = determineVerdict(state.gateResults);
592
+ const totalDuration = computeTotalDuration(state.agentResults);
593
+ const commitHash = process.env['GIT_COMMIT'] ?? process.env['GITHUB_SHA'] ?? 'unknown';
594
+
595
+ const html = `<!DOCTYPE html>
596
+ <html lang="en">
597
+ <head>
598
+ <meta charset="UTF-8">
599
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
600
+ <title>TestTeam Report — ${escapeHtml(state.runId)}</title>
601
+ <style>${buildCss()}</style>
602
+ </head>
603
+ <body>
604
+ <div class="header">
605
+ <h1>TestTeam Report</h1>
606
+ <dl class="header-meta">
607
+ <div>
608
+ <dt>Run ID</dt>
609
+ <dd>${escapeHtml(state.runId)}</dd>
610
+ </div>
611
+ <div>
612
+ <dt>Date</dt>
613
+ <dd>${escapeHtml(state.startedAt)}</dd>
614
+ </div>
615
+ <div>
616
+ <dt>Commit</dt>
617
+ <dd><code>${escapeHtml(commitHash)}</code></dd>
618
+ </div>
619
+ <div>
620
+ <dt>Duration</dt>
621
+ <dd>${formatDuration(totalDuration)}</dd>
622
+ </div>
623
+ <div>
624
+ <dt>Verdict</dt>
625
+ <dd><span class="badge badge-${verdictClass(verdict)}">${verdict}</span></dd>
626
+ </div>
627
+ </dl>
628
+ </div>
629
+
630
+ <div class="section">
631
+ <h2>Phase Gate Summary</h2>
632
+ ${buildPipelineHtml(state.gateResults)}
633
+ </div>
634
+
635
+ <div class="section">
636
+ <h2>Agent Results</h2>
637
+ ${buildAgentTableHtml(state.agentResults)}
638
+ </div>
639
+
640
+ <div class="section">
641
+ <h2>Findings</h2>
642
+ ${buildFindingsHtml(state.agentResults)}
643
+ </div>
644
+
645
+ <div class="section">
646
+ <h2>Coverage Summary</h2>
647
+ ${buildCoverageHtml(state.agentResults)}
648
+ </div>
649
+
650
+ <script>${buildJs()}</script>
651
+ </body>
652
+ </html>`;
653
+
654
+ const dir = path.dirname(outputPath);
655
+ fs.mkdirSync(dir, { recursive: true });
656
+ fs.writeFileSync(outputPath, html, 'utf-8');
657
+ }