@arcadialdev/arcality 4.1.4 → 4.1.5
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/package.json +1 -1
- package/playwright.config.js +56 -56
- package/src/arcalityClient.mjs +67 -64
- package/src/services/mcpStdioClient.mjs +204 -204
- package/tests/_helpers/ArcalityReporter.js +1342 -1342
- package/tests/_helpers/agentic-runner.bundle.spec.js +291 -20
package/package.json
CHANGED
package/playwright.config.js
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
// playwright.config.js
|
|
2
|
-
'use strict';
|
|
3
|
-
const { defineConfig, devices } = require('@playwright/test');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
// ── Dynamic reporter: JUnit is added when running in CI (Azure DevOps, GitHub Actions) ──
|
|
8
|
-
// The JUnit XML file enables native test result publishing in your pipeline dashboard.
|
|
9
|
-
// In local/interactive mode, the JUnit reporter is skipped — only Arcality's HTML reporter runs.
|
|
10
|
-
const reportsDir = process.env.REPORTS_DIR || path.join(os.tmpdir(), 'arcality', 'reports');
|
|
11
|
-
const playwrightOutputDir = process.env.PLAYWRIGHT_OUTPUT_DIR || path.join(os.tmpdir(), 'arcality', 'playwright-output');
|
|
12
|
-
const isCI = process.env.CI === 'true';
|
|
13
|
-
|
|
14
|
-
const reporters = [
|
|
15
|
-
['line'],
|
|
16
|
-
[path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'), { outputDir: reportsDir }],
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
if (isCI) {
|
|
20
|
-
reporters.push(['junit', { outputFile: path.join(reportsDir, 'results.xml') }]);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
module.exports = defineConfig({
|
|
24
|
-
testDir: path.join(__dirname, 'tests', '_helpers'),
|
|
25
|
-
testMatch: ['agentic-runner.bundle.spec.js'],
|
|
26
|
-
reporter: reporters,
|
|
27
|
-
outputDir: playwrightOutputDir,
|
|
28
|
-
use: {
|
|
29
|
-
// ── Dynamic BASE_URL ──
|
|
30
|
-
// Priority: CLI --base-url flag → arcality.config baseUrl → BASE_URL env → default
|
|
31
|
-
// This allows pipelines to inject the target environment dynamically:
|
|
32
|
-
// arcality --run-all --base-url=https://staging.myapp.com
|
|
33
|
-
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
34
|
-
video: '
|
|
35
|
-
screenshot: 'only-on-failure',
|
|
36
|
-
trace: 'retain-on-failure',
|
|
37
|
-
colorScheme: 'dark',
|
|
38
|
-
// In CI: headless is enforced by Playwright automatically when CI=true
|
|
39
|
-
headless: isCI ? true : undefined,
|
|
40
|
-
contextOptions: {
|
|
41
|
-
reducedMotion: 'reduce',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
projects: [
|
|
45
|
-
{
|
|
46
|
-
name: 'AgenticBrowser',
|
|
47
|
-
use: {
|
|
48
|
-
...devices['Desktop Chrome'],
|
|
49
|
-
channel: 'chrome',
|
|
50
|
-
viewport: { width: 1440, height: 900 },
|
|
51
|
-
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ArcalityAgent/1.0',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
});
|
|
56
|
-
|
|
1
|
+
// playwright.config.js
|
|
2
|
+
'use strict';
|
|
3
|
+
const { defineConfig, devices } = require('@playwright/test');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// ── Dynamic reporter: JUnit is added when running in CI (Azure DevOps, GitHub Actions) ──
|
|
8
|
+
// The JUnit XML file enables native test result publishing in your pipeline dashboard.
|
|
9
|
+
// In local/interactive mode, the JUnit reporter is skipped — only Arcality's HTML reporter runs.
|
|
10
|
+
const reportsDir = process.env.REPORTS_DIR || path.join(os.tmpdir(), 'arcality', 'reports');
|
|
11
|
+
const playwrightOutputDir = process.env.PLAYWRIGHT_OUTPUT_DIR || path.join(os.tmpdir(), 'arcality', 'playwright-output');
|
|
12
|
+
const isCI = process.env.CI === 'true';
|
|
13
|
+
|
|
14
|
+
const reporters = [
|
|
15
|
+
['line'],
|
|
16
|
+
[path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'), { outputDir: reportsDir }],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (isCI) {
|
|
20
|
+
reporters.push(['junit', { outputFile: path.join(reportsDir, 'results.xml') }]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = defineConfig({
|
|
24
|
+
testDir: path.join(__dirname, 'tests', '_helpers'),
|
|
25
|
+
testMatch: ['agentic-runner.bundle.spec.js'],
|
|
26
|
+
reporter: reporters,
|
|
27
|
+
outputDir: playwrightOutputDir,
|
|
28
|
+
use: {
|
|
29
|
+
// ── Dynamic BASE_URL ──
|
|
30
|
+
// Priority: CLI --base-url flag → arcality.config baseUrl → BASE_URL env → default
|
|
31
|
+
// This allows pipelines to inject the target environment dynamically:
|
|
32
|
+
// arcality --run-all --base-url=https://staging.myapp.com
|
|
33
|
+
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
34
|
+
video: 'on',
|
|
35
|
+
screenshot: 'only-on-failure',
|
|
36
|
+
trace: 'retain-on-failure',
|
|
37
|
+
colorScheme: 'dark',
|
|
38
|
+
// In CI: headless is enforced by Playwright automatically when CI=true
|
|
39
|
+
headless: isCI ? true : undefined,
|
|
40
|
+
contextOptions: {
|
|
41
|
+
reducedMotion: 'reduce',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
projects: [
|
|
45
|
+
{
|
|
46
|
+
name: 'AgenticBrowser',
|
|
47
|
+
use: {
|
|
48
|
+
...devices['Desktop Chrome'],
|
|
49
|
+
channel: 'chrome',
|
|
50
|
+
viewport: { width: 1440, height: 900 },
|
|
51
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ArcalityAgent/1.0',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/arcalityClient.mjs
|
|
2
2
|
// Client for communicating with Arcality Backend
|
|
3
|
-
// Supports arcality.config (primary) and .env (fallback)
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
|
|
7
|
-
import { isRealProjectId } from './configManager.mjs';
|
|
3
|
+
// Supports arcality.config (primary) and .env (fallback)
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
|
|
7
|
+
import { isRealProjectId } from './configManager.mjs';
|
|
8
8
|
|
|
9
9
|
// ── Backend config (internal, not user-configurable) ──
|
|
10
10
|
const DAILY_MISSION_LIMIT = 35;
|
|
@@ -97,11 +97,11 @@ function getEffectiveApiKey() {
|
|
|
97
97
|
* Gets the effective Project ID.
|
|
98
98
|
* Priority: arcality.config > ARCALITY_PROJECT_ID env var
|
|
99
99
|
*/
|
|
100
|
-
function loadProjectId() {
|
|
101
|
-
const localConfig = loadArcalityConfig();
|
|
102
|
-
const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID || null;
|
|
103
|
-
return isRealProjectId(projectId) ? projectId : null;
|
|
104
|
-
}
|
|
100
|
+
function loadProjectId() {
|
|
101
|
+
const localConfig = loadArcalityConfig();
|
|
102
|
+
const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID || null;
|
|
103
|
+
return isRealProjectId(projectId) ? projectId : null;
|
|
104
|
+
}
|
|
105
105
|
|
|
106
106
|
// ═══════════════════════════════════════════════════════
|
|
107
107
|
// PUBLIC API
|
|
@@ -126,11 +126,11 @@ export async function validateApiKey() {
|
|
|
126
126
|
const key = getEffectiveApiKey();
|
|
127
127
|
|
|
128
128
|
if (!key) {
|
|
129
|
-
return { valid: false, error: 'no_api_key', mode: 'live' };
|
|
129
|
+
return { valid: false, error: 'no_api_key', mode: 'live' };
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (!key.startsWith('arc_')) {
|
|
133
|
-
return { valid: false, error: 'invalid_format', mode: 'live' };
|
|
133
|
+
return { valid: false, error: 'invalid_format', mode: 'live' };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const apiBase = getEffectiveApiBase();
|
|
@@ -162,12 +162,12 @@ export async function validateApiKey() {
|
|
|
162
162
|
return { ...data, valid: true, mode: 'live' };
|
|
163
163
|
} catch (e) {
|
|
164
164
|
const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
|
|
165
|
-
console.warn(`Backend not available (${apiBase}): ${reason}.`);
|
|
166
|
-
return { valid: false, error: 'backend_unavailable', mode: 'live', reason };
|
|
165
|
+
console.warn(`Backend not available (${apiBase}): ${reason}.`);
|
|
166
|
+
return { valid: false, error: 'backend_unavailable', mode: 'live', reason };
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
return { valid: false, error: 'backend_unavailable', mode: 'live' };
|
|
170
|
+
return { valid: false, error: 'backend_unavailable', mode: 'live' };
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
@@ -184,10 +184,10 @@ export async function startMission(prompt, targetUrl) {
|
|
|
184
184
|
// ── LIVE mode ──
|
|
185
185
|
if (apiBase) {
|
|
186
186
|
try {
|
|
187
|
-
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
188
|
-
if (!isRealProjectId(projectId)) {
|
|
189
|
-
return { allowed: false, error: 'no_project_id' };
|
|
190
|
-
}
|
|
187
|
+
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
188
|
+
if (!isRealProjectId(projectId)) {
|
|
189
|
+
return { allowed: false, error: 'no_project_id' };
|
|
190
|
+
}
|
|
191
191
|
|
|
192
192
|
const controller = new AbortController();
|
|
193
193
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
@@ -216,13 +216,13 @@ export async function startMission(prompt, targetUrl) {
|
|
|
216
216
|
return { allowed: true, ...(await res.json()) };
|
|
217
217
|
} catch (e) {
|
|
218
218
|
const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
|
|
219
|
-
console.warn(`startMission failed (${apiBase}): ${reason}.`);
|
|
220
|
-
return { allowed: false, error: 'backend_unavailable', reason };
|
|
219
|
+
console.warn(`startMission failed (${apiBase}): ${reason}.`);
|
|
220
|
+
return { allowed: false, error: 'backend_unavailable', reason };
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
return { allowed: false, error: 'backend_unavailable' };
|
|
225
|
-
}
|
|
224
|
+
return { allowed: false, error: 'backend_unavailable' };
|
|
225
|
+
}
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* Fetches missions from the backend API.
|
|
@@ -234,10 +234,10 @@ export async function fetchMissions(projectId, tags) {
|
|
|
234
234
|
const key = getEffectiveApiKey();
|
|
235
235
|
if (!key) return { success: false, error: 'no_api_key', missions: [] };
|
|
236
236
|
|
|
237
|
-
const apiBase = getEffectiveApiBase();
|
|
238
|
-
if (!apiBase) {
|
|
239
|
-
return { success: false, error: 'backend_unavailable', missions: [] };
|
|
240
|
-
}
|
|
237
|
+
const apiBase = getEffectiveApiBase();
|
|
238
|
+
if (!apiBase) {
|
|
239
|
+
return { success: false, error: 'backend_unavailable', missions: [] };
|
|
240
|
+
}
|
|
241
241
|
|
|
242
242
|
try {
|
|
243
243
|
const url = new URL(`${apiBase}/api/v1/projects/${projectId}/missions`);
|
|
@@ -277,10 +277,10 @@ export async function fetchMissions(projectId, tags) {
|
|
|
277
277
|
* @param {'success'|'failed'|'cancelled'} result
|
|
278
278
|
* @param {object} usage - Optional usage statistics directly from the AI
|
|
279
279
|
*/
|
|
280
|
-
export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
|
|
281
|
-
const apiBase = getEffectiveApiBase();
|
|
282
|
-
if (!apiBase) return { ok: false, error: 'backend_unavailable' };
|
|
283
|
-
if (!missionId) return { ok: false, error: 'no_mission_id' };
|
|
280
|
+
export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
|
|
281
|
+
const apiBase = getEffectiveApiBase();
|
|
282
|
+
if (!apiBase) return { ok: false, error: 'backend_unavailable' };
|
|
283
|
+
if (!missionId) return { ok: false, error: 'no_mission_id' };
|
|
284
284
|
|
|
285
285
|
const key = getEffectiveApiKey();
|
|
286
286
|
try {
|
|
@@ -306,16 +306,19 @@ function simpleHash(str) {
|
|
|
306
306
|
hash = ((hash << 5) - hash) + char;
|
|
307
307
|
hash |= 0;
|
|
308
308
|
}
|
|
309
|
-
return 'ph_' + Math.abs(hash).toString(36);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function normalizeBugClassification(value) {
|
|
313
|
-
const text = String(value || '').trim();
|
|
314
|
-
const allowed = [
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
309
|
+
return 'ph_' + Math.abs(hash).toString(36);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeBugClassification(value) {
|
|
313
|
+
const text = String(value || '').trim();
|
|
314
|
+
const allowed = [
|
|
315
|
+
'[Backend Bug]', '[Frontend Bug]', '[UI Regression]', '[Flaky/Stale Selector]',
|
|
316
|
+
'[Interaction Blocked]', '[Validation Error]'
|
|
317
|
+
];
|
|
318
|
+
return allowed.find(category => text.includes(category)) || '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
319
322
|
* Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
|
|
320
323
|
* Si la organización no tiene la integración configurada (HTTP 400) se registra
|
|
321
324
|
* sólo en modo DEBUG y NO interrumpe el flujo del test runner.
|
|
@@ -324,7 +327,7 @@ function normalizeBugClassification(value) {
|
|
|
324
327
|
* @param {string} description - Descripción detallada del bug detectado
|
|
325
328
|
* @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
|
|
326
329
|
*/
|
|
327
|
-
export async function createAdoTask(title, description, metadata = {}) {
|
|
330
|
+
export async function createAdoTask(title, description, metadata = {}) {
|
|
328
331
|
const apiBase = getEffectiveApiBase();
|
|
329
332
|
if (!apiBase) {
|
|
330
333
|
if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
|
|
@@ -332,7 +335,7 @@ export async function createAdoTask(title, description, metadata = {}) {
|
|
|
332
335
|
}
|
|
333
336
|
|
|
334
337
|
const projectId = loadProjectId();
|
|
335
|
-
if (!isRealProjectId(projectId)) {
|
|
338
|
+
if (!isRealProjectId(projectId)) {
|
|
336
339
|
if (process.env.DEBUG) console.log(`[ADO] No hay Project ID configurado. Omitiendo creación de Task en Azure DevOps.`);
|
|
337
340
|
return { success: false, error: 'no_project_id' };
|
|
338
341
|
}
|
|
@@ -343,26 +346,26 @@ export async function createAdoTask(title, description, metadata = {}) {
|
|
|
343
346
|
return { success: false, error: 'no_api_key' };
|
|
344
347
|
}
|
|
345
348
|
|
|
346
|
-
try {
|
|
347
|
-
const controller = new AbortController();
|
|
348
|
-
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
349
|
-
const bugClassification = normalizeBugClassification(metadata?.classification);
|
|
350
|
-
const taskTitle = bugClassification && !String(title).includes(bugClassification)
|
|
351
|
-
? `${bugClassification} ${title}`
|
|
352
|
-
: title;
|
|
353
|
-
const taskDescription = bugClassification && !String(description).includes('Clasificacion')
|
|
354
|
-
? `**Clasificacion:** ${bugClassification}\n\n${description}`
|
|
355
|
-
: description;
|
|
356
|
-
|
|
357
|
-
const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
|
|
358
|
-
method: 'POST',
|
|
359
|
-
headers: {
|
|
360
|
-
'Content-Type': 'application/json',
|
|
361
|
-
'x-api-key': key
|
|
362
|
-
},
|
|
363
|
-
body: JSON.stringify({ project_id: projectId, title: taskTitle, description: taskDescription }),
|
|
364
|
-
signal: controller.signal
|
|
365
|
-
});
|
|
349
|
+
try {
|
|
350
|
+
const controller = new AbortController();
|
|
351
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
352
|
+
const bugClassification = normalizeBugClassification(metadata?.classification);
|
|
353
|
+
const taskTitle = bugClassification && !String(title).includes(bugClassification)
|
|
354
|
+
? `${bugClassification} ${title}`
|
|
355
|
+
: title;
|
|
356
|
+
const taskDescription = bugClassification && !String(description).includes('Clasificacion')
|
|
357
|
+
? `**Clasificacion:** ${bugClassification}\n\n${description}`
|
|
358
|
+
: description;
|
|
359
|
+
|
|
360
|
+
const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
|
|
361
|
+
method: 'POST',
|
|
362
|
+
headers: {
|
|
363
|
+
'Content-Type': 'application/json',
|
|
364
|
+
'x-api-key': key
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify({ project_id: projectId, title: taskTitle, description: taskDescription }),
|
|
367
|
+
signal: controller.signal
|
|
368
|
+
});
|
|
366
369
|
clearTimeout(timeout);
|
|
367
370
|
|
|
368
371
|
if (res.status === 400) {
|
|
@@ -398,7 +401,7 @@ export async function createAdoTask(title, description, metadata = {}) {
|
|
|
398
401
|
*/
|
|
399
402
|
export async function getEvidenceSasToken(missionId, projectId) {
|
|
400
403
|
const apiBase = getEffectiveApiBase();
|
|
401
|
-
if (!apiBase || !missionId || !isRealProjectId(projectId)) return null;
|
|
404
|
+
if (!apiBase || !missionId || !isRealProjectId(projectId)) return null;
|
|
402
405
|
|
|
403
406
|
const key = getEffectiveApiKey();
|
|
404
407
|
if (!key) return null;
|