@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,117 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateCodeQuality } from '../quality-gate';
3
+ import type { QualityViolation } from '../quality-gate';
4
+
5
+ describe('validateCodeQuality(fileContent, filePath)', () => {
6
+ describe('any type detection', () => {
7
+ it('reports a violation when `any` type is used', () => {
8
+ const content = `const x: any = 5;`;
9
+ const violations: QualityViolation[] = validateCodeQuality(content, 'test.ts');
10
+ expect(violations.length).toBeGreaterThan(0);
11
+ const v = violations.find((v) => v.rule === 'no-any');
12
+ expect(v).toBeDefined();
13
+ expect(v?.message).toMatch(/any/i);
14
+ expect(v?.severity).toBe('error');
15
+ });
16
+
17
+ it('reports no violation for a typed variable without `any`', () => {
18
+ const content = `const x: string = 'hello';`;
19
+ const violations = validateCodeQuality(content, 'test.ts');
20
+ const anyViolations = violations.filter((v) => v.rule === 'no-any');
21
+ expect(anyViolations).toHaveLength(0);
22
+ });
23
+ });
24
+
25
+ describe('long function detection (>30 lines)', () => {
26
+ it('reports a violation for a function body with 35 lines', () => {
27
+ const bodyLines = Array.from({ length: 35 }, (_, i) => ` const v${i} = ${i};`);
28
+ const content = `function longFn() {\n${bodyLines.join('\n')}\n}`;
29
+ const violations = validateCodeQuality(content, 'test.ts');
30
+ const v = violations.find((v) => v.rule === 'max-function-lines');
31
+ expect(v).toBeDefined();
32
+ expect(v?.severity).toBe('warning');
33
+ });
34
+
35
+ it('reports no violation for a function body with 10 lines', () => {
36
+ const bodyLines = Array.from({ length: 10 }, (_, i) => ` const v${i} = ${i};`);
37
+ const content = `function shortFn() {\n${bodyLines.join('\n')}\n}`;
38
+ const violations = validateCodeQuality(content, 'test.ts');
39
+ const fnViolations = violations.filter((v) => v.rule === 'max-function-lines');
40
+ expect(fnViolations).toHaveLength(0);
41
+ });
42
+ });
43
+
44
+ describe('hardcoded secret detection', () => {
45
+ it('reports a violation for a hardcoded password assignment', () => {
46
+ const content = `const password = 'secret123';`;
47
+ const violations = validateCodeQuality(content, 'test.ts');
48
+ const v = violations.find((v) => v.rule === 'no-hardcoded-secrets');
49
+ expect(v).toBeDefined();
50
+ expect(v?.severity).toBe('error');
51
+ });
52
+
53
+ it('reports no violation when password uses process.env', () => {
54
+ const content = `const password = process.env.PASSWORD;`;
55
+ const violations = validateCodeQuality(content, 'test.ts');
56
+ const secretViolations = violations.filter((v) => v.rule === 'no-hardcoded-secrets');
57
+ expect(secretViolations).toHaveLength(0);
58
+ });
59
+
60
+ it('reports a violation for a hardcoded apiKey assignment', () => {
61
+ const content = `const apiKey = 'sk-1234567890';`;
62
+ const violations = validateCodeQuality(content, 'test.ts');
63
+ const v = violations.find((v) => v.rule === 'no-hardcoded-secrets');
64
+ expect(v).toBeDefined();
65
+ expect(v?.severity).toBe('error');
66
+ });
67
+ });
68
+
69
+ describe('deep nesting detection (>3 levels)', () => {
70
+ it('reports a violation for 4 nested if blocks', () => {
71
+ const content = [
72
+ `if (a) {`,
73
+ ` if (b) {`,
74
+ ` if (c) {`,
75
+ ` if (d) {`,
76
+ ` doSomething();`,
77
+ ` }`,
78
+ ` }`,
79
+ ` }`,
80
+ `}`,
81
+ ].join('\n');
82
+ const violations = validateCodeQuality(content, 'test.ts');
83
+ const v = violations.find((v) => v.rule === 'max-nesting-depth');
84
+ expect(v).toBeDefined();
85
+ expect(v?.severity).toBe('warning');
86
+ });
87
+
88
+ it('reports no violation for 2 nested if blocks', () => {
89
+ const content = [
90
+ `if (a) {`,
91
+ ` if (b) {`,
92
+ ` doSomething();`,
93
+ ` }`,
94
+ `}`,
95
+ ].join('\n');
96
+ const violations = validateCodeQuality(content, 'test.ts');
97
+ const nestingViolations = violations.filter((v) => v.rule === 'max-nesting-depth');
98
+ expect(nestingViolations).toHaveLength(0);
99
+ });
100
+ });
101
+
102
+ describe('QualityViolation shape', () => {
103
+ it('each violation has rule, message, severity, and optional line', () => {
104
+ const content = `const x: any = 5;`;
105
+ const violations = validateCodeQuality(content, 'test.ts');
106
+ expect(violations.length).toBeGreaterThan(0);
107
+ for (const v of violations) {
108
+ expect(typeof v.rule).toBe('string');
109
+ expect(typeof v.message).toBe('string');
110
+ expect(['error', 'warning']).toContain(v.severity);
111
+ if (v.line !== undefined) {
112
+ expect(typeof v.line).toBe('number');
113
+ }
114
+ }
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { PNG } from 'pngjs';
5
+ import { compareScreenshots, updateBaseline } from '../screenshot';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const TMP_DIR = join('/tmp', 'tf-screenshot-tests');
12
+
13
+ function createSolidPng(filePath: string, r: number, g: number, b: number): void {
14
+ const width = 10;
15
+ const height = 10;
16
+ const png = new PNG({ width, height });
17
+
18
+ for (let y = 0; y < height; y++) {
19
+ for (let x = 0; x < width; x++) {
20
+ const idx = (y * width + x) * 4;
21
+ png.data[idx] = r;
22
+ png.data[idx + 1] = g;
23
+ png.data[idx + 2] = b;
24
+ png.data[idx + 3] = 255;
25
+ }
26
+ }
27
+
28
+ writeFileSync(filePath, PNG.sync.write(png));
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Setup / teardown
33
+ // ---------------------------------------------------------------------------
34
+
35
+ beforeAll(() => {
36
+ if (!existsSync(TMP_DIR)) {
37
+ mkdirSync(TMP_DIR, { recursive: true });
38
+ }
39
+ });
40
+
41
+ afterAll(() => {
42
+ rmSync(TMP_DIR, { recursive: true, force: true });
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Tests
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe('compareScreenshots(currentPath, baselinePath, threshold?)', () => {
50
+ it('returns match=true and diffPercentage=0 for identical images', async () => {
51
+ const pathA = join(TMP_DIR, 'identical_a.png');
52
+ const pathB = join(TMP_DIR, 'identical_b.png');
53
+ createSolidPng(pathA, 255, 0, 0);
54
+ createSolidPng(pathB, 255, 0, 0);
55
+
56
+ const result = await compareScreenshots(pathA, pathB);
57
+
58
+ expect(result.match).toBe(true);
59
+ expect(result.diffPercentage).toBe(0);
60
+ expect(result.diffPath).toBeUndefined();
61
+ });
62
+
63
+ it('returns match=false and diffPercentage>0 for completely different images', async () => {
64
+ const current = join(TMP_DIR, 'diff_current.png');
65
+ const baseline = join(TMP_DIR, 'diff_baseline.png');
66
+ createSolidPng(current, 255, 0, 0); // red
67
+ createSolidPng(baseline, 0, 0, 255); // blue
68
+
69
+ const result = await compareScreenshots(current, baseline);
70
+
71
+ expect(result.match).toBe(false);
72
+ expect(result.diffPercentage).toBeGreaterThan(0);
73
+ });
74
+
75
+ it('creates a diff PNG file when images differ', async () => {
76
+ const current = join(TMP_DIR, 'diff_file_current.png');
77
+ const baseline = join(TMP_DIR, 'diff_file_baseline.png');
78
+ createSolidPng(current, 255, 0, 0);
79
+ createSolidPng(baseline, 0, 255, 0);
80
+
81
+ const result = await compareScreenshots(current, baseline);
82
+
83
+ expect(result.match).toBe(false);
84
+ expect(result.diffPath).toBeDefined();
85
+ expect(existsSync(result.diffPath!)).toBe(true);
86
+ });
87
+
88
+ it('returns match=false for images with different dimensions', async () => {
89
+ const small = new PNG({ width: 5, height: 5 });
90
+ small.data.fill(255);
91
+ writeFileSync(join(TMP_DIR, 'small.png'), PNG.sync.write(small));
92
+
93
+ const large = new PNG({ width: 10, height: 10 });
94
+ large.data.fill(255);
95
+ writeFileSync(join(TMP_DIR, 'large.png'), PNG.sync.write(large));
96
+
97
+ const result = await compareScreenshots(
98
+ join(TMP_DIR, 'small.png'),
99
+ join(TMP_DIR, 'large.png'),
100
+ );
101
+
102
+ expect(result.match).toBe(false);
103
+ expect(result.diffPercentage).toBe(100);
104
+ });
105
+
106
+ it('respects a custom threshold — identical images pass any positive threshold', async () => {
107
+ const pathA = join(TMP_DIR, 'thresh_a.png');
108
+ const pathB = join(TMP_DIR, 'thresh_b.png');
109
+ createSolidPng(pathA, 128, 128, 128);
110
+ createSolidPng(pathB, 128, 128, 128);
111
+
112
+ const result = await compareScreenshots(pathA, pathB, 0.5);
113
+
114
+ expect(result.match).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('updateBaseline(currentPath, baselineDir)', () => {
119
+ it('copies the current screenshot to the baseline directory', async () => {
120
+ const current = join(TMP_DIR, 'to_baseline.png');
121
+ const baselineDir = join(TMP_DIR, 'baselines');
122
+ createSolidPng(current, 0, 128, 0);
123
+
124
+ const resultPath = await updateBaseline(current, baselineDir);
125
+
126
+ expect(existsSync(resultPath)).toBe(true);
127
+ expect(resultPath).toContain('to_baseline.png');
128
+ });
129
+
130
+ it('creates the baseline directory if it does not exist', async () => {
131
+ const current = join(TMP_DIR, 'new_dir_test.png');
132
+ const baselineDir = join(TMP_DIR, 'new_baselines_dir');
133
+ createSolidPng(current, 0, 0, 128);
134
+
135
+ await updateBaseline(current, baselineDir);
136
+
137
+ expect(existsSync(baselineDir)).toBe(true);
138
+ });
139
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { validateSeedCompleteness } from '../seed-validator';
5
+
6
+ const TMP_DIR = join('/tmp', 'tf-seed-validator-tests');
7
+
8
+ const SEED_CONTENT = `
9
+ import { PrismaClient } from '@prisma/client';
10
+ const prisma = new PrismaClient();
11
+
12
+ async function main() {
13
+ // Seed users
14
+ await prisma.user.createMany({
15
+ data: [
16
+ { email: 'admin@example.com', role: 'ADMIN' },
17
+ { email: 'staff@example.com', role: 'STAFF' },
18
+ ],
19
+ });
20
+
21
+ // Seed clients
22
+ await prisma.client.create({
23
+ data: { name: 'ACME Corp', abn: '12345678901' },
24
+ });
25
+
26
+ // Seed invoices
27
+ await prisma.invoice.upsert({
28
+ where: { id: 1 },
29
+ update: {},
30
+ create: { clientId: 1, amount: 1000 },
31
+ });
32
+
33
+ // Insert raw transactions
34
+ await prisma.$executeRaw\`INSERT INTO transactions (amount) VALUES (500)\`;
35
+ }
36
+
37
+ main();
38
+ `;
39
+
40
+ beforeAll(() => {
41
+ if (!existsSync(TMP_DIR)) {
42
+ mkdirSync(TMP_DIR, { recursive: true });
43
+ }
44
+ writeFileSync(join(TMP_DIR, 'seed.ts'), SEED_CONTENT, 'utf-8');
45
+ });
46
+
47
+ afterAll(() => {
48
+ rmSync(TMP_DIR, { recursive: true, force: true });
49
+ });
50
+
51
+ describe('validateSeedCompleteness(seedFilePath, moduleIds)', () => {
52
+ it('detects covered modules that appear near creation keywords', async () => {
53
+ const result = await validateSeedCompleteness(
54
+ join(TMP_DIR, 'seed.ts'),
55
+ ['user', 'client', 'invoice'],
56
+ );
57
+
58
+ expect(result.covered).toContain('user');
59
+ expect(result.covered).toContain('client');
60
+ expect(result.covered).toContain('invoice');
61
+ expect(result.missing).toHaveLength(0);
62
+ });
63
+
64
+ it('identifies missing modules not present in the seed file', async () => {
65
+ const result = await validateSeedCompleteness(
66
+ join(TMP_DIR, 'seed.ts'),
67
+ ['user', 'client', 'invoice', 'payment', 'subscription'],
68
+ );
69
+
70
+ expect(result.covered).toContain('user');
71
+ expect(result.covered).toContain('client');
72
+ expect(result.missing).toContain('payment');
73
+ expect(result.missing).toContain('subscription');
74
+ });
75
+
76
+ it('returns all modules as missing when seed file is empty', async () => {
77
+ const emptyPath = join(TMP_DIR, 'empty-seed.ts');
78
+ writeFileSync(emptyPath, '', 'utf-8');
79
+
80
+ const result = await validateSeedCompleteness(emptyPath, ['user', 'client']);
81
+
82
+ expect(result.covered).toHaveLength(0);
83
+ expect(result.missing).toContain('user');
84
+ expect(result.missing).toContain('client');
85
+ });
86
+
87
+ it('returns all modules as covered when every module matches', async () => {
88
+ const result = await validateSeedCompleteness(
89
+ join(TMP_DIR, 'seed.ts'),
90
+ ['user'],
91
+ );
92
+
93
+ expect(result.covered).toContain('user');
94
+ expect(result.missing).toHaveLength(0);
95
+ });
96
+
97
+ it('is case-insensitive for module names', async () => {
98
+ const result = await validateSeedCompleteness(
99
+ join(TMP_DIR, 'seed.ts'),
100
+ ['User', 'CLIENT', 'Invoice'],
101
+ );
102
+
103
+ expect(result.covered).toContain('User');
104
+ expect(result.covered).toContain('CLIENT');
105
+ expect(result.covered).toContain('Invoice');
106
+ });
107
+
108
+ it('returns empty covered and missing arrays when moduleIds is empty', async () => {
109
+ const result = await validateSeedCompleteness(join(TMP_DIR, 'seed.ts'), []);
110
+
111
+ expect(result.covered).toHaveLength(0);
112
+ expect(result.missing).toHaveLength(0);
113
+ });
114
+ });
@@ -0,0 +1,111 @@
1
+ export interface ApiResponse {
2
+ status: number;
3
+ body: unknown;
4
+ headers: Record<string, string>;
5
+ durationMs: number;
6
+ }
7
+
8
+ interface LoginResponseBody {
9
+ access_token?: string;
10
+ token?: string;
11
+ accessToken?: string;
12
+ }
13
+
14
+ /**
15
+ * Lightweight HTTP client for integration and API tests.
16
+ * Automatically attaches Authorization headers when a token is present
17
+ * and records response duration for every request.
18
+ */
19
+ export class ApiClient {
20
+ constructor(
21
+ private readonly baseUrl: string,
22
+ private token?: string,
23
+ ) {}
24
+
25
+ /**
26
+ * Creates an authenticated ApiClient by POSTing credentials to /api/auth/login
27
+ * and storing the returned JWT for subsequent requests.
28
+ */
29
+ static async createAuthenticated(
30
+ baseUrl: string,
31
+ credentials: { email: string; password: string },
32
+ ): Promise<ApiClient> {
33
+ const client = new ApiClient(baseUrl);
34
+ const response = await client.post('/api/auth/login', credentials);
35
+
36
+ const body = response.body as LoginResponseBody;
37
+ const jwt = body?.access_token ?? body?.accessToken ?? body?.token;
38
+
39
+ if (!jwt) {
40
+ throw new Error(
41
+ `Authentication failed: no token in response (status ${response.status})`,
42
+ );
43
+ }
44
+
45
+ return new ApiClient(baseUrl, String(jwt));
46
+ }
47
+
48
+ async get(path: string): Promise<ApiResponse> {
49
+ return this.request('GET', path);
50
+ }
51
+
52
+ async post(path: string, body?: unknown): Promise<ApiResponse> {
53
+ return this.request('POST', path, body);
54
+ }
55
+
56
+ async put(path: string, body?: unknown): Promise<ApiResponse> {
57
+ return this.request('PUT', path, body);
58
+ }
59
+
60
+ async delete(path: string): Promise<ApiResponse> {
61
+ return this.request('DELETE', path);
62
+ }
63
+
64
+ private async request(
65
+ method: string,
66
+ path: string,
67
+ body?: unknown,
68
+ ): Promise<ApiResponse> {
69
+ const url = `${this.baseUrl}${path}`;
70
+ const headers: Record<string, string> = {
71
+ 'Content-Type': 'application/json',
72
+ };
73
+
74
+ if (this.token) {
75
+ headers['Authorization'] = `Bearer ${this.token}`;
76
+ }
77
+
78
+ const init: RequestInit = {
79
+ method,
80
+ headers,
81
+ };
82
+
83
+ if (body !== undefined) {
84
+ init.body = JSON.stringify(body);
85
+ }
86
+
87
+ const start = Date.now();
88
+ const response = await fetch(url, init);
89
+ const durationMs = Date.now() - start;
90
+
91
+ const responseHeaders: Record<string, string> = {};
92
+ response.headers.forEach((value, key) => {
93
+ responseHeaders[key] = value;
94
+ });
95
+
96
+ let responseBody: unknown;
97
+ const contentType = response.headers.get('content-type') ?? '';
98
+ if (contentType.includes('application/json')) {
99
+ responseBody = await response.json();
100
+ } else {
101
+ responseBody = await response.text();
102
+ }
103
+
104
+ return {
105
+ status: response.status,
106
+ body: responseBody,
107
+ headers: responseHeaders,
108
+ durationMs,
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,105 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ export interface DiscoveredElement {
4
+ selector: string;
5
+ type: 'button' | 'link' | 'input' | 'select' | 'textarea' | 'tab' | 'menuitem' | 'other';
6
+ text: string;
7
+ isDestructive: boolean;
8
+ ariaLabel?: string;
9
+ }
10
+
11
+ // Keywords that indicate a destructive action
12
+ const DESTRUCTIVE_KEYWORDS = ['delete', 'remove', 'archive', 'destroy', 'clear', 'reset'];
13
+
14
+ /**
15
+ * Returns true when the given text (lowercased) contains any destructive keyword.
16
+ */
17
+ export function classifyAsDestructive(text: string): boolean {
18
+ const lower = text.toLowerCase();
19
+ return DESTRUCTIVE_KEYWORDS.some((keyword) => lower.includes(keyword));
20
+ }
21
+
22
+ // CSS selectors targeting all interactive element categories
23
+ const INTERACTIVE_SELECTORS = [
24
+ 'button',
25
+ 'a[href]',
26
+ 'input',
27
+ 'select',
28
+ 'textarea',
29
+ '[role="button"]',
30
+ '[role="tab"]',
31
+ '[role="menuitem"]',
32
+ '[data-testid]',
33
+ '[aria-haspopup]',
34
+ ].join(', ');
35
+
36
+ type ElementType = DiscoveredElement['type'];
37
+
38
+ function resolveType(tag: string, role: string): ElementType {
39
+ if (role === 'tab') return 'tab';
40
+ if (role === 'menuitem') return 'menuitem';
41
+ if (role === 'button' || tag === 'button') return 'button';
42
+ if (tag === 'a') return 'link';
43
+ if (tag === 'input') return 'input';
44
+ if (tag === 'select') return 'select';
45
+ if (tag === 'textarea') return 'textarea';
46
+ return 'other';
47
+ }
48
+
49
+ function buildSelector(
50
+ testId: string | null,
51
+ ariaLabel: string | null,
52
+ tag: string,
53
+ text: string,
54
+ ): string {
55
+ if (testId) return `[data-testid="${testId}"]`;
56
+ if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
57
+ const safeSuffix = text.trim().slice(0, 40).replace(/"/g, '\\"');
58
+ return safeSuffix ? `${tag}:text("${safeSuffix}")` : tag;
59
+ }
60
+
61
+ /**
62
+ * Discovers all interactive elements on the given Playwright page and returns
63
+ * a deduplicated, classified list with unique selectors.
64
+ */
65
+ export async function discoverInteractiveElements(page: Page): Promise<DiscoveredElement[]> {
66
+ type RawElement = {
67
+ tag: string;
68
+ role: string;
69
+ text: string;
70
+ ariaLabel: string | null;
71
+ testId: string | null;
72
+ };
73
+
74
+ const rawElements: RawElement[] = await page.evaluate((selector: string) => {
75
+ const nodes = Array.from(document.querySelectorAll(selector));
76
+ return nodes.map((el) => ({
77
+ tag: el.tagName.toLowerCase(),
78
+ role: el.getAttribute('role') ?? '',
79
+ text: (el.textContent ?? '').trim(),
80
+ ariaLabel: el.getAttribute('aria-label'),
81
+ testId: el.getAttribute('data-testid'),
82
+ }));
83
+ }, INTERACTIVE_SELECTORS);
84
+
85
+ const seenSelectors = new Set<string>();
86
+ const discovered: DiscoveredElement[] = [];
87
+
88
+ for (const raw of rawElements) {
89
+ const selector = buildSelector(raw.testId, raw.ariaLabel, raw.tag, raw.text);
90
+ if (seenSelectors.has(selector)) continue;
91
+ seenSelectors.add(selector);
92
+
93
+ const labelText = raw.ariaLabel ?? raw.text;
94
+ const discovered_element: DiscoveredElement = {
95
+ selector,
96
+ type: resolveType(raw.tag, raw.role),
97
+ text: raw.text,
98
+ isDestructive: classifyAsDestructive(labelText),
99
+ ...(raw.ariaLabel !== null ? { ariaLabel: raw.ariaLabel } : {}),
100
+ };
101
+ discovered.push(discovered_element);
102
+ }
103
+
104
+ return discovered;
105
+ }
@@ -0,0 +1,69 @@
1
+ import type { ValidatedConfig } from '../core/config';
2
+ import type { Phase } from '../core/types';
3
+
4
+ interface EnvironmentConfig {
5
+ baseUrl: string;
6
+ seed?: boolean;
7
+ }
8
+
9
+ interface ResolvedEnvironment {
10
+ env: EnvironmentConfig;
11
+ /** True when the phase's configured URL was unreachable and we fell back to alpha. */
12
+ fallback: boolean;
13
+ }
14
+
15
+ /**
16
+ * Check whether a URL responds with a non-5xx status within a timeout.
17
+ * Exported so agents can use it directly without duplicating the function.
18
+ */
19
+ export async function isUrlReachable(url: string, timeoutMs = 5000): Promise<boolean> {
20
+ try {
21
+ const response = await fetch(url, {
22
+ method: 'HEAD',
23
+ signal: AbortSignal.timeout(timeoutMs),
24
+ });
25
+ return response.ok || response.status < 500;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Resolve the environment for a given phase with automatic fallback.
33
+ *
34
+ * 1. Try the phase's configured environment (`config.environments[phase]`).
35
+ * 2. If the URL is reachable → return `{ env, fallback: false }`.
36
+ * 3. If unreachable → try `config.environments['alpha']` (localhost).
37
+ * 4. If alpha is reachable → return `{ env: alphaEnv, fallback: true }`.
38
+ * 5. If neither is reachable → return the phase's env with `fallback: false`
39
+ * so the agent can decide how to handle the failure.
40
+ */
41
+ export async function resolveEnvironment(
42
+ config: ValidatedConfig,
43
+ phase: Phase,
44
+ ): Promise<ResolvedEnvironment> {
45
+ // Phase-specific environment
46
+ const phaseEnv = config.environments[phase];
47
+ if (phaseEnv) {
48
+ const reachable = await isUrlReachable(phaseEnv.baseUrl);
49
+ if (reachable) {
50
+ return { env: phaseEnv, fallback: false };
51
+ }
52
+ }
53
+
54
+ // Fallback chain: alpha → first available
55
+ const alphaEnv = config.environments['alpha'];
56
+ if (alphaEnv && alphaEnv.baseUrl !== phaseEnv?.baseUrl) {
57
+ const reachable = await isUrlReachable(alphaEnv.baseUrl);
58
+ if (reachable) {
59
+ return { env: alphaEnv, fallback: true };
60
+ }
61
+ }
62
+
63
+ // Nothing reachable — return phase env (or first available) and let agent handle it
64
+ const fallbackEnv = phaseEnv ?? Object.values(config.environments)[0];
65
+ if (!fallbackEnv) {
66
+ throw new Error(`No environment configured for phase "${phase}"`);
67
+ }
68
+ return { env: fallbackEnv, fallback: false };
69
+ }