qualyx 0.3.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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +523 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/init.d.ts.map +1 -0
  5. package/dist/cli/commands/init.js +124 -0
  6. package/dist/cli/commands/init.js.map +1 -0
  7. package/dist/cli/commands/list.d.ts +3 -0
  8. package/dist/cli/commands/list.d.ts.map +1 -0
  9. package/dist/cli/commands/list.js +122 -0
  10. package/dist/cli/commands/list.js.map +1 -0
  11. package/dist/cli/commands/run.d.ts +12 -0
  12. package/dist/cli/commands/run.d.ts.map +1 -0
  13. package/dist/cli/commands/run.js +160 -0
  14. package/dist/cli/commands/run.js.map +1 -0
  15. package/dist/cli/commands/schedule.d.ts +19 -0
  16. package/dist/cli/commands/schedule.d.ts.map +1 -0
  17. package/dist/cli/commands/schedule.js +240 -0
  18. package/dist/cli/commands/schedule.js.map +1 -0
  19. package/dist/cli/commands/validate.d.ts +3 -0
  20. package/dist/cli/commands/validate.d.ts.map +1 -0
  21. package/dist/cli/commands/validate.js +47 -0
  22. package/dist/cli/commands/validate.js.map +1 -0
  23. package/dist/cli/index.d.ts +3 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +194 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/core/claude-runner.d.ts +23 -0
  28. package/dist/core/claude-runner.d.ts.map +1 -0
  29. package/dist/core/claude-runner.js +196 -0
  30. package/dist/core/claude-runner.js.map +1 -0
  31. package/dist/core/config-loader.d.ts +137 -0
  32. package/dist/core/config-loader.d.ts.map +1 -0
  33. package/dist/core/config-loader.js +239 -0
  34. package/dist/core/config-loader.js.map +1 -0
  35. package/dist/core/executor.d.ts +75 -0
  36. package/dist/core/executor.d.ts.map +1 -0
  37. package/dist/core/executor.js +337 -0
  38. package/dist/core/executor.js.map +1 -0
  39. package/dist/core/index.d.ts +6 -0
  40. package/dist/core/index.d.ts.map +1 -0
  41. package/dist/core/index.js +7 -0
  42. package/dist/core/index.js.map +1 -0
  43. package/dist/core/prompt-builder.d.ts +24 -0
  44. package/dist/core/prompt-builder.d.ts.map +1 -0
  45. package/dist/core/prompt-builder.js +145 -0
  46. package/dist/core/prompt-builder.js.map +1 -0
  47. package/dist/core/retry-handler.d.ts +42 -0
  48. package/dist/core/retry-handler.d.ts.map +1 -0
  49. package/dist/core/retry-handler.js +126 -0
  50. package/dist/core/retry-handler.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +16 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/integrations/email.d.ts +38 -0
  56. package/dist/integrations/email.d.ts.map +1 -0
  57. package/dist/integrations/email.js +216 -0
  58. package/dist/integrations/email.js.map +1 -0
  59. package/dist/integrations/index.d.ts +5 -0
  60. package/dist/integrations/index.d.ts.map +1 -0
  61. package/dist/integrations/index.js +5 -0
  62. package/dist/integrations/index.js.map +1 -0
  63. package/dist/integrations/jira.d.ts +68 -0
  64. package/dist/integrations/jira.d.ts.map +1 -0
  65. package/dist/integrations/jira.js +288 -0
  66. package/dist/integrations/jira.js.map +1 -0
  67. package/dist/integrations/slack.d.ts +66 -0
  68. package/dist/integrations/slack.d.ts.map +1 -0
  69. package/dist/integrations/slack.js +192 -0
  70. package/dist/integrations/slack.js.map +1 -0
  71. package/dist/integrations/teams.d.ts +72 -0
  72. package/dist/integrations/teams.d.ts.map +1 -0
  73. package/dist/integrations/teams.js +197 -0
  74. package/dist/integrations/teams.js.map +1 -0
  75. package/dist/reporters/console.d.ts +83 -0
  76. package/dist/reporters/console.d.ts.map +1 -0
  77. package/dist/reporters/console.js +299 -0
  78. package/dist/reporters/console.js.map +1 -0
  79. package/dist/reporters/html.d.ts +29 -0
  80. package/dist/reporters/html.d.ts.map +1 -0
  81. package/dist/reporters/html.js +105 -0
  82. package/dist/reporters/html.js.map +1 -0
  83. package/dist/storage/results.d.ts +61 -0
  84. package/dist/storage/results.d.ts.map +1 -0
  85. package/dist/storage/results.js +111 -0
  86. package/dist/storage/results.js.map +1 -0
  87. package/dist/storage/sqlite.d.ts +70 -0
  88. package/dist/storage/sqlite.d.ts.map +1 -0
  89. package/dist/storage/sqlite.js +240 -0
  90. package/dist/storage/sqlite.js.map +1 -0
  91. package/dist/types/index.d.ts +1239 -0
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +105 -0
  94. package/dist/types/index.js.map +1 -0
  95. package/package.json +75 -0
  96. package/templates/crontab.hbs +24 -0
  97. package/templates/github-schedule.yml.hbs +153 -0
  98. package/templates/prompt.md.hbs +147 -0
  99. package/templates/report.html.hbs +423 -0
  100. package/templates/slack-message.json.hbs +93 -0
