@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.
@@ -27,6 +27,375 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
28
  mod
29
29
  ));
30
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
+
32
+ // tests/_helpers/asset-resolver.ts
33
+ var asset_resolver_exports = {};
34
+ __export(asset_resolver_exports, {
35
+ createPlaceholderUpload: () => createPlaceholderUpload,
36
+ formatUploadAssetContext: () => formatUploadAssetContext,
37
+ listUploadAssets: () => listUploadAssets,
38
+ resolveUploadAsset: () => resolveUploadAsset,
39
+ syncAssetsFromBackend: () => syncAssetsFromBackend
40
+ });
41
+ function readJsonFile(filePath) {
42
+ const raw = fs2.readFileSync(filePath, "utf8").trim();
43
+ if (!raw)
44
+ return null;
45
+ return JSON.parse(raw);
46
+ }
47
+ function sanitizeIdPart(value) {
48
+ return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
49
+ }
50
+ function normalizeMimeFromPath(filePath) {
51
+ const ext = path2.extname(filePath || "").toLowerCase();
52
+ if (ext === ".jpg" || ext === ".jpeg")
53
+ return "image/jpeg";
54
+ if (ext === ".webp")
55
+ return "image/webp";
56
+ if (ext === ".png")
57
+ return "image/png";
58
+ return "application/octet-stream";
59
+ }
60
+ function inferAssetType(...values) {
61
+ const text = values.join(" ").toLowerCase();
62
+ if (text.includes("banner") || text.includes("hero") || text.includes("cover"))
63
+ return "banner";
64
+ if (text.includes("logo"))
65
+ return "logo";
66
+ if (text.includes("avatar") || text.includes("profile") || text.includes("perfil"))
67
+ return "profile";
68
+ if (text.includes("product") || text.includes("producto"))
69
+ return "product";
70
+ return "image";
71
+ }
72
+ function normalizeAsset(raw, assetsDir = null) {
73
+ const assetId = String(raw.asset_id || raw.assetId || raw.id || "").trim();
74
+ const rawPath = String(raw.path || raw.file_path || raw.filePath || raw.storage_key || raw.storageKey || "").trim();
75
+ const filename = String(raw.filename || raw.fileName || (rawPath ? path2.basename(rawPath) : "") || "").trim();
76
+ const derivedId = filename ? `asset_${sanitizeIdPart(path2.basename(filename, path2.extname(filename)))}` : "";
77
+ const finalId = assetId || derivedId;
78
+ if (!finalId)
79
+ return null;
80
+ let filePath = rawPath || void 0;
81
+ if (filePath && !path2.isAbsolute(filePath)) {
82
+ const cwdCandidate = path2.resolve(process.cwd(), filePath);
83
+ const assetDirCandidate = assetsDir ? path2.resolve(assetsDir, filePath) : "";
84
+ filePath = fs2.existsSync(cwdCandidate) ? cwdCandidate : assetDirCandidate || cwdCandidate;
85
+ }
86
+ const label = String(raw.label || raw.user_label || raw.userLabel || filename || finalId).trim();
87
+ const type = String(raw.type || inferAssetType(label, filename)).trim().toLowerCase();
88
+ const mime = String(raw.mime || raw.mimeType || normalizeMimeFromPath(filePath || filename)).trim();
89
+ return {
90
+ assetId: finalId,
91
+ label,
92
+ type,
93
+ mime,
94
+ width: Number.isFinite(Number(raw.width)) ? Number(raw.width) : void 0,
95
+ height: Number.isFinite(Number(raw.height)) ? Number(raw.height) : void 0,
96
+ filePath,
97
+ filename,
98
+ isDefault: Boolean(raw.is_default || raw.isDefault)
99
+ };
100
+ }
101
+ function getDefaultAssetsDir() {
102
+ return process.env.ARCALITY_ASSETS_DIR || path2.join(process.cwd(), ".arcality", "assets");
103
+ }
104
+ function loadManifestAssets(assetsDir) {
105
+ const explicitFile = process.env.ARCALITY_ASSETS_FILE;
106
+ const manifestPath = explicitFile || path2.join(process.cwd(), ".arcality", "assets.json");
107
+ const inlineJson = process.env.ARCALITY_ASSETS_JSON;
108
+ try {
109
+ const parsed = inlineJson ? JSON.parse(inlineJson) : fs2.existsSync(manifestPath) ? readJsonFile(manifestPath) : null;
110
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.assets) ? parsed.assets : [];
111
+ return list.map((entry) => normalizeAsset(entry, assetsDir)).filter(Boolean);
112
+ } catch (e) {
113
+ console.warn(`>>ARCALITY_STATUS>> Warning: no se pudo leer el inventario de assets (${e.message}).`);
114
+ return [];
115
+ }
116
+ }
117
+ function scanAssetsDir(assetsDir) {
118
+ try {
119
+ if (!fs2.existsSync(assetsDir))
120
+ return [];
121
+ return fs2.readdirSync(assetsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path2.extname(entry.name).toLowerCase())).map((entry) => {
122
+ const filePath = path2.join(assetsDir, entry.name);
123
+ return normalizeAsset({
124
+ id: `asset_${sanitizeIdPart(path2.basename(entry.name, path2.extname(entry.name)))}`,
125
+ label: path2.basename(entry.name, path2.extname(entry.name)).replace(/[_-]+/g, " "),
126
+ path: filePath,
127
+ mime: normalizeMimeFromPath(filePath),
128
+ type: inferAssetType(entry.name)
129
+ }, assetsDir);
130
+ }).filter(Boolean);
131
+ } catch {
132
+ return [];
133
+ }
134
+ }
135
+ function extractBackendAssets(payload) {
136
+ if (Array.isArray(payload))
137
+ return payload;
138
+ if (Array.isArray(payload?.assets))
139
+ return payload.assets;
140
+ if (Array.isArray(payload?.data?.assets))
141
+ return payload.data.assets;
142
+ if (Array.isArray(payload?.items))
143
+ return payload.items;
144
+ return [];
145
+ }
146
+ function listUploadAssets() {
147
+ const assetsDir = getDefaultAssetsDir();
148
+ const merged = [...loadManifestAssets(assetsDir), ...scanAssetsDir(assetsDir)];
149
+ const byId = /* @__PURE__ */ new Map();
150
+ for (const asset of merged) {
151
+ if (!byId.has(asset.assetId))
152
+ byId.set(asset.assetId, asset);
153
+ }
154
+ return Array.from(byId.values());
155
+ }
156
+ function formatUploadAssetContext() {
157
+ const assets = listUploadAssets();
158
+ const lines = assets.slice(0, 12).map((asset) => {
159
+ const size = asset.width && asset.height ? `${asset.width}x${asset.height}` : "unknown-size";
160
+ const defaultFlag = asset.isDefault ? " | default" : "";
161
+ return `- ${asset.label} | asset_id:${asset.assetId} | type:${asset.type} | mime:${asset.mime} | size:${size}${defaultFlag} | use:"arcasset://${asset.assetId}"`;
162
+ });
163
+ return `
164
+ # AVAILABLE UPLOAD ASSETS
165
+ ${lines.length ? lines.join("\n") : "- No user assets registered for this project."}
166
+ - Fallback allowed: use "placeholder:image/banner" for required image uploads when no suitable asset exists.
167
+ `;
168
+ }
169
+ function findAsset(reference) {
170
+ const id = reference.replace(/^arcasset:\/\//i, "").trim();
171
+ const assets = listUploadAssets();
172
+ const lower = id.toLowerCase();
173
+ return assets.find(
174
+ (asset) => asset.assetId.toLowerCase() === lower || asset.label.toLowerCase() === lower || asset.filename?.toLowerCase() === lower
175
+ ) || null;
176
+ }
177
+ function inferPlaceholderSpec(label, reference = "") {
178
+ const text = `${label} ${reference}`.toLowerCase();
179
+ if (text.includes("logo") || text.includes("avatar") || text.includes("perfil")) {
180
+ return { width: 1024, height: 1024, name: "arcality-square-placeholder-1024x1024.png" };
181
+ }
182
+ if (text.includes("product") || text.includes("producto")) {
183
+ return { width: 1200, height: 1200, name: "arcality-product-placeholder-1200x1200.png" };
184
+ }
185
+ return { width: 1920, height: 1080, name: "arcality-banner-placeholder-1920x1080.png" };
186
+ }
187
+ function crc32(buffer) {
188
+ let crc = 4294967295;
189
+ for (let i = 0; i < buffer.length; i++) {
190
+ crc ^= buffer[i];
191
+ for (let j = 0; j < 8; j++) {
192
+ crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
193
+ }
194
+ }
195
+ return (crc ^ 4294967295) >>> 0;
196
+ }
197
+ function pngChunk(type, data) {
198
+ const typeBuffer = Buffer.from(type, "ascii");
199
+ const length = Buffer.alloc(4);
200
+ length.writeUInt32BE(data.length, 0);
201
+ const crc = Buffer.alloc(4);
202
+ crc.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
203
+ return Buffer.concat([length, typeBuffer, data, crc]);
204
+ }
205
+ function createPngBuffer(width, height) {
206
+ const bytesPerPixel = 3;
207
+ const stride = width * bytesPerPixel + 1;
208
+ const raw = Buffer.alloc(stride * height);
209
+ for (let y = 0; y < height; y++) {
210
+ const rowStart = y * stride;
211
+ raw[rowStart] = 0;
212
+ for (let x = 0; x < width; x++) {
213
+ const offset = rowStart + 1 + x * bytesPerPixel;
214
+ const shade = (x + y) % 64;
215
+ raw[offset] = 30 + shade;
216
+ raw[offset + 1] = 108 + Math.floor(shade / 2);
217
+ raw[offset + 2] = 147;
218
+ }
219
+ }
220
+ const header = Buffer.alloc(13);
221
+ header.writeUInt32BE(width, 0);
222
+ header.writeUInt32BE(height, 4);
223
+ header[8] = 8;
224
+ header[9] = 2;
225
+ header[10] = 0;
226
+ header[11] = 0;
227
+ header[12] = 0;
228
+ return Buffer.concat([
229
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
230
+ pngChunk("IHDR", header),
231
+ pngChunk("IDAT", zlib.deflateSync(raw)),
232
+ pngChunk("IEND", Buffer.alloc(0))
233
+ ]);
234
+ }
235
+ function createPlaceholderUpload(label = "file upload", reference = "") {
236
+ const spec = inferPlaceholderSpec(label, reference);
237
+ return {
238
+ name: spec.name,
239
+ mimeType: "image/png",
240
+ buffer: createPngBuffer(spec.width, spec.height)
241
+ };
242
+ }
243
+ function resolveUploadAsset(value, label = "file upload") {
244
+ const raw = String(value || "").replace(/^"|"$/g, "").trim();
245
+ if (!raw || /^placeholder:/i.test(raw) || /^dummy$/i.test(raw)) {
246
+ const assets = listUploadAssets();
247
+ const subtype = raw.startsWith("placeholder:image/") ? raw.substring("placeholder:image/".length).toLowerCase() : "";
248
+ const targetType = subtype || inferAssetType(label, raw);
249
+ const matchingAsset = assets.find((a) => a.type === targetType || a.label.toLowerCase().includes(targetType)) || assets.find((a) => !a.isDefault);
250
+ if (matchingAsset?.filePath && fs2.existsSync(matchingAsset.filePath)) {
251
+ console.log(`>>ARCALITY_STATUS>> Usando asset real configurado para el placeholder "${raw}": ${matchingAsset.filePath}`);
252
+ return matchingAsset.filePath;
253
+ }
254
+ return createPlaceholderUpload(label, raw);
255
+ }
256
+ if (/^arcasset:\/\//i.test(raw) || /^asset_[a-z0-9_-]+$/i.test(raw)) {
257
+ const asset = findAsset(raw);
258
+ if (asset?.filePath && fs2.existsSync(asset.filePath)) {
259
+ return asset.filePath;
260
+ }
261
+ console.warn(`>>ARCALITY_STATUS>> Asset "${raw}" no tiene archivo local disponible. Usando placeholder.`);
262
+ return createPlaceholderUpload(label, raw);
263
+ }
264
+ const filePath = path2.isAbsolute(raw) ? raw : path2.resolve(process.cwd(), raw);
265
+ if (fs2.existsSync(filePath)) {
266
+ return filePath;
267
+ }
268
+ console.warn(`>>ARCALITY_STATUS>> Archivo de upload no encontrado: "${raw}". Usando placeholder.`);
269
+ return createPlaceholderUpload(label, raw);
270
+ }
271
+ async function syncAssetsFromBackend() {
272
+ const apiUrl = process.env.ARCALITY_API_URL;
273
+ const apiKey = process.env.ARCALITY_API_KEY;
274
+ const projectId = process.env.ARCALITY_PROJECT_ID;
275
+ if (!apiUrl || !apiKey || !projectId) {
276
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Asset sync omitido por configuraci\xF3n incompleta: API_URL=${apiUrl ? "OK" : "MISSING"}, API_KEY=${apiKey ? "OK" : "MISSING"}, PROJECT_ID=${projectId ? "OK" : "MISSING"}.`);
277
+ return;
278
+ }
279
+ try {
280
+ const assetsDir = getDefaultAssetsDir();
281
+ if (!fs2.existsSync(assetsDir)) {
282
+ fs2.mkdirSync(assetsDir, { recursive: true });
283
+ }
284
+ const url = `${apiUrl}/api/v1/projects/${projectId}/assets`;
285
+ console.log(`>>ARCALITY_STATUS>> \u{1F4E6} Consultando assets configurados del proyecto...`);
286
+ if (process.env.DEBUG) {
287
+ console.log(`[AssetSync] Fetching assets from: ${url}`);
288
+ }
289
+ const res = await globalThis.fetch(url, {
290
+ headers: {
291
+ "x-api-key": apiKey
292
+ }
293
+ });
294
+ if (!res.ok) {
295
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al obtener assets del portal (${res.status}).`);
296
+ return;
297
+ }
298
+ const payload = await res.json();
299
+ const backendAssets = extractBackendAssets(payload);
300
+ if (!backendAssets.length) {
301
+ const payloadKeys = payload && typeof payload === "object" ? Object.keys(payload).slice(0, 8).join(", ") : typeof payload;
302
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F El proyecto no devolvi\xF3 assets utilizables. Se usar\xE1 placeholder cuando un upload lo requiera.`);
303
+ if (process.env.DEBUG) {
304
+ console.log(`[AssetSync] Payload shape without assets array: ${payloadKeys || "empty"}`);
305
+ }
306
+ return;
307
+ }
308
+ console.log(`>>ARCALITY_STATUS>> \u{1F4E6} Assets detectados en portal: ${backendAssets.length}`);
309
+ const localAssets = [];
310
+ for (const asset of backendAssets) {
311
+ const assetId = asset.asset_id || asset.assetId || asset.id;
312
+ if (!assetId) {
313
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Asset ignorado porque no trae id utilizable: ${JSON.stringify(asset).slice(0, 200)}`);
314
+ continue;
315
+ }
316
+ const prefix = `${assetId}_`;
317
+ const files = fs2.existsSync(assetsDir) ? fs2.readdirSync(assetsDir) : [];
318
+ const existingFile = files.find((f) => f.startsWith(prefix));
319
+ let filename = asset.filename || asset.fileName;
320
+ let localFilePath = "";
321
+ let downloadNeeded = true;
322
+ if (existingFile) {
323
+ const candidatePath = path2.join(assetsDir, existingFile);
324
+ if (fs2.existsSync(candidatePath) && fs2.statSync(candidatePath).size > 0) {
325
+ filename = filename || existingFile.substring(prefix.length);
326
+ localFilePath = candidatePath;
327
+ downloadNeeded = false;
328
+ }
329
+ }
330
+ if (downloadNeeded) {
331
+ console.log(`>>ARCALITY_STATUS>> \u{1F4E5} Materializando asset del portal: "${asset.label}"...`);
332
+ const matRes = await globalThis.fetch(`${apiUrl}/api/v1/assets/${assetId}/materialize`, {
333
+ method: "POST",
334
+ headers: {
335
+ "x-api-key": apiKey,
336
+ "Content-Type": "application/json"
337
+ }
338
+ });
339
+ if (matRes.ok) {
340
+ const matData = await matRes.json();
341
+ const downloadUrl = matData.download_url || matData.downloadUrl;
342
+ const matFilename = matData.filename || matData.fileName || filename || `${assetId}.png`;
343
+ filename = matFilename;
344
+ const localFilename = `${assetId}_${matFilename}`;
345
+ localFilePath = path2.join(assetsDir, localFilename);
346
+ if (downloadUrl) {
347
+ console.log(`>>ARCALITY_STATUS>> \u{1F4E5} Descargando archivo de asset: "${asset.label}"...`);
348
+ const fileRes = await globalThis.fetch(downloadUrl);
349
+ if (fileRes.ok) {
350
+ const buffer = Buffer.from(await fileRes.arrayBuffer());
351
+ fs2.writeFileSync(localFilePath, buffer);
352
+ console.log(`>>ARCALITY_STATUS>> \u2705 Asset guardado localmente: ${localFilePath}`);
353
+ } else {
354
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al descargar archivo de asset: ${fileRes.status}`);
355
+ }
356
+ }
357
+ } else {
358
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al materializar asset: ${matRes.status}`);
359
+ filename = filename || `${assetId}.png`;
360
+ localFilePath = path2.join(assetsDir, `${assetId}_${filename}`);
361
+ }
362
+ }
363
+ localAssets.push({
364
+ asset_id: assetId,
365
+ label: asset.label || filename,
366
+ type: asset.type || "image",
367
+ mime: asset.mime || normalizeMimeFromPath(localFilePath || filename),
368
+ width: asset.width,
369
+ height: asset.height,
370
+ filePath: localFilePath,
371
+ path: localFilePath,
372
+ filename,
373
+ is_default: asset.is_default || asset.isDefault || false
374
+ });
375
+ }
376
+ const manifestPath = process.env.ARCALITY_ASSETS_FILE || path2.join(process.cwd(), ".arcality", "assets.json");
377
+ const manifestDir = path2.dirname(manifestPath);
378
+ if (!fs2.existsSync(manifestDir)) {
379
+ fs2.mkdirSync(manifestDir, { recursive: true });
380
+ }
381
+ fs2.writeFileSync(manifestPath, JSON.stringify(localAssets, null, 2), "utf8");
382
+ console.log(`>>ARCALITY_STATUS>> \u2705 Inventario local de assets actualizado (${localAssets.length}).`);
383
+ if (process.env.DEBUG) {
384
+ console.log(`[AssetSync] Local manifest updated at ${manifestPath}`);
385
+ }
386
+ } catch (e) {
387
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error durante la sincronizaci\xF3n de assets: ${e.message}`);
388
+ }
389
+ }
390
+ var fs2, path2, zlib, IMAGE_EXTENSIONS;
391
+ var init_asset_resolver = __esm({
392
+ "tests/_helpers/asset-resolver.ts"() {
393
+ fs2 = __toESM(require("fs"));
394
+ path2 = __toESM(require("path"));
395
+ zlib = __toESM(require("zlib"));
396
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
397
+ }
398
+ });
30
399
 
31
400
  // src/configManager.mjs
32
401
  function isRealProjectId2(projectId) {
@@ -277,7 +646,12 @@ function simpleHash(str) {
277
646
  }
278
647
  return "ph_" + Math.abs(hash).toString(36);
279
648
  }
280
- async function createAdoTask(title, description) {
649
+ function normalizeBugClassification(value) {
650
+ const text = String(value || "").trim();
651
+ const allowed = ["[Backend Bug]", "[Frontend Bug]", "[UI Regression]", "[Flaky/Stale Selector]"];
652
+ return allowed.find((category) => text.includes(category)) || "";
653
+ }
654
+ async function createAdoTask(title, description, metadata = {}) {
281
655
  const apiBase = getEffectiveApiBase();
282
656
  if (!apiBase) {
283
657
  if (process.env.DEBUG)
@@ -299,13 +673,18 @@ async function createAdoTask(title, description) {
299
673
  try {
300
674
  const controller = new AbortController();
301
675
  const timeout = setTimeout(() => controller.abort(), 15e3);
676
+ const bugClassification = normalizeBugClassification(metadata?.classification);
677
+ const taskTitle = bugClassification && !String(title).includes(bugClassification) ? `${bugClassification} ${title}` : title;
678
+ const taskDescription = bugClassification && !String(description).includes("Clasificacion") ? `**Clasificacion:** ${bugClassification}
679
+
680
+ ${description}` : description;
302
681
  const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
303
682
  method: "POST",
304
683
  headers: {
305
684
  "Content-Type": "application/json",
306
685
  "x-api-key": key
307
686
  },
308
- body: JSON.stringify({ project_id: projectId, title, description }),
687
+ body: JSON.stringify({ project_id: projectId, title: taskTitle, description: taskDescription }),
309
688
  signal: controller.signal
310
689
  });
311
690
  clearTimeout(timeout);
@@ -772,8 +1151,8 @@ async function scan_sensitive_data_exposure(page) {
772
1151
  }
773
1152
 
774
1153
  // tests/_helpers/ai-agent-helper.ts
775
- var fs4 = __toESM(require("fs"));
776
- var path4 = __toESM(require("path"));
1154
+ var fs5 = __toESM(require("fs"));
1155
+ var path5 = __toESM(require("path"));
777
1156
 
778
1157
  // src/KnowledgeService.ts
779
1158
  var crypto = __toESM(require("crypto"));
@@ -1443,25 +1822,29 @@ function formatQaContextSummary(analysis) {
1443
1822
  var qaSecurityTools = [
1444
1823
  {
1445
1824
  name: "test_xss_injection",
1446
- description: "Injects a standard XSS payload into an input field to test for reflected XSS vulnerabilities. Reports a vulnerability if an alert is triggered.",
1825
+ description: "QA Security Skill: Inject a standard XSS payload into a visible input field identified by IDX. Use this to test reflected XSS on a component from CURRENT COMPONENTS.",
1447
1826
  parameters: {
1448
1827
  type: "object",
1449
1828
  properties: {
1450
- selector: { type: "string", description: "The CSS selector of the input field to test." }
1829
+ idx: { type: "number", description: "The IDX of the input field from CURRENT COMPONENTS." },
1830
+ payload_type: {
1831
+ enum: ["script", "image", "svg"],
1832
+ description: "Payload variant to inject. Defaults to script.",
1833
+ default: "script"
1834
+ }
1451
1835
  },
1452
- required: ["selector"]
1836
+ required: ["idx"]
1453
1837
  }
1454
1838
  },
1455
1839
  {
1456
1840
  name: "test_auth_bypass",
1457
- description: "Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",
1841
+ description: "QA Security Skill: Attempts to directly navigate to a protected route to check for authentication bypass vulnerabilities.",
1458
1842
  parameters: {
1459
1843
  type: "object",
1460
1844
  properties: {
1461
- url: { type: "string", description: "The protected URL to test." },
1462
- redirectUrl: { type: "string", description: "The expected URL to be redirected to if unauthorized (e.g., '/login')." }
1845
+ target_url: { type: "string", description: "The protected URL or route to test. Relative paths are resolved against the current origin." }
1463
1846
  },
1464
- required: ["url", "redirectUrl"]
1847
+ required: ["target_url"]
1465
1848
  }
1466
1849
  },
1467
1850
  {
@@ -1551,29 +1934,29 @@ var AIAgentHelper = class {
1551
1934
  try {
1552
1935
  let skillsContext = "";
1553
1936
  let loadedCount = 0;
1554
- const toolsRoot = process.env.ARCALITY_ROOT || path4.join(__dirname, "..", "..");
1937
+ const toolsRoot = process.env.ARCALITY_ROOT || path5.join(__dirname, "..", "..");
1555
1938
  const possibleDirs = [
1556
- path4.join(toolsRoot, ".agent", "skills"),
1557
- path4.join(toolsRoot, ".agents", "skills")
1939
+ path5.join(toolsRoot, ".agent", "skills"),
1940
+ path5.join(toolsRoot, ".agents", "skills")
1558
1941
  ];
1559
1942
  for (const rootDir of possibleDirs) {
1560
- if (!fs4.existsSync(rootDir))
1943
+ if (!fs5.existsSync(rootDir))
1561
1944
  continue;
1562
1945
  if (loadedCount === 0) {
1563
1946
  skillsContext += "\n\u{1F6E0}\uFE0F SKILLS INSTALADAS (Metodolog\xEDas activas):\n";
1564
1947
  }
1565
- const entries = fs4.readdirSync(rootDir, { withFileTypes: true });
1948
+ const entries = fs5.readdirSync(rootDir, { withFileTypes: true });
1566
1949
  for (const entry of entries) {
1567
1950
  let content = "";
1568
1951
  let skillName = entry.name;
1569
1952
  if (entry.isDirectory()) {
1570
- const skillPath = path4.join(rootDir, entry.name, "SKILL.md");
1571
- if (fs4.existsSync(skillPath)) {
1572
- content = fs4.readFileSync(skillPath, "utf8");
1953
+ const skillPath = path5.join(rootDir, entry.name, "SKILL.md");
1954
+ if (fs5.existsSync(skillPath)) {
1955
+ content = fs5.readFileSync(skillPath, "utf8");
1573
1956
  }
1574
1957
  } else if (entry.name.endsWith(".md")) {
1575
- const skillPath = path4.join(rootDir, entry.name);
1576
- content = fs4.readFileSync(skillPath, "utf8");
1958
+ const skillPath = path5.join(rootDir, entry.name);
1959
+ content = fs5.readFileSync(skillPath, "utf8");
1577
1960
  }
1578
1961
  if (content) {
1579
1962
  skillsContext += `
@@ -1596,9 +1979,9 @@ ${content}
1596
1979
  }
