@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,253 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFile } from 'child_process';
|
|
4
|
+
import { BaseAgent } from './base-agent';
|
|
5
|
+
import { classifyFinding, shouldAutoCommit, shouldRequireHumanReview } from '../core/fix-loop';
|
|
6
|
+
import { validateCodeQuality } from '../helpers/quality-gate';
|
|
7
|
+
import type { Finding, FindingType } from '../core/types';
|
|
8
|
+
|
|
9
|
+
/** Maximum number of quality-gate retry attempts per finding. */
|
|
10
|
+
const MAX_ATTEMPTS = 3;
|
|
11
|
+
|
|
12
|
+
/** The date-stamped fix branch name prefix. */
|
|
13
|
+
const FIX_BRANCH_PREFIX = 'fix/testteam-';
|
|
14
|
+
|
|
15
|
+
/** Extended finding with resolution metadata. */
|
|
16
|
+
export interface ResolvedFinding extends Finding {
|
|
17
|
+
autoCommit: boolean;
|
|
18
|
+
requiresHumanReview: boolean;
|
|
19
|
+
resolutionStatus: 'resolved' | 'escalated' | 'pending-review';
|
|
20
|
+
attemptCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function execFilePromise(
|
|
24
|
+
file: string,
|
|
25
|
+
args: string[],
|
|
26
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
execFile(file, args, { shell: true }, (error, stdout, stderr) => {
|
|
29
|
+
if (error) {
|
|
30
|
+
reject(error);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getCurrentBranch(): Promise<string> {
|
|
39
|
+
const result = await execFilePromise('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function isOnFixBranch(): Promise<boolean> {
|
|
44
|
+
try {
|
|
45
|
+
const branch = await getCurrentBranch();
|
|
46
|
+
return branch.startsWith('fix/') || branch.startsWith('hotfix/');
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generates a code patch suggestion for a code-bug finding.
|
|
54
|
+
* Returns a plain-English description (not actual source code) so it can be
|
|
55
|
+
* reviewed by a developer or fed to a downstream AI patch agent.
|
|
56
|
+
*/
|
|
57
|
+
export function generatePatchSuggestion(finding: Finding, findingType: FindingType): string {
|
|
58
|
+
switch (findingType) {
|
|
59
|
+
case 'code-bug-cosmetic':
|
|
60
|
+
return (
|
|
61
|
+
`[Fixer] Cosmetic patch for "${finding.module}": ` +
|
|
62
|
+
`${finding.suggestedFix ?? 'Review and apply the minimal style or copy change indicated by the finding description.'}`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
case 'code-bug-security':
|
|
66
|
+
return (
|
|
67
|
+
`[Fixer] Security patch for "${finding.module}": ` +
|
|
68
|
+
`${finding.suggestedFix ?? 'Apply the security hardening indicated by the finding. ' +
|
|
69
|
+
'Validate input, sanitise output, and ensure no sensitive data is exposed. ' +
|
|
70
|
+
'Human review is mandatory before merging.'}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
case 'code-bug-logic':
|
|
74
|
+
return (
|
|
75
|
+
`[Fixer] Logic patch for "${finding.module}": ` +
|
|
76
|
+
`${finding.suggestedFix ?? 'Review the business logic at the indicated file/line. ' +
|
|
77
|
+
'Ensure edge cases and error paths are handled correctly. ' +
|
|
78
|
+
'Human review is mandatory before merging.'}`
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
return (
|
|
83
|
+
`[Fixer] Patch for "${finding.module}": ` +
|
|
84
|
+
`${finding.suggestedFix ?? 'Manual investigation required.'}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class FixerAgent extends BaseAgent {
|
|
90
|
+
readonly agentId = 11;
|
|
91
|
+
readonly agentName = 'Fixer';
|
|
92
|
+
|
|
93
|
+
/** Set to true when there are no findings to process — makes execute() a no-op. */
|
|
94
|
+
private noFindings = false;
|
|
95
|
+
|
|
96
|
+
protected async preFlight(): Promise<void> {
|
|
97
|
+
const codeBugsDir = path.join(this.runDir, 'findings', 'code-bugs');
|
|
98
|
+
let entries: string[];
|
|
99
|
+
try {
|
|
100
|
+
entries = await fs.readdir(codeBugsDir);
|
|
101
|
+
} catch (err: unknown) {
|
|
102
|
+
if (isNodeError(err) && err.code === 'ENOENT') {
|
|
103
|
+
this.noFindings = true;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const jsonFiles = entries.filter((e) => e.endsWith('.json'));
|
|
110
|
+
if (jsonFiles.length === 0) {
|
|
111
|
+
this.noFindings = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify we are on a fix branch, or that git is available so we can create one
|
|
116
|
+
try {
|
|
117
|
+
await execFilePromise('git', ['--version']);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new Error('[Fixer] preFlight failed: git is not available — cannot create fix branch');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected async execute(): Promise<Finding[]> {
|
|
124
|
+
if (this.noFindings) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const codeBugsDir = path.join(this.runDir, 'findings', 'code-bugs');
|
|
129
|
+
|
|
130
|
+
let entries: string[];
|
|
131
|
+
try {
|
|
132
|
+
entries = await fs.readdir(codeBugsDir);
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const jsonFiles = entries.filter((e) => e.endsWith('.json'));
|
|
138
|
+
const resultFindings: Finding[] = [];
|
|
139
|
+
|
|
140
|
+
// Ensure we are on a fix branch before processing findings
|
|
141
|
+
await this.ensureFixBranch();
|
|
142
|
+
|
|
143
|
+
for (const filename of jsonFiles) {
|
|
144
|
+
const filePath = path.join(codeBugsDir, filename);
|
|
145
|
+
let finding: Finding;
|
|
146
|
+
try {
|
|
147
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
148
|
+
finding = JSON.parse(raw) as Finding;
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const resolved = await this.processFinding(finding, filePath);
|
|
154
|
+
resultFindings.push(resolved);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return resultFindings;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async processFinding(finding: Finding, filePath: string): Promise<ResolvedFinding> {
|
|
161
|
+
// Use classifyFinding to determine canonical type (changedFiles is empty — no source changes)
|
|
162
|
+
const findingType = classifyFinding(finding, []);
|
|
163
|
+
const autoCommit = shouldAutoCommit(findingType, this.phase);
|
|
164
|
+
const requiresHumanReview = shouldRequireHumanReview(findingType, this.phase);
|
|
165
|
+
|
|
166
|
+
let patchSuggestion = generatePatchSuggestion(finding, findingType);
|
|
167
|
+
let attemptCount = 0;
|
|
168
|
+
let qualityPassed = false;
|
|
169
|
+
|
|
170
|
+
while (attemptCount < MAX_ATTEMPTS) {
|
|
171
|
+
attemptCount += 1;
|
|
172
|
+
const violations = validateCodeQuality(patchSuggestion, `fixer-patch-${finding.id}.ts`);
|
|
173
|
+
const hasErrors = violations.some((v) => v.severity === 'error');
|
|
174
|
+
|
|
175
|
+
if (!hasErrors) {
|
|
176
|
+
qualityPassed = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Attempt to refine the patch suggestion by appending quality feedback
|
|
181
|
+
if (attemptCount < MAX_ATTEMPTS) {
|
|
182
|
+
patchSuggestion =
|
|
183
|
+
`${patchSuggestion}\n` +
|
|
184
|
+
`// Quality gate feedback (attempt ${attemptCount}): ` +
|
|
185
|
+
violations.map((v) => v.message).join('; ');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const resolutionStatus: ResolvedFinding['resolutionStatus'] = !qualityPassed
|
|
190
|
+
? 'escalated'
|
|
191
|
+
: requiresHumanReview
|
|
192
|
+
? 'pending-review'
|
|
193
|
+
: 'resolved';
|
|
194
|
+
|
|
195
|
+
const resolvedFinding: ResolvedFinding = {
|
|
196
|
+
...finding,
|
|
197
|
+
suggestedFix: qualityPassed
|
|
198
|
+
? patchSuggestion
|
|
199
|
+
: `[Fixer] Escalated after ${MAX_ATTEMPTS} attempts — quality gate could not be satisfied.`,
|
|
200
|
+
autoCommit,
|
|
201
|
+
requiresHumanReview,
|
|
202
|
+
resolutionStatus,
|
|
203
|
+
attemptCount,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
this.addEvidence({
|
|
207
|
+
type: 'log',
|
|
208
|
+
path: filePath,
|
|
209
|
+
description:
|
|
210
|
+
`Fixer processed "${finding.id}" type="${findingType}" ` +
|
|
211
|
+
`autoCommit=${autoCommit} humanReview=${requiresHumanReview} ` +
|
|
212
|
+
`status=${resolutionStatus} attempts=${attemptCount}`,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Write updated finding back to disk
|
|
216
|
+
await fs.writeFile(filePath, JSON.stringify(resolvedFinding, null, 2), 'utf-8');
|
|
217
|
+
|
|
218
|
+
return resolvedFinding;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async ensureFixBranch(): Promise<void> {
|
|
222
|
+
try {
|
|
223
|
+
const onFix = await isOnFixBranch();
|
|
224
|
+
if (onFix) return;
|
|
225
|
+
|
|
226
|
+
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
227
|
+
const branchName = `${FIX_BRANCH_PREFIX}${date}`;
|
|
228
|
+
|
|
229
|
+
await execFilePromise('git', ['checkout', '-b', branchName]);
|
|
230
|
+
|
|
231
|
+
this.addEvidence({
|
|
232
|
+
type: 'log',
|
|
233
|
+
path: this.runDir,
|
|
234
|
+
description: `Fixer created fix branch: ${branchName}`,
|
|
235
|
+
});
|
|
236
|
+
} catch {
|
|
237
|
+
// If branch creation fails (e.g. branch already exists or git issue) we continue —
|
|
238
|
+
// the findings are still processed and the branch state is best-effort.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Internal helpers
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
interface NodeError extends Error {
|
|
248
|
+
code: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isNodeError(err: unknown): err is NodeError {
|
|
252
|
+
return err instanceof Error && 'code' in err;
|
|
253
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { stat, readdir } from 'node:fs/promises';
|
|
2
|
+
import type { Browser, Page } from 'playwright';
|
|
3
|
+
import type { Finding } from '../core/types';
|
|
4
|
+
import { BaseAgent } from './base-agent';
|
|
5
|
+
|
|
6
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
7
|
+
try { await stat(p); return true; } catch { return false; }
|
|
8
|
+
}
|
|
9
|
+
import { login, navigateToSection } from '../helpers/navigation';
|
|
10
|
+
import { captureScreenshot } from '../helpers/screenshot';
|
|
11
|
+
|
|
12
|
+
/** Selectors that indicate an ErrorBoundary rendered in the application. */
|
|
13
|
+
const ERROR_BOUNDARY_SELECTORS = [
|
|
14
|
+
'[data-testid="error-boundary"]',
|
|
15
|
+
'[data-testid="error-fallback"]',
|
|
16
|
+
'.error-boundary',
|
|
17
|
+
'[role="alert"]',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Selectors that indicate an empty-state placeholder is rendered. */
|
|
21
|
+
const EMPTY_STATE_SELECTORS = [
|
|
22
|
+
'[data-testid="empty-state"]',
|
|
23
|
+
'.empty-state',
|
|
24
|
+
'[aria-label="No data"]',
|
|
25
|
+
'[data-testid="no-results"]',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/** Selectors for inline form validation messages. */
|
|
29
|
+
const VALIDATION_MESSAGE_SELECTORS = [
|
|
30
|
+
'[data-testid="field-error"]',
|
|
31
|
+
'.field-error',
|
|
32
|
+
'[aria-live="polite"]',
|
|
33
|
+
'[role="alert"]',
|
|
34
|
+
'.form-error',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export class UxInspectorAgent extends BaseAgent {
|
|
38
|
+
readonly agentId = 12;
|
|
39
|
+
readonly agentName = 'UX Inspector';
|
|
40
|
+
|
|
41
|
+
protected async preFlight(): Promise<void> {
|
|
42
|
+
const runDirExists = await fileExists(this.runDir);
|
|
43
|
+
if (!runDirExists) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Run directory does not exist: ${this.runDir}. ` +
|
|
46
|
+
'Browser Crawler (Agent 5) must run first to populate screenshots.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = await readdir(this.runDir);
|
|
51
|
+
const screenshotFiles = files.filter((f) => f.endsWith('.png'));
|
|
52
|
+
|
|
53
|
+
if (screenshotFiles.length === 0) {
|
|
54
|
+
// Advisory agent — missing screenshots is informational, not a gate blocker.
|
|
55
|
+
// preFlight will succeed; execute() will handle the absence gracefully.
|
|
56
|
+
}
|
|
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: '12-no-env',
|
|
70
|
+
type: 'infra-issue',
|
|
71
|
+
severity: 'critical',
|
|
72
|
+
agentId: this.agentId,
|
|
73
|
+
module: 'ux-inspector',
|
|
74
|
+
description: 'No environment configured — cannot determine base URL',
|
|
75
|
+
});
|
|
76
|
+
return findings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
80
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${env.baseUrl}${rawLoginUrl}`;
|
|
81
|
+
const credentials =
|
|
82
|
+
this.config.auth.credentials?.['admin'] ??
|
|
83
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
84
|
+
|
|
85
|
+
if (!credentials) {
|
|
86
|
+
findings.push({
|
|
87
|
+
id: '12-no-credentials',
|
|
88
|
+
type: 'infra-issue',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
agentId: this.agentId,
|
|
91
|
+
module: 'ux-inspector',
|
|
92
|
+
description: 'No credentials configured in auth.credentials — cannot log in',
|
|
93
|
+
});
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { chromium } = await import('playwright');
|
|
98
|
+
|
|
99
|
+
let browser: Browser | null = null;
|
|
100
|
+
let page: Page | null = null;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
browser = await chromium.launch({ headless: true });
|
|
104
|
+
page = await browser.newPage();
|
|
105
|
+
|
|
106
|
+
await login(page, credentials, loginUrl);
|
|
107
|
+
|
|
108
|
+
for (const module of this.config.modules) {
|
|
109
|
+
const moduleFindings = await this.inspectModule(page, module.id);
|
|
110
|
+
findings.push(...moduleFindings);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
if (page) await page.close().catch(() => undefined);
|
|
114
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return findings;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async inspectModule(
|
|
121
|
+
page: Page,
|
|
122
|
+
moduleId: string,
|
|
123
|
+
): Promise<Finding[]> {
|
|
124
|
+
const findings: Finding[] = [];
|
|
125
|
+
|
|
126
|
+
// Navigate to module
|
|
127
|
+
try {
|
|
128
|
+
await navigateToSection(page, moduleId, this.config);
|
|
129
|
+
} catch (navErr) {
|
|
130
|
+
findings.push({
|
|
131
|
+
id: `12-nav-${moduleId}`,
|
|
132
|
+
type: 'infra-issue',
|
|
133
|
+
severity: 'high',
|
|
134
|
+
agentId: this.agentId,
|
|
135
|
+
module: moduleId,
|
|
136
|
+
description: `Navigation to module "${moduleId}" failed: ${navErr instanceof Error ? navErr.message : String(navErr)}`,
|
|
137
|
+
});
|
|
138
|
+
return findings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check loading states: spinners/skeletons should eventually disappear
|
|
142
|
+
const loadingVisible = await this.checkLoadingStatesStuck(page);
|
|
143
|
+
if (loadingVisible) {
|
|
144
|
+
const screenshotPath = await captureScreenshot(
|
|
145
|
+
page,
|
|
146
|
+
`ux-loading-stuck-${moduleId}`,
|
|
147
|
+
this.runDir,
|
|
148
|
+
);
|
|
149
|
+
this.addEvidence({
|
|
150
|
+
type: 'screenshot',
|
|
151
|
+
path: screenshotPath,
|
|
152
|
+
description: `Loading spinner/skeleton stuck in module "${moduleId}"`,
|
|
153
|
+
});
|
|
154
|
+
findings.push({
|
|
155
|
+
id: `12-loading-stuck-${moduleId}`,
|
|
156
|
+
type: 'code-bug-cosmetic',
|
|
157
|
+
severity: 'medium',
|
|
158
|
+
agentId: this.agentId,
|
|
159
|
+
module: moduleId,
|
|
160
|
+
description: `Loading indicator remains visible after navigation settled in module "${moduleId}"`,
|
|
161
|
+
screenshot: screenshotPath,
|
|
162
|
+
suggestedFix: 'Ensure loading state is cleared when data resolves or errors occur',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Simulate API error: intercept routes returning 500, then reload
|
|
167
|
+
await this.checkErrorBoundaryOnApiError(page, moduleId, findings);
|
|
168
|
+
|
|
169
|
+
// Re-navigate after route interception test
|
|
170
|
+
try {
|
|
171
|
+
await page.unroute('**/*');
|
|
172
|
+
await navigateToSection(page, moduleId, this.config);
|
|
173
|
+
} catch {
|
|
174
|
+
// Best-effort re-navigation; continue to next checks
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check for empty states
|
|
178
|
+
await this.checkEmptyStates(page, moduleId, findings);
|
|
179
|
+
|
|
180
|
+
// Check form validation inline messages
|
|
181
|
+
await this.checkFormValidation(page, moduleId, findings);
|
|
182
|
+
|
|
183
|
+
// Check for truncated text
|
|
184
|
+
await this.checkTruncatedText(page, moduleId, findings);
|
|
185
|
+
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async checkLoadingStatesStuck(page: Page): Promise<boolean> {
|
|
190
|
+
try {
|
|
191
|
+
const loadingSelectors = [
|
|
192
|
+
'[data-testid="loading-spinner"]',
|
|
193
|
+
'[data-testid="skeleton"]',
|
|
194
|
+
'.loading-spinner',
|
|
195
|
+
'.skeleton',
|
|
196
|
+
'[aria-label="Loading"]',
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const selector of loadingSelectors) {
|
|
200
|
+
const isVisible = await page.isVisible(selector).catch(() => false);
|
|
201
|
+
if (isVisible) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async checkErrorBoundaryOnApiError(
|
|
212
|
+
page: Page,
|
|
213
|
+
moduleId: string,
|
|
214
|
+
findings: Finding[],
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
try {
|
|
217
|
+
// Intercept all API calls and return 500
|
|
218
|
+
await page.route('**/api/**', (route) => {
|
|
219
|
+
return route.fulfill({
|
|
220
|
+
status: 500,
|
|
221
|
+
contentType: 'application/json',
|
|
222
|
+
body: JSON.stringify({ error: 'Internal Server Error' }),
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Reload to trigger the intercepted error
|
|
227
|
+
await page.reload({ waitUntil: 'domcontentloaded' }).catch(() => undefined);
|
|
228
|
+
|
|
229
|
+
// Wait briefly for React to render error boundary
|
|
230
|
+
await page.waitForTimeout(1000).catch(() => undefined);
|
|
231
|
+
|
|
232
|
+
let errorBoundaryFound = false;
|
|
233
|
+
for (const selector of ERROR_BOUNDARY_SELECTORS) {
|
|
234
|
+
const visible = await page.isVisible(selector).catch(() => false);
|
|
235
|
+
if (visible) {
|
|
236
|
+
errorBoundaryFound = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!errorBoundaryFound) {
|
|
242
|
+
const screenshotPath = await captureScreenshot(
|
|
243
|
+
page,
|
|
244
|
+
`ux-error-boundary-missing-${moduleId}`,
|
|
245
|
+
this.runDir,
|
|
246
|
+
);
|
|
247
|
+
this.addEvidence({
|
|
248
|
+
type: 'screenshot',
|
|
249
|
+
path: screenshotPath,
|
|
250
|
+
description: `No ErrorBoundary rendered during API error in module "${moduleId}"`,
|
|
251
|
+
});
|
|
252
|
+
findings.push({
|
|
253
|
+
id: `12-error-boundary-${moduleId}`,
|
|
254
|
+
type: 'code-bug-logic',
|
|
255
|
+
severity: 'high',
|
|
256
|
+
agentId: this.agentId,
|
|
257
|
+
module: moduleId,
|
|
258
|
+
description: `Module "${moduleId}" does not render an ErrorBoundary when API returns 500`,
|
|
259
|
+
screenshot: screenshotPath,
|
|
260
|
+
suggestedFix:
|
|
261
|
+
'Wrap module content with a React ErrorBoundary component that renders a user-friendly fallback',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Remove interception after test
|
|
266
|
+
await page.unroute('**/api/**');
|
|
267
|
+
} catch {
|
|
268
|
+
// Route interception not supported or failed — skip this check
|
|
269
|
+
await page.unroute('**/api/**').catch(() => undefined);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async checkEmptyStates(
|
|
274
|
+
page: Page,
|
|
275
|
+
moduleId: string,
|
|
276
|
+
findings: Finding[],
|
|
277
|
+
): Promise<void> {
|
|
278
|
+
try {
|
|
279
|
+
// Look for data containers that are empty (no rows, no items)
|
|
280
|
+
const hasDataRows = await page
|
|
281
|
+
.evaluate(() => {
|
|
282
|
+
const tables = document.querySelectorAll('table tbody tr');
|
|
283
|
+
const listItems = document.querySelectorAll('[data-testid="list-item"]');
|
|
284
|
+
const cards = document.querySelectorAll('[data-testid="card"]');
|
|
285
|
+
return tables.length > 0 || listItems.length > 0 || cards.length > 0;
|
|
286
|
+
})
|
|
287
|
+
.catch(() => true); // If evaluation fails, assume data present
|
|
288
|
+
|
|
289
|
+
if (!hasDataRows) {
|
|
290
|
+
// Check if an empty state indicator is shown
|
|
291
|
+
let emptyStateShown = false;
|
|
292
|
+
for (const selector of EMPTY_STATE_SELECTORS) {
|
|
293
|
+
const visible = await page.isVisible(selector).catch(() => false);
|
|
294
|
+
if (visible) {
|
|
295
|
+
emptyStateShown = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!emptyStateShown) {
|
|
301
|
+
const screenshotPath = await captureScreenshot(
|
|
302
|
+
page,
|
|
303
|
+
`ux-empty-state-missing-${moduleId}`,
|
|
304
|
+
this.runDir,
|
|
305
|
+
);
|
|
306
|
+
this.addEvidence({
|
|
307
|
+
type: 'screenshot',
|
|
308
|
+
path: screenshotPath,
|
|
309
|
+
description: `No empty-state placeholder in module "${moduleId}" when data is absent`,
|
|
310
|
+
});
|
|
311
|
+
findings.push({
|
|
312
|
+
id: `12-empty-state-${moduleId}`,
|
|
313
|
+
type: 'code-bug-cosmetic',
|
|
314
|
+
severity: 'low',
|
|
315
|
+
agentId: this.agentId,
|
|
316
|
+
module: moduleId,
|
|
317
|
+
description: `Module "${moduleId}" shows no empty-state message when there are no data rows`,
|
|
318
|
+
screenshot: screenshotPath,
|
|
319
|
+
suggestedFix:
|
|
320
|
+
'Add an empty-state component (e.g. <EmptyState />) when list/table has zero records',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Best-effort check
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async checkFormValidation(
|
|
330
|
+
page: Page,
|
|
331
|
+
moduleId: string,
|
|
332
|
+
findings: Finding[],
|
|
333
|
+
): Promise<void> {
|
|
334
|
+
try {
|
|
335
|
+
// Look for a form with required fields and attempt to submit it empty
|
|
336
|
+
const hasForm = await page.isVisible('form').catch(() => false);
|
|
337
|
+
if (!hasForm) return;
|
|
338
|
+
|
|
339
|
+
const hasRequiredField = await page
|
|
340
|
+
.evaluate(() => document.querySelectorAll('input[required], select[required], textarea[required]').length > 0)
|
|
341
|
+
.catch(() => false);
|
|
342
|
+
|
|
343
|
+
if (!hasRequiredField) return;
|
|
344
|
+
|
|
345
|
+
// Submit the form without filling any fields
|
|
346
|
+
const submitButton = page.locator('button[type="submit"]').first();
|
|
347
|
+
const submitVisible = await submitButton.isVisible().catch(() => false);
|
|
348
|
+
if (!submitVisible) return;
|
|
349
|
+
|
|
350
|
+
await submitButton.click().catch(() => undefined);
|
|
351
|
+
|
|
352
|
+
// Wait briefly for validation to appear
|
|
353
|
+
await page.waitForTimeout(500).catch(() => undefined);
|
|
354
|
+
|
|
355
|
+
let validationShown = false;
|
|
356
|
+
for (const selector of VALIDATION_MESSAGE_SELECTORS) {
|
|
357
|
+
const visible = await page.isVisible(selector).catch(() => false);
|
|
358
|
+
if (visible) {
|
|
359
|
+
validationShown = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!validationShown) {
|
|
365
|
+
const screenshotPath = await captureScreenshot(
|
|
366
|
+
page,
|
|
367
|
+
`ux-form-validation-missing-${moduleId}`,
|
|
368
|
+
this.runDir,
|
|
369
|
+
);
|
|
370
|
+
this.addEvidence({
|
|
371
|
+
type: 'screenshot',
|
|
372
|
+
path: screenshotPath,
|
|
373
|
+
description: `No inline validation messages in form in module "${moduleId}"`,
|
|
374
|
+
});
|
|
375
|
+
findings.push({
|
|
376
|
+
id: `12-form-validation-${moduleId}`,
|
|
377
|
+
type: 'code-bug-cosmetic',
|
|
378
|
+
severity: 'medium',
|
|
379
|
+
agentId: this.agentId,
|
|
380
|
+
module: moduleId,
|
|
381
|
+
description: `Form in module "${moduleId}" does not show inline validation messages on empty submit`,
|
|
382
|
+
screenshot: screenshotPath,
|
|
383
|
+
suggestedFix:
|
|
384
|
+
'Display inline validation errors next to each required field on form submit',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// Best-effort check
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async checkTruncatedText(
|
|
393
|
+
page: Page,
|
|
394
|
+
moduleId: string,
|
|
395
|
+
findings: Finding[],
|
|
396
|
+
): Promise<void> {
|
|
397
|
+
try {
|
|
398
|
+
const truncatedCount = await page
|
|
399
|
+
.evaluate(() => {
|
|
400
|
+
const all = Array.from(document.querySelectorAll('*'));
|
|
401
|
+
let count = 0;
|
|
402
|
+
for (const el of all) {
|
|
403
|
+
const style = window.getComputedStyle(el);
|
|
404
|
+
if (
|
|
405
|
+
style.overflow === 'hidden' &&
|
|
406
|
+
el.scrollWidth > el.clientWidth &&
|
|
407
|
+
el.textContent !== null &&
|
|
408
|
+
el.textContent.trim().length > 0
|
|
409
|
+
) {
|
|
410
|
+
count++;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return count;
|
|
414
|
+
})
|
|
415
|
+
.catch(() => 0);
|
|
416
|
+
|
|
417
|
+
if (truncatedCount > 0) {
|
|
418
|
+
const screenshotPath = await captureScreenshot(
|
|
419
|
+
page,
|
|
420
|
+
`ux-truncated-text-${moduleId}`,
|
|
421
|
+
this.runDir,
|
|
422
|
+
);
|
|
423
|
+
this.addEvidence({
|
|
424
|
+
type: 'screenshot',
|
|
425
|
+
path: screenshotPath,
|
|
426
|
+
description: `${truncatedCount} truncated text element(s) detected in module "${moduleId}"`,
|
|
427
|
+
});
|
|
428
|
+
findings.push({
|
|
429
|
+
id: `12-truncated-text-${moduleId}`,
|
|
430
|
+
type: 'code-bug-cosmetic',
|
|
431
|
+
severity: 'low',
|
|
432
|
+
agentId: this.agentId,
|
|
433
|
+
module: moduleId,
|
|
434
|
+
description: `${truncatedCount} element(s) with overflow:hidden are truncating visible text in module "${moduleId}"`,
|
|
435
|
+
screenshot: screenshotPath,
|
|
436
|
+
suggestedFix:
|
|
437
|
+
'Use text-overflow:ellipsis with title attribute, or expand container width to fit content',
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Best-effort check
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|