@arcadialdev/arcality 3.0.3 → 3.0.4

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": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -609,6 +609,7 @@ async function waitForEnter(message = 'Presiona Enter para continuar...', option
609
609
  if (process.stdin.isTTY && process.stdin.setRawMode) {
610
610
  process.stdin.setRawMode(false);
611
611
  }
612
+ process.stdin.resume();
612
613
 
613
614
  const rl = createInterface({
614
615
  input: process.stdin,
@@ -2158,6 +2159,8 @@ async function main() {
2158
2159
  BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
2159
2160
  LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
2160
2161
  LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
2162
+ ARCALITY_ASSETS_FILE: projectConfig?.assets?.manifestPath || dotEnv.ARCALITY_ASSETS_FILE || process.env.ARCALITY_ASSETS_FILE || path.join(ORIGINAL_CWD, '.arcality', 'assets.json'),
2163
+ ARCALITY_ASSETS_DIR: projectConfig?.assets?.directory || dotEnv.ARCALITY_ASSETS_DIR || process.env.ARCALITY_ASSETS_DIR || path.join(ORIGINAL_CWD, '.arcality', 'assets'),
2161
2164
  DOMAIN_DIR: process.env.DOMAIN_DIR,
2162
2165
  MISSIONS_DIR: process.env.MISSIONS_DIR,
2163
2166
  CONTEXT_DIR: process.env.CONTEXT_DIR,
@@ -2245,7 +2248,18 @@ async function main() {
2245
2248
  _origProcessExit(1);
2246
2249
  }
2247
2250
 
2248
- const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera...'));
2251
+ let _postProcessSigintCount = 0;
2252
+ const postProcessSigint = () => {
2253
+ _postProcessSigintCount++;
2254
+ if (_postProcessSigintCount === 1) {
2255
+ console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
2256
+ } else {
2257
+ console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
2258
+ process.removeAllListeners('SIGINT');
2259
+ process.removeAllListeners('SIGTERM');
2260
+ process.exit(0);
2261
+ }
2262
+ };
2249
2263
  let missionResult = null;
2250
2264
  let finalFailReason = null;
2251
2265
  let finalUsageData = undefined;
@@ -2493,7 +2507,10 @@ async function main() {
2493
2507
  // ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
2494
2508
  try {
2495
2509
  const { endMission } = await import('../src/arcalityClient.mjs');
2496
- await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning);
2510
+ await Promise.race([
2511
+ endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning),
2512
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000))
2513
+ ]);
2497
2514
  if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
2498
2515
  } catch (e) {
2499
2516
  if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
@@ -2519,11 +2536,11 @@ async function main() {
2519
2536
 
2520
2537
  await new Promise(resolve => setTimeout(resolve, 2000));
2521
2538
 
2522
- // Ping project after report
2523
- await pingProject(
2524
- process.env.ARCALITY_PROJECT_ID,
2525
- process.env.ARCALITY_API_KEY
2526
- );
2539
+ // Ping project after report (with timeout so it cannot hang the exit path)
2540
+ await Promise.race([
2541
+ pingProject(process.env.ARCALITY_PROJECT_ID, process.env.ARCALITY_API_KEY),
2542
+ new Promise(resolve => setTimeout(resolve, 5000))
2543
+ ]);
2527
2544
 
2528
2545
  // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
2529
2546
  process.removeListener('SIGINT', postProcessSigint);
@@ -30,22 +30,22 @@ export interface PortalContextResponse {
30
30
  fields: Array<{ field_identifier: string; field_type: string; is_required: boolean }>;
31
31
  }
32
32
 
33
- export interface ProjectDto {
34
- id: string;
35
- name: string;
36
- base_url?: string;
37
- }
38
-
39
- function isRealProjectId(projectId?: string | null): boolean {
40
- const id = String(projectId || '').trim();
41
- if (!id) return false;
42
- if (id === 'undefined' || id === 'null') return false;
43
- if (/^(local|mock)_/i.test(id)) return false;
44
- if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
45
- return true;
46
- }
47
-
48
- export class KnowledgeService {
33
+ export interface ProjectDto {
34
+ id: string;
35
+ name: string;
36
+ base_url?: string;
37
+ }
38
+
39
+ function isRealProjectId(projectId?: string | null): boolean {
40
+ const id = String(projectId || '').trim();
41
+ if (!id) return false;
42
+ if (id === 'undefined' || id === 'null') return false;
43
+ if (/^(local|mock)_/i.test(id)) return false;
44
+ if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
45
+ return true;
46
+ }
47
+
48
+ export class KnowledgeService {
49
49
  private static instance: KnowledgeService;
50
50
  private apiBase: string;
51
51
  private apiKey: string;
@@ -89,10 +89,10 @@ export class KnowledgeService {
89
89
  }
90
90
 
91
91
  public getProjectId(): string | null {
92
- if (!isRealProjectId(this.projectId)) {
93
- this.projectId = process.env.ARCALITY_PROJECT_ID || null;
94
- }
95
- return isRealProjectId(this.projectId) ? this.projectId : null;
92
+ if (!isRealProjectId(this.projectId)) {
93
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
94
+ }
95
+ return isRealProjectId(this.projectId) ? this.projectId : null;
96
96
  }
97
97
 
98
98
  /**
@@ -107,7 +107,7 @@ export class KnowledgeService {
107
107
  const data = await res.json();
108
108
  return data.projects || [];
109
109
  } catch (e: any) {
110
- if (process.env.DEBUG) console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
110
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
111
111
  return [];
112
112
  }
113
113
  }
@@ -125,7 +125,7 @@ export class KnowledgeService {
125
125
  if (!res.ok) return null;
126
126
  return await res.json();
127
127
  } catch (e: any) {
128
- if (process.env.DEBUG) console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
128
+ if (process.env.DEBUG) console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
129
129
  return null;
130
130
  }
131
131
  }
@@ -134,8 +134,8 @@ export class KnowledgeService {
134
134
  * 2. Ingesta (Post-Percept)
135
135
  */
136
136
  public async ingest(payload: PortalIngestRequest): Promise<void> {
137
- if (!isRealProjectId(payload.project_id)) {
138
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
137
+ if (!isRealProjectId(payload.project_id)) {
138
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
139
139
  return;
140
140
  }
141
141
 
@@ -157,7 +157,7 @@ export class KnowledgeService {
157
157
  }
158
158
  }
159
159
  } catch (e: any) {
160
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
160
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
161
161
  }
162
162
  }
163
163
 
@@ -179,7 +179,7 @@ export class KnowledgeService {
179
179
  if (!res.ok) return null;
180
180
  return await res.json();
181
181
  } catch (e: any) {
182
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
182
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
183
183
  return null;
184
184
  }
185
185
  }
@@ -207,7 +207,7 @@ export class KnowledgeService {
207
207
  severity
208
208
  })
209
209
  });
