@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,244 @@
|
|
|
1
|
+
import type { Finding } from '../core/types';
|
|
2
|
+
import { BaseAgent } from './base-agent';
|
|
3
|
+
import { ApiClient } from '../helpers/api-client';
|
|
4
|
+
|
|
5
|
+
const MAILHOG_API_URL = 'http://localhost:8025/api/v2/messages?limit=10';
|
|
6
|
+
const MAILHOG_HEALTH_URL = 'http://localhost:8025';
|
|
7
|
+
|
|
8
|
+
interface MailhogMessage {
|
|
9
|
+
ID: string;
|
|
10
|
+
Content: {
|
|
11
|
+
Headers: Record<string, string[]>;
|
|
12
|
+
Body: string;
|
|
13
|
+
};
|
|
14
|
+
Created: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MailhogResponse {
|
|
18
|
+
items: MailhogMessage[];
|
|
19
|
+
total: number;
|
|
20
|
+
count: number;
|
|
21
|
+
start: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function isMailhogReachable(): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(MAILHOG_HEALTH_URL, {
|
|
27
|
+
method: 'HEAD',
|
|
28
|
+
signal: AbortSignal.timeout(5_000),
|
|
29
|
+
});
|
|
30
|
+
return response.ok || response.status < 500;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchMailhogMessages(): Promise<MailhogMessage[]> {
|
|
37
|
+
const response = await fetch(MAILHOG_API_URL, {
|
|
38
|
+
signal: AbortSignal.timeout(10_000),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Mailhog API returned ${response.status}`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await response.json()) as MailhogResponse;
|
|
44
|
+
return data.items ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class EmailNotificationVerifierAgent extends BaseAgent {
|
|
48
|
+
readonly agentId = 22;
|
|
49
|
+
readonly agentName = 'Email & Notification Verifier';
|
|
50
|
+
|
|
51
|
+
/** Set during preFlight so execute() knows whether Mailhog is available. */
|
|
52
|
+
private mailhogAvailable = false;
|
|
53
|
+
|
|
54
|
+
protected async preFlight(): Promise<void> {
|
|
55
|
+
this.mailhogAvailable = await isMailhogReachable();
|
|
56
|
+
// Advisory only — Mailhog is optional in non-local environments
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected async execute(): Promise<Finding[]> {
|
|
60
|
+
const findings: Finding[] = [];
|
|
61
|
+
|
|
62
|
+
const env =
|
|
63
|
+
this.config.environments['beta'] ??
|
|
64
|
+
this.config.environments['alpha'] ??
|
|
65
|
+
Object.values(this.config.environments)[0];
|
|
66
|
+
|
|
67
|
+
if (!env) {
|
|
68
|
+
findings.push({
|
|
69
|
+
id: `${this.agentId}-no-env`,
|
|
70
|
+
type: 'infra-issue',
|
|
71
|
+
severity: 'critical',
|
|
72
|
+
agentId: this.agentId,
|
|
73
|
+
module: 'email',
|
|
74
|
+
description: 'No environment configured — cannot run email/notification checks',
|
|
75
|
+
});
|
|
76
|
+
return findings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------------------------------------
|
|
80
|
+
// Advisory finding when Mailhog is unavailable
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
if (!this.mailhogAvailable) {
|
|
83
|
+
findings.push({
|
|
84
|
+
id: `${this.agentId}-mailhog-unavailable`,
|
|
85
|
+
type: 'infra-issue',
|
|
86
|
+
severity: 'low',
|
|
87
|
+
agentId: this.agentId,
|
|
88
|
+
module: 'email',
|
|
89
|
+
description:
|
|
90
|
+
'Advisory: Mailhog is not reachable at localhost:8025. ' +
|
|
91
|
+
'Email delivery verification will be skipped. ' +
|
|
92
|
+
'Start Mailhog locally to enable full email testing.',
|
|
93
|
+
suggestedFix: 'Run `docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog` before the test suite.',
|
|
94
|
+
});
|
|
95
|
+
return findings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -----------------------------------------------------------------------
|
|
99
|
+
// Step 1: Trigger a password reset to generate an email
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
const testEmail =
|
|
102
|
+
this.config.auth.credentials?.['admin']?.email ??
|
|
103
|
+
Object.values(this.config.auth.credentials ?? {})[0]?.email ??
|
|
104
|
+
'test@example.com';
|
|
105
|
+
|
|
106
|
+
const apiClient = new ApiClient(env.baseUrl);
|
|
107
|
+
let passwordResetTriggered = false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await apiClient.post('/api/auth/forgot-password', { email: testEmail });
|
|
111
|
+
if (response.status === 200 || response.status === 202 || response.status === 204) {
|
|
112
|
+
passwordResetTriggered = true;
|
|
113
|
+
} else {
|
|
114
|
+
findings.push({
|
|
115
|
+
id: `${this.agentId}-password-reset-failed`,
|
|
116
|
+
type: 'infra-issue',
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
agentId: this.agentId,
|
|
119
|
+
module: 'email',
|
|
120
|
+
description:
|
|
121
|
+
`Password reset endpoint POST /api/auth/forgot-password returned ${response.status}. ` +
|
|
122
|
+
`Expected 200/202/204.`,
|
|
123
|
+
suggestedFix: 'Verify the forgot-password endpoint is implemented and returns a 2xx status.',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
findings.push({
|
|
128
|
+
id: `${this.agentId}-password-reset-error`,
|
|
129
|
+
type: 'infra-issue',
|
|
130
|
+
severity: 'medium',
|
|
131
|
+
agentId: this.agentId,
|
|
132
|
+
module: 'email',
|
|
133
|
+
description: `Password reset trigger failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!passwordResetTriggered) {
|
|
138
|
+
return findings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -----------------------------------------------------------------------
|
|
142
|
+
// Step 2: Poll Mailhog for the delivered email (up to 5 seconds)
|
|
143
|
+
// -----------------------------------------------------------------------
|
|
144
|
+
let messages: MailhogMessage[] = [];
|
|
145
|
+
const backoffDelays = [200, 400, 800, 1600, 3200];
|
|
146
|
+
|
|
147
|
+
for (const delay of backoffDelays) {
|
|
148
|
+
try {
|
|
149
|
+
messages = await fetchMailhogMessages();
|
|
150
|
+
if (messages.length > 0) break;
|
|
151
|
+
} catch {
|
|
152
|
+
// Retry on transient errors
|
|
153
|
+
}
|
|
154
|
+
await new Promise<void>((resolve) => setTimeout(resolve, delay));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (messages.length === 0) {
|
|
158
|
+
findings.push({
|
|
159
|
+
id: `${this.agentId}-email-not-delivered`,
|
|
160
|
+
type: 'code-bug-logic',
|
|
161
|
+
severity: 'high',
|
|
162
|
+
agentId: this.agentId,
|
|
163
|
+
module: 'email',
|
|
164
|
+
description:
|
|
165
|
+
`Email delivery failure: no messages received in Mailhog within 5 seconds of ` +
|
|
166
|
+
`triggering password reset for "${testEmail}".`,
|
|
167
|
+
suggestedFix:
|
|
168
|
+
'Verify the mailer service is configured correctly and Mailhog SMTP port (1025) is reachable.',
|
|
169
|
+
});
|
|
170
|
+
return findings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// Step 3: Inspect email bodies for raw template variables like {{variable}}
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
const templateVarPattern = /\{\{[^}]+\}\}/g;
|
|
177
|
+
|
|
178
|
+
for (const message of messages) {
|
|
179
|
+
const body = message.Content.Body;
|
|
180
|
+
const rawVars = body.match(templateVarPattern);
|
|
181
|
+
|
|
182
|
+
if (rawVars && rawVars.length > 0) {
|
|
183
|
+
findings.push({
|
|
184
|
+
id: `${this.agentId}-template-vars-${message.ID.slice(0, 8)}`,
|
|
185
|
+
type: 'code-bug-logic',
|
|
186
|
+
severity: 'high',
|
|
187
|
+
agentId: this.agentId,
|
|
188
|
+
module: 'email',
|
|
189
|
+
description:
|
|
190
|
+
`Email template variable(s) not substituted: found ${rawVars.join(', ')} in email body (message ID: ${message.ID}).`,
|
|
191
|
+
suggestedFix:
|
|
192
|
+
'Check the email template renderer and ensure all variables are provided at send time.',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
// Step 4: Check that all links in the email body resolve (HTTP HEAD → 200)
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
const urlPattern = /https?:\/\/[^\s"'<>)]+/g;
|
|
201
|
+
|
|
202
|
+
for (const message of messages) {
|
|
203
|
+
const body = message.Content.Body;
|
|
204
|
+
const urls = body.match(urlPattern) ?? [];
|
|
205
|
+
const uniqueUrls = [...new Set(urls)];
|
|
206
|
+
|
|
207
|
+
for (const url of uniqueUrls) {
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch(url, {
|
|
210
|
+
method: 'HEAD',
|
|
211
|
+
signal: AbortSignal.timeout(10_000),
|
|
212
|
+
redirect: 'follow',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (response.status >= 400) {
|
|
216
|
+
findings.push({
|
|
217
|
+
id: `${this.agentId}-broken-link-${url.slice(0, 40).replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
218
|
+
type: 'code-bug-logic',
|
|
219
|
+
severity: 'medium',
|
|
220
|
+
agentId: this.agentId,
|
|
221
|
+
module: 'email',
|
|
222
|
+
description:
|
|
223
|
+
`Broken link in email body: HEAD ${url} returned ${response.status}.`,
|
|
224
|
+
suggestedFix: `Update the email template to use a valid URL for: ${url}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
findings.push({
|
|
229
|
+
id: `${this.agentId}-link-error-${url.slice(0, 40).replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
230
|
+
type: 'infra-issue',
|
|
231
|
+
severity: 'low',
|
|
232
|
+
agentId: this.agentId,
|
|
233
|
+
module: 'email',
|
|
234
|
+
description:
|
|
235
|
+
`Could not verify link in email body: ${url}. ` +
|
|
236
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import type { Finding } from '../core/types';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
export class MigrationTesterAgent extends BaseAgent {
|
|
9
|
+
readonly agentId = 23;
|
|
10
|
+
readonly agentName = 'Migration Tester';
|
|
11
|
+
|
|
12
|
+
private get projectRoot(): string {
|
|
13
|
+
return this.config.projectRoot ?? process.cwd();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected async preFlight(): Promise<void> {
|
|
17
|
+
await execFileAsync('npx', ['prisma', '--version'], { shell: true, cwd: this.projectRoot });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected async execute(): Promise<Finding[]> {
|
|
21
|
+
const findings: Finding[] = [];
|
|
22
|
+
const cwd = this.projectRoot;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await execFileAsync('npx', ['prisma', 'migrate', 'status'], { shell: true, cwd });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const stderr = error instanceof Error && 'stderr' in error
|
|
28
|
+
? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
|
|
29
|
+
: String(error);
|
|
30
|
+
// In alpha phase, DB connectivity issues are medium (dev env may lack DB)
|
|
31
|
+
// In beta/uat, they're critical (DB must be available)
|
|
32
|
+
const severity = this.phase === 'alpha' ? 'medium' as const : 'critical' as const;
|
|
33
|
+
findings.push({
|
|
34
|
+
id: `${this.agentId}-migrate-status`,
|
|
35
|
+
type: 'infra-issue',
|
|
36
|
+
severity,
|
|
37
|
+
agentId: this.agentId,
|
|
38
|
+
module: 'database',
|
|
39
|
+
description: `prisma migrate status failed: ${stderr}`,
|
|
40
|
+
});
|
|
41
|
+
return findings;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await execFileAsync('npx', ['prisma', 'migrate', 'deploy'], { shell: true, cwd });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const stderr = error instanceof Error && 'stderr' in error
|
|
48
|
+
? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
|
|
49
|
+
: String(error);
|
|
50
|
+
findings.push({
|
|
51
|
+
id: `${this.agentId}-migrate-deploy`,
|
|
52
|
+
type: 'infra-issue',
|
|
53
|
+
severity: 'critical',
|
|
54
|
+
agentId: this.agentId,
|
|
55
|
+
module: 'database',
|
|
56
|
+
description: `prisma migrate deploy failed: ${stderr}`,
|
|
57
|
+
});
|
|
58
|
+
return findings;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await execFileAsync('npx', ['prisma', 'db', 'seed'], { shell: true, cwd });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const stderr = error instanceof Error && 'stderr' in error
|
|
65
|
+
? String((error as NodeJS.ErrnoException & { stderr?: string }).stderr)
|
|
66
|
+
: String(error);
|
|
67
|
+
findings.push({
|
|
68
|
+
id: `${this.agentId}-db-seed`,
|
|
69
|
+
type: 'infra-issue',
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
agentId: this.agentId,
|
|
72
|
+
module: 'database',
|
|
73
|
+
description: `prisma db seed failed: ${stderr}`,
|
|
74
|
+
});
|
|
75
|
+
return findings;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
7
|
+
|
|
8
|
+
// Mock node:child_process before importing the agent
|
|
9
|
+
vi.mock('node:child_process', () => ({
|
|
10
|
+
execFile: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Use real node:fs/promises so writeFile actually writes to the temp runDir
|
|
14
|
+
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
16
|
+
import { AnalystAgent } from '../01-analyst';
|
|
17
|
+
|
|
18
|
+
const mockedExecFile = vi.mocked(execFile);
|
|
19
|
+
|
|
20
|
+
function makeExecFileCallback(stdout: string, stderr = '', error: Error | null = null) {
|
|
21
|
+
return (...args: unknown[]) => {
|
|
22
|
+
// execFile may be called with or without an options object:
|
|
23
|
+
// execFile(file, args, cb) OR execFile(file, args, options, cb)
|
|
24
|
+
const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string; stderr: string }) => void;
|
|
25
|
+
cb(error, { stdout, stderr });
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stubConfig: ValidatedConfig = {
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
name: 'test',
|
|
32
|
+
auth: { loginUrl: 'http://localhost' },
|
|
33
|
+
modules: [
|
|
34
|
+
{ id: 'triage', route: '/triage', sidebarIcon: true },
|
|
35
|
+
{ id: 'clients', route: '/clients', sidebarIcon: true },
|
|
36
|
+
{ id: 'dashboard', route: '/dashboard', sidebarIcon: false },
|
|
37
|
+
],
|
|
38
|
+
environments: {},
|
|
39
|
+
portals: [],
|
|
40
|
+
breakpoints: [],
|
|
41
|
+
integrations: [],
|
|
42
|
+
workers: [],
|
|
43
|
+
workflows: [],
|
|
44
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
45
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
46
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
47
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
48
|
+
thresholds: {
|
|
49
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
50
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
51
|
+
apiResponseTime: 2000,
|
|
52
|
+
bundleSizeLimit: 500000,
|
|
53
|
+
maxConsoleErrors: 0,
|
|
54
|
+
},
|
|
55
|
+
disabledAgents: [],
|
|
56
|
+
disabledAgentSet: new Set<number>(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function createAgent(runDir: string): AnalystAgent {
|
|
60
|
+
return new AnalystAgent(stubConfig, 'alpha', runDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('AnalystAgent', () => {
|
|
64
|
+
let runDir: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
runDir = mkdtempSync(join(tmpdir(), 'analyst-test-'));
|
|
68
|
+
vi.resetAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('identifies correct modules from 2 changed files', async () => {
|
|
72
|
+
mockedExecFile
|
|
73
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never) // preFlight
|
|
74
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never) // detectDefaultBranch: git rev-parse --verify main
|
|
75
|
+
.mockImplementationOnce(makeExecFileCallback('src/pages/triage/TriagePage.tsx\nsrc/pages/clients/ClientList.tsx\n') as never); // git diff
|
|
76
|
+
|
|
77
|
+
const agent = createAgent(runDir);
|
|
78
|
+
const result = await agent.run();
|
|
79
|
+
|
|
80
|
+
expect(result.status).toBe('passed');
|
|
81
|
+
expect(result.findings).toHaveLength(0);
|
|
82
|
+
|
|
83
|
+
const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
|
|
84
|
+
const triageModule = testPlan.modules.find((m: { id: string }) => m.id === 'triage');
|
|
85
|
+
const clientsModule = testPlan.modules.find((m: { id: string }) => m.id === 'clients');
|
|
86
|
+
const dashboardModule = testPlan.modules.find((m: { id: string }) => m.id === 'dashboard');
|
|
87
|
+
|
|
88
|
+
expect(triageModule.risk).toBe('high');
|
|
89
|
+
expect(triageModule.changedFiles).toContain('src/pages/triage/TriagePage.tsx');
|
|
90
|
+
|
|
91
|
+
expect(clientsModule.risk).toBe('high');
|
|
92
|
+
expect(clientsModule.changedFiles).toContain('src/pages/clients/ClientList.tsx');
|
|
93
|
+
|
|
94
|
+
expect(dashboardModule.risk).toBe('low');
|
|
95
|
+
expect(dashboardModule.changedFiles).toHaveLength(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('marks all modules critical when AuthContext is changed', async () => {
|
|
99
|
+
mockedExecFile
|
|
100
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
|
|
101
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never)
|
|
102
|
+
.mockImplementationOnce(makeExecFileCallback('src/context/AuthContext.tsx\n') as never);
|
|
103
|
+
|
|
104
|
+
const agent = createAgent(runDir);
|
|
105
|
+
const result = await agent.run();
|
|
106
|
+
|
|
107
|
+
expect(result.status).toBe('passed');
|
|
108
|
+
|
|
109
|
+
const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
|
|
110
|
+
for (const module of testPlan.modules) {
|
|
111
|
+
expect(module.risk).toBe('critical');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('marks all modules critical when App.tsx is changed', async () => {
|
|
116
|
+
mockedExecFile
|
|
117
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
|
|
118
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never)
|
|
119
|
+
.mockImplementationOnce(makeExecFileCallback('src/App.tsx\n') as never);
|
|
120
|
+
|
|
121
|
+
const agent = createAgent(runDir);
|
|
122
|
+
await agent.run();
|
|
123
|
+
|
|
124
|
+
const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
|
|
125
|
+
for (const module of testPlan.modules) {
|
|
126
|
+
expect(module.risk).toBe('critical');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('marks all modules critical when router file is changed', async () => {
|
|
131
|
+
mockedExecFile
|
|
132
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
|
|
133
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never)
|
|
134
|
+
.mockImplementationOnce(makeExecFileCallback('src/router/index.ts\n') as never);
|
|
135
|
+
|
|
136
|
+
const agent = createAgent(runDir);
|
|
137
|
+
await agent.run();
|
|
138
|
+
|
|
139
|
+
const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
|
|
140
|
+
for (const module of testPlan.modules) {
|
|
141
|
+
expect(module.risk).toBe('critical');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('produces infra-issue finding when git fails', async () => {
|
|
146
|
+
const gitError = new Error('git: command not found');
|
|
147
|
+
const failImpl = makeExecFileCallback('', 'git: command not found', gitError);
|
|
148
|
+
mockedExecFile.mockImplementationOnce(failImpl as never);
|
|
149
|
+
|
|
150
|
+
const agent = createAgent(runDir);
|
|
151
|
+
const result = await agent.run();
|
|
152
|
+
|
|
153
|
+
expect(result.status).toBe('failed');
|
|
154
|
+
expect(result.findings).toHaveLength(1);
|
|
155
|
+
expect(result.findings[0].type).toBe('infra-issue');
|
|
156
|
+
expect(result.findings[0].severity).toBe('critical');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('assigns low risk to modules with no changed files and no shared dependency changes', async () => {
|
|
160
|
+
mockedExecFile
|
|
161
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
|
|
162
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never)
|
|
163
|
+
.mockImplementationOnce(makeExecFileCallback('src/utils/formatDate.ts\n') as never);
|
|
164
|
+
|
|
165
|
+
const agent = createAgent(runDir);
|
|
166
|
+
await agent.run();
|
|
167
|
+
|
|
168
|
+
const testPlan = JSON.parse(readFileSync(join(runDir, 'test-plan.json'), 'utf-8'));
|
|
169
|
+
for (const module of testPlan.modules) {
|
|
170
|
+
expect(module.risk).toBe('low');
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('writes test-plan.json to the run directory', async () => {
|
|
175
|
+
mockedExecFile
|
|
176
|
+
.mockImplementationOnce(makeExecFileCallback('git version 2.40.0') as never)
|
|
177
|
+
.mockImplementationOnce(makeExecFileCallback('main\n') as never)
|
|
178
|
+
.mockImplementationOnce(makeExecFileCallback('') as never);
|
|
179
|
+
|
|
180
|
+
const agent = createAgent(runDir);
|
|
181
|
+
await agent.run();
|
|
182
|
+
|
|
183
|
+
const testPlanPath = join(runDir, 'test-plan.json');
|
|
184
|
+
const testPlan = JSON.parse(readFileSync(testPlanPath, 'utf-8'));
|
|
185
|
+
expect(testPlan).toHaveProperty('modules');
|
|
186
|
+
expect(Array.isArray(testPlan.modules)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
3
|
+
|
|
4
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
5
|
+
const actual = await importOriginal<typeof import('node:fs')>();
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
existsSync: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
vi.mock('../../helpers/seed-validator', () => ({
|
|
13
|
+
validateSeedCompleteness: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { validateSeedCompleteness } from '../../helpers/seed-validator';
|
|
18
|
+
import { SeedArchitectAgent } from '../02-seed-architect';
|
|
19
|
+
|
|
20
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
21
|
+
const mockedValidateSeedCompleteness = vi.mocked(validateSeedCompleteness);
|
|
22
|
+
|
|
23
|
+
const stubConfig: ValidatedConfig = {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
name: 'test',
|
|
26
|
+
auth: { loginUrl: 'http://localhost' },
|
|
27
|
+
modules: [
|
|
28
|
+
{ id: 'triage', route: '/triage', sidebarIcon: true },
|
|
29
|
+
{ id: 'clients', route: '/clients', sidebarIcon: true },
|
|
30
|
+
{ id: 'dashboard', route: '/dashboard', sidebarIcon: false },
|
|
31
|
+
{ id: 'reports', route: '/reports', sidebarIcon: true },
|
|
32
|
+
],
|
|
33
|
+
environments: {},
|
|
34
|
+
portals: [],
|
|
35
|
+
breakpoints: [],
|
|
36
|
+
integrations: [],
|
|
37
|
+
workers: [],
|
|
38
|
+
workflows: [],
|
|
39
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
40
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
41
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
42
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
43
|
+
thresholds: {
|
|
44
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
45
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
46
|
+
apiResponseTime: 2000,
|
|
47
|
+
bundleSizeLimit: 500000,
|
|
48
|
+
maxConsoleErrors: 0,
|
|
49
|
+
},
|
|
50
|
+
disabledAgents: [],
|
|
51
|
+
disabledAgentSet: new Set<number>(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function createAgent(): SeedArchitectAgent {
|
|
55
|
+
return new SeedArchitectAgent(stubConfig, 'alpha', '/tmp/test-run');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('SeedArchitectAgent', () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.resetAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('has agentId 2 and agentName Seed Architect', () => {
|
|
64
|
+
const agent = createAgent();
|
|
65
|
+
expect(agent.agentId).toBe(2);
|
|
66
|
+
expect(agent.agentName).toBe('Seed Architect');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns 3 medium findings when seed validator reports 3 missing modules', async () => {
|
|
70
|
+
mockedExistsSync.mockReturnValue(true);
|
|
71
|
+
mockedValidateSeedCompleteness.mockResolvedValue({
|
|
72
|
+
covered: ['triage'],
|
|
73
|
+
missing: ['clients', 'dashboard', 'reports'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const agent = createAgent();
|
|
77
|
+
const result = await agent.run();
|
|
78
|
+
|
|
79
|
+
expect(result.status).toBe('passed'); // medium severity does not fail
|
|
80
|
+
expect(result.findings).toHaveLength(3);
|
|
81
|
+
|
|
82
|
+
for (const finding of result.findings) {
|
|
83
|
+
expect(finding.type).toBe('code-bug-logic');
|
|
84
|
+
expect(finding.severity).toBe('medium');
|
|
85
|
+
expect(finding.agentId).toBe(2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const missingModules = result.findings.map(f => f.module);
|
|
89
|
+
expect(missingModules).toContain('clients');
|
|
90
|
+
expect(missingModules).toContain('dashboard');
|
|
91
|
+
expect(missingModules).toContain('reports');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns no findings when all modules have seed coverage', async () => {
|
|
95
|
+
mockedExistsSync.mockReturnValue(true);
|
|
96
|
+
mockedValidateSeedCompleteness.mockResolvedValue({
|
|
97
|
+
covered: ['triage', 'clients', 'dashboard', 'reports'],
|
|
98
|
+
missing: [],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const agent = createAgent();
|
|
102
|
+
const result = await agent.run();
|
|
103
|
+
|
|
104
|
+
expect(result.status).toBe('passed');
|
|
105
|
+
expect(result.findings).toHaveLength(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns warning finding when seed file does not exist', async () => {
|
|
109
|
+
mockedExistsSync.mockReturnValue(false);
|
|
110
|
+
|
|
111
|
+
const agent = createAgent();
|
|
112
|
+
const result = await agent.run();
|
|
113
|
+
|
|
114
|
+
expect(result.status).toBe('passed'); // medium severity does not fail
|
|
115
|
+
expect(result.findings).toHaveLength(1);
|
|
116
|
+
expect(result.findings[0].type).toBe('infra-issue');
|
|
117
|
+
expect(result.findings[0].severity).toBe('medium');
|
|
118
|
+
expect(result.findings[0].description).toContain('Seed file not found');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('passes module IDs from config to seed validator', async () => {
|
|
122
|
+
mockedExistsSync.mockReturnValue(true);
|
|
123
|
+
mockedValidateSeedCompleteness.mockResolvedValue({
|
|
124
|
+
covered: ['triage', 'clients', 'dashboard', 'reports'],
|
|
125
|
+
missing: [],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const agent = createAgent();
|
|
129
|
+
await agent.run();
|
|
130
|
+
|
|
131
|
+
expect(mockedValidateSeedCompleteness).toHaveBeenCalledOnce();
|
|
132
|
+
const [, moduleIds] = mockedValidateSeedCompleteness.mock.calls[0];
|
|
133
|
+
expect(moduleIds).toContain('triage');
|
|
134
|
+
expect(moduleIds).toContain('clients');
|
|
135
|
+
expect(moduleIds).toContain('dashboard');
|
|
136
|
+
expect(moduleIds).toContain('reports');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('includes suggestedFix in each finding for missing modules', async () => {
|
|
140
|
+
mockedExistsSync.mockReturnValue(true);
|
|
141
|
+
mockedValidateSeedCompleteness.mockResolvedValue({
|
|
142
|
+
covered: [],
|
|
143
|
+
missing: ['triage'],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const agent = createAgent();
|
|
147
|
+
const result = await agent.run();
|
|
148
|
+
|
|
149
|
+
expect(result.findings[0].suggestedFix).toBeTruthy();
|
|
150
|
+
expect(result.findings[0].suggestedFix).toContain('triage');
|
|
151
|
+
});
|
|
152
|
+
});
|