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