210
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
210
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
211
211
  } catch (e) { }
212
212
  }
213
213
 
@@ -233,7 +233,7 @@ export class KnowledgeService {
233
233
  source: "agent_auto"
234
234
  })
235
235
  });
236
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
236
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
237
237
  } catch (e) { }
238
238
  }
239
239
 
@@ -259,7 +259,7 @@ export class KnowledgeService {
259
259
  source: "agent_auto"
260
260
  })
261
261
  });
262
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
262
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
263
263
  } catch (e) { }
264
264
  }
265
265
 
@@ -306,10 +306,16 @@ 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
- /**
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
+ /**
313
319
  * Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
314
320
  * Si la organización no tiene la integración configurada (HTTP 400) se registra
315
321
  * sólo en modo DEBUG y NO interrumpe el flujo del test runner.
@@ -318,7 +324,7 @@ function simpleHash(str) {
318
324
  * @param {string} description - Descripción detallada del bug detectado
319
325
  * @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
320
326
  */
321
- export async function createAdoTask(title, description) {
327
+ export async function createAdoTask(title, description, metadata = {}) {
322
328
  const apiBase = getEffectiveApiBase();
323
329
  if (!apiBase) {
324
330
  if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
@@ -337,19 +343,26 @@ export async function createAdoTask(title, description) {
337
343
  return { success: false, error: 'no_api_key' };
338
344
  }
339
345
 
340
- try {
341
- const controller = new AbortController();
342
- const timeout = setTimeout(() => controller.abort(), 15000);
343
-
344
- const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
345
- method: 'POST',
346
- headers: {
347
- 'Content-Type': 'application/json',
348
- 'x-api-key': key
349
- },
350
- body: JSON.stringify({ project_id: projectId, title, description }),
351
- signal: controller.signal
352
- });
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
+ });
353
366
  clearTimeout(timeout);
354
367
 
355
368
  if (res.status === 400) {
@@ -1,10 +1,10 @@
1
1
  // src/configLoader.mjs
2
2
  // Configuration loader — supports arcality.config (primary) and .env / global config (fallback)
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import os from 'node:os';
6
- import dotenv from 'dotenv';
7
- import { isRealProjectId } from './configManager.mjs';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import dotenv from 'dotenv';
7
+ import { isRealProjectId } from './configManager.mjs';
8
8
 
9
9
  dotenv.config();
10
10
 
@@ -59,22 +59,22 @@ export function loadConfig() {
59
59
  ARCALITY_PROJECT_ID: localConfig?.projectId || process.env.ARCALITY_PROJECT_ID,
60
60
  };
61
61
 
62
- if (!config.ARCALITY_API_KEY) {
63
- throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
64
- }
65
-
66
- if (!isRealProjectId(config.ARCALITY_PROJECT_ID)) {
67
- throw new Error('ARCALITY_PROJECT_ID is not connected to a real Arcality project. Run `arcality init`.');
68
- }
69
-
70
- return config;
71
- }
62
+ if (!config.ARCALITY_API_KEY) {
63
+ throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
64
+ }
65
+
66
+ if (!isRealProjectId(config.ARCALITY_PROJECT_ID)) {
67
+ throw new Error('ARCALITY_PROJECT_ID is not connected to a real Arcality project. Run `arcality init`.');
68
+ }
72
69
 
