qaguardian 1.1.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/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # QAG CLI - QA Guardian Command Line Interface
2
+
3
+ Trigger and monitor test suite executions from the command line.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g qaguardian
9
+ ```
10
+
11
+ Or use directly with `npx`:
12
+
13
+ ```bash
14
+ npx qaguardian --tags smoke
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Set your API key
21
+ export QAG_API_KEY=your-api-key-here
22
+
23
+ # Run suites with specific tags
24
+ npx qaguardian --tags auth,login
25
+
26
+ # Exclude certain tags
27
+ npx qaguardian --tags regression --exclude-tags slow,flaky
28
+
29
+ # Run all suites
30
+ npx qaguardian --all
31
+
32
+ # Fire and forget (no polling)
33
+ npx qaguardian --tags smoke --no-wait
34
+ ```
35
+
36
+ ## Options
37
+
38
+ - `--tags <tags>` - Comma-separated tags to match (e.g., "auth,ci,smoke")
39
+ - `--exclude-tags <tags>` - Comma-separated tags to exclude (e.g., "slow,flaky")
40
+ - `--all` - Run all suites (combine with `--exclude-tags` to filter)
41
+ - `--match-mode <mode>` - Tag matching mode: "any" (OR logic, default) or "all" (AND logic)
42
+ - `--no-wait` - Trigger and exit immediately without waiting for results
43
+ - `--webhook-url <url>` - Custom webhook URL for notifications on completion
44
+ - `--notify <service>` - Notify via service: slack, discord, google-chat, or teams
45
+ - `--api-url <url>` - Custom API Gateway URL (default: https://api.qaguardian.com)
46
+
47
+ ## Environment Variables
48
+
49
+ - `QAG_API_KEY` - (Required) Your QA Guardian API key
50
+ - `QAG_API_URL` - (Optional) API Gateway URL (defaults to https://api.qaguardian.com)
51
+
52
+ ## Examples
53
+
54
+ ### Tag-based execution with wait
55
+
56
+ ```bash
57
+ export QAG_API_KEY=guardians-primary-qag-cbaa125e027b417a
58
+ npx qaguardian --tags auth,ci
59
+ ```
60
+
61
+ ### Exclude certain tags
62
+
63
+ ```bash
64
+ npx qaguardian --tags regression --exclude-tags slow,flaky
65
+ ```
66
+
67
+ ### Run everything except auth suites
68
+
69
+ ```bash
70
+ npx qaguardian --all --exclude-tags auth
71
+ ```
72
+
73
+ ### Fire and forget
74
+
75
+ ```bash
76
+ npx qaguardian --tags smoke --no-wait
77
+ ```
78
+
79
+ ### AND logic (all tags required)
80
+
81
+ ```bash
82
+ npx qaguardian --tags auth,login,ui --match-mode all
83
+ ```
84
+
85
+ ### Notifications
86
+
87
+ Receive notifications when tests complete:
88
+
89
+ ```bash
90
+ # Custom webhook URL
91
+ npx qaguardian --tags regression --webhook-url https://my-service.com/webhook
92
+
93
+ # Slack notification
94
+ npx qaguardian --tags smoke --notify slack
95
+
96
+ # Discord notification
97
+ npx qaguardian --tags auth --notify discord
98
+
99
+ # Google Chat notification
100
+ npx qaguardian --tags ci --notify google-chat
101
+
102
+ # Microsoft Teams notification
103
+ npx qaguardian --tags regression --notify teams
104
+ ```
105
+
106
+ ### Local development
107
+
108
+ ```bash
109
+ cd qag-sdk
110
+ npm install
111
+ npm link
112
+ export QAG_API_KEY=your-api-key
113
+ npx qaguardian --tags smoke --api-url http://localhost:8080
114
+ ```
115
+
116
+ ## Exit Codes
117
+
118
+ - `0` - All tests passed
119
+ - `1` - Any test failed or execution failed
120
+
121
+ ## API Key
122
+
123
+ Get your API key from the QA Guardian dashboard at: https://app.qaguardian.com/settings/api-keys
124
+
125
+ ## Polling Behavior
126
+
127
+ By default, the CLI polls the API every 30 seconds to check for completion. You can observe:
128
+
129
+ - Real-time test count updates
130
+ - Pass/fail counts as tests complete
131
+ - Automatic exit with appropriate code when all suites finish
132
+
133
+ Use `--no-wait` to skip polling and exit immediately after triggering.
134
+
135
+ ## Notifications
136
+
137
+ The CLI can send notifications when test suites complete. Choose one of:
138
+
139
+ ### Webhook Notifications
140
+
141
+ Send results to any HTTP endpoint:
142
+
143
+ ```bash
144
+ qag --tags regression --webhook-url https://my-service.com/webhook
145
+ ```
146
+
147
+ The webhook receives a POST request with execution results.
148
+
149
+ ### Platform Notifications
150
+
151
+ Integrated notification support for popular chat and communication platforms:
152
+
153
+ - **Slack**: `--notify slack` (requires Slack integration configured in QA Guardian)
154
+ - **Discord**: `--notify discord` (requires Discord integration configured)
155
+ - **Google Chat**: `--notify google-chat` (requires Google Chat integration configured)
156
+ - **Microsoft Teams**: `--notify teams` (requires Teams integration configured)
157
+
158
+ Example:
159
+
160
+ ```bash
161
+ qag --tags smoke --notify slack
162
+ ```
163
+
164
+ Notifications are sent when all tests complete, with summary of pass/fail counts.
165
+
166
+ ## License
167
+
168
+ MIT
package/bin/qag.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../src/index.js').catch((error) => {
4
+ console.error('Failed to start QAG CLI:', error.message);
5
+ process.exit(1);
6
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "qaguardian",
3
+ "version": "1.1.0",
4
+ "description": "QA Guardian CLI for triggering and monitoring test suite executions",
5
+ "keywords": [
6
+ "testing",
7
+ "automation",
8
+ "playwright",
9
+ "ci-cd"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://gitlab.com/qaguardian/qaguardian.git"
14
+ },
15
+ "bin": {
16
+ "qaguardian": "bin/qag.js",
17
+ "qag": "bin/qag.js"
18
+ },
19
+ "main": "src/index.js",
20
+ "type": "module",
21
+ "engines": {
22
+ "node": ">=20.0.0"
23
+ },
24
+ "dependencies": {
25
+ "commander": "^12.0.0",
26
+ "axios": "^1.6.7",
27
+ "ora": "^8.0.1",
28
+ "chalk": "^5.3.0"
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "src/",
33
+ "README.md"
34
+ ]
35
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * [QAG-CLI] API Client
3
+ * Handles HTTP communication with QA Guardian API Gateway
4
+ */
5
+
6
+ import axios from 'axios';
7
+
8
+ const MAX_RETRIES = 3;
9
+ const RETRY_DELAY_MS = 1000;
10
+ const RETRY_BACKOFF = 2;
11
+
12
+ /**
13
+ * Create axios instance with configuration
14
+ * @param {string} apiKey - API key for authentication
15
+ * @param {string} baseUrl - API Gateway base URL
16
+ * @returns {AxiosInstance} Configured axios instance
17
+ */
18
+ function createClient(apiKey, baseUrl) {
19
+ return axios.create({
20
+ baseURL: baseUrl,
21
+ headers: {
22
+ 'X-API-Key': apiKey,
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ timeout: 30000,
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Retry logic with exponential backoff for transient failures
31
+ * @param {Function} fn - Async function to retry
32
+ * @param {number} retries - Number of retries remaining
33
+ * @param {number} delayMs - Current delay in milliseconds
34
+ * @returns {Promise<any>} Result of the function
35
+ */
36
+ async function retryWithBackoff(fn, retries = MAX_RETRIES, delayMs = RETRY_DELAY_MS) {
37
+ try {
38
+ return await fn();
39
+ } catch (error) {
40
+ const isTransient =
41
+ error.code === 'ECONNREFUSED' ||
42
+ error.code === 'ETIMEDOUT' ||
43
+ error.code === 'ENOTFOUND' ||
44
+ (error.response && error.response.status >= 500);
45
+
46
+ if (isTransient && retries > 0) {
47
+ await new Promise(resolve => setTimeout(resolve, delayMs));
48
+ return retryWithBackoff(fn, retries - 1, delayMs * RETRY_BACKOFF);
49
+ }
50
+
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Trigger flow executions by tags or run_all flag.
57
+ *
58
+ * Tenant and environment are NOT sent in the request body. The API Gateway
59
+ * validates the X-API-Key header and injects X-Tenant-Slug / X-Environment-Slug
60
+ * headers into the forwarded request; the coordinator reads them from there.
61
+ *
62
+ * @param {Object} client - Axios client instance
63
+ * @param {Object} options - Execution options
64
+ * @param {Array<string>} options.tags - Tags to match
65
+ * @param {Array<string>} options.excludeTags - Tags to exclude
66
+ * @param {string} options.tagMatchMode - Tag matching mode ("any" or "all")
67
+ * @param {boolean} options.runAll - Run all flows for the tenant (skips tags requirement)
68
+ * @param {string} options.webhookUrl - Optional webhook URL for notifications
69
+ * @param {string} options.notifyService - Optional notification service (slack, discord, google-chat, teams)
70
+ * @returns {Promise<Object>} Execution response {triggered_flows, executions, total_triggered}
71
+ */
72
+ export async function triggerExecution(client, options) {
73
+ const {
74
+ tags = [],
75
+ excludeTags = [],
76
+ tagMatchMode = 'any',
77
+ runAll = false,
78
+ webhookUrl,
79
+ notifyService,
80
+ } = options;
81
+
82
+ // Validate notification options
83
+ if (webhookUrl && notifyService) {
84
+ throw new Error(
85
+ '[QAG-CLI] Cannot use both --webhook-url and --notify. Choose one.'
86
+ );
87
+ }
88
+
89
+ if (notifyService) {
90
+ const validServices = ['slack', 'discord', 'google-chat', 'teams'];
91
+ if (!validServices.includes(notifyService)) {
92
+ throw new Error(
93
+ `[QAG-CLI] Invalid notification service: ${notifyService}. ` +
94
+ `Valid options: ${validServices.join(', ')}`
95
+ );
96
+ }
97
+ }
98
+
99
+ // Require --tags unless --all is set
100
+ if (!runAll && tags.length === 0) {
101
+ throw new Error(
102
+ '[QAG-CLI] Must provide --tags to specify which flows to run, or use --all to run all flows.'
103
+ );
104
+ }
105
+
106
+ // Build request payload. Tenant/env are omitted — the coordinator reads them
107
+ // from the X-Tenant-Slug / X-Environment-Slug headers injected by the API Gateway.
108
+ const payload = {
109
+ tag_match_mode: tagMatchMode,
110
+ tags,
111
+ run_all: runAll,
112
+ };
113
+
114
+ if (excludeTags.length > 0) {
115
+ payload.exclude_tags = excludeTags;
116
+ }
117
+
118
+ // Add notification config if provided
119
+ if (webhookUrl || notifyService) {
120
+ payload.notification_config = {};
121
+
122
+ if (webhookUrl) {
123
+ payload.notification_config.webhook_url = webhookUrl;
124
+ payload.notification_config.webhook_enabled = true;
125
+ }
126
+
127
+ if (notifyService) {
128
+ payload.notification_config.notify_service = notifyService;
129
+ payload.notification_config.service_enabled = true;
130
+ }
131
+ }
132
+
133
+ try {
134
+ const response = await retryWithBackoff(() =>
135
+ client.post('/api/v1/flows/executions', payload)
136
+ );
137
+
138
+ return response.data;
139
+ } catch (error) {
140
+ if (error.response?.status === 401) {
141
+ throw new Error('[QAG-CLI] Authentication failed. Check your QAG_API_KEY.');
142
+ }
143
+ if (error.response?.status === 404) {
144
+ throw new Error('[QAG-CLI] API endpoint not found. Check QAG_API_URL.');
145
+ }
146
+ if (error.response?.data?.detail) {
147
+ const detail = error.response.data.detail;
148
+ const detailStr = typeof detail === 'string' ? detail : JSON.stringify(detail);
149
+ throw new Error(`[QAG-CLI] Server error: ${detailStr}`);
150
+ }
151
+ throw new Error(
152
+ `[QAG-CLI] Failed to trigger executions: ${error.message}`
153
+ );
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get execution status for a flow run
159
+ * @param {Object} client - Axios client instance
160
+ * @param {string} flowRunId - Flow run ID to poll
161
+ * @returns {Promise<Object>} Execution status object
162
+ */
163
+ export async function getExecutionStatus(client, flowRunId) {
164
+ try {
165
+ const response = await retryWithBackoff(() =>
166
+ client.get(`/api/v1/flows/executions/${flowRunId}`)
167
+ );
168
+
169
+ return response.data;
170
+ } catch (error) {
171
+ if (error.response?.status === 404) {
172
+ throw new Error(
173
+ `[QAG-CLI] Flow run ${flowRunId} not found. It may have been cancelled.`
174
+ );
175
+ }
176
+ if (error.response?.data?.detail) {
177
+ const detail = error.response.data.detail;
178
+ const detailStr = typeof detail === 'string' ? detail : JSON.stringify(detail);
179
+ throw new Error(`[QAG-CLI] Server error: ${detailStr}`);
180
+ }
181
+ throw new Error(
182
+ `[QAG-CLI] Failed to get execution status: ${error.message}`
183
+ );
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Create and return configured API client
189
+ * @param {string} apiKey - API key for authentication
190
+ * @param {string} baseUrl - API Gateway base URL
191
+ * @returns {AxiosInstance} Configured axios instance
192
+ */
193
+ export function createApiClient(apiKey, baseUrl) {
194
+ return createClient(apiKey, baseUrl);
195
+ }
package/src/config.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * [QAG-CLI] Config module
3
+ * Validates environment variables and defaults
4
+ */
5
+
6
+ const DEFAULT_API_URL = 'https://api.qaguardian.com';
7
+ const DEFAULT_POLL_INTERVAL_MS = 30000; // 30 seconds
8
+ const POLL_TIMEOUT_MS = 3600000; // 1 hour max polling time
9
+
10
+ /**
11
+ * Validate and load configuration
12
+ * @returns {Object} Configuration object
13
+ * @throws {Error} If required config is missing
14
+ */
15
+ export function loadConfig() {
16
+ const apiKey = process.env.QAG_API_KEY;
17
+
18
+ if (!apiKey) {
19
+ throw new Error(
20
+ '[QAG-CLI] QAG_API_KEY environment variable is required. ' +
21
+ 'Get your API key from https://app.qaguardian.com/settings/api-keys'
22
+ );
23
+ }
24
+
25
+ return {
26
+ apiKey,
27
+ apiUrl: process.env.QAG_API_URL || DEFAULT_API_URL,
28
+ pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
29
+ pollTimeoutMs: POLL_TIMEOUT_MS,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Parse comma-separated tag string into array
35
+ * @param {string} tagString - Comma-separated tags (e.g. "auth,ci,smoke")
36
+ * @returns {Array<string>} Array of trimmed tags
37
+ */
38
+ export function parseTags(tagString) {
39
+ if (!tagString) return [];
40
+ return tagString
41
+ .split(',')
42
+ .map(tag => tag.trim())
43
+ .filter(tag => tag.length > 0);
44
+ }
45
+
46
+ /**
47
+ * Parse API key to extract tenant and environment slugs
48
+ * Format: {tenant_slug}-{environment_slug}-qag-{hash}
49
+ * Example: guardians-primary-qag-cbaa125e027b417a
50
+ * @param {string} apiKey - API key to parse
51
+ * @returns {{tenant_slug: string, environment_slug: string}|null}
52
+ */
53
+ export function parseApiKey(apiKey) {
54
+ if (!apiKey) return null;
55
+
56
+ // Split on '-qag-' to isolate the hash suffix
57
+ const parts = apiKey.split('-qag-');
58
+ if (parts.length !== 2) return null;
59
+
60
+ const hash = parts[1];
61
+ if (!/^[a-f0-9]+$/.test(hash)) return null;
62
+
63
+ // The prefix is '{tenant_slug}-{environment_slug}'
64
+ // Use the last hyphen to split, so multi-segment tenant slugs work
65
+ const prefix = parts[0];
66
+ const lastDash = prefix.lastIndexOf('-');
67
+ if (lastDash === -1) return null;
68
+
69
+ return {
70
+ tenant_slug: prefix.substring(0, lastDash),
71
+ environment_slug: prefix.substring(lastDash + 1),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Validate tag matching mode
77
+ * @param {string} mode - Matching mode ("any" or "all")
78
+ * @returns {string} Validated mode
79
+ * @throws {Error} If mode is invalid
80
+ */
81
+ export function validateTagMatchMode(mode) {
82
+ const valid = ['any', 'all'];
83
+ if (!valid.includes(mode)) {
84
+ throw new Error(
85
+ `[QAG-CLI] Invalid tag match mode: ${mode}. Must be "any" (OR) or "all" (AND).`
86
+ );
87
+ }
88
+ return mode;
89
+ }
package/src/index.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * [QAG-CLI] Main CLI Entry Point
3
+ * Command-line interface for triggering and monitoring QA Guardian test executions
4
+ */
5
+
6
+ import { createRequire } from 'module';
7
+ import { Command } from 'commander';
8
+ import chalk from 'chalk';
9
+ import { loadConfig, parseTags, validateTagMatchMode, parseApiKey } from './config.js';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const { version } = require('../package.json');
13
+ import { createApiClient, triggerExecution } from './api-client.js';
14
+ import { pollUntilComplete } from './poller.js';
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('qag')
20
+ .description('QA Guardian CLI - Trigger and monitor test suite executions')
21
+ .version(version)
22
+ .usage('[options]')
23
+ .option(
24
+ '--tags <tags>',
25
+ 'Comma-separated tags to match (e.g., "auth,ci,smoke")'
26
+ )
27
+ .option(
28
+ '--exclude-tags <tags>',
29
+ 'Comma-separated tags to exclude (e.g., "slow,flaky")'
30
+ )
31
+ .option(
32
+ '--all',
33
+ 'Run all flows (combine with --exclude-tags to filter)'
34
+ )
35
+ .option(
36
+ '--match-mode <mode>',
37
+ 'Tag matching mode: "any" (OR logic, default) or "all" (AND logic)',
38
+ 'any'
39
+ )
40
+ .option(
41
+ '--no-wait',
42
+ 'Trigger and exit immediately without waiting for results'
43
+ )
44
+ .option(
45
+ '--webhook-url <url>',
46
+ 'Custom webhook URL for completion notifications'
47
+ )
48
+ .option(
49
+ '--notify <service>',
50
+ 'Notify via service: slack, discord, google-chat, or teams'
51
+ )
52
+ .option(
53
+ '--api-url <url>',
54
+ 'Custom API Gateway URL (default: https://api.qaguardian.com)',
55
+ 'https://api.qaguardian.com'
56
+ )
57
+ .action(runCLI);
58
+
59
+ program.parse();
60
+
61
+ /**
62
+ * Main CLI execution handler
63
+ * @param {Object} options - Parsed CLI options
64
+ */
65
+ async function runCLI(options) {
66
+ try {
67
+ // Load and validate configuration
68
+ const config = loadConfig();
69
+ const client = createApiClient(config.apiKey, options.apiUrl);
70
+
71
+ // Parse and validate options
72
+ const tags = parseTags(options.tags || '');
73
+ const excludeTags = parseTags(options.excludeTags || '');
74
+ const tagMatchMode = validateTagMatchMode(options.matchMode);
75
+ const runAll = options.all || false;
76
+ const wait = options.wait; // true by default, false with --no-wait
77
+ const apiUrl = options.apiUrl;
78
+
79
+ // Extract tenant and environment from API key
80
+ const apiKeyInfo = parseApiKey(config.apiKey);
81
+ if (!apiKeyInfo) {
82
+ throw new Error(
83
+ '[QAG-CLI] Invalid API key format. Expected: {tenant_slug}-{environment_slug}-qag-{hash}'
84
+ );
85
+ }
86
+
87
+ const { tenant_slug, environment_slug } = apiKeyInfo;
88
+
89
+ console.log(
90
+ chalk.blue('[QAG-CLI]'),
91
+ 'Triggering flow executions...'
92
+ );
93
+
94
+ if (runAll && excludeTags.length > 0) {
95
+ console.log(chalk.gray(' Excluding tags:'), excludeTags.join(', '));
96
+ } else if (tags.length > 0) {
97
+ console.log(chalk.gray(' Tags:'), tags.join(', '));
98
+ if (excludeTags.length > 0) {
99
+ console.log(chalk.gray(' Excluding:'), excludeTags.join(', '));
100
+ }
101
+ }
102
+
103
+ if (options.webhookUrl) {
104
+ console.log(chalk.gray(' Webhook:'), options.webhookUrl);
105
+ }
106
+
107
+ if (options.notify) {
108
+ console.log(chalk.gray(' Notify via:'), options.notify);
109
+ }
110
+
111
+ // Validate that we have tags
112
+ if (!runAll && tags.length === 0) {
113
+ throw new Error('[QAG-CLI] Must provide --tags to specify which flows to run');
114
+ }
115
+
116
+ // Trigger execution
117
+ const response = await triggerExecution(client, {
118
+ tags,
119
+ excludeTags,
120
+ tagMatchMode,
121
+ runAll,
122
+ tenantSlug: tenant_slug,
123
+ environmentName: environment_slug,
124
+ webhookUrl: options.webhookUrl,
125
+ notifyService: options.notify,
126
+ });
127
+
128
+ const { triggered_flows: flowRunIds, executions, total_triggered: totalTriggered } = response;
129
+
130
+ if (!flowRunIds || flowRunIds.length === 0) {
131
+ console.log(chalk.yellow('⚠ No flows matched the specified criteria'));
132
+ process.exit(0);
133
+ }
134
+
135
+ console.log(
136
+ chalk.green('✓'),
137
+ `Triggered ${totalTriggered} flow run(s):`,
138
+ flowRunIds.join(', ')
139
+ );
140
+
141
+ // If --no-wait, exit immediately
142
+ if (!wait) {
143
+ console.log(
144
+ chalk.blue('[QAG-CLI]'),
145
+ 'Not waiting for completion (--no-wait flag set)'
146
+ );
147
+ process.exit(0);
148
+ }
149
+
150
+ // Poll for completion
151
+ console.log(chalk.blue('[QAG-CLI]') + ' Polling for results (30s intervals)...');
152
+ const result = await pollUntilComplete(client, flowRunIds, {
153
+ pollIntervalMs: 30000,
154
+ pollTimeoutMs: 3600000,
155
+ });
156
+
157
+ // Print final summary
158
+ const { summary } = result;
159
+ console.log();
160
+ console.log(chalk.blue('═══════════════════════════════════'));
161
+ console.log(chalk.blue('[QAG-CLI] Execution Summary:'));
162
+ console.log(chalk.blue('═══════════════════════════════════'));
163
+ console.log(` Flow Runs: ${summary.flowRuns}`);
164
+ console.log(` Total Tests: ${summary.totalTests}`);
165
+ console.log(
166
+ ` ${chalk.green('Passed:')} ${summary.passed}`
167
+ );
168
+ console.log(
169
+ ` ${chalk.red('Failed:')} ${summary.failed}`
170
+ );
171
+ console.log(chalk.blue('═══════════════════════════════════'));
172
+
173
+ // Exit with appropriate code
174
+ if (summary.hasFailures) {
175
+ console.log(
176
+ chalk.red('✗ Tests completed with failures')
177
+ );
178
+ process.exit(1);
179
+ } else {
180
+ console.log(
181
+ chalk.green('✓ All tests passed!')
182
+ );
183
+ process.exit(0);
184
+ }
185
+ } catch (error) {
186
+ console.error(chalk.red(error.message || 'Unknown error'));
187
+ process.exit(1);
188
+ }
189
+ }
package/src/poller.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * [QAG-CLI] Polling Module
3
+ * Handles real-time polling of flow executions with progress display
4
+ */
5
+
6
+ import ora from 'ora';
7
+ import { getExecutionStatus } from './api-client.js';
8
+
9
+ /**
10
+ * Poll flow runs until completion
11
+ * @param {Object} client - Axios client instance
12
+ * @param {Array<string>} flowRunIds - Flow run IDs to monitor
13
+ * @param {Object} config - Configuration {pollIntervalMs, pollTimeoutMs}
14
+ * @returns {Promise<Object>} Final execution results
15
+ */
16
+ export async function pollUntilComplete(client, flowRunIds, config) {
17
+ if (!flowRunIds || flowRunIds.length === 0) {
18
+ throw new Error('[QAG-CLI] No flow runs to poll');
19
+ }
20
+
21
+ const { pollIntervalMs = 30000, pollTimeoutMs = 3600000 } = config;
22
+
23
+ const spinner = ora({
24
+ text: `[QAG-CLI] Polling ${flowRunIds.length} flow run(s)...`,
25
+ prefixText: '',
26
+ spinner: 'dots',
27
+ }).start();
28
+
29
+ const startTime = Date.now();
30
+ const statuses = new Map(); // Track individual run statuses
31
+
32
+ try {
33
+ while (true) {
34
+ // Check for timeout
35
+ if (Date.now() - startTime > pollTimeoutMs) {
36
+ spinner.fail('[QAG-CLI] Polling timeout reached (1 hour)');
37
+ return createFailureResult(statuses);
38
+ }
39
+
40
+ // Poll all flow runs in parallel
41
+ const pollPromises = flowRunIds.map(id =>
42
+ getExecutionStatus(client, id)
43
+ .then(status => ({ id, status, error: null }))
44
+ .catch(error => ({ id, status: null, error }))
45
+ );
46
+
47
+ const results = await Promise.all(pollPromises);
48
+
49
+ // Update statuses map
50
+ let allComplete = true;
51
+ let totalTests = 0;
52
+ let completedTests = 0;
53
+ let passedTests = 0;
54
+ let failedTests = 0;
55
+
56
+ for (const result of results) {
57
+ if (result.error) {
58
+ statuses.set(result.id, { status: 'error', error: result.error.message });
59
+ allComplete = false;
60
+ continue;
61
+ }
62
+
63
+ const { status } = result.status;
64
+ statuses.set(result.id, result.status);
65
+
66
+ // Aggregate stats
67
+ totalTests += result.status.total_tests || 0;
68
+ completedTests += result.status.completed_tests || 0;
69
+ failedTests += result.status.failed_tests || 0;
70
+
71
+ if (status === 'starting' || status === 'running' || status === 'pending') {
72
+ allComplete = false;
73
+ }
74
+ }
75
+
76
+ // Calculate passed from completed and failed
77
+ passedTests = completedTests - failedTests;
78
+
79
+ // Update spinner text with progress
80
+ const progressText = `Running... (${completedTests}/${totalTests} tests completed, ${failedTests} failed)`;
81
+ spinner.text = progressText;
82
+
83
+ // Check if all runs are complete
84
+ if (allComplete) {
85
+ break;
86
+ }
87
+
88
+ // Wait before next poll
89
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
90
+ }
91
+
92
+ spinner.succeed('[QAG-CLI] All flow runs completed');
93
+ return createSuccessResult(statuses);
94
+ } catch (error) {
95
+ spinner.fail(`[QAG-CLI] Error during polling: ${error.message}`);
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create success result object
102
+ * @param {Map} statuses - Flow run statuses
103
+ * @returns {Object} Result with exit code and summary
104
+ */
105
+ function createSuccessResult(statuses) {
106
+ let hasFailures = false;
107
+ let totalTests = 0;
108
+ let totalPassed = 0;
109
+ let totalFailed = 0;
110
+
111
+ for (const [id, status] of statuses.entries()) {
112
+ if (status.error) {
113
+ hasFailures = true;
114
+ continue;
115
+ }
116
+
117
+ totalTests += status.total_tests || 0;
118
+ totalPassed += (status.total_tests || 0) - (status.failed_tests || 0);
119
+ totalFailed += status.failed_tests || 0;
120
+
121
+ if (status.status === 'failed' || status.failed_tests > 0) {
122
+ hasFailures = true;
123
+ }
124
+ }
125
+
126
+ return {
127
+ exitCode: hasFailures ? 1 : 0,
128
+ summary: {
129
+ totalTests,
130
+ passed: totalPassed,
131
+ failed: totalFailed,
132
+ flowRuns: statuses.size,
133
+ hasFailures,
134
+ },
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Create failure result object
140
+ * @param {Map} statuses - Flow run statuses
141
+ * @returns {Object} Result with exit code and summary
142
+ */
143
+ function createFailureResult(statuses) {
144
+ return {
145
+ exitCode: 1,
146
+ summary: {
147
+ totalTests: 0,
148
+ passed: 0,
149
+ failed: 0,
150
+ flowRuns: statuses.size,
151
+ hasFailures: true,
152
+ },
153
+ };
154
+ }