@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,240 @@
|
|
|
1
|
+
import type { RunState, AgentResult, PhaseGateResult } from './types';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
|
|
4
|
+
/** Exit codes for CI integration. */
|
|
5
|
+
export const EXIT_CODE = {
|
|
6
|
+
/** All phase gates passed, no issues. */
|
|
7
|
+
SUCCESS: 0,
|
|
8
|
+
/** One or more blocking agent failures. */
|
|
9
|
+
BLOCKING_FAILURE: 1,
|
|
10
|
+
/** All blocking agents passed, but advisory agents found issues. */
|
|
11
|
+
ADVISORY_WARNINGS: 2,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type ExitCode = typeof EXIT_CODE[keyof typeof EXIT_CODE];
|
|
15
|
+
|
|
16
|
+
/** Detected CI environment. */
|
|
17
|
+
export type CIEnvironment = 'github' | 'azure' | 'gitlab' | 'none';
|
|
18
|
+
|
|
19
|
+
/** Detect which CI environment is running. */
|
|
20
|
+
export function detectCIEnvironment(): CIEnvironment {
|
|
21
|
+
if (process.env['GITHUB_ACTIONS'] === 'true') {
|
|
22
|
+
return 'github';
|
|
23
|
+
}
|
|
24
|
+
if (process.env['TF_BUILD'] === 'True') {
|
|
25
|
+
return 'azure';
|
|
26
|
+
}
|
|
27
|
+
if (process.env['GITLAB_CI'] === 'true') {
|
|
28
|
+
return 'gitlab';
|
|
29
|
+
}
|
|
30
|
+
return 'none';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determine the appropriate exit code from run state.
|
|
35
|
+
* - 0: All gates passed, no advisory failures
|
|
36
|
+
* - 1: Blocking agent failures
|
|
37
|
+
* - 2: Advisory warnings only (all blocking passed)
|
|
38
|
+
*/
|
|
39
|
+
export function determineExitCode(gateResults: PhaseGateResult[]): ExitCode {
|
|
40
|
+
const hasBlockingFailures = gateResults.some(
|
|
41
|
+
gate => gate.failedBlockingAgents.length > 0,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (hasBlockingFailures) {
|
|
45
|
+
return EXIT_CODE.BLOCKING_FAILURE;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hasAdvisoryFailures = gateResults.some(gate =>
|
|
49
|
+
gate.advisoryResults.some(result => result.status === 'failed'),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (hasAdvisoryFailures) {
|
|
53
|
+
return EXIT_CODE.ADVISORY_WARNINGS;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return EXIT_CODE.SUCCESS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Format duration in human-readable form. */
|
|
60
|
+
function formatDuration(ms: number): string {
|
|
61
|
+
if (ms < 1000) {
|
|
62
|
+
return `${ms}ms`;
|
|
63
|
+
}
|
|
64
|
+
const seconds = Math.floor(ms / 1000);
|
|
65
|
+
if (seconds < 60) {
|
|
66
|
+
return `${seconds}s`;
|
|
67
|
+
}
|
|
68
|
+
const minutes = Math.floor(seconds / 60);
|
|
69
|
+
const remainingSeconds = seconds % 60;
|
|
70
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get a status icon for an agent result. */
|
|
74
|
+
function statusIcon(status: AgentResult['status']): string {
|
|
75
|
+
switch (status) {
|
|
76
|
+
case 'passed':
|
|
77
|
+
return '✅';
|
|
78
|
+
case 'failed':
|
|
79
|
+
return '❌';
|
|
80
|
+
case 'skipped':
|
|
81
|
+
return '⏭️';
|
|
82
|
+
case 'running':
|
|
83
|
+
return '🔄';
|
|
84
|
+
case 'pending':
|
|
85
|
+
return '⏳';
|
|
86
|
+
default:
|
|
87
|
+
return '❓';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Generate markdown summary for GitHub Actions GITHUB_STEP_SUMMARY. */
|
|
92
|
+
export function generateGitHubSummary(results: RunState): string {
|
|
93
|
+
const lines: string[] = [];
|
|
94
|
+
|
|
95
|
+
const overallExitCode = determineExitCode(results.gateResults);
|
|
96
|
+
const overallStatus = overallExitCode === EXIT_CODE.SUCCESS
|
|
97
|
+
? '✅ All Gates Passed'
|
|
98
|
+
: overallExitCode === EXIT_CODE.ADVISORY_WARNINGS
|
|
99
|
+
? '⚠️ Advisory Warnings'
|
|
100
|
+
: '❌ Blocking Failures';
|
|
101
|
+
|
|
102
|
+
lines.push(`## TestTeam — ${overallStatus}`);
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push(`| Detail | Value |`);
|
|
105
|
+
lines.push(`|--------|-------|`);
|
|
106
|
+
lines.push(`| Run ID | \`${results.runId}\` |`);
|
|
107
|
+
lines.push(`| Started | ${results.startedAt} |`);
|
|
108
|
+
lines.push(`| Total Cost | $${results.totalCost.toFixed(4)} |`);
|
|
109
|
+
lines.push('');
|
|
110
|
+
|
|
111
|
+
for (const gate of results.gateResults) {
|
|
112
|
+
const gateIcon = gate.passed ? '✅' : '❌';
|
|
113
|
+
lines.push(`### ${gateIcon} Phase: \`${gate.phase}\``);
|
|
114
|
+
lines.push('');
|
|
115
|
+
|
|
116
|
+
if (gate.failedBlockingAgents.length > 0) {
|
|
117
|
+
lines.push(`**Failed blocking agents:** ${gate.failedBlockingAgents.join(', ')}`);
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Agent results table
|
|
122
|
+
const allResults = [...gate.blockingResults, ...gate.advisoryResults];
|
|
123
|
+
if (allResults.length > 0) {
|
|
124
|
+
lines.push(`| Agent | Status | Duration | Findings |`);
|
|
125
|
+
lines.push(`|-------|--------|----------|----------|`);
|
|
126
|
+
|
|
127
|
+
for (const result of allResults) {
|
|
128
|
+
const icon = statusIcon(result.status);
|
|
129
|
+
const duration = formatDuration(result.durationMs);
|
|
130
|
+
const findingCount = result.findings.length;
|
|
131
|
+
lines.push(
|
|
132
|
+
`| ${icon} ${result.agentName} (#${result.agentId}) | ${result.status} | ${duration} | ${findingCount} |`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
lines.push('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Findings summary
|
|
140
|
+
const allFindings = Array.from(results.agentResults.values())
|
|
141
|
+
.flatMap(r => r.findings);
|
|
142
|
+
|
|
143
|
+
if (allFindings.length > 0) {
|
|
144
|
+
lines.push(`### Findings Summary`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
|
|
147
|
+
const bySeverity = new Map<string, number>();
|
|
148
|
+
for (const finding of allFindings) {
|
|
149
|
+
bySeverity.set(finding.severity, (bySeverity.get(finding.severity) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.push(`| Severity | Count |`);
|
|
153
|
+
lines.push(`|----------|-------|`);
|
|
154
|
+
for (const [severity, count] of bySeverity) {
|
|
155
|
+
lines.push(`| ${severity} | ${count} |`);
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return lines.join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Generate Azure Pipelines build tags from run results. */
|
|
164
|
+
export function generateAzureTags(results: RunState): string[] {
|
|
165
|
+
const tags: string[] = [];
|
|
166
|
+
|
|
167
|
+
const exitCode = determineExitCode(results.gateResults);
|
|
168
|
+
|
|
169
|
+
if (exitCode === EXIT_CODE.SUCCESS) {
|
|
170
|
+
tags.push('testteam:passed');
|
|
171
|
+
} else if (exitCode === EXIT_CODE.ADVISORY_WARNINGS) {
|
|
172
|
+
tags.push('testteam:warnings');
|
|
173
|
+
} else {
|
|
174
|
+
tags.push('testteam:failed');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const gate of results.gateResults) {
|
|
178
|
+
tags.push(`testteam:phase:${gate.phase}`);
|
|
179
|
+
tags.push(`testteam:phase:${gate.phase}:${gate.passed ? 'passed' : 'failed'}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const allFindings = Array.from(results.agentResults.values())
|
|
183
|
+
.flatMap(r => r.findings);
|
|
184
|
+
|
|
185
|
+
const severities = new Set(allFindings.map(f => f.severity));
|
|
186
|
+
for (const severity of severities) {
|
|
187
|
+
tags.push(`testteam:severity:${severity}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return tags;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Injectable IO operations for testability in ESM. */
|
|
194
|
+
export interface CIOutputIO {
|
|
195
|
+
appendFile: (path: string, data: string) => void;
|
|
196
|
+
writeFile: (path: string, data: string) => void;
|
|
197
|
+
log: (message: string) => void;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const defaultIO: CIOutputIO = {
|
|
201
|
+
appendFile: (filePath, data) => fs.appendFileSync(filePath, data),
|
|
202
|
+
writeFile: (filePath, data) => fs.writeFileSync(filePath, data),
|
|
203
|
+
log: (message) => console.log(message),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** Write CI summary to the appropriate CI environment output. */
|
|
207
|
+
export function writeCISummary(results: RunState, io: CIOutputIO = defaultIO): void {
|
|
208
|
+
const env = detectCIEnvironment();
|
|
209
|
+
|
|
210
|
+
switch (env) {
|
|
211
|
+
case 'github': {
|
|
212
|
+
const summaryPath = process.env['GITHUB_STEP_SUMMARY'];
|
|
213
|
+
if (summaryPath) {
|
|
214
|
+
const markdown = generateGitHubSummary(results);
|
|
215
|
+
io.appendFile(summaryPath, markdown + '\n');
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case 'azure': {
|
|
220
|
+
const tags = generateAzureTags(results);
|
|
221
|
+
for (const tag of tags) {
|
|
222
|
+
io.log(`##vso[build.addbuildtag]${tag}`);
|
|
223
|
+
}
|
|
224
|
+
// Also output markdown to the task log
|
|
225
|
+
const markdown = generateGitHubSummary(results);
|
|
226
|
+
io.log(markdown);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'gitlab': {
|
|
230
|
+
// GitLab CI doesn't have a native summary mechanism,
|
|
231
|
+
// so write to a file that can be picked up as an artifact
|
|
232
|
+
const markdown = generateGitHubSummary(results);
|
|
233
|
+
io.writeFile('.run/testteam-summary.md', markdown);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case 'none':
|
|
237
|
+
// Not in CI; no summary written
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
package/core/cli.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { Phase, PhaseGateResult, AgentResult } from './types';
|
|
2
|
+
import type { TestTeamConfig } from './config';
|
|
3
|
+
import { validateConfig } from './config';
|
|
4
|
+
import { Orchestrator } from './orchestrator';
|
|
5
|
+
import { registerAllAgents } from '../agents/registry';
|
|
6
|
+
import { runInitWizard } from './init';
|
|
7
|
+
import { determineExitCode, writeCISummary, detectCIEnvironment } from './ci-output';
|
|
8
|
+
import type { ExitCode } from './ci-output';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
const PHASES: readonly Phase[] = ['alpha', 'beta', 'uat', 'release'];
|
|
12
|
+
|
|
13
|
+
interface ParsedArgs {
|
|
14
|
+
phase: Phase | 'all' | null;
|
|
15
|
+
dryRun: boolean;
|
|
16
|
+
agent: number | null;
|
|
17
|
+
configPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printUsage(): void {
|
|
21
|
+
console.log(`
|
|
22
|
+
Usage: testteam [options]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
init Initialize TestTeam in current directory
|
|
26
|
+
--phase <alpha|beta|uat|release|all> Phase to run (required unless --dry-run)
|
|
27
|
+
--dry-run Show dispatch plan without executing
|
|
28
|
+
--agent <id> Run a single agent by ID
|
|
29
|
+
--config <path> Path to config file (default: testteam.config.ts)
|
|
30
|
+
--help Show this help message
|
|
31
|
+
`.trim());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
35
|
+
const args = argv.slice(2); // skip node + script path
|
|
36
|
+
let phase: Phase | 'all' | null = null;
|
|
37
|
+
let dryRun = false;
|
|
38
|
+
let agent: number | null = null;
|
|
39
|
+
let configPath = 'testteam.config.ts';
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
switch (arg) {
|
|
44
|
+
case '--phase': {
|
|
45
|
+
const value = args[++i];
|
|
46
|
+
if (!value || (!PHASES.includes(value as Phase) && value !== 'all')) {
|
|
47
|
+
throw new Error(`Invalid phase: ${value}. Must be one of: ${PHASES.join(', ')}, all`);
|
|
48
|
+
}
|
|
49
|
+
phase = value as Phase | 'all';
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case '--dry-run':
|
|
53
|
+
dryRun = true;
|
|
54
|
+
break;
|
|
55
|
+
case '--agent': {
|
|
56
|
+
const value = args[++i];
|
|
57
|
+
const parsed = Number(value);
|
|
58
|
+
if (!value || isNaN(parsed) || parsed < 1) {
|
|
59
|
+
throw new Error(`Invalid agent ID: ${value}. Must be a positive integer.`);
|
|
60
|
+
}
|
|
61
|
+
agent = parsed;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case '--config':
|
|
65
|
+
configPath = args[++i];
|
|
66
|
+
if (!configPath) {
|
|
67
|
+
throw new Error('--config requires a path argument');
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case '--help':
|
|
71
|
+
printUsage();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { phase, dryRun, agent, configPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadConfig(configPath: string): Promise<TestTeamConfig> {
|
|
83
|
+
const absolutePath = path.resolve(configPath);
|
|
84
|
+
// On Windows, dynamic import requires file:// URLs
|
|
85
|
+
const fileUrl = new URL(`file:///${absolutePath.replace(/\\/g, '/')}`).href;
|
|
86
|
+
const module = await import(fileUrl) as { default?: TestTeamConfig; config?: TestTeamConfig };
|
|
87
|
+
const raw = module.default ?? module.config;
|
|
88
|
+
if (!raw) {
|
|
89
|
+
throw new Error(`Config file does not export a default or named 'config': ${absolutePath}`);
|
|
90
|
+
}
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function main(argv: string[] = process.argv): Promise<number> {
|
|
95
|
+
// Handle init command before normal arg parsing
|
|
96
|
+
const rawArgs = argv.slice(2);
|
|
97
|
+
if (rawArgs.includes('init') || rawArgs.includes('--init')) {
|
|
98
|
+
return runInitWizard();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (argv.length <= 2) {
|
|
102
|
+
printUsage();
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let parsed: ParsedArgs;
|
|
107
|
+
try {
|
|
108
|
+
parsed = parseArgs(argv);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
111
|
+
printUsage();
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { phase, dryRun, configPath } = parsed;
|
|
116
|
+
|
|
117
|
+
if (!dryRun && !phase) {
|
|
118
|
+
console.error('--phase is required unless using --dry-run');
|
|
119
|
+
printUsage();
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load and validate config
|
|
124
|
+
let rawConfig: TestTeamConfig;
|
|
125
|
+
try {
|
|
126
|
+
rawConfig = await loadConfig(configPath);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const config = validateConfig(rawConfig);
|
|
133
|
+
|
|
134
|
+
// Shared run directory so all phases write results to the same location.
|
|
135
|
+
// This ensures the Reporter (Agent 10, release phase) can read results
|
|
136
|
+
// from prior phases (alpha, beta, uat).
|
|
137
|
+
const sharedRunDir = path.join(process.cwd(), '.run', `run-${Date.now()}`);
|
|
138
|
+
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
const targetPhases: Phase[] = phase === 'all' || !phase
|
|
141
|
+
? [...PHASES]
|
|
142
|
+
: [phase];
|
|
143
|
+
|
|
144
|
+
for (const p of targetPhases) {
|
|
145
|
+
const phaseOrchestrator = new Orchestrator(config, sharedRunDir);
|
|
146
|
+
registerAllAgents(phaseOrchestrator, config, p, sharedRunDir);
|
|
147
|
+
const plan = phaseOrchestrator.dryRun(p);
|
|
148
|
+
console.log(`\nPhase: ${p}`);
|
|
149
|
+
for (const entry of plan) {
|
|
150
|
+
console.log(` Agent ${entry.agentId}: ${entry.status}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Run mode — create a fresh orchestrator per phase to avoid agent instance
|
|
157
|
+
// conflicts (GAP-023: registerAgent uses Map.set which overwrites).
|
|
158
|
+
const targetPhases: Phase[] = phase === 'all'
|
|
159
|
+
? [...PHASES]
|
|
160
|
+
: [phase as Phase];
|
|
161
|
+
|
|
162
|
+
const collectedGates: PhaseGateResult[] = [];
|
|
163
|
+
const collectedResults = new Map<number, AgentResult>();
|
|
164
|
+
let hadError = false;
|
|
165
|
+
|
|
166
|
+
for (const p of targetPhases) {
|
|
167
|
+
console.log(`\nRunning phase: ${p}`);
|
|
168
|
+
try {
|
|
169
|
+
const phaseOrchestrator = new Orchestrator(config, sharedRunDir);
|
|
170
|
+
registerAllAgents(phaseOrchestrator, config, p, sharedRunDir);
|
|
171
|
+
const result = await phaseOrchestrator.runPhase(p);
|
|
172
|
+
|
|
173
|
+
if (result.skippedAgents.length > 0) {
|
|
174
|
+
console.log(` Skipped agents: ${result.skippedAgents.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resultCount = Object.keys(result.agentResults).length;
|
|
178
|
+
console.log(` Completed ${resultCount} agent(s)`);
|
|
179
|
+
|
|
180
|
+
if (result.gate.passed) {
|
|
181
|
+
console.log(` Gate: PASSED`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` Gate: FAILED (blocking agents: ${result.gate.failedBlockingAgents.join(', ')})`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
collectedGates.push(result.gate);
|
|
187
|
+
|
|
188
|
+
// Merge agent results for CI summary
|
|
189
|
+
for (const [agentId, agentResult] of Object.entries(result.agentResults)) {
|
|
190
|
+
collectedResults.set(Number(agentId), agentResult);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(` Phase ${p} error: ${error instanceof Error ? error.message : String(error)}`);
|
|
194
|
+
hadError = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Determine structured exit code:
|
|
199
|
+
// - 0: All gates passed, no advisory failures
|
|
200
|
+
// - 1: Blocking agent failures (or runtime errors)
|
|
201
|
+
// - 2: Advisory warnings only
|
|
202
|
+
const exitCode: ExitCode = hadError
|
|
203
|
+
? 1
|
|
204
|
+
: determineExitCode(collectedGates);
|
|
205
|
+
|
|
206
|
+
// Write CI summary if running in a CI environment
|
|
207
|
+
const ciEnv = detectCIEnvironment();
|
|
208
|
+
if (ciEnv !== 'none') {
|
|
209
|
+
const runState: import('./types').RunState = {
|
|
210
|
+
runId: path.basename(sharedRunDir),
|
|
211
|
+
startedAt: new Date().toISOString(),
|
|
212
|
+
currentPhase: targetPhases[targetPhases.length - 1],
|
|
213
|
+
phaseAttempt: 1,
|
|
214
|
+
agentResults: collectedResults,
|
|
215
|
+
gateResults: collectedGates,
|
|
216
|
+
totalCost: 0,
|
|
217
|
+
};
|
|
218
|
+
writeCISummary(runState);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return exitCode;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Direct execution — works with both tsx and node
|
|
225
|
+
const isDirectExecution =
|
|
226
|
+
typeof process !== 'undefined' &&
|
|
227
|
+
process.argv[1] &&
|
|
228
|
+
(process.argv[1].includes('cli.ts') || process.argv[1].includes('cli.js'));
|
|
229
|
+
|
|
230
|
+
if (isDirectExecution) {
|
|
231
|
+
main().then((code) => process.exit(code));
|
|
232
|
+
}
|
package/core/config.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export interface WorkflowStep {
|
|
2
|
+
action: 'navigate' | 'click' | 'fill-form' | 'wait' | 'verify';
|
|
3
|
+
target: string;
|
|
4
|
+
verify?: string;
|
|
5
|
+
data?: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
contains?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModuleConfig {
|
|
11
|
+
id: string;
|
|
12
|
+
route: string;
|
|
13
|
+
sidebarIcon: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CredentialEntry {
|
|
17
|
+
email: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RoleEntry {
|
|
22
|
+
name: string;
|
|
23
|
+
email: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AuthConfig {
|
|
27
|
+
loginUrl?: string;
|
|
28
|
+
credentials?: Record<string, CredentialEntry>;
|
|
29
|
+
roles?: RoleEntry[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EnvironmentConfig {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
seed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PortalConfig {
|
|
38
|
+
name: string;
|
|
39
|
+
queryParam: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IntegrationConfig {
|
|
43
|
+
name: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkerConfig {
|
|
48
|
+
name: string;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkflowConfig {
|
|
53
|
+
name: string;
|
|
54
|
+
steps: WorkflowStep[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AccuracyConfig {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
decimalPrecision: number;
|
|
60
|
+
currencySymbol: string;
|
|
61
|
+
timezone: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TenancyConfig {
|
|
65
|
+
enabled: boolean;
|
|
66
|
+
isolationField: string;
|
|
67
|
+
testFirms: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CostBudgetConfig {
|
|
71
|
+
maxPerRun: number;
|
|
72
|
+
maxPerAgent: number;
|
|
73
|
+
alertThreshold: number;
|
|
74
|
+
onExceeded: 'abort' | 'alert-continue' | 'skip-remaining';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AgentClassificationConfig {
|
|
78
|
+
blocking: number[];
|
|
79
|
+
advisory: number[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CoverageThresholds {
|
|
83
|
+
lines: number;
|
|
84
|
+
functions: number;
|
|
85
|
+
branches: number;
|
|
86
|
+
statements: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface LighthouseThresholds {
|
|
90
|
+
accessibility: number;
|
|
91
|
+
performance: number;
|
|
92
|
+
bestPractices: number;
|
|
93
|
+
seo: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ThresholdsConfig {
|
|
97
|
+
unitCoverage: CoverageThresholds;
|
|
98
|
+
lighthouse: LighthouseThresholds;
|
|
99
|
+
apiResponseTime: number;
|
|
100
|
+
bundleSizeLimit: number;
|
|
101
|
+
maxConsoleErrors: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TestTeamConfig {
|
|
105
|
+
schemaVersion: number;
|
|
106
|
+
name: string;
|
|
107
|
+
/** Absolute path to the target project root (where package.json, prisma/, apps/ live) */
|
|
108
|
+
projectRoot?: string;
|
|
109
|
+
auth: AuthConfig;
|
|
110
|
+
modules: ModuleConfig[];
|
|
111
|
+
environments: Record<string, EnvironmentConfig>;
|
|
112
|
+
portals: PortalConfig[];
|
|
113
|
+
breakpoints: number[];
|
|
114
|
+
integrations: IntegrationConfig[];
|
|
115
|
+
workers: WorkerConfig[];
|
|
116
|
+
workflows: WorkflowConfig[];
|
|
117
|
+
accuracy: AccuracyConfig;
|
|
118
|
+
tenancy: TenancyConfig;
|
|
119
|
+
costBudget: CostBudgetConfig;
|
|
120
|
+
agentClassification: AgentClassificationConfig;
|
|
121
|
+
thresholds: ThresholdsConfig;
|
|
122
|
+
disabledAgents: number[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ValidatedConfig extends TestTeamConfig {
|
|
126
|
+
disabledAgentSet: Set<number>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function validateConfig(raw: TestTeamConfig): ValidatedConfig {
|
|
130
|
+
if (raw.schemaVersion === undefined || raw.schemaVersion === null) {
|
|
131
|
+
throw new Error('schemaVersion is required');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!raw.name) {
|
|
135
|
+
throw new Error('name is required');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (raw.modules !== undefined && !Array.isArray(raw.modules)) {
|
|
139
|
+
throw new Error('modules must be an array');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (raw.modules !== undefined && Array.isArray(raw.modules) && raw.modules.length === 0) {
|
|
143
|
+
throw new Error('modules must contain at least one module');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!raw.auth) {
|
|
147
|
+
throw new Error('auth is required');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const disabledAgentSet = new Set<number>(raw.disabledAgents ?? []);
|
|
151
|
+
|
|
152
|
+
// Warn on empty credential passwords and empty emails
|
|
153
|
+
if (raw.auth.credentials) {
|
|
154
|
+
const entries = Object.entries(raw.auth.credentials);
|
|
155
|
+
let allPasswordsEmpty = entries.length > 0;
|
|
156
|
+
|
|
157
|
+
for (const [role, entry] of entries) {
|
|
158
|
+
if (entry.password === '') {
|
|
159
|
+
console.warn(`[TestTeam] Warning: credential "${role}" has an empty password. Set a real password in your config.`);
|
|
160
|
+
} else {
|
|
161
|
+
allPasswordsEmpty = false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (entry.email === '') {
|
|
165
|
+
console.warn(`[TestTeam] Warning: credential "${role}" has an empty email. Set TF_${role.toUpperCase()}_EMAIL in your environment.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (allPasswordsEmpty && entries.length > 0) {
|
|
170
|
+
throw new Error('All credentials have empty passwords. At least one credential must have a non-empty password.');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
...raw,
|
|
176
|
+
disabledAgentSet,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface CostBudget {
|
|
2
|
+
maxPerRun: number;
|
|
3
|
+
maxPerAgent: number;
|
|
4
|
+
alertThreshold: number;
|
|
5
|
+
onExceeded: 'abort' | 'alert-continue' | 'skip-remaining';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BudgetCheckResult {
|
|
9
|
+
status: 'ok' | 'alert' | 'exceeded';
|
|
10
|
+
totalCost: number;
|
|
11
|
+
budgetRemaining: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CostTracker {
|
|
15
|
+
private readonly budget: CostBudget;
|
|
16
|
+
private readonly agentCosts: Map<string, number> = new Map();
|
|
17
|
+
|
|
18
|
+
constructor(budget: CostBudget) {
|
|
19
|
+
this.budget = budget;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
trackCost(agentId: string, amount: number): void {
|
|
23
|
+
const current = this.agentCosts.get(agentId) ?? 0;
|
|
24
|
+
this.agentCosts.set(agentId, current + amount);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getTotalCost(): number {
|
|
28
|
+
let total = 0;
|
|
29
|
+
for (const cost of this.agentCosts.values()) {
|
|
30
|
+
total += cost;
|
|
31
|
+
}
|
|
32
|
+
return total;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getAgentCost(agentId: string): number {
|
|
36
|
+
return this.agentCosts.get(agentId) ?? 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
checkBudget(): BudgetCheckResult {
|
|
40
|
+
const totalCost = this.getTotalCost();
|
|
41
|
+
const budgetRemaining = this.budget.maxPerRun - totalCost;
|
|
42
|
+
const alertLimit = this.budget.maxPerRun * this.budget.alertThreshold;
|
|
43
|
+
|
|
44
|
+
let status: 'ok' | 'alert' | 'exceeded';
|
|
45
|
+
if (totalCost >= this.budget.maxPerRun) {
|
|
46
|
+
status = 'exceeded';
|
|
47
|
+
} else if (totalCost >= alertLimit) {
|
|
48
|
+
status = 'alert';
|
|
49
|
+
} else {
|
|
50
|
+
status = 'ok';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { status, totalCost, budgetRemaining };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isAgentOverBudget(agentId: string): boolean {
|
|
57
|
+
return this.getAgentCost(agentId) >= this.budget.maxPerAgent;
|
|
58
|
+
}
|
|
59
|
+
}
|