1597
1980
  loadMemory(prompt) {
1598
1981
  try {
1599
- const memoryFile = path4.join(this.contextDir, "memoria-agente.json");
1600
- if (fs4.existsSync(memoryFile)) {
1601
- const memories = JSON.parse(fs4.readFileSync(memoryFile, "utf8"));
1982
+ const memoryFile = path5.join(this.contextDir, "memoria-agente.json");
1983
+ if (fs5.existsSync(memoryFile)) {
1984
+ const memories = JSON.parse(fs5.readFileSync(memoryFile, "utf8"));
1602
1985
  const keywords = prompt.toLowerCase().split(" ").filter((w) => w.length > 4);
1603
1986
  const relevantSuccess = memories.filter((m) => {
1604
1987
  if (!m.success)
@@ -1963,19 +2346,94 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1963
2346
  components: sortedComponents
1964
2347
  };
1965
2348
  }
2349
+ fallbackFailureClassification(failReason, failReasoning = "") {
2350
+ const reason = `${failReason} ${failReasoning}`.toLowerCase();
2351
+ if (reason.includes("network") || reason.includes("backend") || reason.includes("server") || reason.includes("internal_error") || reason.includes("500")) {
2352
+ return "[Backend Bug]";
2353
+ }
2354
+ if (reason.includes("stale") || reason.includes("timeout") || reason.includes("selector") || reason.includes("idx")) {
2355
+ return "[Flaky/Stale Selector]";
2356
+ }
2357
+ if (reason.includes("render") || reason.includes("blank") || reason.includes("ui_validation")) {
2358
+ return "[UI Regression]";
2359
+ }
2360
+ return "[Frontend Bug]";
2361
+ }
2362
+ normalizeFailureClassification(raw) {
2363
+ const text = (raw || "").trim();
2364
+ const allowed = ["[Backend Bug]", "[Frontend Bug]", "[UI Regression]", "[Flaky/Stale Selector]"];
2365
+ return allowed.find((category) => text.includes(category)) || null;
2366
+ }
2367
+ /**
2368
+ * Classifies a terminal QA failure into a normalized bug bucket for reports and ADO.
2369
+ */
2370
+ async classifyFailure(prompt, failReason, failReasoning, history) {
2371
+ const fallback = this.fallbackFailureClassification(failReason, failReasoning);
2372
+ const isProxyMode = !!process.env.ARCALITY_API_URL;
2373
+ if (!isProxyMode && !process.env.ANTHROPIC_API_KEY)
2374
+ return fallback;
2375
+ try {
2376
+ const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
2377
+ const headers = {
2378
+ "Content-Type": "application/json"
2379
+ };
2380
+ if (isProxyMode) {
2381
+ headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
2382
+ const missionId = process.env.ARCALITY_MISSION_ID || "";
2383
+ if (missionId)
2384
+ headers["x-mission-id"] = missionId;
2385
+ } else {
2386
+ headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
2387
+ headers["anthropic-version"] = "2023-06-01";
2388
+ }
2389
+ const diagnosticPayload = {
2390
+ mission: prompt,
2391
+ fail_reason: failReason,
2392
+ fail_reasoning: failReasoning || "None",
2393
+ critical_network_error: this.criticalNetworkError || "None",
2394
+ critical_js_error: this.criticalJsError || "None",
2395
+ console_logs: this.logs.slice(-30),
2396
+ recent_history: history.slice(-12)
2397
+ };
2398
+ const response = await fetch(endpointUrl, {
2399
+ method: "POST",
2400
+ headers,
2401
+ body: JSON.stringify({
2402
+ model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2403
+ max_tokens: 32,
2404
+ system: "You classify terminal E2E QA failures. Reply with exactly one category string and no explanation: [Backend Bug], [Frontend Bug], [UI Regression], or [Flaky/Stale Selector].",
2405
+ messages: [
2406
+ {
2407
+ role: "user",
2408
+ content: `Classify this Arcality QA failure:
2409
+ ${JSON.stringify(diagnosticPayload, null, 2)}`
2410
+ }
2411
+ ],
2412
+ temperature: 0
2413
+ })
2414
+ });
2415
+ if (!response.ok)
2416
+ return fallback;
2417
+ const data = await response.json();
2418
+ const classification = this.normalizeFailureClassification(data.content?.[0]?.text);
2419
+ return classification || fallback;
2420
+ } catch {
2421
+ return fallback;
2422
+ }
2423
+ }
1966
2424
  /**
1967
2425
  * Busca una acción sugerida en la memoria SIN usar tokens.
1968
2426
  * Compara URL, componentes visuales Y palabras clave del prompt.
1969
2427
  */
1970
2428
  async getActionFromGuia(prompt, historyLength) {
1971
2429
  try {
1972
- const fs6 = require("fs");
1973
- const path6 = require("path");
1974
- const memoryFile = path6.join(this.contextDir, "memoria-agente.json");
1975
- if (!fs6.existsSync(memoryFile)) {
2430
+ const fs7 = require("fs");
2431
+ const path7 = require("path");
2432
+ const memoryFile = path7.join(this.contextDir, "memoria-agente.json");
2433
+ if (!fs7.existsSync(memoryFile)) {
1976
2434
  return null;
1977
2435
  }
1978
- const memories = JSON.parse(fs6.readFileSync(memoryFile, "utf8"));
2436
+ const memories = JSON.parse(fs7.readFileSync(memoryFile, "utf8"));
1979
2437
  const state = await this.getPageState();
1980
2438
  const currentUrl = this.page.url();
1981
2439
  const extractCriticalKeywords = (text) => {
@@ -2044,8 +2502,14 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2044
2502
  let bestMatch = null;
2045
2503
  let bestMatchScore = -1;
2046
2504
  for (const c of state.components) {
2047
- const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2048
- const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2505
+ let cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2506
+ if (!cleanMemName) {
2507
+ cleanMemName = (memStep.componentName || "").trim().toLowerCase();
2508
+ }
2509
+ let cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2510
+ if (!cleanCurrName) {
2511
+ cleanCurrName = (c.name || "").trim().toLowerCase();
2512
+ }
2049
2513
  let score = -1;
2050
2514
  if (c.selector === memStep.action_data?.selector) {
2051
2515
  score = 100;
@@ -2215,6 +2679,18 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2215
2679
  parameters: t.input_schema || t.parameters
2216
2680
  }))
2217
2681
  ];
2682
+ const anthropicTools = rawTools.map((t) => ({
2683
+ name: t.name,
2684
+ description: t.description,
2685
+ input_schema: t.parameters || t.input_schema
2686
+ }));
2687
+ if (anthropicTools.length > 0) {
2688
+ const lastTool = anthropicTools[anthropicTools.length - 1];
2689
+ anthropicTools[anthropicTools.length - 1] = {
2690
+ ...lastTool,
2691
+ cache_control: { type: "ephemeral" }
2692
+ };
2693
+ }
2218
2694
  const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
2219
2695
  \u{1F511} QA CREDENTIALS (Use if login is needed):
2220
2696
  - User: ${process.env.LOGIN_USER}
@@ -2222,6 +2698,12 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2222
2698
  ` : "";
2223
2699
  const memoryContext = this.loadMemory(prompt);
2224
2700
  const skillsContext = this.loadSkills();
2701
+ let assetContext = "";
2702
+ try {
2703
+ const { formatUploadAssetContext: formatUploadAssetContext2 } = (init_asset_resolver(), __toCommonJS(asset_resolver_exports));
2704
+ assetContext = formatUploadAssetContext2();
2705
+ } catch {
2706
+ }
2225
2707
  let projectInfo = "";
2226
2708
  try {
2227
2709
  const { getProjectContext } = require("../src/projectInspector");
@@ -2263,11 +2745,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2263
2745
  }
2264
2746
  let customUserContext = "";
2265
2747
  try {
2266
- const fs6 = require("fs");
2267
- const path6 = require("path");
2268
- const customContextPath = path6.join(process.cwd(), ".arcality", "qa-context.md");
2748
+ const fs7 = require("fs");
2749
+ const path7 = require("path");
2750
+ const customContextPath = path7.join(process.cwd(), ".arcality", "qa-context.md");
2269
2751
  if (false) {
2270
- const content = fs6.readFileSync(customContextPath, "utf8");
2752
+ const content = fs7.readFileSync(customContextPath, "utf8");
2271
2753
  customUserContext = `
2272
2754
  <CUSTOMER_BUSINESS_RULES>
2273
2755
  El Ing. de QA o Cliente final ha provisto las siguientes reglas estrictas para este negocio/dominio. DEBES priorizar esta informaci\xF3n sobre todas tus skills base:
@@ -2315,15 +2797,7 @@ ${this.qaContextPromptCache}
2315
2797
  text: `# IDENTITY
2316
2798
  You are **Arcality**, an elite Senior QA Web Testing Agent. You have 10+ years of experience in manual and automated testing. You are meticulous, thorough, and NEVER take shortcuts. You treat every mission as a real client engagement where your professional reputation is on the line. You MUST use your full arsenal of QA tools \u2014 guessing and blind retries are UNACCEPTABLE.
2317
2799
 
2318
- # MISSION
2319
- ${prompt}
2320
-
2321
- ${projectInfo}
2322
- ${skillsContext}
2323
- ${customUserContext}
2324
- ${memoryContext}
2325
- ${credentialsContext}
2326
- ${memoryBackendContext}`
2800
+ `
2327
2801
  },
2328
2802
  {
2329
2803
  type: "text",
@@ -2421,6 +2895,13 @@ These are NOT optional. You MUST use these tools in these situations:
2421
2895
  13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.
2422
2896
  14. **USER-DEFINED BUGS (MANDATORY & IMMEDIATE)**: If the prompt explicitly defines a condition as a bug (e.g., "Si no te deja eliminarla, es un bug de la aplicaci\xF3n", or "If X fails it's a bug"), and you encounter that condition, you MUST call \`report_application_bug\` IMMEDIATELY in that exact same turn. DO NOT perform any intermediate UI actions (such as clicking "Cancelar", closing modals, or navigating away) before calling the tool. Any intermediate UI action will cause you to lose your reasoning context in the next turn and enter an infinite loop. The call to \`report_application_bug\` must take absolute priority over any UI cleanup or close actions. DO NOT dismiss it as "expected business logic" or "referential integrity". The user's definition of a bug OVERRIDES all general reasoning.
2423
2897
 
2898
+ 15. **FILE UPLOAD FIELDS (MANDATORY PROTOCOL)**: When the current step requires uploading an image or file (a dropzone component with text like "arrastra", "explora", "formatos", "JPEG", "PNG", or an input with aria-label containing "imagen" or "file"):
2899
+ - You MUST use the \`fill\` action on the file input with one of the values from the \`# AVAILABLE UPLOAD ASSETS\` section below (format: \`arcasset://<asset_id>\`).
2900
+ - If no asset matches the required type, use the value \`"placeholder:image/banner"\` \u2014 the runner will automatically generate a valid placeholder PNG image.
2901
+ - **NEVER** report a bug or inability to proceed just because you see a file upload field. You ALWAYS have a fallback.
2902
+ - **NEVER** skip the image step. If clicking "Siguiente" is blocked by a required image field, locate the file input and use \`fill\` with \`arcasset://\` or \`placeholder:image/banner\`.
2903
+ - To locate the file input: look for a component with type \`[input]\` adjacent to dropzone text, OR use \`inspect_element_details\` on the dropzone container to find its nested \`input[type=file]\`.
2904
+
2424
2905
  # WIZARD & MULTI-STEP FORMS (CRITICAL)
2425
2906
  When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 Step 3):
2426
2907
 
@@ -2460,7 +2941,22 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
2460
2941
 
2461
2942
  11. **COMBOBOX KEYBOARD FALLBACK**: If you click a combobox/select field and the dropdown options (\`[li]\`, \`[OPTION]\`) are NOT visible after 1 turn, use this keyboard sequence: a) Click the combobox field ONCE. b) Use \`send_keyboard_event\` with key "ArrowDown" to open the options and highlight the first one. c) Use \`send_keyboard_event\` with key "Enter" to select it. This avoids the toggle problem and works with all dropdown implementations.
2462
2943
 
2463
- 12. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`
2944
+ 12. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) AND click the save button in the SAME turn. Do NOT waste a turn closing the error toast. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`,
2945
+ cache_control: { type: "ephemeral" }
2946
+ },
2947
+ {
2948
+ type: "text",
2949
+ text: `
2950
+ # MISSION
2951
+ ${prompt}
2952
+
2953
+ ${projectInfo}
2954
+ ${skillsContext}
2955
+ ${customUserContext}
2956
+ ${memoryContext}
2957
+ ${credentialsContext}
2958
+ ${memoryBackendContext}
2959
+ ${assetContext}`
2464
2960
  }
2465
2961
  ];
2466
2962
  if (this.testInfo) {
@@ -2544,11 +3040,7 @@ ${history.slice(-25).join("\n") || "None"}`
2544
3040
  model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2545
3041
  max_tokens: 4096,
2546
3042
  system: systemPromptBlocks,
2547
- tools: rawTools.map((t) => ({
2548
- name: t.name,
2549
- description: t.description,
2550
- input_schema: t.parameters || t.input_schema
2551
- })),
3043
+ tools: anthropicTools,
2552
3044
  messages: anthropicMessages,
2553
3045
  temperature: 0.2
2554
3046
  })
@@ -2571,7 +3063,7 @@ ${history.slice(-25).join("\n") || "None"}`
2571
3063
  const delay = Math.pow(2, retryCount) * 1e3;
2572
3064
  console.log(`
2573
3065
  >>ARCALITY_STATUS>> \u26A0\uFE0F Arcality est\xE1 saturado (${response.status}). Reintentando...`);
2574
- await new Promise((resolve) => setTimeout(resolve, delay));
3066
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
2575
3067
  continue;
2576
3068
  }
2577
3069
  throw new Error(`Arcality Brain Error ${response.status}: ${errorText}`);
@@ -2886,7 +3378,7 @@ ${history.slice(-25).join("\n") || "None"}`
2886
3378
  }
2887
3379
  }
2888
3380
  } else if (toolName === "test_xss_injection") {
2889
- const { idx, payload_type } = toolInput;
3381
+ const { idx, payload_type = "script" } = toolInput;
2890
3382
  const el = state.components.find((c) => c.idx === idx);
2891
3383
  if (!el) {
2892
3384
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
@@ -2898,7 +3390,8 @@ ${history.slice(-25).join("\n") || "None"}`
2898
3390
  if (payload_type === "svg")
2899
3391
  payload = "<svg onload=alert(1)>";
2900
3392
  try {
2901
- const locator = this.page.locator(`xpath=${el.selector}`).first();
3393
+ const frame = this.page.frames()[el.frameIdx] || this.page.mainFrame();
3394
+ const locator = frame.locator(el.selector).first();
2902
3395
  await locator.fill(payload);
2903
3396
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\xFArate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\xF3 renderizado tal cual o sanitizado.` });
2904
3397
  if (this.testInfo)
