@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,402 @@
|
|
|
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
|
|
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: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
|
|
20
|
+
environments: {
|
|
21
|
+
alpha: { baseUrl: 'http://localhost:3000', seed: false },
|
|
22
|
+
},
|
|
23
|
+
portals: [],
|
|
24
|
+
breakpoints: [],
|
|
25
|
+
integrations: [],
|
|
26
|
+
workers: [],
|
|
27
|
+
workflows: [],
|
|
28
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
29
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
30
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
31
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
32
|
+
thresholds: {
|
|
33
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
34
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
35
|
+
apiResponseTime: 2000,
|
|
36
|
+
bundleSizeLimit: 500000,
|
|
37
|
+
maxConsoleErrors: 0,
|
|
38
|
+
},
|
|
39
|
+
disabledAgents: [],
|
|
40
|
+
disabledAgentSet: new Set<number>(),
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Mock node:fs/promises — we fully control what the agent "sees" on disk
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const statMock = vi.fn();
|
|
50
|
+
const readdirMock = vi.fn();
|
|
51
|
+
const readFileMock = vi.fn();
|
|
52
|
+
|
|
53
|
+
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
54
|
+
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
|
55
|
+
return {
|
|
56
|
+
...actual,
|
|
57
|
+
stat: (...args: unknown[]) => statMock(...args),
|
|
58
|
+
readdir: (...args: unknown[]) => readdirMock(...args),
|
|
59
|
+
readFile: (...args: unknown[]) => readFileMock(...args),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Mock fetch (URL health checks)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
const fetchMock = vi.fn();
|
|
68
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Dynamic import AFTER mocks are hoisted
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
const { DocumentationValidatorAgent } = await import('../17-documentation-validator');
|
|
75
|
+
|
|
76
|
+
function createAgent(
|
|
77
|
+
config: ValidatedConfig = makeConfig(),
|
|
78
|
+
phase: Phase = 'uat',
|
|
79
|
+
): InstanceType<typeof DocumentationValidatorAgent> {
|
|
80
|
+
return new DocumentationValidatorAgent(config, phase, '/tmp/test-run');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/** Configure fs mock so the agent finds a README.md in project root. */
|
|
88
|
+
function setupBasicFs(readmeContent: string = '# Hello', docsExists = false): void {
|
|
89
|
+
statMock.mockImplementation((p: string) => {
|
|
90
|
+
if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
|
|
91
|
+
if (p.endsWith('docs')) {
|
|
92
|
+
if (docsExists) return Promise.resolve({ isDirectory: () => true });
|
|
93
|
+
return Promise.reject(new Error('ENOENT'));
|
|
94
|
+
}
|
|
95
|
+
return Promise.reject(new Error('ENOENT'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
99
|
+
if (!dir.toString().includes('docs')) return Promise.resolve(['README.md']);
|
|
100
|
+
return Promise.resolve([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
readFileMock.mockImplementation((filePath: string) => {
|
|
104
|
+
if (String(filePath).endsWith('README.md')) return Promise.resolve(readmeContent);
|
|
105
|
+
return Promise.resolve('');
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Tests
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
describe('DocumentationValidatorAgent', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
vi.resetAllMocks();
|
|
116
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
// Identity
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
it('has agentId 17 and agentName "Documentation Validator"', () => {
|
|
124
|
+
const agent = createAgent();
|
|
125
|
+
expect(agent.agentId).toBe(17);
|
|
126
|
+
expect(agent.agentName).toBe('Documentation Validator');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// -------------------------------------------------------------------------
|
|
130
|
+
// preFlight()
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
describe('preFlight()', () => {
|
|
134
|
+
it('throws when neither README.md nor docs/ directory exists', async () => {
|
|
135
|
+
statMock.mockRejectedValue(new Error('ENOENT'));
|
|
136
|
+
|
|
137
|
+
const agent = createAgent();
|
|
138
|
+
const result = await agent.run();
|
|
139
|
+
|
|
140
|
+
expect(result.status).toBe('failed');
|
|
141
|
+
expect(result.findings[0].description).toContain('README.md');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('passes when README.md exists', async () => {
|
|
145
|
+
setupBasicFs();
|
|
146
|
+
|
|
147
|
+
const agent = createAgent();
|
|
148
|
+
const result = await agent.run();
|
|
149
|
+
|
|
150
|
+
expect(result.findings.some(f => f.description.includes('README.md'))).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('passes when only docs/ directory exists (no README.md)', async () => {
|
|
154
|
+
statMock.mockImplementation((p: string) => {
|
|
155
|
+
if (p.endsWith('docs')) return Promise.resolve({ isDirectory: () => true });
|
|
156
|
+
return Promise.reject(new Error('ENOENT'));
|
|
157
|
+
});
|
|
158
|
+
readdirMock.mockResolvedValue([]);
|
|
159
|
+
|
|
160
|
+
const agent = createAgent();
|
|
161
|
+
const result = await agent.run();
|
|
162
|
+
|
|
163
|
+
expect(result.status).toBe('passed');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
// URL extraction from markdown
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
describe('URL extraction', () => {
|
|
172
|
+
it('extracts URLs from markdown link syntax', async () => {
|
|
173
|
+
setupBasicFs('See [docs](https://example.com/docs) for details.');
|
|
174
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
175
|
+
|
|
176
|
+
const agent = createAgent();
|
|
177
|
+
await agent.run();
|
|
178
|
+
|
|
179
|
+
const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
|
|
180
|
+
const urls = fetchCalls.map(([url]) => url);
|
|
181
|
+
expect(urls).toContain('https://example.com/docs');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('extracts bare URLs from markdown content', async () => {
|
|
185
|
+
setupBasicFs('Visit https://bare-url.example.com/page for info.');
|
|
186
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
187
|
+
|
|
188
|
+
const agent = createAgent();
|
|
189
|
+
await agent.run();
|
|
190
|
+
|
|
191
|
+
const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
|
|
192
|
+
const urls = fetchCalls.map(([url]) => url);
|
|
193
|
+
expect(urls.some((u) => u.includes('bare-url.example.com'))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('does not duplicate URLs that appear in both link syntax and bare form', async () => {
|
|
197
|
+
setupBasicFs(
|
|
198
|
+
'See [link](https://example.com/dup) and also https://example.com/dup here.',
|
|
199
|
+
);
|
|
200
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
201
|
+
|
|
202
|
+
const agent = createAgent();
|
|
203
|
+
await agent.run();
|
|
204
|
+
|
|
205
|
+
const fetchCalls = fetchMock.mock.calls as Array<[string, unknown]>;
|
|
206
|
+
const dupUrls = fetchCalls.filter(
|
|
207
|
+
([url]) => url === 'https://example.com/dup',
|
|
208
|
+
);
|
|
209
|
+
expect(dupUrls.length).toBe(1);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
// Broken link → finding
|
|
215
|
+
// -------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
describe('broken link detection', () => {
|
|
218
|
+
it('creates a finding for a 404 URL', async () => {
|
|
219
|
+
setupBasicFs('[broken](https://dead.example.com/gone)');
|
|
220
|
+
fetchMock.mockResolvedValue({ ok: false, status: 404 });
|
|
221
|
+
|
|
222
|
+
const agent = createAgent();
|
|
223
|
+
const result = await agent.run();
|
|
224
|
+
|
|
225
|
+
const brokenFinding = result.findings.find(
|
|
226
|
+
f => f.description.includes('https://dead.example.com/gone'),
|
|
227
|
+
);
|
|
228
|
+
expect(brokenFinding).toBeDefined();
|
|
229
|
+
expect(brokenFinding!.severity).toBe('low');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('creates a finding for a 5xx URL', async () => {
|
|
233
|
+
setupBasicFs('[server-error](https://error.example.com/500)');
|
|
234
|
+
fetchMock.mockResolvedValue({ ok: false, status: 500 });
|
|
235
|
+
|
|
236
|
+
const agent = createAgent();
|
|
237
|
+
const result = await agent.run();
|
|
238
|
+
|
|
239
|
+
const brokenFinding = result.findings.find(
|
|
240
|
+
f => f.description.includes('https://error.example.com/500'),
|
|
241
|
+
);
|
|
242
|
+
expect(brokenFinding).toBeDefined();
|
|
243
|
+
expect(brokenFinding!.severity).toBe('low');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('creates a finding for an unreachable URL (network error)', async () => {
|
|
247
|
+
setupBasicFs('[unreachable](https://unreachable.example.com/)');
|
|
248
|
+
fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
249
|
+
|
|
250
|
+
const agent = createAgent();
|
|
251
|
+
const result = await agent.run();
|
|
252
|
+
|
|
253
|
+
const brokenFinding = result.findings.find(
|
|
254
|
+
f => f.description.includes('https://unreachable.example.com/'),
|
|
255
|
+
);
|
|
256
|
+
expect(brokenFinding).toBeDefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('does not create findings for valid links (200)', async () => {
|
|
260
|
+
setupBasicFs('[ok](https://ok.example.com/)');
|
|
261
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
262
|
+
|
|
263
|
+
const agent = createAgent();
|
|
264
|
+
const result = await agent.run();
|
|
265
|
+
|
|
266
|
+
const linkFindings = result.findings.filter(
|
|
267
|
+
f => f.description.includes('https://ok.example.com/'),
|
|
268
|
+
);
|
|
269
|
+
expect(linkFindings).toHaveLength(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('does not create findings for redirect responses (301/302)', async () => {
|
|
273
|
+
setupBasicFs('[redirect](https://redirect.example.com/)');
|
|
274
|
+
fetchMock.mockResolvedValue({ ok: false, status: 301 });
|
|
275
|
+
|
|
276
|
+
const agent = createAgent();
|
|
277
|
+
const result = await agent.run();
|
|
278
|
+
|
|
279
|
+
const linkFindings = result.findings.filter(
|
|
280
|
+
f => f.description.includes('redirect.example.com'),
|
|
281
|
+
);
|
|
282
|
+
expect(linkFindings).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('sets finding.file to the markdown file path', async () => {
|
|
286
|
+
setupBasicFs('[broken](https://broken2.example.com/)');
|
|
287
|
+
fetchMock.mockResolvedValue({ ok: false, status: 404 });
|
|
288
|
+
|
|
289
|
+
const agent = createAgent();
|
|
290
|
+
const result = await agent.run();
|
|
291
|
+
|
|
292
|
+
const brokenFinding = result.findings.find(
|
|
293
|
+
f => f.description.includes('https://broken2.example.com/'),
|
|
294
|
+
);
|
|
295
|
+
expect(brokenFinding!.file).toBeDefined();
|
|
296
|
+
expect(brokenFinding!.file).toContain('README.md');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// -------------------------------------------------------------------------
|
|
301
|
+
// Bare catch-log pattern
|
|
302
|
+
// -------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
describe('bare catch log detection', () => {
|
|
305
|
+
it('creates a finding when src/ contains a catch block that only logs error.message', async () => {
|
|
306
|
+
setupBasicFs();
|
|
307
|
+
|
|
308
|
+
// Override stat and readdir to include a fake src/ directory
|
|
309
|
+
statMock.mockImplementation((p: string) => {
|
|
310
|
+
if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
|
|
311
|
+
if (p.endsWith('docs')) return Promise.reject(new Error('ENOENT'));
|
|
312
|
+
if (p.endsWith('src')) return Promise.resolve({ isDirectory: () => true });
|
|
313
|
+
if (p.endsWith('service.ts')) return Promise.resolve({ isDirectory: () => false });
|
|
314
|
+
return Promise.reject(new Error('ENOENT'));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
318
|
+
if (String(dir).endsWith('src')) return Promise.resolve(['service.ts']);
|
|
319
|
+
return Promise.resolve(['README.md']);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
readFileMock.mockImplementation((filePath: string) => {
|
|
323
|
+
if (String(filePath).endsWith('README.md')) return Promise.resolve('# Docs');
|
|
324
|
+
if (String(filePath).endsWith('service.ts')) {
|
|
325
|
+
return Promise.resolve(`
|
|
326
|
+
async function doSomething() {
|
|
327
|
+
try {
|
|
328
|
+
await risky();
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(err.message);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
`);
|
|
334
|
+
}
|
|
335
|
+
return Promise.resolve('');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const agent = createAgent();
|
|
339
|
+
const result = await agent.run();
|
|
340
|
+
|
|
341
|
+
const catchFinding = result.findings.find(
|
|
342
|
+
f => f.description.includes('catch') || f.type === 'code-bug-logic',
|
|
343
|
+
);
|
|
344
|
+
expect(catchFinding).toBeDefined();
|
|
345
|
+
expect(catchFinding!.severity).toBe('low');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('does not create a bare-catch finding when no such pattern exists in source', async () => {
|
|
349
|
+
setupBasicFs();
|
|
350
|
+
|
|
351
|
+
statMock.mockImplementation((p: string) => {
|
|
352
|
+
if (p.endsWith('README.md')) return Promise.resolve({ isDirectory: () => false });
|
|
353
|
+
if (p.endsWith('docs')) return Promise.reject(new Error('ENOENT'));
|
|
354
|
+
if (p.endsWith('src')) return Promise.resolve({ isDirectory: () => true });
|
|
355
|
+
if (p.endsWith('clean-service.ts')) return Promise.resolve({ isDirectory: () => false });
|
|
356
|
+
return Promise.reject(new Error('ENOENT'));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
readdirMock.mockImplementation((dir: string) => {
|
|
360
|
+
if (String(dir).endsWith('src')) return Promise.resolve(['clean-service.ts']);
|
|
361
|
+
return Promise.resolve(['README.md']);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
readFileMock.mockImplementation((filePath: string) => {
|
|
365
|
+
if (String(filePath).endsWith('README.md')) return Promise.resolve('# Docs');
|
|
366
|
+
if (String(filePath).endsWith('clean-service.ts')) {
|
|
367
|
+
return Promise.resolve(`
|
|
368
|
+
async function doSomething() {
|
|
369
|
+
try {
|
|
370
|
+
await risky();
|
|
371
|
+
} catch (err) {
|
|
372
|
+
throw new BadRequestException('Something went wrong — please try again');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
return Promise.resolve('');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const agent = createAgent();
|
|
381
|
+
const result = await agent.run();
|
|
382
|
+
|
|
383
|
+
const catchFindings = result.findings.filter(f => f.type === 'code-bug-logic');
|
|
384
|
+
expect(catchFindings).toHaveLength(0);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// -------------------------------------------------------------------------
|
|
389
|
+
// No findings when everything is valid
|
|
390
|
+
// -------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
it('returns passed status with no findings for clean docs and valid links', async () => {
|
|
393
|
+
setupBasicFs('# README\n\nThis project has no links.');
|
|
394
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
395
|
+
|
|
396
|
+
const agent = createAgent();
|
|
397
|
+
const result = await agent.run();
|
|
398
|
+
|
|
399
|
+
expect(result.status).toBe('passed');
|
|
400
|
+
expect(result.findings).toHaveLength(0);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Stub config
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function makeConfig(overrides: Partial<ValidatedConfig> = {}): ValidatedConfig {
|
|
9
|
+
return {
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
name: 'test',
|
|
12
|
+
auth: { loginUrl: 'http://localhost/login' },
|
|
13
|
+
modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
|
|
14
|
+
environments: { alpha: { baseUrl: 'http://localhost:3000', seed: false } },
|
|
15
|
+
portals: [],
|
|
16
|
+
breakpoints: [],
|
|
17
|
+
integrations: [{ name: 'xero' }],
|
|
18
|
+
workers: [],
|
|
19
|
+
workflows: [],
|
|
20
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
21
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
22
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
23
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
24
|
+
thresholds: {
|
|
25
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
26
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
27
|
+
apiResponseTime: 2000,
|
|
28
|
+
bundleSizeLimit: 500000,
|
|
29
|
+
maxConsoleErrors: 0,
|
|
30
|
+
},
|
|
31
|
+
disabledAgents: [],
|
|
32
|
+
disabledAgentSet: new Set<number>(),
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Mock node:fs and node:child_process
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const existsSyncMock = vi.fn();
|
|
42
|
+
vi.mock('node:fs', () => ({
|
|
43
|
+
existsSync: (...args: unknown[]) => existsSyncMock(...args),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
type ExecFileCallback = (err: Error | null, stdout: string, stderr: string) => void;
|
|
47
|
+
|
|
48
|
+
const execFileMock = vi.fn();
|
|
49
|
+
vi.mock('node:child_process', () => ({
|
|
50
|
+
execFile: (...args: unknown[]) => execFileMock(...args),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
/** Helper: make execFile resolve with success */
|
|
54
|
+
function setupExecSuccess(stdout = ''): void {
|
|
55
|
+
execFileMock.mockImplementation(
|
|
56
|
+
(_file: string, _args: string[], _opts: unknown, cb: ExecFileCallback) => {
|
|
57
|
+
cb(null, stdout, '');
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Helper: make execFile reject with a failure */
|
|
63
|
+
function setupExecFailure(stderr: string): void {
|
|
64
|
+
const error = Object.assign(new Error('Command failed'), { stderr, stdout: '' });
|
|
65
|
+
execFileMock.mockImplementation(
|
|
66
|
+
(_file: string, _args: string[], _opts: unknown, cb: ExecFileCallback) => {
|
|
67
|
+
cb(error, '', stderr);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Import after mocks
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const { IntegrationWatchdogAgent } = await import('../18-integration-watchdog');
|
|
77
|
+
|
|
78
|
+
function createAgent(config: ValidatedConfig = makeConfig()): InstanceType<typeof IntegrationWatchdogAgent> {
|
|
79
|
+
return new IntegrationWatchdogAgent(config, 'beta', '/tmp/test-run');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Tests
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
describe('IntegrationWatchdogAgent', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.resetAllMocks();
|
|
89
|
+
// Default: workers dir exists
|
|
90
|
+
existsSyncMock.mockReturnValue(true);
|
|
91
|
+
// Default: vitest runs succeed
|
|
92
|
+
setupExecSuccess('All tests passed');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('has agentId 18 and agentName "Integration Watchdog"', () => {
|
|
96
|
+
const agent = createAgent();
|
|
97
|
+
expect(agent.agentId).toBe(18);
|
|
98
|
+
expect(agent.agentName).toBe('Integration Watchdog');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// -------------------------------------------------------------------------
|
|
102
|
+
// preFlight()
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe('preFlight()', () => {
|
|
106
|
+
it('throws when config.integrations is empty', async () => {
|
|
107
|
+
const config = makeConfig({ integrations: [] });
|
|
108
|
+
const agent = createAgent(config);
|
|
109
|
+
const result = await agent.run();
|
|
110
|
+
|
|
111
|
+
expect(result.status).toBe('failed');
|
|
112
|
+
expect(result.findings[0].description).toContain('at least one integration');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('throws when worker directories do not exist', async () => {
|
|
116
|
+
existsSyncMock.mockReturnValue(false);
|
|
117
|
+
const agent = createAgent();
|
|
118
|
+
const result = await agent.run();
|
|
119
|
+
|
|
120
|
+
expect(result.status).toBe('failed');
|
|
121
|
+
expect(result.findings[0].description).toContain('Worker directory not found');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('passes when integrations are configured and worker dir exists', async () => {
|
|
125
|
+
existsSyncMock.mockReturnValue(true);
|
|
126
|
+
const agent = createAgent();
|
|
127
|
+
const result = await agent.run();
|
|
128
|
+
|
|
129
|
+
expect(result.findings.some((f) => f.description.includes('at least one integration'))).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
// execute() — worker checks
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
describe('execute() — worker source file checks', () => {
|
|
138
|
+
it('creates a medium finding when a worker source file is missing', async () => {
|
|
139
|
+
const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
|
|
140
|
+
|
|
141
|
+
// workers dir exists, but worker source file and vitest config do not
|
|
142
|
+
existsSyncMock.mockImplementation((p: string) => {
|
|
143
|
+
const n = p.replace(/\\/g, '/');
|
|
144
|
+
// apps/workers dir must exist for preFlight
|
|
145
|
+
if (n.endsWith('apps/workers')) return true;
|
|
146
|
+
// Everything else (source files, vitest config) does not exist
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const agent = createAgent(config);
|
|
151
|
+
const result = await agent.run();
|
|
152
|
+
|
|
153
|
+
const workerFinding = result.findings.find((f) => f.id.includes('worker-missing'));
|
|
154
|
+
expect(workerFinding).toBeDefined();
|
|
155
|
+
expect(workerFinding!.severity).toBe('medium');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('creates a medium finding when a worker has no test file', async () => {
|
|
159
|
+
const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
|
|
160
|
+
|
|
161
|
+
existsSyncMock.mockImplementation((p: string) => {
|
|
162
|
+
const n = p.replace(/\\/g, '/');
|
|
163
|
+
// apps/workers dir exists for preFlight
|
|
164
|
+
if (n.endsWith('apps/workers')) return true;
|
|
165
|
+
// vitest config does NOT exist — avoids running tests in execute()
|
|
166
|
+
if (n.endsWith('vitest.config.ts')) return false;
|
|
167
|
+
// worker source file exists, test files do not
|
|
168
|
+
if (n.includes('sync-xero') && !n.includes('.test.') && !n.includes('.spec.')) return true;
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const agent = createAgent(config);
|
|
173
|
+
const result = await agent.run();
|
|
174
|
+
|
|
175
|
+
const testFinding = result.findings.find((f) => f.id.includes('worker-no-tests'));
|
|
176
|
+
expect(testFinding).toBeDefined();
|
|
177
|
+
expect(testFinding!.severity).toBe('medium');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('creates no worker finding when source and test file both exist', async () => {
|
|
181
|
+
const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
|
|
182
|
+
|
|
183
|
+
// All paths exist — source, tests, and vitest config
|
|
184
|
+
existsSyncMock.mockReturnValue(true);
|
|
185
|
+
setupExecSuccess('All tests passed');
|
|
186
|
+
|
|
187
|
+
const agent = createAgent(config);
|
|
188
|
+
const result = await agent.run();
|
|
189
|
+
|
|
190
|
+
const workerFindings = result.findings.filter(
|
|
191
|
+
(f) => f.id.includes('worker-missing') || f.id.includes('worker-no-tests'),
|
|
192
|
+
);
|
|
193
|
+
expect(workerFindings).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// -------------------------------------------------------------------------
|
|
198
|
+
// execute() — worker test runner
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe('execute() — worker test runner', () => {
|
|
202
|
+
it('creates a high finding when worker tests fail', async () => {
|
|
203
|
+
const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
|
|
204
|
+
|
|
205
|
+
existsSyncMock.mockReturnValue(true);
|
|
206
|
+
setupExecFailure('FAIL sync-xero.test.ts');
|
|
207
|
+
|
|
208
|
+
const agent = createAgent(config);
|
|
209
|
+
const result = await agent.run();
|
|
210
|
+
|
|
211
|
+
const testFailFinding = result.findings.find((f) => f.id.includes('worker-tests-failed'));
|
|
212
|
+
expect(testFailFinding).toBeDefined();
|
|
213
|
+
expect(testFailFinding!.severity).toBe('high');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('creates no finding when all worker tests pass', async () => {
|
|
217
|
+
const config = makeConfig({ workers: [{ name: 'sync-xero' }] });
|
|
218
|
+
|
|
219
|
+
existsSyncMock.mockReturnValue(true);
|
|
220
|
+
setupExecSuccess('All tests passed');
|
|
221
|
+
|
|
222
|
+
const agent = createAgent(config);
|
|
223
|
+
const result = await agent.run();
|
|
224
|
+
|
|
225
|
+
const testFailFinding = result.findings.find((f) => f.id.includes('worker-tests-failed'));
|
|
226
|
+
expect(testFailFinding).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
// execute() — integration sandbox checks
|
|
232
|
+
// -------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
describe('execute() — integration sandbox checks', () => {
|
|
235
|
+
it('creates a low advisory finding when no sandbox config exists for an integration', async () => {
|
|
236
|
+
// Workers dir exists but integration sandbox config files do not
|
|
237
|
+
existsSyncMock.mockImplementation((p: string) => {
|
|
238
|
+
const n = p.replace(/\\/g, '/');
|
|
239
|
+
if (n.endsWith('apps/workers')) return true;
|
|
240
|
+
if (n.includes('xero')) return false;
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const agent = createAgent();
|
|
245
|
+
const result = await agent.run();
|
|
246
|
+
|
|
247
|
+
const sandboxFinding = result.findings.find((f) => f.id.includes('integration-no-sandbox'));
|
|
248
|
+
expect(sandboxFinding).toBeDefined();
|
|
249
|
+
expect(sandboxFinding!.severity).toBe('low');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('creates no sandbox finding when sandbox config exists', async () => {
|
|
253
|
+
// All paths exist — sandbox config found
|
|
254
|
+
existsSyncMock.mockReturnValue(true);
|
|
255
|
+
|
|
256
|
+
const agent = createAgent();
|
|
257
|
+
const result = await agent.run();
|
|
258
|
+
|
|
259
|
+
const sandboxFinding = result.findings.find((f) => f.id.includes('integration-no-sandbox'));
|
|
260
|
+
expect(sandboxFinding).toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|