73
- export function loadProjectId() {
74
- const localConfig = loadLocalArcalityConfig();
75
- const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
76
- return isRealProjectId(projectId) ? projectId : null;
77
- }
70
+ return config;
71
+ }
72
+
73
+ export function loadProjectId() {
74
+ const localConfig = loadLocalArcalityConfig();
75
+ const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
76
+ return isRealProjectId(projectId) ? projectId : null;
77
+ }
78
78
 
79
79
 
80
80
  /**
@@ -133,25 +133,25 @@ export function getApiKey() {
133
133
  return { key: null, source: 'none' };
134
134
  }
135
135
 
136
- /**
137
- * Loads supported Arcality runtime values into process.env.
138
- */
139
- export function setupProcessEnv() {
140
- const config = loadGlobalConfig();
141
- if (!config) return;
142
-
143
- // Load Arcality API Key
144
- getApiKey();
145
-
146
- // Also inject arcality.config values into process.env for backward compat
147
- const localConfig = loadLocalArcalityConfig();
136
+ /**
137
+ * Loads supported Arcality runtime values into process.env.
138
+ */
139
+ export function setupProcessEnv() {
140
+ const config = loadGlobalConfig();
141
+ if (!config) return;
142
+
143
+ // Load Arcality API Key
144
+ getApiKey();
145
+
146
+ // Also inject arcality.config values into process.env for backward compat
147
+ const localConfig = loadLocalArcalityConfig();
148
148
  if (localConfig) {
149
149
  if (localConfig.apiKey && !process.env.ARCALITY_API_KEY) {
150
150
  process.env.ARCALITY_API_KEY = localConfig.apiKey;
151
151
  }
152
- if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
153
- process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
154
- }
152
+ if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
153
+ process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
154
+ }
155
155
  if (localConfig.project?.baseUrl && !process.env.BASE_URL) {
156
156
  process.env.BASE_URL = localConfig.project.baseUrl;
157
157
  }
@@ -747,6 +747,11 @@ class ArcalityReporter {
747
747
  background: var(--brand-soft);
748
748
  border-color: rgba(181,108,255,0.26);
749
749
  }
750
+ .tag-bug {
751
+ color: #b7e4ff;
752
+ background: rgba(80,170,255,0.12);
753
+ border-color: rgba(80,170,255,0.3);
754
+ }
750
755
  .tag-retry {
751
756
  color: #ffe2a6;
752
757
  background: var(--warning-soft);
@@ -941,6 +946,8 @@ class ArcalityReporter {
941
946
  const isOpen = this.results.length === 1 || tone === 'failed' || i === 0;
942
947
  const successSummary = r.attachments.find(a => a.name === 'success_summary');
943
948
  const successText = this.getAttachmentText(successSummary);
949
+ const bugClassificationAtt = r.attachments.find(a => a.name === 'bug_classification');
950
+ const bugClassification = (this.getAttachmentText(bugClassificationAtt) || '').trim();
944
951
  const evidenceHtml = this.renderVisualEvidence(r.attachments);
945
952
  const contextHtml = this.renderTextAttachmentSection(r.attachments, 'qa_context_summary', 'Contexto QA aplicado');
946
953
  const securityHtml = this._renderSecuritySection(r.attachments, i);
@@ -954,6 +961,7 @@ class ArcalityReporter {
954
961
  <div class="test-meta">
955
962
  <span class="tag tag-status-${tone}">${this.getStatusLabel(r.result.status)}</span>
956
963
  <span class="tag tag-project">${this.escapeHtml(projectName)}</span>
964
+ ${bugClassification ? `<span class="tag tag-bug">${this.escapeHtml(bugClassification)}</span>` : ''}
957
965
  ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
958
966
  <span class="tag">${this.formatDuration(r.result.duration)}</span>
959
967
  </div>
@@ -972,6 +980,13 @@ class ArcalityReporter {
972
980
  </div>
973
981
  ` : ''}
974
982
 
983
+ ${r.result.error && bugClassification ? `
984
+ <div class="outcome-block error">
985
+ <span class="outcome-label">Clasificacion AI</span>
986
+ <pre class="error-block">${this.escapeHtml(bugClassification)}</pre>
987
+ </div>
988
+ ` : ''}
989
+
975
990
  ${r.result.error ? `
976
991
  <div class="outcome-block error">
977
992
  <span class="outcome-label">Diagnostico del bloqueo</span>