@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "4.1.4",
3
+ "version": "4.1.5",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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: 'retain-on-failure',
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
+
@@ -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 = ['[Backend Bug]', '[Frontend Bug]', '[UI Regression]', '[Flaky/Stale Selector]'];
315
- return allowed.find(category => text.includes(category)) || '';
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;