agentq-webdriverio 1.0.2 → 1.0.3

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 CHANGED
@@ -1 +1,113 @@
1
- # agentq-webdriverio
1
+ # AgentQ for WebdriverIO
2
+
3
+ Integrate WebdriverIO (TypeScript) with AgentQ AI for AI-driven automation and automated test-result reporting.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install agentq-webdriverio
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ### 1. AgentQ config (AI steps)
14
+
15
+ Create `agentq.config.json` in your project root with your company API key (from your account profile; you must sign in first):
16
+
17
+ ```json
18
+ {
19
+ "TOKEN": "YOUR_API_KEY"
20
+ }
21
+ ```
22
+
23
+ Or export it as an environment variable:
24
+
25
+ ```bash
26
+ export AGENTQ_TOKEN="YOUR_API_KEY"
27
+ ```
28
+
29
+ ### 2. Environment variables (result reporting)
30
+
31
+ Set these in `.env` or your CI environment. The recommended way is the company API key — the same key as above, no email/password needed:
32
+
33
+ ```bash
34
+ AGENTQ_API_KEY=your_company_api_key # from your AgentQ profile
35
+ AGENTQ_PROJECT_ID=your_project_id
36
+ AGENTQ_TESTRUN_ID=your_testrun_id
37
+ AGENTQ_EMAIL=your_email # optional: results show this member as "Created By" (owner or invited member of the key's company)
38
+ ```
39
+
40
+ Email/password login is still supported as a fallback (when no API key is set):
41
+
42
+ ```bash
43
+ AGENTQ_EMAIL=your_email
44
+ AGENTQ_PASSWORD=your_password
45
+ ```
46
+
47
+ ### 3. WebdriverIO hooks (`wdio.conf.ts`)
48
+
49
+ ```typescript
50
+ import { initAgentQ, handleTestConclusion } from 'agentq-webdriverio';
51
+
52
+ export const config: WebdriverIO.Config = {
53
+ // ... other config
54
+
55
+ before: function () {
56
+ initAgentQ(browser);
57
+ },
58
+
59
+ afterTest: async function (test, context, { error, duration, passed }) {
60
+ await handleTestConclusion(
61
+ test.title,
62
+ passed ? 'passed' : 'failed',
63
+ Date.now() - duration,
64
+ error?.message
65
+ );
66
+ },
67
+ };
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ Prefix test titles with the AgentQ test case ID (e.g. `17-`) — results are matched to test cases by that numeric prefix:
73
+
74
+ ```typescript
75
+ import { q, test } from 'agentq-webdriverio';
76
+
77
+ describe('AI Automation Flow', () => {
78
+ test('17-should login using AI instructions', async () => {
79
+ await browser.url('https://the-internet.herokuapp.com/login');
80
+
81
+ await q('user fill username tomsmith');
82
+ await q('user fill password SuperSecretPassword!');
83
+ await q('user click login button');
84
+
85
+ const flashAlert = await $('#flash');
86
+ await expect(flashAlert).toHaveText(
87
+ expect.stringContaining('You logged into a secure area!')
88
+ );
89
+ });
90
+ });
91
+ ```
92
+
93
+ ## Test Run Sync (pull test suite & push results)
94
+
95
+ Pull a test suite from a test run and report each test's status back to AgentQ:
96
+
97
+ ```bash
98
+ npx agentq-pull-testcase -- --tcid=YOUR_TC_ID
99
+ npx agentq-pull-testsuite -- --testrunid=YOUR_TESTRUN_ID
100
+ ```
101
+
102
+ Then run the generated specs:
103
+
104
+ ```bash
105
+ npx wdio run ./wdio.conf.ts
106
+ ```
107
+
108
+ > Tip for CI (e.g. GitHub Actions): store `AGENTQ_API_KEY` as a secret and export it in the workflow. API key auth skips the login endpoint entirely.
109
+
110
+ ## Plans and Rate Limits
111
+
112
+ - Free: Free of charge
113
+ - Pro & Enterprise: visit [www.agentq.id](https://www.agentq.id) or contact support@agentq.id to upgrade your plan.
package/dist/index.js CHANGED
@@ -29,7 +29,8 @@ Object.defineProperty(exports, "uploadArtifact", { enumerable: true, get: functi
29
29
  async function handleTestConclusion(testTitle, status, startTime, errorDetails) {
30
30
  const tcId = parseInt(testTitle.split('-')[0]);
31
31
  if (!isNaN(tcId) && process.env.AGENTQ_TESTRUN_ID) {
32
- if (!cachedAccessToken) {
32
+ // With AGENTQ_API_KEY set, requests use X-API-Key and no login is needed.
33
+ if (!process.env.AGENTQ_API_KEY && !cachedAccessToken) {
33
34
  cachedAccessToken = await (0, testResult_2.getAccessToken)();
34
35
  }
35
36
  const executionTime = (Date.now() - startTime) / 1000;
@@ -14,6 +14,7 @@ const config = {
14
14
  apiBaseUrl: process.env.AGENTQ_API_URL || 'https://backend-app.agentq.id',
15
15
  projectId: `${process.env.AGENTQ_PROJECT_ID}`,
16
16
  testRunId: `${process.env.AGENTQ_TESTRUN_ID}`,
17
+ apiKey: process.env.AGENTQ_API_KEY || null,
17
18
  authEndpoint: '/auth/login',
18
19
  authData: {
19
20
  email: `${process.env.AGENTQ_EMAIL}`,
@@ -21,6 +22,33 @@ const config = {
21
22
  },
22
23
  outputDir: path_1.default.join(process.cwd(), 'tests')
23
24
  };
25
+ // Identify ourselves instead of the default axios/x.y UA, which bot
26
+ // protections (e.g. Cloudflare) commonly flag in CI environments.
27
+ let pkgVersion = '';
28
+ try {
29
+ pkgVersion = require('../package.json').version;
30
+ }
31
+ catch { /* best effort */ }
32
+ const USER_AGENT = `agentq-webdriverio${pkgVersion ? `/${pkgVersion}` : ''}`;
33
+ // API key auth (X-API-Key) when AGENTQ_API_KEY is set, otherwise the
34
+ // Bearer token obtained from email/password login.
35
+ function authHeaders(accessToken) {
36
+ const headers = {
37
+ 'accept': 'application/json',
38
+ 'User-Agent': USER_AGENT
39
+ };
40
+ if (config.apiKey) {
41
+ headers['X-API-Key'] = config.apiKey;
42
+ // Optional attribution: see testResult.ts getAuthHeaders.
43
+ if (process.env.AGENTQ_EMAIL) {
44
+ headers['X-Actor-Email'] = process.env.AGENTQ_EMAIL;
45
+ }
46
+ }
47
+ else if (accessToken) {
48
+ headers['Authorization'] = `Bearer ${accessToken}`;
49
+ }
50
+ return headers;
51
+ }
24
52
  // Parse command line arguments more robustly
25
53
  function parseArgs() {
26
54
  const args = process.argv.slice(2);
@@ -63,12 +91,18 @@ if (!fs_1.default.existsSync(config.outputDir)) {
63
91
  fs_1.default.mkdirSync(config.outputDir, { recursive: true });
64
92
  }
65
93
  async function getAccessToken() {
94
+ if (!process.env.AGENTQ_EMAIL || !process.env.AGENTQ_PASSWORD) {
95
+ console.error('Error: No credentials provided.');
96
+ console.error('Set AGENTQ_API_KEY (recommended), or AGENTQ_EMAIL and AGENTQ_PASSWORD.');
97
+ process.exit(1);
98
+ }
66
99
  try {
67
100
  console.log(`Authenticating with: ${config.apiBaseUrl}${config.authEndpoint}`);
68
101
  const response = await axios_1.default.post(`${config.apiBaseUrl}${config.authEndpoint}`, config.authData, {
69
102
  headers: {
70
103
  'accept': 'application/json',
71
- 'Content-Type': 'application/json'
104
+ 'Content-Type': 'application/json',
105
+ 'User-Agent': USER_AGENT
72
106
  }
73
107
  });
74
108
  return response.data.access_token;
@@ -92,10 +126,7 @@ async function fetchTestCase(tcId, accessToken) {
92
126
  try {
93
127
  console.log(`Fetching test case from: ${config.apiBaseUrl}/projects/${config.projectId}/test-cases/tcId/${tcId}`);
94
128
  const response = await axios_1.default.get(`${config.apiBaseUrl}/projects/${config.projectId}/test-cases/tcId/${tcId}`, {
95
- headers: {
96
- 'accept': 'application/json',
97
- 'Authorization': `Bearer ${accessToken}`
98
- }
129
+ headers: authHeaders(accessToken)
99
130
  });
100
131
  return response.data;
101
132
  }
@@ -119,10 +150,7 @@ async function fetchTestSuite(testRunId, accessToken) {
119
150
  const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results?page=1&limit=10000`;