@@ -3423,9 +3916,10 @@ var SecurityScanner = class {
3423
3916
 
3424
3917
  // tests/_helpers/agentic-runner.spec.ts
3425
3918
  var import_config = require("dotenv/config");
3426
- var fs5 = __toESM(require("fs"));
3427
- var path5 = __toESM(require("path"));
3919
+ var fs6 = __toESM(require("fs"));
3920
+ var path6 = __toESM(require("path"));
3428
3921
  var crypto2 = __toESM(require("crypto"));
3922
+ init_asset_resolver();
3429
3923
  var _sessionRuleCache = /* @__PURE__ */ new Set();
3430
3924
  function captureValidationRule(errorMessage, context) {
3431
3925
  const key = errorMessage.substring(0, 80).toLowerCase().trim();
@@ -3455,11 +3949,11 @@ function writeBatchMissionResult(payload) {
3455
3949
  if (!runDomainDir)
3456
3950
  return;
3457
3951
  try {
3458
- if (!fs5.existsSync(runDomainDir)) {
3459
- fs5.mkdirSync(runDomainDir, { recursive: true });
3952
+ if (!fs6.existsSync(runDomainDir)) {
3953
+ fs6.mkdirSync(runDomainDir, { recursive: true });
3460
3954
  }
3461
- fs5.writeFileSync(
3462
- path5.join(runDomainDir, "mission-result.json"),
3955
+ fs6.writeFileSync(
3956
+ path6.join(runDomainDir, "mission-result.json"),
3463
3957
  JSON.stringify(payload, null, 2)
3464
3958
  );
3465
3959
  } catch {
@@ -3480,10 +3974,15 @@ function writeBatchMissionResult(payload) {
3480
3974
  const history = [];
3481
3975
  const urlHistory = [];
3482
3976
  const stepsData = [];
3483
- const memoryFile = path5.join(contextDir, "memoria-arcality.json");
3484
- if (fs5.existsSync(memoryFile)) {
3977
+ try {
3978
+ await syncAssetsFromBackend();
3979
+ } catch (e) {
3980
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al sincronizar assets: ${e.message}`);
3981
+ }
3982
+ const memoryFile = path6.join(contextDir, "memoria-arcality.json");
3983
+ if (fs6.existsSync(memoryFile)) {
3485
3984
  try {
3486
- fs5.unlinkSync(memoryFile);
3985
+ fs6.unlinkSync(memoryFile);
3487
3986
  } catch {
3488
3987
  }
3489
3988
  }
@@ -3522,18 +4021,34 @@ function writeBatchMissionResult(payload) {
3522
4021
  let lastSeenErrorToast = "";
3523
4022
  let accumulatedCost = 0;
3524
4023
  let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
4024
+ let bugClassification = "";
4025
+ const missionSummaryPaths = /* @__PURE__ */ new Set();
4026
+ const updateSavedMissionClassification = () => {
4027
+ if (!bugClassification)
4028
+ return;
4029
+ for (const summaryPath of missionSummaryPaths) {
4030
+ try {
4031
+ if (!fs6.existsSync(summaryPath))
4032
+ continue;
4033
+ const data = JSON.parse(fs6.readFileSync(summaryPath, "utf8"));
4034
+ data.bug_classification = bugClassification;
4035
+ fs6.writeFileSync(summaryPath, JSON.stringify(data, null, 2));
4036
+ } catch {
4037
+ }
4038
+ }
4039
+ };
3525
4040
  const saveMissionResults = () => {
3526
4041
  if (resultsSaved)
3527
4042
  return;
3528
4043
  resultsSaved = true;
3529
- if (!fs5.existsSync(contextDir))
3530
- fs5.mkdirSync(contextDir, { recursive: true });
4044
+ if (!fs6.existsSync(contextDir))
4045
+ fs6.mkdirSync(contextDir, { recursive: true });
3531
4046
  const finalSuccess = aiMarkedSuccess && !hasCriticalError;
3532
4047
  try {
3533
- const memoryFile2 = path5.join(contextDir, "memoria-arcality.json");
4048
+ const memoryFile2 = path6.join(contextDir, "memoria-arcality.json");
3534
4049
  let memories = [];
3535
- if (fs5.existsSync(memoryFile2)) {
3536
- const content = fs5.readFileSync(memoryFile2, "utf8").trim();
4050
+ if (fs6.existsSync(memoryFile2)) {
4051
+ const content = fs6.readFileSync(memoryFile2, "utf8").trim();
3537
4052
  if (content) {
3538
4053
  try {
3539
4054
  memories = JSON.parse(content);
@@ -3604,10 +4119,11 @@ function writeBatchMissionResult(payload) {
3604
4119
  error: hasCriticalError,
3605
4120
  fail_reason: failReason,
3606
4121
  fail_reasoning: failReasoning,
4122
+ bug_classification: bugClassification || null,
3607
4123
  timestamp: Date.now()
3608
4124
  };
3609
4125
  memories.push(missionData);
3610
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4126
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3611
4127
  if (process.env.DEBUG)
3612
4128
  console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${cleanSteps.length} pasos limpios (${rawSteps.length - cleanSteps.length} corruptos filtrados)`);
3613
4129
  } catch (err) {
@@ -3624,22 +4140,26 @@ function writeBatchMissionResult(payload) {
3624
4140
  error: hasCriticalError,
3625
4141
  fail_reason: failReason,
3626
4142
  fail_reasoning: failReasoning,
4143
+ bug_classification: bugClassification || null,
3627
4144
  usage: totalMissionUsage,
3628
4145
  target: process.env.BASE_URL || "",
3629
4146
  target_path: process.env.TARGET_PATH || "",
3630
4147
  report_dir: process.env.REPORTS_DIR || "",
3631
4148
  timestamp: timestampIso
3632
4149
  };
3633
- const logPath = path5.join(contextDir, `agent-log-${timestampMs}.json`);
3634
- fs5.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
3635
- const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR || path5.dirname(contextDir);
3636
- const durableLogsDir = path5.join(runDomainDir, "logs");
3637
- fs5.mkdirSync(durableLogsDir, { recursive: true });
3638
- fs5.writeFileSync(
3639
- path5.join(durableLogsDir, `mission-log-${timestampMs}.json`),
3640
- JSON.stringify(summaryPayload, null, 2)
3641
- );
4150
+ const logPath = path6.join(contextDir, `agent-log-${timestampMs}.json`);
4151
+ fs6.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
4152
+ missionSummaryPaths.add(logPath);
4153
+ const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR || path6.dirname(contextDir);
4154
+ const durableLogsDir = path6.join(runDomainDir, "logs");
4155
+ fs6.mkdirSync(durableLogsDir, { recursive: true });
4156
+ const durableLogPath = path6.join(durableLogsDir, `mission-log-${timestampMs}.json`);
4157
+ fs6.writeFileSync(durableLogPath, JSON.stringify(summaryPayload, null, 2));
4158
+ missionSummaryPaths.add(durableLogPath);
3642
4159
  writeBatchMissionResult(summaryPayload);
4160
+ if (runDomainDir) {
4161
+ missionSummaryPaths.add(path6.join(runDomainDir, "mission-result.json"));
4162
+ }
3643
4163
  try {
3644
4164
  const historyLine = JSON.stringify({
3645
4165
  ts: timestampIso,
@@ -3649,12 +4169,12 @@ function writeBatchMissionResult(payload) {
3649
4169
  fail_reason: failReason,
3650
4170
  report_dir: process.env.REPORTS_DIR || ""
3651
4171
  }) + "\n";
3652
- fs5.appendFileSync(
3653
- path5.join(contextDir, "arcality-history.log"),
4172
+ fs6.appendFileSync(
4173
+ path6.join(contextDir, "arcality-history.log"),
3654
4174
  historyLine
3655
4175
  );
3656
- fs5.appendFileSync(
3657
- path5.join(durableLogsDir, "arcality-history.log"),
4176
+ fs6.appendFileSync(
4177
+ path6.join(durableLogsDir, "arcality-history.log"),
3658
4178
  historyLine
3659
4179
  );
3660
4180
  } catch (e) {
@@ -3849,11 +4369,11 @@ function writeBatchMissionResult(payload) {
3849
4369
  timestamp: Date.now()
3850
4370
  };
3851
4371
  try {
3852
- const memoryFile2 = path5.join(contextDir, "memoria-agente.json");
4372
+ const memoryFile2 = path6.join(contextDir, "memoria-agente.json");
3853
4373
  let memories = [];
3854
- if (fs5.existsSync(memoryFile2)) {
4374
+ if (fs6.existsSync(memoryFile2)) {
3855
4375
  try {
3856
- memories = JSON.parse(fs5.readFileSync(memoryFile2, "utf8"));
4376
+ memories = JSON.parse(fs6.readFileSync(memoryFile2, "utf8"));
3857
4377
  } catch {
3858
4378
  memories = [];
3859
4379
  }
@@ -3865,14 +4385,14 @@ function writeBatchMissionResult(payload) {
3865
4385
  if (existingIdx < 0) {
3866
4386
  if (resolvedStepsData.length > 0) {
3867
4387
  memories.push(localMemoryData);
3868
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4388
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3869
4389
  console.log(`>>ARCALITY_STATUS>> \u2728 Memoria sincronizada en memoria-agente.json (${resolvedStepsData.length} pasos). Arcality entrar\xE1 en MODO GU\xCDA.`);
3870
4390
  } else {
3871
4391
  console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Memoria NO sincronizada: el patr\xF3n del backend tiene 0 pasos t\xE9cnicos v\xE1lidos (steps_technical vac\xEDo).`);
3872
4392
  }
3873
4393
  } else if (!existingHasSteps && resolvedStepsData.length > 0) {
3874
4394
  memories[existingIdx] = localMemoryData;
3875
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4395
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3876
4396
  console.log(`>>ARCALITY_STATUS>> \u{1F504} Patr\xF3n corrupto (0 pasos) sobreescrito en memoria-agente.json (${resolvedStepsData.length} pasos nuevos). MODO GU\xCDA activado.`);
3877
4397
  } else {
3878
4398
  console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n ya existe en memoria-agente.json con ${(memories[existingIdx]?.steps_data || []).length} pasos. MODO GU\xCDA activado sin re-escritura.`);
@@ -4142,7 +4662,16 @@ ${patternContext}` : prompt;
4142
4662
  saveMissionResults();
4143
4663
  break;
4144
4664
  } else {
4145
- throw err;
4665
+ const errMsg = err?.message || String(err);
4666
+ console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Error terminal del motor IA: ${errMsg}`);
4667
+ aiMarkedSuccess = false;
4668
+ hasCriticalError = true;
4669
+ failReason = "agent_runtime_error";
4670
+ failReasoning = errMsg;
4671
+ isFinished = true;
4672
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AGENT RUNTIME ERROR] ${errMsg}`);
4673
+ saveMissionResults();
4674
+ break;
4146
4675
  }
4147
4676
  }
4148
4677
  if (response.usage) {
@@ -4216,7 +4745,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4216
4745
  const lastSixHistory = history.slice(-6);
4217
4746
  const consecutiveWaits = lastSixHistory.filter((h) => h.includes('wait en "')).length;
4218
4747
  if (consecutiveWaits >= 3) {
4219
- const blankEvidencePath = path5.join(contextDir, `portal-blank-state-${Date.now()}.png`);
4748
+ const blankEvidencePath = path6.join(contextDir, `portal-blank-state-${Date.now()}.png`);
4220
4749
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
4221
4750
  });
4222
4751
  const blankUrl = page.url();
@@ -4431,12 +4960,17 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4431
4960
  } else {
4432
4961
  await page.waitForTimeout(500);
4433
4962
  }
4434
- } else if (step.action === "fill" && loc && step.value) {
4963
+ } else if (step.action === "fill" && loc) {
4435
4964
  const inputType = await loc.evaluate((el) => el.tagName === "INPUT" ? (el.type || "").toLowerCase() : null).catch(() => null);
4436
4965
  if (inputType === "file") {
4437
- console.log(` \u{1F4C2} Detectado input de archivo. Subiendo: "${step.value}"`);
4438
- const filePath = step.value.replace(/^"|"$/g, "").trim();
4439
- await loc.setInputFiles(filePath);
4966
+ const rawUploadValue = String(step.value || "placeholder:image/banner");
4967
+ console.log(` \u{1F4C2} Detectado input de archivo. Subiendo: "${rawUploadValue}"`);
4968
+ const fileVal = rawUploadValue.replace(/^"|"$/g, "").trim();
4969
+ const desc = step.description || "file upload";
4970
+ const resolvedAsset = resolveUploadAsset(fileVal, desc);
4971
+ await loc.setInputFiles(resolvedAsset);
4972
+ } else if (!step.value) {
4973
+ console.log(` \u26A0\uFE0F Acci\xF3n fill sin valor para un campo no-file. Se omite para evitar corrupci\xF3n de estado.`);
4440
4974
  } else if (inputType === "date" || inputType === "time" || inputType === "datetime-local") {
4441
4975
  console.log(` \u{1F39B}\uFE0F Detectado input nativo (${inputType}). Usando estrategia segura para: "${step.value}"`);
4442
4976
  try {
@@ -4488,7 +5022,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4488
5022
  const panelCount = Math.min(panels.length, 6);
4489
5023
  if (panelCount >= 2 && emptyPanelCount >= panelCount * 0.6) {
4490
5024
  const blankUrl = page.url();
4491
- const blankEvidencePath = path5.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
5025
+ const blankEvidencePath = path6.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
4492
5026
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
4493
5027
  });
4494
5028
  console.error(`
@@ -4772,8 +5306,8 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4772
5306
  console.log("\u{1F916} [SMART-YAML] Generating mission card...");
4773
5307
  const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
4774
5308
  if (smartYaml) {
4775
- const yamlPath = path5.join(contextDir, "last-mission-smart.yaml");
4776
- fs5.writeFileSync(yamlPath, smartYaml);
5309
+ const yamlPath = path6.join(contextDir, "last-mission-smart.yaml");
5310
+ fs6.writeFileSync(yamlPath, smartYaml);
4777
5311
  console.log(`>>ARCALITY_STATUS>> \u{1F4DD} Smart Mission Card generated.`);
4778
5312
  await testInfo.attach("smart_mission_card", {
4779
5313
  body: Buffer.from(smartYaml, "utf-8"),
@@ -4827,12 +5361,39 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
4827
5361
  if (!aiMarkedSuccess) {
4828
5362
  if (process.env.DEBUG)
4829
5363
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
5364
+ bugClassification = await agent.classifyFailure(prompt, failReason || "unknown_failure", failReasoning, history);
5365
+ console.log(`>>ARCALITY_STATUS>> Bug classification: ${bugClassification}`);
5366
+ updateSavedMissionClassification();
5367
+ await testInfo.attach("bug_classification", {
5368
+ body: Buffer.from(bugClassification, "utf-8"),
5369
+ contentType: "text/plain"
5370
+ });
5371
+ try {
5372
+ const { createAdoTask: createAdoTask2 } = await Promise.resolve().then(() => (init_arcalityClient(), arcalityClient_exports));
5373
+ const shortPrompt = prompt.length > 90 ? prompt.substring(0, 87).trimEnd() + "..." : prompt;
5374
+ const adoTitle = `[Arcality QA] ${shortPrompt}`;
5375
+ const adoDescription = [
5376
+ `**Bug detectado automaticamente por Arcality QA**`,
5377
+ ``,
5378
+ `**Clasificacion:** ${bugClassification}`,
5379
+ `**Fail reason:** ${failReason || "unknown_failure"}`,
5380
+ failReasoning ? `**Analisis:** ${failReasoning}` : `**Analisis:** No disponible.`,
5381
+ `**Mision:** ${prompt}`,
5382
+ `**Historial reciente:**`,
5383
+ history.slice(-8).map((h) => `- ${h}`).join("\n"),
5384
+ `**Fecha:** ${(/* @__PURE__ */ new Date()).toISOString()}`
5385
+ ].join("\n");
5386
+ await createAdoTask2(adoTitle, adoDescription, { classification: bugClassification }).catch(() => null);
5387
+ } catch {
5388
+ }
4830
5389
  await persistCollectiveMemory(true).catch(() => {
4831
5390
  });
4832
5391
  let finalErrorMsg = `Misi\xF3n no completada o finalizada con errores.
4833
5392
 
4834
5393
  `;
4835
5394
  finalErrorMsg += `======================================================
5395
+ `;
5396
+ finalErrorMsg += `Bug classification: ${bugClassification}
4836
5397
  `;
4837
5398
  finalErrorMsg += `\u{1F3AF} TIPO DE FALLO: ${failReason}
4838
5399
  `;
@@ -4848,15 +5409,15 @@ ${failReasoning}
4848
5409
  if (process.env.DEBUG)
4849
5410
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
4850
5411
  }
4851
- if (fs5.existsSync(contextDir)) {
5412
+ if (fs6.existsSync(contextDir)) {
4852
5413
  try {
4853
- const files = fs5.readdirSync(contextDir);
5414
+ const files = fs6.readdirSync(contextDir);
4854
5415
  for (const file of files) {
4855
5416
  if (file.endsWith(".json") || file.endsWith(".png")) {
4856
- fs5.unlinkSync(path5.join(contextDir, file));
5417
+ fs6.unlinkSync(path6.join(contextDir, file));
4857
5418
  }
4858
5419
  }
4859
- fs5.rmSync(contextDir, { recursive: true, force: true });
5420
+ fs6.rmSync(contextDir, { recursive: true, force: true });
4860
5421
  console.log(`>>ARCALITY_STATUS>> \u{1F9F9} Carpeta context eliminada (Memoria ef\xEDmera completada).`);
4861
5422
  } catch (err) {
4862
5423
  console.warn(`No se pudo limpiar la carpeta context: ${err}`);