@arcadialdev/arcality 3.0.2 → 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/README.md +12 -6
- package/bin/arcality.mjs +9 -8
- package/package.json +1 -1
- package/playwright.config.js +18 -16
- package/scripts/arcality-logs.mjs +167 -93
- package/scripts/gen-and-run.mjs +1723 -1158
- package/scripts/init.mjs +55 -19
- package/scripts/setup.mjs +120 -166
- package/src/KnowledgeService.ts +20 -11
- package/src/arcalityClient.mjs +64 -81
- package/src/configLoader.mjs +9 -13
- package/src/configManager.mjs +48 -39
- package/src/playwrightAssets.mjs +27 -0
- package/src/services/collectiveMemoryService.ts +37 -26
- package/tests/_helpers/ArcalityReporter.js +19 -1
- package/tests/_helpers/agentic-runner.bundle.spec.js +856 -222
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
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
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
|
-
|
|
103
|
-
return
|
|
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: '
|
|
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: '
|
|
133
|
+
return { valid: false, error: 'invalid_format', mode: 'live' };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const apiBase = getEffectiveApiBase();
|
|
@@ -162,20 +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(
|
|
165
|
+
console.warn(`Backend not available (${apiBase}): ${reason}.`);
|
|
166
|
+
return { valid: false, error: 'backend_unavailable', mode: 'live', reason };
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
const dailyUsed = getLocalDailyUsage();
|
|
171
|
-
return {
|
|
172
|
-
valid: true,
|
|
173
|
-
mode: 'mock',
|
|
174
|
-
plan: 'internal',
|
|
175
|
-
daily_used: dailyUsed,
|
|
176
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
177
|
-
remaining: Math.max(0, DAILY_MISSION_LIMIT - dailyUsed)
|
|
178
|
-
};
|
|
170
|
+
return { valid: false, error: 'backend_unavailable', mode: 'live' };
|
|
179
171
|
}
|
|
180
172
|
|
|
181
173
|
/**
|
|
@@ -192,7 +184,10 @@ export async function startMission(prompt, targetUrl) {
|
|
|
192
184
|
// ── LIVE mode ──
|
|
193
185
|
if (apiBase) {
|
|
194
186
|
try {
|
|
195
|
-
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
187
|
+
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
188
|
+
if (!isRealProjectId(projectId)) {
|
|
189
|
+
return { allowed: false, error: 'no_project_id' };
|
|
190
|
+
}
|
|
196
191
|
|
|
197
192
|
const controller = new AbortController();
|
|
198
193
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
@@ -221,38 +216,13 @@ export async function startMission(prompt, targetUrl) {
|
|
|
221
216
|
return { allowed: true, ...(await res.json()) };
|
|
222
217
|
} catch (e) {
|
|
223
218
|
const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
|
|
224
|
-
console.warn(
|
|
219
|
+
console.warn(`startMission failed (${apiBase}): ${reason}.`);
|
|
220
|
+
return { allowed: false, error: 'backend_unavailable', reason };
|
|
225
221
|
}
|
|
226
222
|
}
|
|
227
223
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (dailyUsed >= DAILY_MISSION_LIMIT) {
|
|
232
|
-
const tomorrow = new Date();
|
|
233
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
234
|
-
tomorrow.setHours(0, 0, 0, 0);
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
allowed: false,
|
|
238
|
-
error: 'mission_limit_exceeded',
|
|
239
|
-
daily_used: dailyUsed,
|
|
240
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
241
|
-
remaining: 0,
|
|
242
|
-
resets_at: tomorrow.toISOString()
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const newCount = incrementLocalUsage();
|
|
247
|
-
return {
|
|
248
|
-
allowed: true,
|
|
249
|
-
mode: 'mock',
|
|
250
|
-
mission_id: `mock_${Date.now().toString(36)}`,
|
|
251
|
-
daily_used: newCount,
|
|
252
|
-
daily_limit: DAILY_MISSION_LIMIT,
|
|
253
|
-
remaining: DAILY_MISSION_LIMIT - newCount
|
|
254
|
-
};
|
|
255
|
-
}
|
|
224
|
+
return { allowed: false, error: 'backend_unavailable' };
|
|
225
|
+
}
|
|
256
226
|
|
|
257
227
|
/**
|
|
258
228
|
* Fetches missions from the backend API.
|
|
@@ -264,11 +234,10 @@ export async function fetchMissions(projectId, tags) {
|
|
|
264
234
|
const key = getEffectiveApiKey();
|
|
265
235
|
if (!key) return { success: false, error: 'no_api_key', missions: [] };
|
|
266
236
|
|
|
267
|
-
const apiBase = getEffectiveApiBase();
|
|
268
|
-
if (!apiBase) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
237
|
+
const apiBase = getEffectiveApiBase();
|
|
238
|
+
if (!apiBase) {
|
|
239
|
+
return { success: false, error: 'backend_unavailable', missions: [] };
|
|
240
|
+
}
|
|
272
241
|
|
|
273
242
|
try {
|
|
274
243
|
const url = new URL(`${apiBase}/api/v1/projects/${projectId}/missions`);
|
|
@@ -308,9 +277,10 @@ export async function fetchMissions(projectId, tags) {
|
|
|
308
277
|
* @param {'success'|'failed'|'cancelled'} result
|
|
309
278
|
* @param {object} usage - Optional usage statistics directly from the AI
|
|
310
279
|
*/
|
|
311
|
-
export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
|
|
312
|
-
const apiBase = getEffectiveApiBase();
|
|
313
|
-
if (!apiBase
|
|
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' };
|
|
314
284
|
|
|
315
285
|
const key = getEffectiveApiKey();
|
|
316
286
|
try {
|
|
@@ -336,10 +306,16 @@ function simpleHash(str) {
|
|
|
336
306
|
hash = ((hash << 5) - hash) + char;
|
|
337
307
|
hash |= 0;
|
|
338
308
|
}
|
|
339
|
-
return 'ph_' + Math.abs(hash).toString(36);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
/**
|
|
343
319
|
* Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
|
|
344
320
|
* Si la organización no tiene la integración configurada (HTTP 400) se registra
|
|
345
321
|
* sólo en modo DEBUG y NO interrumpe el flujo del test runner.
|
|
@@ -348,7 +324,7 @@ function simpleHash(str) {
|
|
|
348
324
|
* @param {string} description - Descripción detallada del bug detectado
|
|
349
325
|
* @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
|
|
350
326
|
*/
|
|
351
|
-
export async function createAdoTask(title, description) {
|
|
327
|
+
export async function createAdoTask(title, description, metadata = {}) {
|
|
352
328
|
const apiBase = getEffectiveApiBase();
|
|
353
329
|
if (!apiBase) {
|
|
354
330
|
if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
|
|
@@ -356,7 +332,7 @@ export async function createAdoTask(title, description) {
|
|
|
356
332
|
}
|
|
357
333
|
|
|
358
334
|
const projectId = loadProjectId();
|
|
359
|
-
if (!projectId) {
|
|
335
|
+
if (!isRealProjectId(projectId)) {
|
|
360
336
|
if (process.env.DEBUG) console.log(`[ADO] No hay Project ID configurado. Omitiendo creación de Task en Azure DevOps.`);
|
|
361
337
|
return { success: false, error: 'no_project_id' };
|
|
362
338
|
}
|
|
@@ -367,19 +343,26 @@ export async function createAdoTask(title, description) {
|
|
|
367
343
|
return { success: false, error: 'no_api_key' };
|
|
368
344
|
}
|
|
369
345
|
|
|
370
|
-
try {
|
|
371
|
-
const controller = new AbortController();
|
|
372
|
-
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
});
|
|
383
366
|
clearTimeout(timeout);
|
|
384
367
|
|
|
385
368
|
if (res.status === 400) {
|
|
@@ -415,7 +398,7 @@ export async function createAdoTask(title, description) {
|
|
|
415
398
|
*/
|
|
416
399
|
export async function getEvidenceSasToken(missionId, projectId) {
|
|
417
400
|
const apiBase = getEffectiveApiBase();
|
|
418
|
-
if (!apiBase || !missionId) return null;
|
|
401
|
+
if (!apiBase || !missionId || !isRealProjectId(projectId)) return null;
|
|
419
402
|
|
|
420
403
|
const key = getEffectiveApiKey();
|
|
421
404
|
if (!key) return null;
|
package/src/configLoader.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
+
import { isRealProjectId } from './configManager.mjs';
|
|
7
8
|
|
|
8
9
|
dotenv.config();
|
|
9
10
|
|
|
@@ -62,12 +63,17 @@ export function loadConfig() {
|
|
|
62
63
|
throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
|
|
63
64
|
}
|
|
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
|
+
|
|
65
70
|
return config;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
export function loadProjectId() {
|
|
69
74
|
const localConfig = loadLocalArcalityConfig();
|
|
70
|
-
|
|
75
|
+
const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
|
|
76
|
+
return isRealProjectId(projectId) ? projectId : null;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
|
|
@@ -128,7 +134,7 @@ export function getApiKey() {
|
|
|
128
134
|
}
|
|
129
135
|
|
|
130
136
|
/**
|
|
131
|
-
* Loads
|
|
137
|
+
* Loads supported Arcality runtime values into process.env.
|
|
132
138
|
*/
|
|
133
139
|
export function setupProcessEnv() {
|
|
134
140
|
const config = loadGlobalConfig();
|
|
@@ -137,23 +143,13 @@ export function setupProcessEnv() {
|
|
|
137
143
|
// Load Arcality API Key
|
|
138
144
|
getApiKey();
|
|
139
145
|
|
|
140
|
-
// Load Anthropic API Key if not already in env
|
|
141
|
-
if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
|
|
142
|
-
process.env.ANTHROPIC_API_KEY = config.anthropic_api_key;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Load default model if exists
|
|
146
|
-
if (config.model && !process.env.CLAUDE_MODEL) {
|
|
147
|
-
process.env.CLAUDE_MODEL = config.model;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
146
|
// Also inject arcality.config values into process.env for backward compat
|
|
151
147
|
const localConfig = loadLocalArcalityConfig();
|
|
152
148
|
if (localConfig) {
|
|
153
149
|
if (localConfig.apiKey && !process.env.ARCALITY_API_KEY) {
|
|
154
150
|
process.env.ARCALITY_API_KEY = localConfig.apiKey;
|
|
155
151
|
}
|
|
156
|
-
if (localConfig.projectId && !process.env.ARCALITY_PROJECT_ID) {
|
|
152
|
+
if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
|
|
157
153
|
process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
|
|
158
154
|
}
|
|
159
155
|
if (localConfig.project?.baseUrl && !process.env.BASE_URL) {
|
package/src/configManager.mjs
CHANGED
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
const CONFIG_FILENAME = 'arcality.config';
|
|
8
|
+
const CONFIG_FILENAME = 'arcality.config';
|
|
9
|
+
|
|
10
|
+
export function isRealProjectId(projectId) {
|
|
11
|
+
const id = String(projectId || '').trim();
|
|
12
|
+
if (!id) return false;
|
|
13
|
+
if (id === 'undefined' || id === 'null') return false;
|
|
14
|
+
if (/^(local|mock)_/i.test(id)) return false;
|
|
15
|
+
if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Resolves the path to arcality.config in the project root.
|
|
@@ -59,21 +68,21 @@ export function saveProjectConfig(config, projectRoot) {
|
|
|
59
68
|
* @param {ArcalityConfig} config
|
|
60
69
|
* @returns {{ valid: boolean, missing: string[] }}
|
|
61
70
|
*/
|
|
62
|
-
export function validateConfig(config) {
|
|
63
|
-
const missing = [];
|
|
71
|
+
export function validateConfig(config) {
|
|
72
|
+
const missing = [];
|
|
64
73
|
|
|
65
74
|
if (!config) {
|
|
66
75
|
return { valid: false, missing: ['arcality.config file'] };
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
if (!config.apiKey) missing.push('apiKey');
|
|
70
|
-
if (!config.projectId) missing.push('projectId');
|
|
71
|
-
if (!config.project?.baseUrl) missing.push('project.baseUrl');
|
|
72
|
-
if (!config.auth?.username) missing.push('auth.username');
|
|
73
|
-
if (!config.auth?.password) missing.push('auth.password');
|
|
74
|
-
|
|
75
|
-
return { valid: missing.length === 0, missing };
|
|
76
|
-
}
|
|
78
|
+
if (!config.apiKey) missing.push('apiKey');
|
|
79
|
+
if (!isRealProjectId(config.projectId)) missing.push('projectId');
|
|
80
|
+
if (!config.project?.baseUrl) missing.push('project.baseUrl');
|
|
81
|
+
if (!config.auth?.username) missing.push('auth.username');
|
|
82
|
+
if (!config.auth?.password) missing.push('auth.password');
|
|
83
|
+
|
|
84
|
+
return { valid: missing.length === 0, missing };
|
|
85
|
+
}
|
|
77
86
|
|
|
78
87
|
/**
|
|
79
88
|
* Creates a fresh arcality.config structure.
|
|
@@ -84,30 +93,30 @@ export function createConfig({
|
|
|
84
93
|
apiKey,
|
|
85
94
|
organizationId,
|
|
86
95
|
projectId,
|
|
87
|
-
projectName,
|
|
88
|
-
baseUrl,
|
|
89
|
-
frameworkDetected,
|
|
90
|
-
username,
|
|
91
|
-
password,
|
|
92
|
-
arcalityVersion,
|
|
93
|
-
yamlOutputDir = './arcality',
|
|
94
|
-
}) {
|
|
95
|
-
return {
|
|
96
|
+
projectName,
|
|
97
|
+
baseUrl,
|
|
98
|
+
frameworkDetected,
|
|
99
|
+
username,
|
|
100
|
+
password,
|
|
101
|
+
arcalityVersion,
|
|
102
|
+
yamlOutputDir = './arcality',
|
|
103
|
+
}) {
|
|
104
|
+
return {
|
|
96
105
|
version: '1',
|
|
97
106
|
apiKey,
|
|
98
107
|
organizationId: organizationId || null,
|
|
99
108
|
projectId,
|
|
100
109
|
project: {
|
|
101
110
|
name: projectName,
|
|
102
|
-
baseUrl,
|
|
103
|
-
frameworkDetected: frameworkDetected || null,
|
|
104
|
-
},
|
|
105
|
-
auth: {
|
|
106
|
-
username,
|
|
107
|
-
password,
|
|
108
|
-
},
|
|
109
|
-
runtime: {
|
|
110
|
-
yamlOutputDir,
|
|
111
|
+
baseUrl,
|
|
112
|
+
frameworkDetected: frameworkDetected || null,
|
|
113
|
+
},
|
|
114
|
+
auth: {
|
|
115
|
+
username,
|
|
116
|
+
password,
|
|
117
|
+
},
|
|
118
|
+
runtime: {
|
|
119
|
+
yamlOutputDir,
|
|
111
120
|
reuseSuccessfulYamls: true,
|
|
112
121
|
singleConfigurationMode: true,
|
|
113
122
|
},
|
|
@@ -122,17 +131,17 @@ export function createConfig({
|
|
|
122
131
|
* with existing modules that read from process.env.
|
|
123
132
|
* @param {ArcalityConfig} config
|
|
124
133
|
*/
|
|
125
|
-
export function injectConfigToEnv(config) {
|
|
126
|
-
if (!config) return;
|
|
134
|
+
export function injectConfigToEnv(config) {
|
|
135
|
+
if (!config) return;
|
|
127
136
|
|
|
128
137
|
if (config.apiKey) process.env.ARCALITY_API_KEY = config.apiKey;
|
|
129
|
-
if (config.projectId) process.env.ARCALITY_PROJECT_ID = config.projectId;
|
|
130
|
-
if (config.project?.baseUrl && !process.env.BASE_URL) {
|
|
131
|
-
process.env.BASE_URL = config.project.baseUrl;
|
|
132
|
-
}
|
|
133
|
-
if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
|
|
134
|
-
if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
|
|
135
|
-
}
|
|
138
|
+
if (isRealProjectId(config.projectId)) process.env.ARCALITY_PROJECT_ID = config.projectId;
|
|
139
|
+
if (config.project?.baseUrl && !process.env.BASE_URL) {
|
|
140
|
+
process.env.BASE_URL = config.project.baseUrl;
|
|
141
|
+
}
|
|
142
|
+
if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
|
|
143
|
+
if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
|
|
144
|
+
}
|
|
136
145
|
|
|
137
146
|
/**
|
|
138
147
|
* Gets the YAML output directory from config, falling back to default.
|
|
@@ -166,7 +175,7 @@ export function ensureYamlOutputDir(config, projectRoot) {
|
|
|
166
175
|
* @property {string|null} organizationId
|
|
167
176
|
* @property {string} projectId
|
|
168
177
|
* @property {{ name: string, baseUrl: string, frameworkDetected: string|null }} project
|
|
169
|
-
* @property {{ username: string, password: string }} auth
|
|
178
|
+
* @property {{ username: string, password: string }} auth
|
|
170
179
|
* @property {{ yamlOutputDir: string, reuseSuccessfulYamls: boolean, singleConfigurationMode: boolean }} runtime
|
|
171
180
|
* @property {{ arcalityVersion: string }} meta
|
|
172
181
|
*/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const PLAYWRIGHT_ASSETS = ['chromium', 'ffmpeg'];
|
|
4
|
+
|
|
5
|
+
export function getPlaywrightInstallCommand() {
|
|
6
|
+
return `npx playwright install ${PLAYWRIGHT_ASSETS.join(' ')}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function installPlaywrightAssets({ cwd, stdio = 'inherit' } = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn('npx', ['playwright', 'install', ...PLAYWRIGHT_ASSETS], {
|
|
12
|
+
stdio,
|
|
13
|
+
shell: true,
|
|
14
|
+
cwd,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on('exit', code => {
|
|
18
|
+
if (code === 0) {
|
|
19
|
+
resolve();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
reject(new Error(`Playwright install failed with exit code ${code}`));
|
|
24
|
+
});
|
|
25
|
+
child.on('error', reject);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -16,25 +16,32 @@ const getPid = () => process.env.ARCALITY_PROJECT_ID || '';
|
|
|
16
16
|
const getOrgId = () => process.env.ARCALITY_ORG_ID;
|
|
17
17
|
const getMissionId = () => process.env.ARCALITY_MISSION_ID || EMPTY_GUID;
|
|
18
18
|
|
|
19
|
-
const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
|
|
20
|
-
|
|
21
|
-
let configWarningLogged = false;
|
|
22
|
-
|
|
23
|
-
function
|
|
19
|
+
const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
|
|
20
|
+
|
|
21
|
+
let configWarningLogged = false;
|
|
22
|
+
|
|
23
|
+
function hasRealProjectId(pid: string): boolean {
|
|
24
|
+
if (!pid || pid === EMPTY_GUID) return false;
|
|
25
|
+
if (/^(local|mock)_/i.test(pid)) return false;
|
|
26
|
+
if (pid === 'undefined' || pid === 'null') return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isConfigured(): boolean {
|
|
24
31
|
const base = getBase();
|
|
25
32
|
const key = getKey();
|
|
26
33
|
const pid = getPid();
|
|
27
34
|
const orgId = getOrgId();
|
|
28
35
|
|
|
29
|
-
if (!base || !key || !pid ||
|
|
30
|
-
if (!configWarningLogged) {
|
|
31
|
-
const missing = [];
|
|
32
|
-
if (!base) missing.push('ARCALITY_API_URL');
|
|
33
|
-
if (!key) missing.push('ARCALITY_API_KEY');
|
|
34
|
-
if (!pid
|
|
36
|
+
if (!base || !key || !hasRealProjectId(pid) || !orgId) {
|
|
37
|
+
if (!configWarningLogged) {
|
|
38
|
+
const missing = [];
|
|
39
|
+
if (!base) missing.push('ARCALITY_API_URL');
|
|
40
|
+
if (!key) missing.push('ARCALITY_API_KEY');
|
|
41
|
+
if (!hasRealProjectId(pid)) missing.push('ARCALITY_PROJECT_ID');
|
|
35
42
|
if (!orgId) missing.push('ARCALITY_ORG_ID');
|
|
36
43
|
|
|
37
|
-
console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(', ')}`);
|
|
44
|
+
if (process.env.DEBUG) console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(', ')}`);
|
|
38
45
|
configWarningLogged = true;
|
|
39
46
|
}
|
|
40
47
|
return false;
|
|
@@ -191,8 +198,10 @@ export async function searchPromptPattern(prompt: string, limit: number = 5): Pr
|
|
|
191
198
|
limit: limit
|
|
192
199
|
};
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
const debugPatternSearch = process.env.DEBUG === 'true' || process.env.ARCALITY_DEBUG === 'true';
|
|
202
|
+
if (debugPatternSearch) {
|
|
203
|
+
console.log(`[PatternSearch] POST ${apiUrl} | prompt_chars=${prompt.length} | limit=${limit}`);
|
|
204
|
+
}
|
|
196
205
|
|
|
197
206
|
const res = await fetch(apiUrl, {
|
|
198
207
|
method: 'POST',
|
|
@@ -204,18 +213,20 @@ export async function searchPromptPattern(prompt: string, limit: number = 5): Pr
|
|
|
204
213
|
});
|
|
205
214
|
|
|
206
215
|
if (!res.ok) {
|
|
207
|
-
console.warn(`[PatternSearch] HTTP ${res.status} from ${apiUrl}`);
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
210
|
-
const data = await res.json();
|
|
211
|
-
const results = Array.isArray(data) ? data : (data.matches || data.results || []);
|
|
212
|
-
console.log(`[PatternSearch]
|
|
213
|
-
return results;
|
|
214
|
-
} catch (err: any) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
if (debugPatternSearch) console.warn(`[PatternSearch] HTTP ${res.status} from ${apiUrl}`);
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const data = await res.json();
|
|
220
|
+
const results = Array.isArray(data) ? data : (data.matches || data.results || []);
|
|
221
|
+
if (debugPatternSearch) console.log(`[PatternSearch] ${results.length} patrones encontrados.`);
|
|
222
|
+
return results;
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
if (process.env.DEBUG === 'true' || process.env.ARCALITY_DEBUG === 'true') {
|
|
225
|
+
console.warn(`[PatternSearch] Error buscando patrones: ${err?.message || 'unknown'}`);
|
|
226
|
+
}
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
219
230
|
|
|
220
231
|
export async function savePromptPattern(patternData: any): Promise<boolean> {
|
|
221
232
|
if (!isConfigured()) return false;
|
|
@@ -46,13 +46,16 @@ class ArcalityReporter {
|
|
|
46
46
|
fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
|
|
47
47
|
}
|
|
48
48
|
try {
|
|
49
|
+
let copied = false;
|
|
49
50
|
if (att.path && fs_1.default.existsSync(att.path)) {
|
|
50
51
|
fs_1.default.copyFileSync(att.path, destPath);
|
|
52
|
+
copied = true;
|
|
51
53
|
}
|
|
52
54
|
else if (att.body) {
|
|
53
55
|
fs_1.default.writeFileSync(destPath, att.body);
|
|
56
|
+
copied = true;
|
|
54
57
|
}
|
|
55
|
-
return { ...att, path: `attachments/${fileName}` };
|
|
58
|
+
return copied ? { ...att, path: `attachments/${fileName}` } : att;
|
|
56
59
|
}
|
|
57
60
|
catch (e) {
|
|
58
61
|
console.error(`Failed to process attachment ${att.name}:`, e);
|
|
@@ -744,6 +747,11 @@ class ArcalityReporter {
|
|
|
744
747
|
background: var(--brand-soft);
|
|
745
748
|
border-color: rgba(181,108,255,0.26);
|
|
746
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
|
+
}
|
|
747
755
|
.tag-retry {
|
|
748
756
|
color: #ffe2a6;
|
|
749
757
|
background: var(--warning-soft);
|
|
@@ -938,6 +946,8 @@ class ArcalityReporter {
|
|
|
938
946
|
const isOpen = this.results.length === 1 || tone === 'failed' || i === 0;
|
|
939
947
|
const successSummary = r.attachments.find(a => a.name === 'success_summary');
|
|
940
948
|
const successText = this.getAttachmentText(successSummary);
|
|
949
|
+
const bugClassificationAtt = r.attachments.find(a => a.name === 'bug_classification');
|
|
950
|
+
const bugClassification = (this.getAttachmentText(bugClassificationAtt) || '').trim();
|
|
941
951
|
const evidenceHtml = this.renderVisualEvidence(r.attachments);
|
|
942
952
|
const contextHtml = this.renderTextAttachmentSection(r.attachments, 'qa_context_summary', 'Contexto QA aplicado');
|
|
943
953
|
const securityHtml = this._renderSecuritySection(r.attachments, i);
|
|
@@ -951,6 +961,7 @@ class ArcalityReporter {
|
|
|
951
961
|
<div class="test-meta">
|
|
952
962
|
<span class="tag tag-status-${tone}">${this.getStatusLabel(r.result.status)}</span>
|
|
953
963
|
<span class="tag tag-project">${this.escapeHtml(projectName)}</span>
|
|
964
|
+
${bugClassification ? `<span class="tag tag-bug">${this.escapeHtml(bugClassification)}</span>` : ''}
|
|
954
965
|
${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
|
|
955
966
|
<span class="tag">${this.formatDuration(r.result.duration)}</span>
|
|
956
967
|
</div>
|
|
@@ -969,6 +980,13 @@ class ArcalityReporter {
|
|
|
969
980
|
</div>
|
|
970
981
|
` : ''}
|
|
971
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
|
+
|
|
972
990
|
${r.result.error ? `
|
|
973
991
|
<div class="outcome-block error">
|
|
974
992
|
<span class="outcome-label">Diagnostico del bloqueo</span>
|