@@ -0,0 +1,239 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { QualyxConfigSchema } from '../types/index.js';
5
+ const DEFAULT_CONFIG_FILES = ['qualyx.yml', 'qualyx.yaml', '.qualyx.yml', '.qualyx.yaml'];
6
+ export class ConfigLoadError extends Error {
7
+ filePath;
8
+ validationErrors;
9
+ constructor(message, filePath, validationErrors) {
10
+ super(message);
11
+ this.filePath = filePath;
12
+ this.validationErrors = validationErrors;
13
+ this.name = 'ConfigLoadError';
14
+ }
15
+ }
16
+ /**
17
+ * Substitute environment variables in a string.
18
+ * Supports ${VAR_NAME} syntax.
19
+ */
20
+ function substituteEnvVars(value) {
21
+ return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
22
+ const envValue = process.env[varName];
23
+ if (envValue === undefined) {
24
+ // Return the original placeholder if env var is not set
25
+ // This allows for optional env vars and better error messages later
26
+ return match;
27
+ }
28
+ return envValue;
29
+ });
30
+ }
31
+ /**
32
+ * Recursively substitute environment variables in an object.
33
+ */
34
+ function substituteEnvVarsInObject(obj) {
35
+ if (typeof obj === 'string') {
36
+ return substituteEnvVars(obj);
37
+ }
38
+ if (Array.isArray(obj)) {
39
+ return obj.map(substituteEnvVarsInObject);
40
+ }
41
+ if (obj !== null && typeof obj === 'object') {
42
+ const result = {};
43
+ for (const [key, value] of Object.entries(obj)) {
44
+ result[key] = substituteEnvVarsInObject(value);
45
+ }
46
+ return result;
47
+ }
48
+ return obj;
49
+ }
50
+ /**
51
+ * Find the configuration file path.
52
+ * Searches for default config file names if no path is specified.
53
+ */
54
+ export function findConfigFile(configPath) {
55
+ if (configPath) {
56
+ const absolutePath = resolve(configPath);
57
+ if (!existsSync(absolutePath)) {
58
+ throw new ConfigLoadError(`Configuration file not found: ${absolutePath}`, absolutePath);
59
+ }
60
+ return absolutePath;
61
+ }
62
+ // Search for default config files in the current directory
63
+ const cwd = process.cwd();
64
+ for (const fileName of DEFAULT_CONFIG_FILES) {
65
+ const filePath = resolve(cwd, fileName);
66
+ if (existsSync(filePath)) {
67
+ return filePath;
68
+ }
69
+ }
70
+ throw new ConfigLoadError(`No configuration file found. Create a qualyx.yml file or specify a path with --config`);
71
+ }
72
+ /**
73
+ * Load and parse YAML configuration file.
74
+ */
75
+ function loadYamlFile(filePath) {
76
+ try {
77
+ const content = readFileSync(filePath, 'utf-8');
78
+ return parseYaml(content);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof Error) {
82
+ throw new ConfigLoadError(`Failed to parse YAML: ${error.message}`, filePath);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ /**
88
+ * Validate configuration against the Zod schema.
89
+ */
90
+ function validateConfig(config, filePath) {
91
+ const result = QualyxConfigSchema.safeParse(config);
92
+ if (!result.success) {
93
+ throw new ConfigLoadError(`Configuration validation failed:\n${formatZodErrors(result.error)}`, filePath, result.error);
94
+ }
95
+ return result.data;
96
+ }
97
+ /**
98
+ * Format Zod validation errors for display.
99
+ */
100
+ function formatZodErrors(error) {
101
+ return error.errors
102
+ .map((err) => {
103
+ const path = err.path.length > 0 ? ` at "${err.path.join('.')}"` : '';
104
+ return ` - ${err.message}${path}`;
105
+ })
106
+ .join('\n');
107
+ }
108
+ /**
109
+ * Check for unresolved environment variables and warn about them.
110
+ */
111
+ function checkUnresolvedEnvVars(config) {
112
+ const warnings = [];
113
+ const configStr = JSON.stringify(config);
114
+ const unresolvedMatches = configStr.match(/\$\{[^}]+\}/g);
115
+ if (unresolvedMatches) {
116
+ const uniqueVars = [...new Set(unresolvedMatches)];
117
+ for (const varMatch of uniqueVars) {
118
+ const varName = varMatch.slice(2, -1);
119
+ warnings.push(`Environment variable ${varName} is not set`);
120
+ }
121
+ }
122
+ return warnings;
123
+ }
124
+ /**
125
+ * Validate that all rule IDs within an app are unique.
126
+ */
127
+ function validateUniqueRuleIds(config) {
128
+ for (const app of config.apps) {
129
+ const ruleIds = new Set();
130
+ for (const rule of app.rules) {
131
+ if (ruleIds.has(rule.id)) {
132
+ throw new ConfigLoadError(`Duplicate rule ID "${rule.id}" in app "${app.name}"`);
133
+ }
134
+ ruleIds.add(rule.id);
135
+ }
136
+ }
137
+ }
138
+ /**
139
+ * Validate that all app names are unique.
140
+ */
141
+ function validateUniqueAppNames(config) {
142
+ const appNames = new Set();
143
+ for (const app of config.apps) {
144
+ if (appNames.has(app.name)) {
145
+ throw new ConfigLoadError(`Duplicate app name "${app.name}"`);
146
+ }
147
+ appNames.add(app.name);
148
+ }
149
+ }
150
+ /**
151
+ * Load, parse, and validate the Qualyx configuration file.
152
+ */
153
+ export function loadConfig(configPath) {
154
+ // Find the config file
155
+ const filePath = findConfigFile(configPath);
156
+ // Load and parse YAML
157
+ const rawConfig = loadYamlFile(filePath);
158
+ // Substitute environment variables
159
+ const configWithEnvVars = substituteEnvVarsInObject(rawConfig);
160
+ // Validate against schema
161
+ const config = validateConfig(configWithEnvVars, filePath);
162
+ // Additional validations
163
+ validateUniqueAppNames(config);
164
+ validateUniqueRuleIds(config);
165
+ // Check for unresolved env vars
166
+ const warnings = checkUnresolvedEnvVars(config);
167
+ return {
168
+ config,
169
+ filePath,
170
+ warnings,
171
+ };
172
+ }
173
+ /**
174
+ * Get a specific app from the configuration by name.
175
+ */
176
+ export function getApp(config, appName) {
177
+ const app = config.apps.find((a) => a.name === appName);
178
+ if (!app) {
179
+ throw new ConfigLoadError(`App "${appName}" not found in configuration`);
180
+ }
181
+ return app;
182
+ }
183
+ /**
184
+ * Get a specific rule from an app by ID.
185
+ */
186
+ export function getRule(config, appName, ruleId) {
187
+ const app = getApp(config, appName);
188
+ const rule = app.rules.find((r) => r.id === ruleId);
189
+ if (!rule) {
190
+ throw new ConfigLoadError(`Rule "${ruleId}" not found in app "${appName}"`);
191
+ }
192
+ return { app, rule };
193
+ }
194
+ /**
195
+ * Get the URL for a specific environment, with fallback to the default URL.
196
+ */
197
+ export function getEnvironmentUrl(app, environment) {
198
+ if (!environment) {
199
+ return app.url;
200
+ }
201
+ if (app.environments && app.environments[environment]) {
202
+ return app.environments[environment];
203
+ }
204
+ // Fall back to default URL if environment not found
205
+ return app.url;
206
+ }
207
+ /**
208
+ * Get the directory containing the configuration file.
209
+ */
210
+ export function getConfigDir(filePath) {
211
+ return dirname(filePath);
212
+ }
213
+ /**
214
+ * Get all scheduled rules from the configuration.
215
+ */
216
+ export function getScheduledRules(config) {
217
+ const scheduledRules = [];
218
+ for (const app of config.apps) {
219
+ for (const rule of app.rules) {
220
+ if (rule.schedule) {
221
+ scheduledRules.push({
222
+ appName: app.name,
223
+ ruleId: rule.id,
224
+ ruleName: rule.name,
225
+ schedule: rule.schedule,
226
+ severity: rule.severity,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ return scheduledRules;
232
+ }
233
+ /**
234
+ * Get setup steps for an app.
235
+ */
236
+ export function getAppSetup(app) {
237
+ return app.setup || [];
238
+ }
239
+ //# sourceMappingURL=config-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../src/core/config-loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import type { QualyxConfig, App, Rule, TestResult, RunResult, RunOptions } from '../types/index.js';
2
+ export interface ExecutorCallbacks {
3
+ onTestStart?: (app: App, rule: Rule) => void;
4
+ onTestComplete?: (result: TestResult) => void;
5
+ onTestRetry?: (app: App, rule: Rule, attempt: number, maxRetries: number) => void;
6
+ onRunStart?: (totalTests: number) => void;
7
+ onRunComplete?: (result: RunResult) => void;
8
+ onSetupStart?: (app: App) => void;
9
+ onSetupComplete?: (app: App, success: boolean, error?: string) => void;
10
+ }
11
+ export interface ExecutorOptions extends RunOptions {
12
+ callbacks?: ExecutorCallbacks;
13
+ }
14
+ /**
15
+ * Main executor class that orchestrates test runs.
16
+ */
17
+ export declare class Executor {
18
+ private config;
19
+ private options;
20
+ private callbacks;
21
+ private setupCompleted;
22
+ private setupErrors;
23
+ constructor(config: QualyxConfig, options?: ExecutorOptions);
24
+ /**
25
+ * Execute all tests or filtered tests based on options.
26
+ */
27
+ run(): Promise<RunResult>;
28
+ /**
29
+ * Execute tests sequentially.
30
+ */
31
+ private executeSequential;
32
+ /**
33
+ * Execute tests in parallel with concurrency limit.
34
+ */
35
+ private executeParallel;
36
+ /**
37
+ * Create a skip result for a test.
38
+ */
39
+ private createSkipResult;
40
+ /**
41
+ * Get the list of tests to run based on filters.
42
+ */
43
+ private getTestsToRun;
44
+ /**
45
+ * Execute a single test with retry logic.
46
+ */
47
+ private executeTest;
48
+ /**
49
+ * Build the prompt context for a test.
50
+ */
51
+ private buildContext;
52
+ /**
53
+ * Resolve credentials from app auth configuration.
54
+ */
55
+ private resolveCredentials;
56
+ /**
57
+ * Run setup steps for an app.
58
+ */
59
+ private runSetup;
60
+ /**
61
+ * Get a preview of what would be executed (for dry-run mode).
62
+ */
63
+ getExecutionPreview(): Array<{
64
+ app: string;
65
+ rule: string;
66
+ severity: string;
67
+ steps: number;
68
+ prompt: string;
69
+ }>;
70
+ }
71
+ /**
72
+ * Create and run an executor with the given configuration.
73
+ */
74
+ export declare function executeTests(config: QualyxConfig, options?: ExecutorOptions): Promise<RunResult>;
75
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/core/executor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,GAAG,EACH,IAAI,EACJ,UAAU,EACV,SAAS,EACT,UAAU,EAGX,MAAM,mBAAmB,CAAC;AAY3B,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC7C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC9C,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAClF,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAC5C,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;IAClC,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,WAAW,CAAsB;gBAE7B,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB;IAQ/D;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;IA+C/B;;OAEG;YACW,iBAAiB;IAwB/B;;OAEG;YACW,eAAe;IAgE7B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;OAEG;IACH,OAAO,CAAC,aAAa;IAsBrB;;OAEG;YACW,WAAW;IAsEzB;;OAEG;IACH,OAAO,CAAC,YAAY;IAUpB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;YACW,QAAQ;IAoDtB;;OAEG;IACH,mBAAmB,IAAI,KAAK,CAAC;QAC3B,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CAgBH;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,SAAS,CAAC,CAGpB"}
@@ -0,0 +1,337 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getAppSetup } from './config-loader.js';
3
+ import { buildExecutionPrompt, buildDryRunPrompt } from './prompt-builder.js';
4
+ import { runClaude, isClaudeAvailable } from './claude-runner.js';
5
+ import { shouldRetry, buildRetryContext, calculateRetryDelay, sleep, } from './retry-handler.js';
6
+ /**
7
+ * Main executor class that orchestrates test runs.
8
+ */
9
+ export class Executor {
10
+ config;
11
+ options;
12
+ callbacks;
13
+ setupCompleted;
14
+ setupErrors;
15
+ constructor(config, options = {}) {
16
+ this.config = config;
17
+ this.options = options;
18
+ this.callbacks = options.callbacks || {};
19
+ this.setupCompleted = new Map();
20
+ this.setupErrors = new Map();
21
+ }
22
+ /**
23
+ * Execute all tests or filtered tests based on options.
24
+ */
25
+ async run() {
26
+ // Check if Claude is available (unless dry-run)
27
+ if (!this.options.dryRun) {
28
+ const claudeAvailable = await isClaudeAvailable();
29
+ if (!claudeAvailable) {
30
+ throw new Error('Claude Code CLI is not available. Please install it with: npm install -g @anthropic-ai/claude-code');
31
+ }
32
+ }
33
+ const runId = randomUUID();
34
+ const startedAt = new Date().toISOString();
35
+ // Get tests to run based on filters
36
+ const testsToRun = this.getTestsToRun();
37
+ // Notify run start
38
+ this.callbacks.onRunStart?.(testsToRun.length);
39
+ // Execute tests (parallel or sequential)
40
+ const results = this.options.parallel
41
+ ? await this.executeParallel(testsToRun)
42
+ : await this.executeSequential(testsToRun);
43
+ const completedAt = new Date().toISOString();
44
+ const duration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
45
+ const runResult = {
46
+ runId,
47
+ startedAt,
48
+ completedAt,
49
+ duration,
50
+ totalTests: results.length,
51
+ passed: results.filter((r) => r.status === 'passed').length,
52
+ failed: results.filter((r) => r.status === 'failed').length,
53
+ skipped: results.filter((r) => r.status === 'skipped').length,
54
+ results,
55
+ environment: this.options.environment || 'default',
56
+ };
57
+ // Notify run complete
58
+ this.callbacks.onRunComplete?.(runResult);
59
+ return runResult;
60
+ }
61
+ /**
62
+ * Execute tests sequentially.
63
+ */
64
+ async executeSequential(testsToRun) {
65
+ const results = [];
66
+ for (const { app, rule } of testsToRun) {
67
+ // Run setup if needed and not skipped
68
+ if (!rule.skip_setup && app.setup?.length && !this.setupCompleted.has(app.name)) {
69
+ await this.runSetup(app);
70
+ }
71
+ // Check if setup failed for this app
72
+ if (this.setupErrors.has(app.name) && !rule.skip_setup) {
73
+ const skipResult = this.createSkipResult(app, rule, `Setup failed: ${this.setupErrors.get(app.name)}`);
74
+ results.push(skipResult);
75
+ this.callbacks.onTestComplete?.(skipResult);
76
+ continue;
77
+ }
78
+ const result = await this.executeTest(app, rule);
79
+ results.push(result);
80
+ }
81
+ return results;
82
+ }
83
+ /**
84
+ * Execute tests in parallel with concurrency limit.
85
+ */
86
+ async executeParallel(testsToRun) {
87
+ const maxParallel = this.options.maxParallel ?? 3;
88
+ const results = [];
89
+ const pending = [];
90
+ // First, run all setups sequentially to avoid race conditions
91
+ const appsWithSetup = new Set();
92
+ for (const { app, rule } of testsToRun) {
93
+ if (!rule.skip_setup && app.setup?.length && !appsWithSetup.has(app.name)) {
94
+ appsWithSetup.add(app.name);
95
+ if (!this.setupCompleted.has(app.name)) {
96
+ await this.runSetup(app);
97
+ }
98
+ }
99
+ }
100
+ // Create a queue of tests
101
+ const queue = [...testsToRun];
102
+ let running = 0;
103
+ const executeNext = async () => {
104
+ if (queue.length === 0)
105
+ return;
106
+ const test = queue.shift();
107
+ if (!test)
108
+ return;
109
+ const { app, rule } = test;
110
+ running++;
111
+ try {
112
+ // Check if setup failed for this app
113
+ if (this.setupErrors.has(app.name) && !rule.skip_setup) {
114
+ const skipResult = this.createSkipResult(app, rule, `Setup failed: ${this.setupErrors.get(app.name)}`);
115
+ results.push(skipResult);
116
+ this.callbacks.onTestComplete?.(skipResult);
117
+ }
118
+ else {
119
+ const result = await this.executeTest(app, rule);
120
+ results.push(result);
121
+ }
122
+ }
123
+ finally {
124
+ running--;
125
+ // Start next test if there are more in queue
126
+ if (queue.length > 0 && running < maxParallel) {
127
+ pending.push(executeNext());
128
+ }
129
+ }
130
+ };
131
+ // Start initial batch of parallel tests
132
+ const initialBatch = Math.min(maxParallel, queue.length);
133
+ for (let i = 0; i < initialBatch; i++) {
134
+ pending.push(executeNext());
135
+ }
136
+ // Wait for all tests to complete
137
+ await Promise.all(pending);
138
+ // Sort results to maintain original order
139
+ const orderMap = new Map(testsToRun.map(({ rule }, i) => [rule.id, i]));
140
+ results.sort((a, b) => (orderMap.get(a.ruleId) ?? 0) - (orderMap.get(b.ruleId) ?? 0));
141
+ return results;
142
+ }
143
+ /**
144
+ * Create a skip result for a test.
145
+ */
146
+ createSkipResult(app, rule, error) {
147
+ return {
148
+ ruleId: rule.id,
149
+ ruleName: rule.name,
150
+ appName: app.name,
151
+ status: 'skipped',
152
+ severity: rule.severity,
153
+ startedAt: new Date().toISOString(),
154
+ completedAt: new Date().toISOString(),
155
+ duration: 0,
156
+ steps: [],
157
+ validations: [],
158
+ error,
159
+ retryCount: 0,
160
+ };
161
+ }
162
+ /**
163
+ * Get the list of tests to run based on filters.
164
+ */
165
+ getTestsToRun() {
166
+ const tests = [];
167
+ for (const app of this.config.apps) {
168
+ // Filter by app name if specified
169
+ if (this.options.app && app.name !== this.options.app) {
170
+ continue;
171
+ }
172
+ for (const rule of app.rules) {
173
+ // Filter by rule ID if specified
174
+ if (this.options.rule && rule.id !== this.options.rule) {
175
+ continue;
176
+ }
177
+ tests.push({ app, rule });
178
+ }
179
+ }
180
+ return tests;
181
+ }
182
+ /**
183
+ * Execute a single test with retry logic.
184
+ */
185
+ async executeTest(app, rule) {
186
+ // Notify test start
187
+ this.callbacks.onTestStart?.(app, rule);
188
+ const startedAt = new Date().toISOString();
189
+ const maxRetries = this.options.retries ?? this.config.organization.defaults?.retries ?? 2;
190
+ const timeout = this.options.timeout ?? rule.timeout ?? this.config.organization.defaults?.timeout ?? 30000;
191
+ let result;
192
+ let retryCount = 0;
193
+ let lastContext = this.buildContext(app, rule);
194
+ // Initial execution
195
+ const prompt = this.options.dryRun
196
+ ? buildDryRunPrompt(lastContext)
197
+ : buildExecutionPrompt(lastContext);
198
+ result = await runClaude(prompt, {
199
+ timeout,
200
+ dryRun: this.options.dryRun,
201
+ headless: !this.options.headed,
202
+ });
203
+ // Retry loop
204
+ while (shouldRetry(result, retryCount, maxRetries)) {
205
+ retryCount++;
206
+ // Notify retry
207
+ this.callbacks.onTestRetry?.(app, rule, retryCount, maxRetries);
208
+ // Wait before retry (exponential backoff)
209
+ await sleep(calculateRetryDelay(retryCount - 1));
210
+ // Build enhanced context with failure information
211
+ lastContext = buildRetryContext(lastContext, result, retryCount);
212
+ // Execute with enhanced context
213
+ const retryPrompt = buildExecutionPrompt(lastContext);
214
+ result = await runClaude(retryPrompt, {
215
+ timeout,
216
+ dryRun: this.options.dryRun,
217
+ headless: !this.options.headed,
218
+ });
219
+ }
220
+ const completedAt = new Date().toISOString();
221
+ const duration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
222
+ const testResult = {
223
+ ruleId: rule.id,
224
+ ruleName: rule.name,
225
+ appName: app.name,
226
+ status: result.status,
227
+ severity: rule.severity,
228
+ startedAt,
229
+ completedAt,
230
+ duration,
231
+ steps: result.steps,
232
+ validations: result.validations,
233
+ error: result.error,
234
+ screenshot: result.screenshot,
235
+ retryCount,
236
+ };
237
+ // Notify test complete
238
+ this.callbacks.onTestComplete?.(testResult);
239
+ return testResult;
240
+ }
241
+ /**
242
+ * Build the prompt context for a test.
243
+ */
244
+ buildContext(app, rule) {
245
+ return {
246
+ app,
247
+ rule,
248
+ environment: this.options.environment,
249
+ credentials: this.resolveCredentials(app),
250
+ collectMetrics: this.options.collectMetrics,
251
+ };
252
+ }
253
+ /**
254
+ * Resolve credentials from app auth configuration.
255
+ */
256
+ resolveCredentials(app) {
257
+ if (!app.auth?.credentials) {
258
+ return {};
259
+ }
260
+ return app.auth.credentials;
261
+ }
262
+ /**
263
+ * Run setup steps for an app.
264
+ */
265
+ async runSetup(app) {
266
+ const setupSteps = getAppSetup(app);
267
+ if (!setupSteps.length) {
268
+ this.setupCompleted.set(app.name, true);
269
+ return;
270
+ }
271
+ // Notify setup start
272
+ this.callbacks.onSetupStart?.(app);
273
+ // Create a virtual rule for setup
274
+ const setupRule = {
275
+ id: `__setup__${app.name}`,
276
+ name: `Setup for ${app.name}`,
277
+ severity: 'critical',
278
+ steps: setupSteps,
279
+ validations: [],
280
+ };
281
+ const timeout = this.config.organization.defaults?.timeout ?? 30000;
282
+ const context = {
283
+ app,
284
+ rule: setupRule,
285
+ environment: this.options.environment,
286
+ credentials: this.resolveCredentials(app),
287
+ };
288
+ try {
289
+ const prompt = this.options.dryRun
290
+ ? buildDryRunPrompt(context)
291
+ : buildExecutionPrompt(context);
292
+ const result = await runClaude(prompt, {
293
+ timeout,
294
+ dryRun: this.options.dryRun,
295
+ headless: !this.options.headed,
296
+ });
297
+ if (result.status === 'passed') {
298
+ this.setupCompleted.set(app.name, true);
299
+ this.callbacks.onSetupComplete?.(app, true);
300
+ }
301
+ else {
302
+ this.setupErrors.set(app.name, result.error || 'Setup failed');
303
+ this.callbacks.onSetupComplete?.(app, false, result.error);
304
+ }
305
+ }
306
+ catch (error) {
307
+ const errorMessage = error instanceof Error ? error.message : 'Unknown setup error';
308
+ this.setupErrors.set(app.name, errorMessage);
309
+ this.callbacks.onSetupComplete?.(app, false, errorMessage);
310
+ }
311
+ }
312
+ /**
313
+ * Get a preview of what would be executed (for dry-run mode).
314
+ */
315
+ getExecutionPreview() {
316
+ const testsToRun = this.getTestsToRun();
317
+ return testsToRun.map(({ app, rule }) => {
318
+ const context = this.buildContext(app, rule);
319
+ const prompt = buildDryRunPrompt(context);
320
+ return {
321
+ app: app.name,
322
+ rule: rule.id,
323
+ severity: rule.severity,
324
+ steps: rule.steps.length,
325
+ prompt,
326
+ };
327
+ });
328
+ }
329
+ }
330
+ /**
331
+ * Create and run an executor with the given configuration.
332
+ */
333
+ export async function executeTests(config, options = {}) {
334
+ const executor = new Executor(config, options);
335
+ return executor.run();
336
+ }
337
+ //# sourceMappingURL=executor.js.map