@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.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/agents/01-analyst.ts +100 -0
- package/agents/02-seed-architect.ts +59 -0
- package/agents/03-test-generator.ts +191 -0
- package/agents/04-unit-runner.ts +160 -0
- package/agents/05-browser-crawler.ts +790 -0
- package/agents/06-api-exerciser.ts +311 -0
- package/agents/07-security-scout.ts +188 -0
- package/agents/08-a11y-guardian.ts +212 -0
- package/agents/09-healer.ts +228 -0
- package/agents/10-reporter.ts +266 -0
- package/agents/11-fixer.ts +253 -0
- package/agents/12-ux-inspector.ts +444 -0
- package/agents/13-performance-profiler.ts +271 -0
- package/agents/14-data-integrity-auditor.ts +417 -0
- package/agents/15-regression-sentinel.ts +307 -0
- package/agents/16-chaos-agent.ts +228 -0
- package/agents/17-documentation-validator.ts +266 -0
- package/agents/18-integration-watchdog.ts +178 -0
- package/agents/19-tenant-isolation-auditor.ts +199 -0
- package/agents/20-workflow-completion-tester.ts +203 -0
- package/agents/21-state-session-tester.ts +262 -0
- package/agents/22-email-notification-verifier.ts +244 -0
- package/agents/23-migration-tester.ts +80 -0
- package/agents/__tests__/01-analyst.test.ts +188 -0
- package/agents/__tests__/02-seed-architect.test.ts +152 -0
- package/agents/__tests__/03-test-generator-full.test.ts +321 -0
- package/agents/__tests__/03-test-generator.test.ts +318 -0
- package/agents/__tests__/04-unit-runner.test.ts +320 -0
- package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
- package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
- package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
- package/agents/__tests__/05-browser-crawler.test.ts +518 -0
- package/agents/__tests__/06-api-exerciser.test.ts +619 -0
- package/agents/__tests__/07-security-scout.test.ts +382 -0
- package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
- package/agents/__tests__/09-healer.test.ts +384 -0
- package/agents/__tests__/10-reporter.test.ts +366 -0
- package/agents/__tests__/11-fixer.test.ts +406 -0
- package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
- package/agents/__tests__/12-ux-inspector.test.ts +443 -0
- package/agents/__tests__/13-performance-profiler.test.ts +411 -0
- package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
- package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
- package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
- package/agents/__tests__/16-chaos-agent.test.ts +427 -0
- package/agents/__tests__/17-documentation-validator.test.ts +402 -0
- package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
- package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
- package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
- package/agents/__tests__/21-state-session-tester.test.ts +374 -0
- package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
- package/agents/__tests__/23-migration-tester.test.ts +145 -0
- package/agents/__tests__/base-agent.test.ts +188 -0
- package/agents/__tests__/registry.test.ts +218 -0
- package/agents/base-agent.ts +77 -0
- package/agents/registry.ts +136 -0
- package/baselines/api-schemas/.gitkeep +0 -0
- package/baselines/performance/.gitkeep +0 -0
- package/baselines/screenshots/.gitkeep +0 -0
- package/bin/testteam.js +10 -0
- package/core/__tests__/ci-output.test.ts +430 -0
- package/core/__tests__/cli.test.ts +387 -0
- package/core/__tests__/config.test.ts +78 -0
- package/core/__tests__/cost-tracker.test.ts +158 -0
- package/core/__tests__/evidence.test.ts +265 -0
- package/core/__tests__/fix-loop.test.ts +210 -0
- package/core/__tests__/health-check.test.ts +44 -0
- package/core/__tests__/init.test.ts +609 -0
- package/core/__tests__/integration.test.ts +204 -0
- package/core/__tests__/license-gen.test.ts +227 -0
- package/core/__tests__/license.test.ts +326 -0
- package/core/__tests__/multi-browser.test.ts +278 -0
- package/core/__tests__/orchestrator.test.ts +519 -0
- package/core/__tests__/phase-gate.test.ts +43 -0
- package/core/__tests__/report-html.test.ts +398 -0
- package/core/__tests__/report-upload.test.ts +325 -0
- package/core/__tests__/run-counter.test.ts +234 -0
- package/core/ci-output.ts +240 -0
- package/core/cli.ts +232 -0
- package/core/config.ts +178 -0
- package/core/cost-tracker.ts +59 -0
- package/core/evidence.ts +132 -0
- package/core/fix-loop.ts +85 -0
- package/core/health-check.ts +54 -0
- package/core/init.ts +546 -0
- package/core/license-gen.ts +212 -0
- package/core/license.ts +211 -0
- package/core/messages.ts +67 -0
- package/core/multi-browser.ts +136 -0
- package/core/orchestrator.ts +354 -0
- package/core/phase-gate.ts +55 -0
- package/core/report-html.ts +657 -0
- package/core/report-upload.ts +188 -0
- package/core/run-counter.ts +175 -0
- package/core/types.ts +57 -0
- package/dist/agents/01-analyst.d.ts +11 -0
- package/dist/agents/01-analyst.d.ts.map +1 -0
- package/dist/agents/01-analyst.js +75 -0
- package/dist/agents/01-analyst.js.map +1 -0
- package/dist/agents/02-seed-architect.d.ts +11 -0
- package/dist/agents/02-seed-architect.d.ts.map +1 -0
- package/dist/agents/02-seed-architect.js +51 -0
- package/dist/agents/02-seed-architect.js.map +1 -0
- package/dist/agents/03-test-generator.d.ts +9 -0
- package/dist/agents/03-test-generator.d.ts.map +1 -0
- package/dist/agents/03-test-generator.js +167 -0
- package/dist/agents/03-test-generator.js.map +1 -0
- package/dist/agents/04-unit-runner.d.ts +9 -0
- package/dist/agents/04-unit-runner.d.ts.map +1 -0
- package/dist/agents/04-unit-runner.js +113 -0
- package/dist/agents/04-unit-runner.js.map +1 -0
- package/dist/agents/05-browser-crawler.d.ts +30 -0
- package/dist/agents/05-browser-crawler.d.ts.map +1 -0
- package/dist/agents/05-browser-crawler.js +685 -0
- package/dist/agents/05-browser-crawler.js.map +1 -0
- package/dist/agents/06-api-exerciser.d.ts +23 -0
- package/dist/agents/06-api-exerciser.d.ts.map +1 -0
- package/dist/agents/06-api-exerciser.js +253 -0
- package/dist/agents/06-api-exerciser.js.map +1 -0
- package/dist/agents/07-security-scout.d.ts +11 -0
- package/dist/agents/07-security-scout.d.ts.map +1 -0
- package/dist/agents/07-security-scout.js +142 -0
- package/dist/agents/07-security-scout.js.map +1 -0
- package/dist/agents/08-a11y-guardian.d.ts +13 -0
- package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
- package/dist/agents/08-a11y-guardian.js +176 -0
- package/dist/agents/08-a11y-guardian.js.map +1 -0
- package/dist/agents/09-healer.d.ts +33 -0
- package/dist/agents/09-healer.d.ts.map +1 -0
- package/dist/agents/09-healer.js +167 -0
- package/dist/agents/09-healer.js.map +1 -0
- package/dist/agents/10-reporter.d.ts +26 -0
- package/dist/agents/10-reporter.d.ts.map +1 -0
- package/dist/agents/10-reporter.js +215 -0
- package/dist/agents/10-reporter.js.map +1 -0
- package/dist/agents/11-fixer.d.ts +26 -0
- package/dist/agents/11-fixer.d.ts.map +1 -0
- package/dist/agents/11-fixer.js +195 -0
- package/dist/agents/11-fixer.js.map +1 -0
- package/dist/agents/12-ux-inspector.d.ts +15 -0
- package/dist/agents/12-ux-inspector.d.ts.map +1 -0
- package/dist/agents/12-ux-inspector.js +364 -0
- package/dist/agents/12-ux-inspector.js.map +1 -0
- package/dist/agents/13-performance-profiler.d.ts +13 -0
- package/dist/agents/13-performance-profiler.d.ts.map +1 -0
- package/dist/agents/13-performance-profiler.js +216 -0
- package/dist/agents/13-performance-profiler.js.map +1 -0
- package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
- package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
- package/dist/agents/14-data-integrity-auditor.js +356 -0
- package/dist/agents/14-data-integrity-auditor.js.map +1 -0
- package/dist/agents/15-regression-sentinel.d.ts +25 -0
- package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
- package/dist/agents/15-regression-sentinel.js +251 -0
- package/dist/agents/15-regression-sentinel.js.map +1 -0
- package/dist/agents/16-chaos-agent.d.ts +9 -0
- package/dist/agents/16-chaos-agent.d.ts.map +1 -0
- package/dist/agents/16-chaos-agent.js +207 -0
- package/dist/agents/16-chaos-agent.js.map +1 -0
- package/dist/agents/17-documentation-validator.d.ts +31 -0
- package/dist/agents/17-documentation-validator.d.ts.map +1 -0
- package/dist/agents/17-documentation-validator.js +246 -0
- package/dist/agents/17-documentation-validator.js.map +1 -0
- package/dist/agents/18-integration-watchdog.d.ts +10 -0
- package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
- package/dist/agents/18-integration-watchdog.js +138 -0
- package/dist/agents/18-integration-watchdog.js.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.js +166 -0
- package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
- package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
- package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
- package/dist/agents/20-workflow-completion-tester.js +159 -0
- package/dist/agents/20-workflow-completion-tester.js.map +1 -0
- package/dist/agents/21-state-session-tester.d.ts +10 -0
- package/dist/agents/21-state-session-tester.d.ts.map +1 -0
- package/dist/agents/21-state-session-tester.js +233 -0
- package/dist/agents/21-state-session-tester.js.map +1 -0
- package/dist/agents/22-email-notification-verifier.d.ts +11 -0
- package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
- package/dist/agents/22-email-notification-verifier.js +199 -0
- package/dist/agents/22-email-notification-verifier.js.map +1 -0
- package/dist/agents/23-migration-tester.d.ts +10 -0
- package/dist/agents/23-migration-tester.d.ts.map +1 -0
- package/dist/agents/23-migration-tester.js +74 -0
- package/dist/agents/23-migration-tester.js.map +1 -0
- package/dist/agents/base-agent.d.ts +19 -0
- package/dist/agents/base-agent.d.ts.map +1 -0
- package/dist/agents/base-agent.js +67 -0
- package/dist/agents/base-agent.js.map +1 -0
- package/dist/agents/registry.d.ts +29 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +117 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/core/ci-output.d.ts +35 -0
- package/dist/core/ci-output.d.ts.map +1 -0
- package/dist/core/ci-output.js +193 -0
- package/dist/core/ci-output.js.map +1 -0
- package/dist/core/cli.d.ts +11 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +197 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +111 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cost-tracker.d.ts +22 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +41 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/evidence.d.ts +28 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +95 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/fix-loop.d.ts +29 -0
- package/dist/core/fix-loop.d.ts.map +1 -0
- package/dist/core/fix-loop.js +70 -0
- package/dist/core/fix-loop.js.map +1 -0
- package/dist/core/health-check.d.ts +21 -0
- package/dist/core/health-check.d.ts.map +1 -0
- package/dist/core/health-check.js +26 -0
- package/dist/core/health-check.js.map +1 -0
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +435 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/license-gen.d.ts +12 -0
- package/dist/core/license-gen.d.ts.map +1 -0
- package/dist/core/license-gen.js +169 -0
- package/dist/core/license-gen.js.map +1 -0
- package/dist/core/license.d.ts +33 -0
- package/dist/core/license.d.ts.map +1 -0
- package/dist/core/license.js +170 -0
- package/dist/core/license.js.map +1 -0
- package/dist/core/messages.d.ts +10 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js +47 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/multi-browser.d.ts +36 -0
- package/dist/core/multi-browser.d.ts.map +1 -0
- package/dist/core/multi-browser.js +88 -0
- package/dist/core/multi-browser.js.map +1 -0
- package/dist/core/orchestrator.d.ts +48 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +291 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/phase-gate.d.ts +4 -0
- package/dist/core/phase-gate.d.ts.map +1 -0
- package/dist/core/phase-gate.js +39 -0
- package/dist/core/phase-gate.js.map +1 -0
- package/dist/core/report-html.d.ts +9 -0
- package/dist/core/report-html.d.ts.map +1 -0
- package/dist/core/report-html.js +617 -0
- package/dist/core/report-html.js.map +1 -0
- package/dist/core/report-upload.d.ts +16 -0
- package/dist/core/report-upload.d.ts.map +1 -0
- package/dist/core/report-upload.js +124 -0
- package/dist/core/report-upload.js.map +1 -0
- package/dist/core/run-counter.d.ts +40 -0
- package/dist/core/run-counter.d.ts.map +1 -0
- package/dist/core/run-counter.js +120 -0
- package/dist/core/run-counter.js.map +1 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/helpers/api-client.d.ts +30 -0
- package/dist/helpers/api-client.d.ts.map +1 -0
- package/dist/helpers/api-client.js +77 -0
- package/dist/helpers/api-client.js.map +1 -0
- package/dist/helpers/element-discovery.d.ts +18 -0
- package/dist/helpers/element-discovery.d.ts.map +1 -0
- package/dist/helpers/element-discovery.js +82 -0
- package/dist/helpers/element-discovery.js.map +1 -0
- package/dist/helpers/env-resolver.d.ts +29 -0
- package/dist/helpers/env-resolver.d.ts.map +1 -0
- package/dist/helpers/env-resolver.js +51 -0
- package/dist/helpers/env-resolver.js.map +1 -0
- package/dist/helpers/form-filler.d.ts +13 -0
- package/dist/helpers/form-filler.d.ts.map +1 -0
- package/dist/helpers/form-filler.js +98 -0
- package/dist/helpers/form-filler.js.map +1 -0
- package/dist/helpers/modal-handler.d.ts +16 -0
- package/dist/helpers/modal-handler.d.ts.map +1 -0
- package/dist/helpers/modal-handler.js +95 -0
- package/dist/helpers/modal-handler.js.map +1 -0
- package/dist/helpers/navigation.d.ts +37 -0
- package/dist/helpers/navigation.d.ts.map +1 -0
- package/dist/helpers/navigation.js +83 -0
- package/dist/helpers/navigation.js.map +1 -0
- package/dist/helpers/quality-gate.d.ts +17 -0
- package/dist/helpers/quality-gate.d.ts.map +1 -0
- package/dist/helpers/quality-gate.js +144 -0
- package/dist/helpers/quality-gate.js.map +1 -0
- package/dist/helpers/screenshot.d.ts +24 -0
- package/dist/helpers/screenshot.d.ts.map +1 -0
- package/dist/helpers/screenshot.js +76 -0
- package/dist/helpers/screenshot.js.map +1 -0
- package/dist/helpers/seed-validator.d.ts +15 -0
- package/dist/helpers/seed-validator.d.ts.map +1 -0
- package/dist/helpers/seed-validator.js +53 -0
- package/dist/helpers/seed-validator.js.map +1 -0
- package/helpers/__tests__/api-client.test.ts +199 -0
- package/helpers/__tests__/element-discovery.test.ts +202 -0
- package/helpers/__tests__/form-filler-extended.test.ts +212 -0
- package/helpers/__tests__/form-filler.test.ts +99 -0
- package/helpers/__tests__/modal-handler.test.ts +152 -0
- package/helpers/__tests__/navigation.test.ts +214 -0
- package/helpers/__tests__/quality-gate.test.ts +117 -0
- package/helpers/__tests__/screenshot.test.ts +139 -0
- package/helpers/__tests__/seed-validator.test.ts +114 -0
- package/helpers/api-client.ts +111 -0
- package/helpers/element-discovery.ts +105 -0
- package/helpers/env-resolver.ts +69 -0
- package/helpers/form-filler.ts +126 -0
- package/helpers/modal-handler.ts +108 -0
- package/helpers/navigation.ts +100 -0
- package/helpers/quality-gate.ts +180 -0
- package/helpers/screenshot.ts +111 -0
- package/helpers/seed-validator.ts +70 -0
- package/package.json +88 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
4
|
+
import type { Phase } from '../../core/types';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Stub config
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const stubConfig = {
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
name: 'test',
|
|
13
|
+
auth: {
|
|
14
|
+
loginUrl: 'http://localhost/login',
|
|
15
|
+
credentials: { admin: { email: 'admin@test.com', password: 'secret' } },
|
|
16
|
+
},
|
|
17
|
+
modules: [
|
|
18
|
+
{ id: 'dashboard', route: '/dashboard', sidebarIcon: true },
|
|
19
|
+
{ id: 'clients', route: '/clients', sidebarIcon: true },
|
|
20
|
+
],
|
|
21
|
+
environments: {
|
|
22
|
+
beta: { baseUrl: 'http://localhost:3000', seed: false },
|
|
23
|
+
},
|
|
24
|
+
portals: [],
|
|
25
|
+
breakpoints: [],
|
|
26
|
+
integrations: [],
|
|
27
|
+
workers: [],
|
|
28
|
+
workflows: [],
|
|
29
|
+
accuracy: { enabled: true, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
30
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
31
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' as const },
|
|
32
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
33
|
+
thresholds: {
|
|
34
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
35
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
36
|
+
apiResponseTime: 2000,
|
|
37
|
+
bundleSizeLimit: 500000,
|
|
38
|
+
maxConsoleErrors: 0,
|
|
39
|
+
},
|
|
40
|
+
disabledAgents: [],
|
|
41
|
+
disabledAgentSet: new Set<number>(),
|
|
42
|
+
} satisfies ValidatedConfig;
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Mock node:fs/promises
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const statMock = vi.fn();
|
|
49
|
+
const readdirMock = vi.fn();
|
|
50
|
+
const mkdirMock = vi.fn();
|
|
51
|
+
const readFileMock = vi.fn();
|
|
52
|
+
const writeFileMock = vi.fn();
|
|
53
|
+
|
|
54
|
+
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
55
|
+
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
56
|
+
return {
|
|
57
|
+
...actual,
|
|
58
|
+
stat: (...args: unknown[]) => statMock(...args),
|
|
59
|
+
readdir: (...args: unknown[]) => readdirMock(...args),
|
|
60
|
+
mkdir: (...args: unknown[]) => mkdirMock(...args),
|
|
61
|
+
readFile: (...args: unknown[]) => readFileMock(...args),
|
|
62
|
+
writeFile: (...args: unknown[]) => writeFileMock(...args),
|
|
63
|
+
copyFile: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Mock screenshot helper
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const compareScreenshotsMock = vi.fn();
|
|
72
|
+
const updateBaselineMock = vi.fn();
|
|
73
|
+
|
|
74
|
+
vi.mock('../../helpers/screenshot', () => ({
|
|
75
|
+
compareScreenshots: (...args: unknown[]) => compareScreenshotsMock(...args),
|
|
76
|
+
updateBaseline: (...args: unknown[]) => updateBaselineMock(...args),
|
|
77
|
+
captureScreenshot: vi.fn().mockResolvedValue('/tmp/test-run/screenshot.png'),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Dynamic import AFTER mocks
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const { RegressionSentinelAgent } = await import('../15-regression-sentinel');
|
|
85
|
+
|
|
86
|
+
// Compute paths the same way the agent does so they match on all platforms
|
|
87
|
+
const RUN_DIR = join('/tmp', 'test-runs', 'run-001');
|
|
88
|
+
const BASELINES_DIR = join('/tmp', 'test-runs', 'baselines');
|
|
89
|
+
const PARENT_DIR = join('/tmp', 'test-runs');
|
|
90
|
+
|
|
91
|
+
function createAgent(phase: Phase = 'beta'): InstanceType<typeof RegressionSentinelAgent> {
|
|
92
|
+
return new RegressionSentinelAgent(stubConfig, phase, RUN_DIR);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Tests
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
describe('RegressionSentinelAgent', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
statMock.mockReset();
|
|
102
|
+
readdirMock.mockReset();
|
|
103
|
+
mkdirMock.mockReset();
|
|
104
|
+
readFileMock.mockReset();
|
|
105
|
+
writeFileMock.mockReset();
|
|
106
|
+
compareScreenshotsMock.mockReset();
|
|
107
|
+
updateBaselineMock.mockReset().mockResolvedValue(join(BASELINES_DIR, 'screenshot.png'));
|
|
108
|
+
|
|
109
|
+
// Default: parent dir exists, run dir exists, baselines dir does NOT yet exist (first run)
|
|
110
|
+
statMock.mockImplementation((p: string) => {
|
|
111
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
112
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
113
|
+
if (p === BASELINES_DIR) return Promise.reject(new Error('ENOENT'));
|
|
114
|
+
return Promise.reject(new Error('ENOENT'));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
118
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
119
|
+
return Promise.resolve([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
mkdirMock.mockResolvedValue(undefined);
|
|
123
|
+
writeFileMock.mockResolvedValue(undefined);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('has agentId 15 and agentName "Regression Sentinel"', () => {
|
|
127
|
+
const agent = createAgent();
|
|
128
|
+
expect(agent.agentId).toBe(15);
|
|
129
|
+
expect(agent.agentName).toBe('Regression Sentinel');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// -------------------------------------------------------------------------
|
|
133
|
+
// preFlight()
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('preFlight()', () => {
|
|
137
|
+
it('throws when parent directory does not exist', async () => {
|
|
138
|
+
statMock.mockImplementation((p: string) => {
|
|
139
|
+
if (p === PARENT_DIR) return Promise.reject(new Error('ENOENT')); // parent missing
|
|
140
|
+
return Promise.reject(new Error('ENOENT'));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const agent = createAgent();
|
|
144
|
+
const result = await agent.run();
|
|
145
|
+
|
|
146
|
+
expect(result.status).toBe('failed');
|
|
147
|
+
expect(result.findings[0].description).toContain('Parent directory does not exist');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('passes when parent directory exists (baselines may not yet exist)', async () => {
|
|
151
|
+
const agent = createAgent();
|
|
152
|
+
const result = await agent.run();
|
|
153
|
+
|
|
154
|
+
expect(result.findings.some((f) => f.description.includes('Parent directory'))).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
// First-run baseline creation
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
describe('first-run baseline creation', () => {
|
|
163
|
+
it('creates baselines directory and copies all screenshots on first run', async () => {
|
|
164
|
+
const agent = createAgent();
|
|
165
|
+
await agent.run();
|
|
166
|
+
|
|
167
|
+
expect(mkdirMock).toHaveBeenCalledWith(BASELINES_DIR, { recursive: true });
|
|
168
|
+
expect(updateBaselineMock).toHaveBeenCalledTimes(2); // one per screenshot
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns no findings on first run', async () => {
|
|
172
|
+
const agent = createAgent();
|
|
173
|
+
const result = await agent.run();
|
|
174
|
+
|
|
175
|
+
expect(result.findings).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns passed status on first run', async () => {
|
|
179
|
+
const agent = createAgent();
|
|
180
|
+
const result = await agent.run();
|
|
181
|
+
|
|
182
|
+
expect(result.status).toBe('passed');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('adds evidence for each baseline created on first run', async () => {
|
|
186
|
+
updateBaselineMock.mockImplementation(async (_src: string, dir: string) => {
|
|
187
|
+
return `${dir}/baseline.png`;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const agent = createAgent();
|
|
191
|
+
const result = await agent.run();
|
|
192
|
+
|
|
193
|
+
const screenshotEvidence = result.evidence.filter((e) => e.type === 'screenshot');
|
|
194
|
+
expect(screenshotEvidence.length).toBeGreaterThanOrEqual(2);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// -------------------------------------------------------------------------
|
|
199
|
+
// First-run: baselines dir exists but contains only .gitkeep
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe('first-run when baselines dir contains only .gitkeep', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
// Baselines dir EXISTS (committed as empty placeholder) but has only .gitkeep
|
|
205
|
+
statMock.mockImplementation((p: string) => {
|
|
206
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
207
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
208
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true }); // dir exists...
|
|
209
|
+
return Promise.reject(new Error('ENOENT'));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ...but only contains .gitkeep
|
|
213
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
214
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['.gitkeep']);
|
|
215
|
+
return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('treats .gitkeep-only baselines dir as first run and creates baselines', async () => {
|
|
220
|
+
const agent = createAgent();
|
|
221
|
+
await agent.run();
|
|
222
|
+
|
|
223
|
+
expect(mkdirMock).toHaveBeenCalledWith(BASELINES_DIR, { recursive: true });
|
|
224
|
+
expect(updateBaselineMock).toHaveBeenCalledTimes(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns no findings when baselines dir has only .gitkeep', async () => {
|
|
228
|
+
const agent = createAgent();
|
|
229
|
+
const result = await agent.run();
|
|
230
|
+
|
|
231
|
+
expect(result.findings).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('returns passed status when baselines dir has only .gitkeep', async () => {
|
|
235
|
+
const agent = createAgent();
|
|
236
|
+
const result = await agent.run();
|
|
237
|
+
|
|
238
|
+
expect(result.status).toBe('passed');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('logs the first-run message to console', async () => {
|
|
242
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
243
|
+
|
|
244
|
+
const agent = createAgent();
|
|
245
|
+
await agent.run();
|
|
246
|
+
|
|
247
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
248
|
+
expect.stringContaining('No baselines found — creating initial baselines from this run'),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
consoleSpy.mockRestore();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
// Second-run: baselines dir has real files (not .gitkeep-only) → compare
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
describe('second-run baseline comparison (baselines exist beyond .gitkeep)', () => {
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
statMock.mockImplementation((p: string) => {
|
|
262
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
263
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
264
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
265
|
+
if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
|
|
266
|
+
return Promise.reject(new Error('ENOENT'));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Baselines dir has real PNG baselines — NOT treated as first run
|
|
270
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
271
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
272
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
273
|
+
return Promise.resolve([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('does NOT create new baselines when real baseline files exist', async () => {
|
|
280
|
+
const agent = createAgent();
|
|
281
|
+
await agent.run();
|
|
282
|
+
|
|
283
|
+
expect(mkdirMock).not.toHaveBeenCalled();
|
|
284
|
+
expect(updateBaselineMock).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('compares screenshots against existing baselines', async () => {
|
|
288
|
+
const agent = createAgent();
|
|
289
|
+
await agent.run();
|
|
290
|
+
|
|
291
|
+
expect(compareScreenshotsMock).toHaveBeenCalledTimes(2);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
// Subsequent run: matching screenshots → no findings
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
describe('matching screenshots → no findings', () => {
|
|
300
|
+
beforeEach(() => {
|
|
301
|
+
// Baselines now exist
|
|
302
|
+
statMock.mockImplementation((p: string) => {
|
|
303
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
304
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
305
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
306
|
+
if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
|
|
307
|
+
return Promise.reject(new Error('ENOENT'));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Baselines dir has real PNG files — NOT treated as first run
|
|
311
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
312
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
313
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
314
|
+
return Promise.resolve([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('returns no findings when all screenshots match baselines', async () => {
|
|
321
|
+
const agent = createAgent();
|
|
322
|
+
const result = await agent.run();
|
|
323
|
+
|
|
324
|
+
const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
|
|
325
|
+
expect(regressionFindings).toHaveLength(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns passed status when all screenshots match', async () => {
|
|
329
|
+
const agent = createAgent();
|
|
330
|
+
const result = await agent.run();
|
|
331
|
+
|
|
332
|
+
expect(result.status).toBe('passed');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// -------------------------------------------------------------------------
|
|
337
|
+
// Subsequent run: different screenshots → findings
|
|
338
|
+
// -------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
describe('visual regression detection', () => {
|
|
341
|
+
beforeEach(() => {
|
|
342
|
+
statMock.mockImplementation((p: string) => {
|
|
343
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
344
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
345
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
346
|
+
if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
|
|
347
|
+
return Promise.reject(new Error('ENOENT'));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Baselines dir has real PNG files — NOT treated as first run
|
|
351
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
352
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
353
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
354
|
+
return Promise.resolve([]);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('creates a medium-severity finding when screenshot diff exceeds threshold', async () => {
|
|
359
|
+
compareScreenshotsMock.mockResolvedValue({
|
|
360
|
+
match: false,
|
|
361
|
+
diffPercentage: 5.3,
|
|
362
|
+
diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const agent = createAgent();
|
|
366
|
+
const result = await agent.run();
|
|
367
|
+
|
|
368
|
+
const regressionFinding = result.findings.find(
|
|
369
|
+
(f) => f.id.includes('visual-regression') && f.id.includes('dashboard'),
|
|
370
|
+
);
|
|
371
|
+
expect(regressionFinding).toBeDefined();
|
|
372
|
+
expect(regressionFinding!.severity).toBe('medium');
|
|
373
|
+
expect(regressionFinding!.type).toBe('code-bug-cosmetic');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('includes the diff percentage in the finding description', async () => {
|
|
377
|
+
compareScreenshotsMock.mockResolvedValue({
|
|
378
|
+
match: false,
|
|
379
|
+
diffPercentage: 2.75,
|
|
380
|
+
diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const agent = createAgent();
|
|
384
|
+
const result = await agent.run();
|
|
385
|
+
|
|
386
|
+
const regressionFinding = result.findings.find((f) => f.id.includes('visual-regression'));
|
|
387
|
+
expect(regressionFinding!.description).toContain('2.75');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('adds diff image as evidence for each regression finding', async () => {
|
|
391
|
+
compareScreenshotsMock.mockResolvedValue({
|
|
392
|
+
match: false,
|
|
393
|
+
diffPercentage: 3.0,
|
|
394
|
+
diffPath: join(RUN_DIR, 'smoke-dashboard_diff.png'),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const agent = createAgent();
|
|
398
|
+
const result = await agent.run();
|
|
399
|
+
|
|
400
|
+
const diffEvidence = result.evidence.filter((e) =>
|
|
401
|
+
e.path.includes('_diff.png'),
|
|
402
|
+
);
|
|
403
|
+
expect(diffEvidence.length).toBeGreaterThan(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('creates a finding for each differing screenshot', async () => {
|
|
407
|
+
compareScreenshotsMock.mockResolvedValue({
|
|
408
|
+
match: false,
|
|
409
|
+
diffPercentage: 1.5,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const agent = createAgent();
|
|
413
|
+
const result = await agent.run();
|
|
414
|
+
|
|
415
|
+
const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
|
|
416
|
+
expect(regressionFindings).toHaveLength(2); // one per screenshot
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('does NOT create findings for screenshots that match', async () => {
|
|
420
|
+
// First screenshot matches, second does not
|
|
421
|
+
compareScreenshotsMock
|
|
422
|
+
.mockResolvedValueOnce({ match: true, diffPercentage: 0 })
|
|
423
|
+
.mockResolvedValueOnce({ match: false, diffPercentage: 2.0 });
|
|
424
|
+
|
|
425
|
+
const agent = createAgent();
|
|
426
|
+
const result = await agent.run();
|
|
427
|
+
|
|
428
|
+
const regressionFindings = result.findings.filter((f) => f.id.includes('visual-regression'));
|
|
429
|
+
expect(regressionFindings).toHaveLength(1);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// -------------------------------------------------------------------------
|
|
434
|
+
// API schema comparison
|
|
435
|
+
// -------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
describe('API schema comparison', () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
statMock.mockImplementation((p: string) => {
|
|
440
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
441
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
442
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
443
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
444
|
+
return Promise.reject(new Error('ENOENT'));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Baselines dir has real PNG files — NOT treated as first run
|
|
448
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
449
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
450
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
451
|
+
return Promise.resolve([]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// All screenshots match
|
|
455
|
+
compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('creates a schema baseline when current snapshot exists but no baseline schema', async () => {
|
|
459
|
+
statMock.mockImplementation((p: string) => {
|
|
460
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
461
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
462
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
463
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
464
|
+
if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true }); // current schema exists
|
|
465
|
+
if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.reject(new Error('ENOENT')); // baseline does not
|
|
466
|
+
return Promise.reject(new Error('ENOENT'));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
readFileMock.mockResolvedValue(JSON.stringify({ '/api/clients': { id: 'string' } }));
|
|
470
|
+
|
|
471
|
+
const agent = createAgent();
|
|
472
|
+
await agent.run();
|
|
473
|
+
|
|
474
|
+
expect(writeFileMock).toHaveBeenCalledWith(
|
|
475
|
+
join(BASELINES_DIR, 'api-schema-snapshot.json'),
|
|
476
|
+
expect.anything(),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('creates a finding when an endpoint is removed from schema', async () => {
|
|
481
|
+
statMock.mockImplementation((p: string) => {
|
|
482
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
483
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
484
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
485
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
486
|
+
if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
487
|
+
if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
488
|
+
return Promise.reject(new Error('ENOENT'));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const currentSchema = { '/api/clients': { id: 'string' } };
|
|
492
|
+
const baselineSchema = { '/api/clients': { id: 'string' }, '/api/invoices': { id: 'string' } }; // invoices removed!
|
|
493
|
+
|
|
494
|
+
readFileMock
|
|
495
|
+
.mockResolvedValueOnce(JSON.stringify(currentSchema))
|
|
496
|
+
.mockResolvedValueOnce(JSON.stringify(baselineSchema));
|
|
497
|
+
|
|
498
|
+
const agent = createAgent();
|
|
499
|
+
const result = await agent.run();
|
|
500
|
+
|
|
501
|
+
const schemaFinding = result.findings.find((f) => f.id.includes('schema-removed'));
|
|
502
|
+
expect(schemaFinding).toBeDefined();
|
|
503
|
+
expect(schemaFinding!.severity).toBe('high');
|
|
504
|
+
expect(schemaFinding!.description).toContain('/api/invoices');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('creates a finding when an endpoint schema changes', async () => {
|
|
508
|
+
statMock.mockImplementation((p: string) => {
|
|
509
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
510
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
511
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
512
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
513
|
+
if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
514
|
+
if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
515
|
+
return Promise.reject(new Error('ENOENT'));
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const currentSchema = { '/api/clients': { id: 'string', name: 'string', status: 'string' } }; // added status
|
|
519
|
+
const baselineSchema = { '/api/clients': { id: 'string', name: 'string' } };
|
|
520
|
+
|
|
521
|
+
readFileMock
|
|
522
|
+
.mockResolvedValueOnce(JSON.stringify(currentSchema))
|
|
523
|
+
.mockResolvedValueOnce(JSON.stringify(baselineSchema));
|
|
524
|
+
|
|
525
|
+
const agent = createAgent();
|
|
526
|
+
const result = await agent.run();
|
|
527
|
+
|
|
528
|
+
const schemaFinding = result.findings.find((f) => f.id.includes('schema-changed'));
|
|
529
|
+
expect(schemaFinding).toBeDefined();
|
|
530
|
+
expect(schemaFinding!.severity).toBe('medium');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('creates a finding when schema JSON is malformed', async () => {
|
|
534
|
+
statMock.mockImplementation((p: string) => {
|
|
535
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
536
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
537
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
538
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
539
|
+
if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
540
|
+
if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
541
|
+
return Promise.reject(new Error('ENOENT'));
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
readFileMock.mockResolvedValue('not valid json');
|
|
545
|
+
|
|
546
|
+
const agent = createAgent();
|
|
547
|
+
const result = await agent.run();
|
|
548
|
+
|
|
549
|
+
const parseFinding = result.findings.find(f => f.id.includes('schema-parse-error'));
|
|
550
|
+
expect(parseFinding).toBeDefined();
|
|
551
|
+
expect(parseFinding!.severity).toBe('low');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('creates a new baseline for a screenshot with no prior baseline', async () => {
|
|
555
|
+
statMock.mockImplementation((p: string) => {
|
|
556
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
557
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
558
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
559
|
+
// baseline for dashboard exists, but NOT for clients
|
|
560
|
+
if (p === join(BASELINES_DIR, 'smoke-dashboard.png')) return Promise.resolve({ isFile: () => true });
|
|
561
|
+
if (p === join(BASELINES_DIR, 'smoke-clients.png')) return Promise.reject(new Error('ENOENT'));
|
|
562
|
+
return Promise.reject(new Error('ENOENT'));
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
566
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png']);
|
|
567
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png', 'smoke-clients.png']);
|
|
568
|
+
return Promise.resolve([]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
compareScreenshotsMock.mockResolvedValue({ match: true, diffPercentage: 0 });
|
|
572
|
+
|
|
573
|
+
const agent = createAgent();
|
|
574
|
+
await agent.run();
|
|
575
|
+
|
|
576
|
+
// updateBaseline called for the new screenshot (clients)
|
|
577
|
+
expect(updateBaselineMock).toHaveBeenCalledTimes(1);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('creates a finding when compareScreenshots throws', async () => {
|
|
581
|
+
statMock.mockImplementation((p: string) => {
|
|
582
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
583
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
584
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
585
|
+
if (p.startsWith(BASELINES_DIR)) return Promise.resolve({ isFile: () => true });
|
|
586
|
+
return Promise.reject(new Error('ENOENT'));
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
590
|
+
if (dir === BASELINES_DIR) return Promise.resolve(['smoke-dashboard.png']);
|
|
591
|
+
if (dir === RUN_DIR) return Promise.resolve(['smoke-dashboard.png']);
|
|
592
|
+
return Promise.resolve([]);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
compareScreenshotsMock.mockRejectedValue(new Error('Image decode error'));
|
|
596
|
+
|
|
597
|
+
const agent = createAgent();
|
|
598
|
+
const result = await agent.run();
|
|
599
|
+
|
|
600
|
+
const compareErrorFinding = result.findings.find(f => f.id.includes('compare-error'));
|
|
601
|
+
expect(compareErrorFinding).toBeDefined();
|
|
602
|
+
expect(compareErrorFinding!.severity).toBe('medium');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('creates a critical finding when run directory does not exist', async () => {
|
|
606
|
+
statMock.mockImplementation((p: string) => {
|
|
607
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
608
|
+
if (p === RUN_DIR) return Promise.reject(new Error('ENOENT'));
|
|
609
|
+
return Promise.reject(new Error('ENOENT'));
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const agent = createAgent();
|
|
613
|
+
const result = await agent.run();
|
|
614
|
+
|
|
615
|
+
const noRunDirFinding = result.findings.find(f => f.id === '15-no-run-dir');
|
|
616
|
+
expect(noRunDirFinding).toBeDefined();
|
|
617
|
+
expect(noRunDirFinding!.severity).toBe('critical');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('handles readdir failure for run directory gracefully', async () => {
|
|
621
|
+
statMock.mockImplementation((p: string) => {
|
|
622
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
623
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
624
|
+
return Promise.reject(new Error('ENOENT'));
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
readdirMock.mockRejectedValue(new Error('Permission denied'));
|
|
628
|
+
|
|
629
|
+
const agent = createAgent();
|
|
630
|
+
const result = await agent.run();
|
|
631
|
+
|
|
632
|
+
// Should still complete (empty screenshot list, first run)
|
|
633
|
+
expect(result.status).toBe('passed');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('returns no schema findings when schemas are identical', async () => {
|
|
637
|
+
statMock.mockImplementation((p: string) => {
|
|
638
|
+
if (p === PARENT_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
639
|
+
if (p === RUN_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
640
|
+
if (p === BASELINES_DIR) return Promise.resolve({ isDirectory: () => true });
|
|
641
|
+
if (p.startsWith(BASELINES_DIR) && p.endsWith('.png')) return Promise.resolve({ isFile: () => true });
|
|
642
|
+
if (p === join(RUN_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
643
|
+
if (p === join(BASELINES_DIR, 'api-schema-snapshot.json')) return Promise.resolve({ isFile: () => true });
|
|
644
|
+
return Promise.reject(new Error('ENOENT'));
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const schema = JSON.stringify({ '/api/clients': { id: 'string' } });
|
|
648
|
+
readFileMock.mockResolvedValue(schema);
|
|
649
|
+
|
|
650
|
+
const agent = createAgent();
|
|
651
|
+
const result = await agent.run();
|
|
652
|
+
|
|
653
|
+
const schemaFindings = result.findings.filter((f) => f.id.includes('schema'));
|
|
654
|
+
expect(schemaFindings).toHaveLength(0);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
});
|