120
151
  console.log(`Fetching test suite results from: ${apiUrl}`);
121
152
  const response = await axios_1.default.get(apiUrl, {
122
- headers: {
123
- 'accept': 'application/json',
124
- 'Authorization': `Bearer ${accessToken}`
125
- }
153
+ headers: authHeaders(accessToken)
126
154
  });
127
155
  return response.data;
128
156
  }
@@ -225,13 +253,21 @@ ${expectationCode}
225
253
  console.log(`✅ Created: ${filePath}`);
226
254
  return filePath;
227
255
  }
256
+ // Resolve auth once: API key needs no login round-trip.
257
+ async function resolveAccessToken() {
258
+ if (config.apiKey) {
259
+ console.log(`🔑 Using AGENTQ_API_KEY for authentication.`);
260
+ return null;
261
+ }
262
+ const accessToken = await getAccessToken();
263
+ console.log(`🔑 Successfully obtained access token.`);
264
+ return accessToken;
265
+ }
228
266
  // Main execution
229
267
  (async () => {
230
268
  if (tcId) {
231
269
  console.log(`🔍 Fetching test case with tcId: ${tcId}`);
232
- // Get the access token
233
- const accessToken = await getAccessToken();
234
- console.log(`🔑 Successfully obtained access token.`);
270
+ const accessToken = await resolveAccessToken();
235
271
  // Fetch the test case using the access token
236
272
  const testCase = await fetchTestCase(tcId, accessToken);
237
273
  console.log(`📄 Found test case: ${testCase.title}`);
@@ -241,9 +277,7 @@ ${expectationCode}
241
277
  }
242
278
  else if (testRunId) {
243
279
  console.log(`🔍 Fetching test suite results for testRunId: ${testRunId}`);
244
- // Get the access token
245
- const accessToken = await getAccessToken();
246
- console.log(`🔑 Successfully obtained access token.`);
280
+ const accessToken = await resolveAccessToken();
247
281
  // Fetch the test suite results
248
282
  const testSuiteResponse = await fetchTestSuite(testRunId, accessToken);
249
283
  const testResults = testSuiteResponse.results;
@@ -1,4 +1,5 @@
1
1
  import { TestResult } from './types';
2
+ export declare function getAuthHeaders(): Promise<Record<string, string>>;
2
3
  export declare function getAccessToken(): Promise<string>;
3
4
  export declare function exportTestResult(tcId: string, testRunId: string, result: Partial<TestResult>): Promise<any>;
4
5
  export declare function uploadArtifact(testRunId: string, testResultId: string, type: 'screenshot' | 'video', filePath: string): Promise<any>;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getAuthHeaders = getAuthHeaders;
6
7
  exports.getAccessToken = getAccessToken;
7
8
  exports.exportTestResult = exportTestResult;
8
9
  exports.uploadArtifact = uploadArtifact;
@@ -16,17 +17,50 @@ dotenv.config({ quiet: true });
16
17
  const config = {
17
18
  apiBaseUrl: process.env.AGENTQ_API_URL || 'https://backend-app.agentq.id',
18
19
  projectId: `${process.env.AGENTQ_PROJECT_ID}`,
20
+ apiKey: process.env.AGENTQ_API_KEY || null,
19
21
  authEndpoint: '/auth/login',
20
22
  authData: {
21
23
  email: `${process.env.AGENTQ_EMAIL}`,
22
24
  password: `${process.env.AGENTQ_PASSWORD}`
23
25
  }
24
26
  };
27
+ // Identify ourselves instead of the default axios/x.y UA, which bot
28
+ // protections (e.g. Cloudflare) commonly flag in CI environments.
29
+ let pkgVersion = '';
30
+ try {
31
+ pkgVersion = require('../package.json').version;
32
+ }
33
+ catch { /* best effort */ }
34
+ const USER_AGENT = `agentq-webdriverio${pkgVersion ? `/${pkgVersion}` : ''}`;
25
35
  let cachedToken = null;
36
+ // API key auth (X-API-Key) when AGENTQ_API_KEY is set, otherwise the
37
+ // Bearer token obtained from email/password login.
38
+ async function getAuthHeaders() {
39
+ const headers = {
40
+ 'accept': 'application/json',
41
+ 'User-Agent': USER_AGENT
42
+ };
43
+ if (config.apiKey) {
44
+ headers['X-API-Key'] = config.apiKey;
45
+ // Optional attribution: with API key auth, AGENTQ_EMAIL names the company
46
+ // member shown as "Created By" on pushed results (no password needed).
47
+ if (process.env.AGENTQ_EMAIL) {
48
+ headers['X-Actor-Email'] = process.env.AGENTQ_EMAIL;
49
+ }
50
+ return headers;
51
+ }
52
+ headers['Authorization'] = `Bearer ${await getAccessToken()}`;
53
+ return headers;
54
+ }
26
55
  async function getAccessToken() {
27
56
  if (cachedToken) {
28
57
  return cachedToken;
29
58
  }
59
+ if (!process.env.AGENTQ_EMAIL || !process.env.AGENTQ_PASSWORD) {
60
+ console.error('Error: No credentials provided.');
61
+ console.error('Set AGENTQ_API_KEY (recommended), or AGENTQ_EMAIL and AGENTQ_PASSWORD.');
62
+ process.exit(1);
63
+ }
30
64
  try {
31
65
  // console.log(`Authenticating with: ${config.apiBaseUrl}${config.authEndpoint}`);
32
66
  const response = await axios_1.default.post(`${config.apiBaseUrl}${config.authEndpoint}`, config.authData, {
@@ -54,14 +88,13 @@ async function getAccessToken() {
54
88
  }
55
89
  }
56
90
  async function exportTestResult(tcId, testRunId, result) {
57
- const accessToken = await getAccessToken();
91
+ const authHeaders = await getAuthHeaders();
58
92
  const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results/tcId/${tcId}`;
59
93
  try {
60
94
  // console.log(`Pushing test result to: ${apiUrl}`);
61
95
  const response = await axios_1.default.patch(apiUrl, result, {
62
96
  headers: {
63
- 'accept': 'application/json',
64
- 'Authorization': `Bearer ${accessToken}`,
97
+ ...authHeaders,
65
98
  'Content-Type': 'application/json'
66
99
  }
67
100
  });
@@ -84,7 +117,7 @@ async function exportTestResult(tcId, testRunId, result) {
84
117
  }
85
118
  }
86
119
  async function uploadArtifact(testRunId, testResultId, type, filePath) {
87
- const accessToken = await getAccessToken();
120
+ const authHeaders = await getAuthHeaders();
88
121
  const apiUrl = `${config.apiBaseUrl}/projects/${config.projectId}/test-runs/${testRunId}/test-results/${testResultId}/${type}`;
89
122
  if (!fs_1.default.existsSync(filePath)) {
90
123
  console.warn(`⚠️ Artifact file not found: ${filePath}`);
@@ -100,7 +133,7 @@ async function uploadArtifact(testRunId, testResultId, type, filePath) {
100
133
  // console.log(`Uploading ${type} to: ${apiUrl}`);
101
134
  const response = await axios_1.default.post(apiUrl, formData, {
102
135
  headers: {
103
- 'Authorization': `Bearer ${accessToken}`,
136
+ ...authHeaders,
104
137
  'Content-Type': 'multipart/form-data'
105
138
  }
106
139
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentq-webdriverio",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {
@@ -1,3 +0,0 @@
1
- {
2
- "TOKEN": "efe9c3a612d6196c369c41de02fd63a8d1da8b9f00640daabf799bd01e7b6c1c"
3
- }