@arcadialdev/arcality 3.0.3 → 4.0.0

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);
@@ -601,6 +980,34 @@ var sendKeyboardEventTool = {
601
980
  required: ["key"]
602
981
  }
603
982
  };
983
+ var typeSequentiallyTool = {
984
+ name: "type_sequentially",
985
+ description: "QA Skill: Type a full text string character by character into a specific field. Use this for masked numeric inputs, OTP/PIN/CLABE/identification codes, virtual-keyboard flows, or whenever the mission explicitly says to type manually, digit by digit, or character by character. Prefer this over repeated clicks when the field is already visible but normal fill may not reflect the mask correctly.",
986
+ input_schema: {
987
+ type: "object",
988
+ properties: {
989
+ idx: {
990
+ type: "number",
991
+ description: "The IDX of the input-like element to focus before typing"
992
+ },
993
+ text: {
994
+ type: "string",
995
+ description: "The exact text to type sequentially"
996
+ },
997
+ delay_ms: {
998
+ type: "number",
999
+ description: "Delay between keystrokes in milliseconds (default: 80)",
1000
+ default: 80
1001
+ },
1002
+ clear_first: {
1003
+ type: "boolean",
1004
+ description: "Whether to clear any existing value before typing (default: true)",
1005
+ default: true
1006
+ }
1007
+ },
1008
+ required: ["idx", "text"]
1009
+ }
1010
+ };
604
1011
  var interactNativeControlTool = {
605
1012
  name: "interact_native_control",
606
1013
  description: "QA Skill: Interact with native HTML controls that standard click/fill may not handle correctly. This includes: <input type='time'>, <input type='date'>, <input type='datetime-local'>, <input type='color'>, <input type='range'>, <select> with optgroup, and contenteditable elements. This tool uses Playwright's specialized methods (selectOption, fill with format coercion, press sequences) to reliably set values. ALWAYS prefer this tool over manual fill for time/date inputs.",
@@ -656,6 +1063,7 @@ var qaAdvancedTools = [
656
1063
  restoreNavigationCheckpointTool,
657
1064
  createTestEvidenceTool,
658
1065
  sendKeyboardEventTool,
1066
+ typeSequentiallyTool,
659
1067
  interactNativeControlTool,
660
1068
  scrollPageTool
661
1069
  ];
@@ -772,8 +1180,8 @@ async function scan_sensitive_data_exposure(page) {
772
1180
  }
773
1181
 
774
1182
  // tests/_helpers/ai-agent-helper.ts
775
- var fs4 = __toESM(require("fs"));
776
- var path4 = __toESM(require("path"));
1183
+ var fs5 = __toESM(require("fs"));
1184
+ var path5 = __toESM(require("path"));
777
1185
 
778
1186
  // src/KnowledgeService.ts
779
1187
  var crypto = __toESM(require("crypto"));
@@ -1300,6 +1708,112 @@ function isPlaceholderRule(rule) {
1300
1708
  function isRecognizedSection(title) {
1301
1709
  return RECOGNIZED_SECTION_PATTERNS.some((pattern) => pattern.test(title));
1302
1710
  }
1711
+ function asTrimmedString(value) {
1712
+ if (typeof value !== "string")
1713
+ return null;
1714
+ const trimmed = value.trim();
1715
+ return trimmed ? trimmed : null;
1716
+ }
1717
+ function asStringList(value) {
1718
+ if (!Array.isArray(value))
1719
+ return [];
1720
+ return value.map((item) => asTrimmedString(item)).filter((item) => Boolean(item));
1721
+ }
1722
+ function loadQaContextGovernance(projectRoot, contextExists) {
1723
+ const metadataPath = path.join(projectRoot, ".arcality", "qa-context.meta.json");
1724
+ const warnings = [];
1725
+ const emptyResult = {
1726
+ path: metadataPath,
1727
+ exists: false,
1728
+ isValid: false,
1729
+ enforcementMode: getQaContextGovernanceMode(),
1730
+ version: null,
1731
+ updatedBy: null,
1732
+ approvedBy: null,
1733
+ ownerTeam: null,
1734
+ changeSummary: null,
1735
+ effectiveFrom: null,
1736
+ tags: [],
1737
+ warnings
1738
+ };
1739
+ if (!fs.existsSync(metadataPath)) {
1740
+ if (contextExists) {
1741
+ warnings.push("No se encontro qa-context.meta.json. Agrega metadata de gobernanza para versionar responsables y cambios.");
1742
+ }
1743
+ return emptyResult;
1744
+ }
1745
+ try {
1746
+ const raw = fs.readFileSync(metadataPath, "utf8");
1747
+ const parsed = JSON.parse(raw);
1748
+ const metadata = {
1749
+ path: metadataPath,
1750
+ exists: true,
1751
+ isValid: true,
1752
+ enforcementMode: getQaContextGovernanceMode(),
1753
+ version: asTrimmedString(parsed?.version),
1754
+ updatedBy: asTrimmedString(parsed?.updated_by ?? parsed?.updatedBy),
1755
+ approvedBy: asTrimmedString(parsed?.approved_by ?? parsed?.approvedBy),
1756
+ ownerTeam: asTrimmedString(parsed?.owner_team ?? parsed?.ownerTeam),
1757
+ changeSummary: asTrimmedString(parsed?.change_summary ?? parsed?.changeSummary),
1758
+ effectiveFrom: asTrimmedString(parsed?.effective_from ?? parsed?.effectiveFrom),
1759
+ tags: asStringList(parsed?.tags),
1760
+ warnings
1761
+ };
1762
+ if (!metadata.version)
1763
+ warnings.push('qa-context.meta.json no define "version".');
1764
+ if (!metadata.updatedBy)
1765
+ warnings.push('qa-context.meta.json no define "updated_by".');
1766
+ if (!metadata.ownerTeam)
1767
+ warnings.push('qa-context.meta.json no define "owner_team".');
1768
+ if (!metadata.changeSummary)
1769
+ warnings.push('qa-context.meta.json no define "change_summary".');
1770
+ metadata.isValid = warnings.length === 0;
1771
+ return metadata;
1772
+ } catch (error) {
1773
+ warnings.push("qa-context.meta.json no se pudo leer o parsear correctamente.");
1774
+ return {
1775
+ ...emptyResult,
1776
+ exists: true,
1777
+ warnings
1778
+ };
1779
+ }
1780
+ }
1781
+ function getQaContextGovernanceMode() {
1782
+ const raw = String(process.env.ARCALITY_QA_CONTEXT_GOVERNANCE || "").trim().toLowerCase();
1783
+ if (raw === "off")
1784
+ return "off";
1785
+ if (raw === "strict" || raw === "enforce" || raw === "required")
1786
+ return "strict";
1787
+ if (raw === "warn")
1788
+ return "warn";
1789
+ if (process.env.CI === "true")
1790
+ return "strict";
1791
+ return "warn";
1792
+ }
1793
+ function getQaContextGovernanceIssues(analysis) {
1794
+ if (!analysis.exists)
1795
+ return [];
1796
+ const issues = [];
1797
+ if (!analysis.governance.exists) {
1798
+ issues.push("Falta el archivo .arcality/qa-context.meta.json para gobernanza del contexto.");
1799
+ return issues;
1800
+ }
1801
+ if (!analysis.governance.version)
1802
+ issues.push("La metadata no define version.");
1803
+ if (!analysis.governance.updatedBy)
1804
+ issues.push("La metadata no define updated_by.");
1805
+ if (!analysis.governance.ownerTeam)
1806
+ issues.push("La metadata no define owner_team.");
1807
+ if (!analysis.governance.changeSummary)
1808
+ issues.push("La metadata no define change_summary.");
1809
+ return issues;
1810
+ }
1811
+ function shouldBlockOnQaContextGovernance(analysis) {
1812
+ const mode = analysis.governance.enforcementMode;
1813
+ if (mode !== "strict")
1814
+ return false;
1815
+ return getQaContextGovernanceIssues(analysis).length > 0;
1816
+ }
1303
1817
  function analyzeQaContext(projectRoot = process.cwd()) {
1304
1818
  const filePath = path.join(projectRoot, ".arcality", "qa-context.md");
1305
1819
  const emptyResult = {
@@ -1311,7 +1825,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1311
1825
  ruleCount: 0,
1312
1826
  sectionCount: 0,
1313
1827
  recognizedSectionCount: 0,
1828
+ unrecognizedSectionCount: 0,
1314
1829
  ignoredExampleCount: 0,
1830
+ duplicateRuleCount: 0,
1831
+ lastModifiedAt: null,
1832
+ fingerprint: null,
1833
+ governance: loadQaContextGovernance(projectRoot, false),
1315
1834
  warnings: [],
1316
1835
  status: "missing",
1317
1836
  isValid: false
@@ -1320,12 +1839,16 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1320
1839
  return emptyResult;
1321
1840
  }
1322
1841
  const rawContent = fs.readFileSync(filePath, "utf8");
1842
+ const stat = fs.statSync(filePath);
1323
1843
  const cleanedContent = stripHtmlComments(rawContent);
1324
1844
  const lines = cleanedContent.split(/\r?\n/);
1325
- const warnings = [];
1845
+ const governance = loadQaContextGovernance(projectRoot, true);
1846
+ const warnings = [...governance.warnings];
1326
1847
  const sections = [];
1327
1848
  let currentSection = null;
1328
1849
  let ignoredExampleCount = 0;
1850
+ let duplicateRuleCount = 0;
1851
+ const seenRules = /* @__PURE__ */ new Set();
1329
1852
  const openSection = (title) => {
1330
1853
  currentSection = {
1331
1854
  title: title.trim(),
@@ -1352,6 +1875,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1352
1875
  ignoredExampleCount += 1;
1353
1876
  continue;
1354
1877
  }
1878
+ const dedupeKey = rule.toLowerCase();
1879
+ if (seenRules.has(dedupeKey)) {
1880
+ duplicateRuleCount += 1;
1881
+ continue;
1882
+ }
1883
+ seenRules.add(dedupeKey);
1355
1884
  if (currentSection) {
1356
1885
  currentSection.rules.push(rule);
1357
1886
  }
@@ -1359,6 +1888,7 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1359
1888
  const nonEmptySections = sections.filter((section) => section.rules.length > 0);
1360
1889
  const ruleCount = nonEmptySections.reduce((total, section) => total + section.rules.length, 0);
1361
1890
  const recognizedSectionCount = nonEmptySections.filter((section) => section.recognized).length;
1891
+ const unrecognizedSectionCount = nonEmptySections.length - recognizedSectionCount;
1362
1892
  if (!rawContent.trim()) {
1363
1893
  warnings.push("El archivo existe pero esta vacio.");
1364
1894
  }
@@ -1375,6 +1905,12 @@ function analyzeQaContext(projectRoot = process.cwd()) {
1375
1905
  if (ruleCount > 0 && recognizedSectionCount === 0) {
1376
1906
  warnings.push("Hay reglas, pero ninguna cae en las secciones sugeridas. Revisa la estructura del archivo.");
1377
1907
  }
1908
+ if (duplicateRuleCount > 0) {
1909
+ warnings.push(`Se omitieron ${duplicateRuleCount} regla(s) duplicadas del QA Context.`);
1910
+ }
1911
+ if (unrecognizedSectionCount > 0) {
1912
+ warnings.push(`Hay ${unrecognizedSectionCount} seccion(es) con reglas que no coinciden con la taxonomia sugerida.`);
1913
+ }
1378
1914
  const sanitizedContent = nonEmptySections.map((section) => {
1379
1915
  const rules = section.rules.map((rule) => `- ${rule}`).join("\n");
1380
1916
  return `## ${section.title}
@@ -1396,7 +1932,12 @@ ${rules}`;
1396
1932
  ruleCount,
1397
1933
  sectionCount: nonEmptySections.length,
1398
1934
  recognizedSectionCount,
1935
+ unrecognizedSectionCount,
1399
1936
  ignoredExampleCount,
1937
+ duplicateRuleCount,
1938
+ lastModifiedAt: stat.mtime.toISOString(),
1939
+ fingerprint: `${stat.size}:${stat.mtimeMs}`,
1940
+ governance,
1400
1941
  warnings,
1401
1942
  status,
1402
1943
  isValid: ruleCount > 0
@@ -1414,10 +1955,28 @@ function formatQaContextSummary(analysis) {
1414
1955
  lines.push("QA Context Summary");
1415
1956
  lines.push(`Status: ${analysis.status}`);
1416
1957
  lines.push(`Path: ${analysis.filePath}`);
1958
+ lines.push(`Priority: local qa-context > backend collective memory > base heuristics`);
1417
1959
  lines.push(`Rules loaded: ${analysis.ruleCount}`);
1418
1960
  lines.push(`Sections with rules: ${analysis.sectionCount}`);
1419
1961
  lines.push(`Recognized sections: ${analysis.recognizedSectionCount}`);
1962
+ lines.push(`Unrecognized sections: ${analysis.unrecognizedSectionCount}`);
1420
1963
  lines.push(`Ignored examples/placeholders: ${analysis.ignoredExampleCount}`);
1964
+ lines.push(`Duplicate rules removed: ${analysis.duplicateRuleCount}`);
1965
+ lines.push(`Last modified: ${analysis.lastModifiedAt || "N/A"}`);
1966
+ lines.push(`Fingerprint: ${analysis.fingerprint || "N/A"}`);
1967
+ lines.push(`Governance metadata: ${analysis.governance.exists ? analysis.governance.isValid ? "valid" : "warning" : "missing"}`);
1968
+ lines.push(`Governance enforcement: ${analysis.governance.enforcementMode}`);
1969
+ if (analysis.governance.exists) {
1970
+ lines.push("");
1971
+ lines.push("Governance metadata:");
1972
+ lines.push(`- Version: ${analysis.governance.version || "N/A"}`);
1973
+ lines.push(`- Updated by: ${analysis.governance.updatedBy || "N/A"}`);
1974
+ lines.push(`- Approved by: ${analysis.governance.approvedBy || "N/A"}`);
1975
+ lines.push(`- Owner team: ${analysis.governance.ownerTeam || "N/A"}`);
1976
+ lines.push(`- Effective from: ${analysis.governance.effectiveFrom || "N/A"}`);
1977
+ lines.push(`- Change summary: ${analysis.governance.changeSummary || "N/A"}`);
1978
+ lines.push(`- Tags: ${analysis.governance.tags.length > 0 ? analysis.governance.tags.join(", ") : "N/A"}`);
1979
+ }
1421
1980
  if (analysis.sections.length > 0) {
1422
1981
  lines.push("");
1423
1982
  lines.push("Sections applied:");
@@ -1432,36 +1991,106 @@ function formatQaContextSummary(analysis) {
1432
1991
  lines.push(`- ${warning}`);
1433
1992
  }
1434
1993
  }
1994
+ const governanceIssues = getQaContextGovernanceIssues(analysis);
1995
+ if (governanceIssues.length > 0) {
1996
+ lines.push("");
1997
+ lines.push("Governance issues:");
1998
+ for (const issue of governanceIssues) {
1999
+ lines.push(`- ${issue}`);
2000
+ }
2001
+ }
1435
2002
  if (!analysis.exists) {
1436
2003
  lines.push("");
1437
2004
  lines.push("No local QA Context file was found for this mission.");
1438
2005
  }
1439
2006
  return lines.join("\n");
1440
2007
  }
2008
+ function buildEffectiveContextBlock(params) {
2009
+ const { localAnalysis, localPromptBlock, backendContext } = params;
2010
+ const backendRules = backendContext?.rules || [];
2011
+ const backendFields = backendContext?.fields || [];
2012
+ const blocks = [];
2013
+ if (localPromptBlock) {
2014
+ const governanceLines = [
2015
+ `Version: ${localAnalysis.governance.version || "N/A"}`,
2016
+ `Updated by: ${localAnalysis.governance.updatedBy || "N/A"}`,
2017
+ `Approved by: ${localAnalysis.governance.approvedBy || "N/A"}`,
2018
+ `Owner team: ${localAnalysis.governance.ownerTeam || "N/A"}`,
2019
+ `Change summary: ${localAnalysis.governance.changeSummary || "N/A"}`
2020
+ ].join("\n");
2021
+ blocks.push(
2022
+ `<LOCAL_QA_CONTEXT>
2023
+ Priority: Highest.
2024
+ If any local rule conflicts with backend collective memory, follow the local rule.
2025
+ Governance metadata:
2026
+ ${governanceLines}
2027
+ ${localPromptBlock}
2028
+ </LOCAL_QA_CONTEXT>`
2029
+ );
2030
+ }
2031
+ if (backendRules.length > 0 || backendFields.length > 0) {
2032
+ const backendLines = [];
2033
+ backendLines.push(`<BACKEND_COLLECTIVE_MEMORY>`);
2034
+ backendLines.push(`Priority: Secondary. Use this when local qa-context does not define the rule explicitly.`);
2035
+ if (backendRules.length > 0) {
2036
+ backendLines.push(`Rules:`);
2037
+ for (const rule of backendRules) {
2038
+ backendLines.push(`- [${rule.severity}] ${rule.title}: ${rule.description}`);
2039
+ }
2040
+ }
2041
+ if (backendFields.length > 0) {
2042
+ backendLines.push(`Known fields:`);
2043
+ for (const field of backendFields) {
2044
+ backendLines.push(`- ${field.field_identifier} (${field.field_type}) - Required: ${field.is_required}`);
2045
+ }
2046
+ }
2047
+ backendLines.push(`</BACKEND_COLLECTIVE_MEMORY>`);
2048
+ blocks.push(backendLines.join("\n"));
2049
+ }
2050
+ if (blocks.length === 0)
2051
+ return "";
2052
+ return `
2053
+ <CUSTOMER_CONTEXT_POLICY>
2054
+ Use customer context with this precedence order:
2055
+ 1. Local qa-context file
2056
+ 2. Backend collective memory
2057
+ 3. Base QA heuristics
2058
+ Never ignore a valid local qa-context rule because of a conflicting backend memory entry.
2059
+ Local context status: ${localAnalysis.status}
2060
+ Local context fingerprint: ${localAnalysis.fingerprint || "N/A"}
2061
+ </CUSTOMER_CONTEXT_POLICY>
2062
+
2063
+ ` + blocks.join("\n\n") + `
2064
+ `;
2065
+ }
1441
2066
 
1442
2067
  // tests/_helpers/ai-agent-helper.ts
1443
2068
  var qaSecurityTools = [
1444
2069
  {
1445
2070
  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.",
2071
+ 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
2072
  parameters: {
1448
2073
  type: "object",
1449
2074
  properties: {
1450
- selector: { type: "string", description: "The CSS selector of the input field to test." }
2075
+ idx: { type: "number", description: "The IDX of the input field from CURRENT COMPONENTS." },
2076
+ payload_type: {
2077
+ enum: ["script", "image", "svg"],
2078
+ description: "Payload variant to inject. Defaults to script.",
2079
+ default: "script"
2080
+ }
1451
2081
  },
1452
- required: ["selector"]
2082
+ required: ["idx"]
1453
2083
  }
1454
2084
  },
1455
2085
  {
1456
2086
  name: "test_auth_bypass",
1457
- description: "Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",
2087
+ description: "QA Security Skill: Attempts to directly navigate to a protected route to check for authentication bypass vulnerabilities.",
1458
2088
  parameters: {
1459
2089
  type: "object",
1460
2090
  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')." }
2091
+ target_url: { type: "string", description: "The protected URL or route to test. Relative paths are resolved against the current origin." }
1463
2092
  },
1464
- required: ["url", "redirectUrl"]
2093
+ required: ["target_url"]
1465
2094
  }
1466
2095
  },
1467
2096
  {
@@ -1490,7 +2119,7 @@ var AIAgentHelper = class {
1490
2119
  qaContextAnalysisCache = null;
1491
2120
  qaContextPromptCache = "";
1492
2121
  qaContextStatusLogged = false;
1493
- constructor(page, contextDir = "out", testInfo, knowledgeService) {
2122
+ constructor(page, contextDir, testInfo, knowledgeService) {
1494
2123
  this.page = page;
1495
2124
  this.contextDir = contextDir;
1496
2125
  this.testInfo = testInfo;
@@ -1551,29 +2180,29 @@ var AIAgentHelper = class {
1551
2180
  try {
1552
2181
  let skillsContext = "";
1553
2182
  let loadedCount = 0;
1554
- const toolsRoot = process.env.ARCALITY_ROOT || path4.join(__dirname, "..", "..");
2183
+ const toolsRoot = process.env.ARCALITY_ROOT || path5.join(__dirname, "..", "..");
1555
2184
  const possibleDirs = [
1556
- path4.join(toolsRoot, ".agent", "skills"),
1557
- path4.join(toolsRoot, ".agents", "skills")
2185
+ path5.join(toolsRoot, ".agent", "skills"),
2186
+ path5.join(toolsRoot, ".agents", "skills")
1558
2187
  ];
1559
2188
  for (const rootDir of possibleDirs) {
1560
- if (!fs4.existsSync(rootDir))
2189
+ if (!fs5.existsSync(rootDir))
1561
2190
  continue;
1562
2191
  if (loadedCount === 0) {
1563
2192
  skillsContext += "\n\u{1F6E0}\uFE0F SKILLS INSTALADAS (Metodolog\xEDas activas):\n";
1564
2193
  }
1565
- const entries = fs4.readdirSync(rootDir, { withFileTypes: true });
2194
+ const entries = fs5.readdirSync(rootDir, { withFileTypes: true });
1566
2195
  for (const entry of entries) {
1567
2196
  let content = "";
1568
2197
  let skillName = entry.name;
1569
2198
  if (entry.isDirectory()) {
1570
- const skillPath = path4.join(rootDir, entry.name, "SKILL.md");
1571
- if (fs4.existsSync(skillPath)) {
1572
- content = fs4.readFileSync(skillPath, "utf8");
2199
+ const skillPath = path5.join(rootDir, entry.name, "SKILL.md");
2200
+ if (fs5.existsSync(skillPath)) {
2201
+ content = fs5.readFileSync(skillPath, "utf8");
1573
2202
  }
1574
2203
  } else if (entry.name.endsWith(".md")) {
1575
- const skillPath = path4.join(rootDir, entry.name);
1576
- content = fs4.readFileSync(skillPath, "utf8");
2204
+ const skillPath = path5.join(rootDir, entry.name);
2205
+ content = fs5.readFileSync(skillPath, "utf8");
1577
2206
  }
1578
2207
  if (content) {
1579
2208
  skillsContext += `
@@ -1596,9 +2225,9 @@ ${content}
1596
2225
  }
1597
2226
  loadMemory(prompt) {
1598
2227
  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"));
2228
+ const memoryFile = path5.join(this.contextDir, "memoria-agente.json");
2229
+ if (fs5.existsSync(memoryFile)) {
2230
+ const memories = JSON.parse(fs5.readFileSync(memoryFile, "utf8"));
1602
2231
  const keywords = prompt.toLowerCase().split(" ").filter((w) => w.length > 4);
1603
2232
  const relevantSuccess = memories.filter((m) => {
1604
2233
  if (!m.success)
@@ -1963,19 +2592,94 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1963
2592
  components: sortedComponents
1964
2593
  };
1965
2594
  }
2595
+ fallbackFailureClassification(failReason, failReasoning = "") {
2596
+ const reason = `${failReason} ${failReasoning}`.toLowerCase();
2597
+ if (reason.includes("network") || reason.includes("backend") || reason.includes("server") || reason.includes("internal_error") || reason.includes("500")) {
2598
+ return "[Backend Bug]";
2599
+ }
2600
+ if (reason.includes("stale") || reason.includes("timeout") || reason.includes("selector") || reason.includes("idx")) {
2601
+ return "[Flaky/Stale Selector]";
2602
+ }
2603
+ if (reason.includes("render") || reason.includes("blank") || reason.includes("ui_validation")) {
2604
+ return "[UI Regression]";
2605
+ }
2606
+ return "[Frontend Bug]";
2607
+ }
2608
+ normalizeFailureClassification(raw) {
2609
+ const text = (raw || "").trim();
2610
+ const allowed = ["[Backend Bug]", "[Frontend Bug]", "[UI Regression]", "[Flaky/Stale Selector]"];
2611
+ return allowed.find((category) => text.includes(category)) || null;
2612
+ }
2613
+ /**
2614
+ * Classifies a terminal QA failure into a normalized bug bucket for reports and ADO.
2615
+ */
2616
+ async classifyFailure(prompt, failReason, failReasoning, history) {
2617
+ const fallback = this.fallbackFailureClassification(failReason, failReasoning);
2618
+ const isProxyMode = !!process.env.ARCALITY_API_URL;
2619
+ if (!isProxyMode && !process.env.ANTHROPIC_API_KEY)
2620
+ return fallback;
2621
+ try {
2622
+ const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
2623
+ const headers = {
2624
+ "Content-Type": "application/json"
2625
+ };
2626
+ if (isProxyMode) {
2627
+ headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
2628
+ const missionId = process.env.ARCALITY_MISSION_ID || "";
2629
+ if (missionId)
2630
+ headers["x-mission-id"] = missionId;
2631
+ } else {
2632
+ headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
2633
+ headers["anthropic-version"] = "2023-06-01";
2634
+ }
2635
+ const diagnosticPayload = {
2636
+ mission: prompt,
2637
+ fail_reason: failReason,
2638
+ fail_reasoning: failReasoning || "None",
2639
+ critical_network_error: this.criticalNetworkError || "None",
2640
+ critical_js_error: this.criticalJsError || "None",
2641
+ console_logs: this.logs.slice(-30),
2642
+ recent_history: history.slice(-12)
2643
+ };
2644
+ const response = await fetch(endpointUrl, {
2645
+ method: "POST",
2646
+ headers,
2647
+ body: JSON.stringify({
2648
+ model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2649
+ max_tokens: 32,
2650
+ 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].",
2651
+ messages: [
2652
+ {
2653
+ role: "user",
2654
+ content: `Classify this Arcality QA failure:
2655
+ ${JSON.stringify(diagnosticPayload, null, 2)}`
2656
+ }
2657
+ ],
2658
+ temperature: 0
2659
+ })
2660
+ });
2661
+ if (!response.ok)
2662
+ return fallback;
2663
+ const data = await response.json();
2664
+ const classification = this.normalizeFailureClassification(data.content?.[0]?.text);
2665
+ return classification || fallback;
2666
+ } catch {
2667
+ return fallback;
2668
+ }
2669
+ }
1966
2670
  /**
1967
2671
  * Busca una acción sugerida en la memoria SIN usar tokens.
1968
2672
  * Compara URL, componentes visuales Y palabras clave del prompt.
1969
2673
  */
1970
2674
  async getActionFromGuia(prompt, historyLength) {
1971
2675
  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)) {
2676
+ const fs7 = require("fs");
2677
+ const path7 = require("path");
2678
+ const memoryFile = path7.join(this.contextDir, "memoria-agente.json");
2679
+ if (!fs7.existsSync(memoryFile)) {
1976
2680
  return null;
1977
2681
  }
1978
- const memories = JSON.parse(fs6.readFileSync(memoryFile, "utf8"));
2682
+ const memories = JSON.parse(fs7.readFileSync(memoryFile, "utf8"));
1979
2683
  const state = await this.getPageState();
1980
2684
  const currentUrl = this.page.url();
1981
2685
  const extractCriticalKeywords = (text) => {
@@ -2044,8 +2748,14 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2044
2748
  let bestMatch = null;
2045
2749
  let bestMatchScore = -1;
2046
2750
  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();
2751
+ let cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2752
+ if (!cleanMemName) {
2753
+ cleanMemName = (memStep.componentName || "").trim().toLowerCase();
2754
+ }
2755
+ let cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
2756
+ if (!cleanCurrName) {
2757
+ cleanCurrName = (c.name || "").trim().toLowerCase();
2758
+ }
2049
2759
  let score = -1;
2050
2760
  if (c.selector === memStep.action_data?.selector) {
2051
2761
  score = 100;
@@ -2215,6 +2925,18 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2215
2925
  parameters: t.input_schema || t.parameters
2216
2926
  }))
2217
2927
  ];
2928
+ const anthropicTools = rawTools.map((t) => ({
2929
+ name: t.name,
2930
+ description: t.description,
2931
+ input_schema: t.parameters || t.input_schema
2932
+ }));
2933
+ if (anthropicTools.length > 0) {
2934
+ const lastTool = anthropicTools[anthropicTools.length - 1];
2935
+ anthropicTools[anthropicTools.length - 1] = {
2936
+ ...lastTool,
2937
+ cache_control: { type: "ephemeral" }
2938
+ };
2939
+ }
2218
2940
  const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
2219
2941
  \u{1F511} QA CREDENTIALS (Use if login is needed):
2220
2942
  - User: ${process.env.LOGIN_USER}
@@ -2222,10 +2944,17 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2222
2944
  ` : "";
2223
2945
  const memoryContext = this.loadMemory(prompt);
2224
2946
  const skillsContext = this.loadSkills();
2947
+ let assetContext = "";
2948
+ try {
2949
+ const { formatUploadAssetContext: formatUploadAssetContext2 } = (init_asset_resolver(), __toCommonJS(asset_resolver_exports));
2950
+ assetContext = formatUploadAssetContext2();
2951
+ } catch {
2952
+ }
2225
2953
  let projectInfo = "";
2226
2954
  try {
2227
2955
  const { getProjectContext } = require("../src/projectInspector");
2228
- const ctx = getProjectContext(process.cwd());
2956
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2957
+ const ctx = getProjectContext(runtimeProjectRoot);
2229
2958
  projectInfo = `
2230
2959
  \u{1F3D7}\uFE0F PROJECT CONTEXT:
2231
2960
  - Name: ${ctx.name}
@@ -2235,51 +2964,22 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
2235
2964
  `;
2236
2965
  } catch {
2237
2966
  }
2238
- let memoryBackendContext = "";
2967
+ let backendContextData = null;
2239
2968
  try {
2240
- const contextData = await this.knowledgeService.getContext(state.url);
2241
- if (contextData && (contextData.rules?.length > 0 || contextData.fields?.length > 0)) {
2242
- memoryBackendContext = `
2243
- \u{1F9E0} BACKEND COLLECTIVE MEMORY FOR THIS PAGE:
2244
- `;
2245
- if (contextData.rules?.length > 0) {
2246
- memoryBackendContext += `BUSINESS RULES:
2247
- `;
2248
- contextData.rules.forEach((r) => memoryBackendContext += `- [${r.severity}] ${r.title}: ${r.description}
2249
- `);
2250
- }
2251
- if (contextData.fields?.length > 0) {
2252
- memoryBackendContext += `KNOWN FIELDS:
2253
- `;
2254
- contextData.fields.forEach((f) => memoryBackendContext += `- ${f.field_identifier} (${f.field_type}) - Required: ${f.is_required}
2255
- `);
2256
- }
2969
+ backendContextData = await this.knowledgeService.getContext(state.url);
2970
+ if (backendContextData && (backendContextData.rules?.length > 0 || backendContextData.fields?.length > 0)) {
2257
2971
  if (process.env.DEBUG)
2258
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${contextData.rules?.length || 0} reglas, ${contextData.fields?.length || 0} campos).`);
2972
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Contexto de Memoria Colectiva inyectado (${backendContextData.rules?.length || 0} reglas, ${backendContextData.fields?.length || 0} campos).`);
2259
2973
  }
2260
2974
  } catch (e) {
2261
2975
  if (process.env.DEBUG)
2262
2976
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error en hook de contexto: ${e.message}`);
2263
2977
  }
2264
2978
  let customUserContext = "";
2265
- try {
2266
- const fs6 = require("fs");
2267
- const path6 = require("path");
2268
- const customContextPath = path6.join(process.cwd(), ".arcality", "qa-context.md");
2269
- if (false) {
2270
- const content = fs6.readFileSync(customContextPath, "utf8");
2271
- customUserContext = `
2272
- <CUSTOMER_BUSINESS_RULES>
2273
- 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:
2274
- ${content}
2275
- </CUSTOMER_BUSINESS_RULES>
2276
- `;
2277
- }
2278
- } catch (e) {
2279
- }
2280
2979
  try {
2281
2980
  if (!this.qaContextAnalysisCache) {
2282
- const qaContextState = loadQaContextForPrompt(process.cwd());
2981
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
2982
+ const qaContextState = loadQaContextForPrompt(runtimeProjectRoot);
2283
2983
  this.qaContextAnalysisCache = qaContextState.analysis;
2284
2984
  this.qaContextPromptCache = qaContextState.promptBlock;
2285
2985
  }
@@ -2295,18 +2995,16 @@ ${content}
2295
2995
  console.log(`>>ARCALITY_STATUS>> QA Context warning: ${warning}`);
2296
2996
  }
2297
2997
  }
2998
+ if (qaContextAnalysis.exists || backendContextData?.rules?.length || backendContextData?.fields?.length) {
2999
+ console.log(`>>ARCALITY_STATUS>> Pol\xEDtica efectiva de contexto: local qa-context > memoria backend > heur\xEDsticas base.`);
3000
+ }
2298
3001
  this.qaContextStatusLogged = true;
2299
3002
  }
2300
- if (this.qaContextPromptCache) {
2301
- customUserContext = `
2302
- <CUSTOMER_BUSINESS_RULES>
2303
- El Ing. de QA o Cliente final ha provisto reglas locales para este negocio/dominio. Debes priorizar esta informacion sobre tus heuristicas base cuando exista conflicto.
2304
- ${this.qaContextPromptCache}
2305
- </CUSTOMER_BUSINESS_RULES>
2306
- `;
2307
- } else {
2308
- customUserContext = "";
2309
- }
3003
+ customUserContext = buildEffectiveContextBlock({
3004
+ localAnalysis: qaContextAnalysis,
3005
+ localPromptBlock: this.qaContextPromptCache,
3006
+ backendContext: backendContextData
3007
+ });
2310
3008
  } catch (e) {
2311
3009
  }
2312
3010
  const systemPromptBlocks = [
@@ -2315,15 +3013,7 @@ ${this.qaContextPromptCache}
2315
3013
  text: `# IDENTITY
2316
3014
  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
3015
 
2318
- # MISSION
2319
- ${prompt}
2320
-
2321
- ${projectInfo}
2322
- ${skillsContext}
2323
- ${customUserContext}
2324
- ${memoryContext}
2325
- ${credentialsContext}
2326
- ${memoryBackendContext}`
3016
+ `
2327
3017
  },
2328
3018
  {
2329
3019
  type: "text",
@@ -2377,6 +3067,7 @@ These are NOT optional. You MUST use these tools in these situations:
2377
3067
  | Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
2378
3068
  | Mission complete | \`create_test_evidence\` | To document the final successful state |
2379
3069
  | Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
3070
+ | Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\xEDgito por d\xEDgito", "car\xE1cter por car\xE1cter" | \`type_sequentially\` | After ONE focus attempt on the field; do not keep clicking |
2380
3071
  | Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \u2014 native inputs need special handling |
2381
3072
  | Element not found on screen | \`scroll_page\` | To reveal off-screen content |
2382
3073
  | Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \u2014 MANDATORY |
@@ -2421,6 +3112,19 @@ These are NOT optional. You MUST use these tools in these situations:
2421
3112
  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
3113
  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
3114
 
3115
+ 15. **MASKED / SEQUENTIAL INPUTS (MANDATORY PROTOCOL)**: If the field is a masked identifier/code input (examples: placeholders like \`000-000-000-0000\`, OTP/PIN boxes, CLABE/account identifiers, ID/INE codes) OR the mission explicitly says "manualmente", "n\xFAmero por n\xFAmero", "d\xEDgito por d\xEDgito", or "car\xE1cter por car\xE1cter":
3116
+ - You get EXACTLY ONE focus attempt: click the field once, or click a nearby keyboard button/icon ONCE if the UI shows one (e.g. "Teclado").
3117
+ - Then you MUST use \`type_sequentially\` with the full string. Do NOT simulate manual typing by repeatedly clicking the field.
3118
+ - If the field still does not accept input after that, use \`inspect_element_details\` on the field or the adjacent keyboard trigger. Never click the same input 3+ times in a row.
3119
+ - For numeric codes, preserve exact length. Count the digits from the mission and type all of them.
3120
+
3121
+ 16. **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"):
3122
+ - 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>\`).
3123
+ - If no asset matches the required type, use the value \`"placeholder:image/banner"\` \u2014 the runner will automatically generate a valid placeholder PNG image.
3124
+ - **NEVER** report a bug or inability to proceed just because you see a file upload field. You ALWAYS have a fallback.
3125
+ - **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\`.
3126
+ - 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]\`.
3127
+
2424
3128
  # WIZARD & MULTI-STEP FORMS (CRITICAL)
2425
3129
  When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 Step 3):
2426
3130
 
@@ -2460,7 +3164,23 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
2460
3164
 
2461
3165
  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
3166
 
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.`
3167
+ 12. **SEQUENTIAL CODE INPUT FALLBACK (CRITICAL)**: If you are on a code/identifier step and the UI shows a field like "Identificador", an adjacent "Teclado" button, or a mask such as \`000-000-000-0000\`, NEVER spend multiple turns trying to focus the field. One click is enough. After that, use \`type_sequentially\` with the complete code string. If the keyboard button exists and the first direct focus did not work, click the keyboard button ONCE and then use \`type_sequentially\`.
3168
+
3169
+ 13. **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.`,
3170
+ cache_control: { type: "ephemeral" }
3171
+ },
3172
+ {
3173
+ type: "text",
3174
+ text: `
3175
+ # MISSION
3176
+ ${prompt}
3177
+
3178
+ ${projectInfo}
3179
+ ${skillsContext}
3180
+ ${customUserContext}
3181
+ ${memoryContext}
3182
+ ${credentialsContext}
3183
+ ${assetContext}`
2464
3184
  }
2465
3185
  ];
2466
3186
  if (this.testInfo) {
@@ -2544,11 +3264,7 @@ ${history.slice(-25).join("\n") || "None"}`
2544
3264
  model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2545
3265
  max_tokens: 4096,
2546
3266
  system: systemPromptBlocks,
2547
- tools: rawTools.map((t) => ({
2548
- name: t.name,
2549
- description: t.description,
2550
- input_schema: t.parameters || t.input_schema
2551
- })),
3267
+ tools: anthropicTools,
2552
3268
  messages: anthropicMessages,
2553
3269
  temperature: 0.2
2554
3270
  })
@@ -2571,7 +3287,7 @@ ${history.slice(-25).join("\n") || "None"}`
2571
3287
  const delay = Math.pow(2, retryCount) * 1e3;
2572
3288
  console.log(`
2573
3289
  >>ARCALITY_STATUS>> \u26A0\uFE0F Arcality est\xE1 saturado (${response.status}). Reintentando...`);
2574
- await new Promise((resolve) => setTimeout(resolve, delay));
3290
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
2575
3291
  continue;
2576
3292
  }
2577
3293
  throw new Error(`Arcality Brain Error ${response.status}: ${errorText}`);
@@ -2886,7 +3602,7 @@ ${history.slice(-25).join("\n") || "None"}`
2886
3602
  }
2887
3603
  }
2888
3604
  } else if (toolName === "test_xss_injection") {
2889
- const { idx, payload_type } = toolInput;
3605
+ const { idx, payload_type = "script" } = toolInput;
2890
3606
  const el = state.components.find((c) => c.idx === idx);
2891
3607
  if (!el) {
2892
3608
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
@@ -2898,7 +3614,8 @@ ${history.slice(-25).join("\n") || "None"}`
2898
3614
  if (payload_type === "svg")
2899
3615
  payload = "<svg onload=alert(1)>";
2900
3616
  try {
2901
- const locator = this.page.locator(`xpath=${el.selector}`).first();
3617
+ const frame = this.page.frames()[el.frameIdx] || this.page.mainFrame();
3618
+ const locator = frame.locator(el.selector).first();
2902
3619
  await locator.fill(payload);
2903
3620
  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
3621
  if (this.testInfo)
@@ -3041,6 +3758,45 @@ ${report}` });
3041
3758
  } catch (e) {
3042
3759
  toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
3043
3760
  }
3761
+ } else if (toolName === "type_sequentially") {
3762
+ const { idx, text, delay_ms = 80, clear_first = true } = toolInput;
3763
+ console.log(`>>ARCALITY_STATUS>> \u2328\uFE0F\u270D\uFE0F Type sequentially IDX:${idx} \u2192 "${text}"`);
3764
+ const c = state.components.find((comp) => comp.idx === idx);
3765
+ if (!c) {
3766
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
3767
+ } else {
3768
+ try {
3769
+ const frame = this.page.frames()[c.frameIdx];
3770
+ const loc = frame.locator(c.selector).first();
3771
+ await loc.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
3772
+ });
3773
+ await loc.focus({ timeout: 3e3 }).catch(async () => {
3774
+ await loc.click({ timeout: 5e3, force: true });
3775
+ });
3776
+ if (clear_first) {
3777
+ await this.page.keyboard.press("Control+a").catch(() => {
3778
+ });
3779
+ await this.page.keyboard.press("Delete").catch(() => {
3780
+ });
3781
+ await this.page.waitForTimeout(100);
3782
+ }
3783
+ await this.page.keyboard.type(String(text), { delay: Math.max(10, Math.min(Number(delay_ms) || 80, 400)) });
3784
+ const finalValue = await loc.evaluate((el) => {
3785
+ if (typeof el.value === "string")
3786
+ return el.value;
3787
+ if (el.isContentEditable)
3788
+ return el.innerText || "";
3789
+ return document.activeElement?.value || "";
3790
+ }).catch(() => "");
3791
+ toolResults.push({
3792
+ type: "tool_result",
3793
+ tool_use_id: toolUse.id,
3794
+ content: `\u2705 Sequential typing completed. Current value: "${finalValue}".`
3795
+ });
3796
+ } catch (e) {
3797
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error typing sequentially: ${e.message}` });
3798
+ }
3799
+ }
3044
3800
  } else if (toolName === "interact_native_control") {
3045
3801
  const { idx, control_type, value } = toolInput;
3046
3802
  const c = state.components.find((comp) => comp.idx === idx);
@@ -3423,9 +4179,10 @@ var SecurityScanner = class {
3423
4179
 
3424
4180
  // tests/_helpers/agentic-runner.spec.ts
3425
4181
  var import_config = require("dotenv/config");
3426
- var fs5 = __toESM(require("fs"));
3427
- var path5 = __toESM(require("path"));
4182
+ var fs6 = __toESM(require("fs"));
4183
+ var path6 = __toESM(require("path"));
3428
4184
  var crypto2 = __toESM(require("crypto"));
4185
+ init_asset_resolver();
3429
4186
  var _sessionRuleCache = /* @__PURE__ */ new Set();
3430
4187
  function captureValidationRule(errorMessage, context) {
3431
4188
  const key = errorMessage.substring(0, 80).toLowerCase().trim();
@@ -3450,16 +4207,43 @@ async function readLocatorText(locator) {
3450
4207
  const innerText = await locator.innerText().catch(() => "");
3451
4208
  return typeof innerText === "string" ? innerText.trim() : "";
3452
4209
  }
4210
+ async function readHiddenPortalResult(page) {
4211
+ const selectors = [
4212
+ "input#result",
4213
+ 'input[name="result"]',
4214
+ 'input[readonly][id*="result" i]',
4215
+ 'input[readonly][name*="result" i]'
4216
+ ];
4217
+ for (const selector of selectors) {
4218
+ try {
4219
+ const locator = page.locator(selector).first();
4220
+ if (!await locator.count().catch(() => 0))
4221
+ continue;
4222
+ const rawValue = await locator.inputValue().catch(async () => await locator.getAttribute("value"));
4223
+ const raw = typeof rawValue === "string" ? rawValue.trim() : "";
4224
+ if (!raw)
4225
+ continue;
4226
+ const parsed = JSON.parse(raw);
4227
+ const status = String(parsed?.status || parsed?.Status || "").trim().toLowerCase();
4228
+ const message = String(parsed?.message || parsed?.Message || parsed?.detail || parsed?.error || "").trim();
4229
+ if (status) {
4230
+ return { status, message, raw };
4231
+ }
4232
+ } catch {
4233
+ }
4234
+ }
4235
+ return null;
4236
+ }
3453
4237
  function writeBatchMissionResult(payload) {
3454
4238
  const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
3455
4239
  if (!runDomainDir)
3456
4240
  return;
3457
4241
  try {
3458
- if (!fs5.existsSync(runDomainDir)) {
3459
- fs5.mkdirSync(runDomainDir, { recursive: true });
4242
+ if (!fs6.existsSync(runDomainDir)) {
4243
+ fs6.mkdirSync(runDomainDir, { recursive: true });
3460
4244
  }
3461
- fs5.writeFileSync(
3462
- path5.join(runDomainDir, "mission-result.json"),
4245
+ fs6.writeFileSync(
4246
+ path6.join(runDomainDir, "mission-result.json"),
3463
4247
  JSON.stringify(payload, null, 2)
3464
4248
  );
3465
4249
  } catch {
@@ -3467,7 +4251,10 @@ function writeBatchMissionResult(payload) {
3467
4251
  }
3468
4252
  (0, import_test.test)("Arcality AI Runner", async ({ page }, testInfo) => {
3469
4253
  import_test.test.setTimeout(12e5);
3470
- const contextDir = process.env.CONTEXT_DIR || "out";
4254
+ const contextDir = process.env.CONTEXT_DIR;
4255
+ if (!contextDir) {
4256
+ throw new Error("Missing CONTEXT_DIR. Arcality must run through the CLI wrapper so outputs stay inside the user project .arcality directory.");
4257
+ }
3471
4258
  const activeConfig = process.env.ACTIVE_CONFIG || "Default";
3472
4259
  if (!process.env.BASE_URL && process.env[`${activeConfig}_URL`])
3473
4260
  process.env.BASE_URL = process.env[`${activeConfig}_URL`];
@@ -3480,16 +4267,22 @@ function writeBatchMissionResult(payload) {
3480
4267
  const history = [];
3481
4268
  const urlHistory = [];
3482
4269
  const stepsData = [];
3483
- const memoryFile = path5.join(contextDir, "memoria-arcality.json");
3484
- if (fs5.existsSync(memoryFile)) {
4270
+ try {
4271
+ await syncAssetsFromBackend();
4272
+ } catch (e) {
4273
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al sincronizar assets: ${e.message}`);
4274
+ }
4275
+ const memoryFile = path6.join(contextDir, "memoria-arcality.json");
4276
+ if (fs6.existsSync(memoryFile)) {
3485
4277
  try {
3486
- fs5.unlinkSync(memoryFile);
4278
+ fs6.unlinkSync(memoryFile);
3487
4279
  } catch {
3488
4280
  }
3489
4281
  }
3490
4282
  const agent = new AIAgentHelper(page, contextDir, testInfo);
3491
4283
  const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
3492
- const qaContextAnalysis = analyzeQaContext(process.cwd());
4284
+ const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
4285
+ const qaContextAnalysis = analyzeQaContext(runtimeProjectRoot);
3493
4286
  if (qaContextAnalysis.exists && qaContextAnalysis.isValid) {
3494
4287
  console.log(`>>ARCALITY_STATUS>> QA Context validado al arranque (${qaContextAnalysis.ruleCount} reglas, ${qaContextAnalysis.sectionCount} secciones).`);
3495
4288
  } else if (qaContextAnalysis.exists) {
@@ -3500,10 +4293,24 @@ function writeBatchMissionResult(payload) {
3500
4293
  } else {
3501
4294
  console.log(`>>ARCALITY_STATUS>> QA Context local no configurado para esta mision.`);
3502
4295
  }
4296
+ if (qaContextAnalysis.governance.exists) {
4297
+ console.log(`>>ARCALITY_STATUS>> QA Context metadata: version=${qaContextAnalysis.governance.version || "N/A"} | updated_by=${qaContextAnalysis.governance.updatedBy || "N/A"} | owner_team=${qaContextAnalysis.governance.ownerTeam || "N/A"}`);
4298
+ }
3503
4299
  await testInfo.attach("qa_context_summary", {
3504
4300
  body: Buffer.from(formatQaContextSummary(qaContextAnalysis), "utf-8"),
3505
4301
  contentType: "text/plain"
3506
4302
  });
4303
+ const governanceIssues = getQaContextGovernanceIssues(qaContextAnalysis);
4304
+ if (governanceIssues.length > 0) {
4305
+ for (const issue of governanceIssues) {
4306
+ console.log(`>>ARCALITY_STATUS>> QA Context governance issue: ${issue}`);
4307
+ }
4308
+ }
4309
+ if (shouldBlockOnQaContextGovernance(qaContextAnalysis)) {
4310
+ throw new Error(
4311
+ `QA Context governance is required for this run (${qaContextAnalysis.governance.enforcementMode}) and is incomplete. Fix .arcality/qa-context.meta.json before continuing.`
4312
+ );
4313
+ }
3507
4314
  const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
3508
4315
  const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|existente|exist|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|conflict|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
3509
4316
  const systemErrorKeywords = /error interno|internal error|intentarlo más tarde|try again later|inténtelo más tarde|servicio no disponible|service unavailable|something went wrong|algo salió mal|ha ocurrido un error inesperado|unexpected error|server error|no se pudo procesar|could not be processed|comuníquese con soporte|contact support/i;
@@ -3522,18 +4329,34 @@ function writeBatchMissionResult(payload) {
3522
4329
  let lastSeenErrorToast = "";
3523
4330
  let accumulatedCost = 0;
3524
4331
  let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
4332
+ let bugClassification = "";
4333
+ const missionSummaryPaths = /* @__PURE__ */ new Set();
4334
+ const updateSavedMissionClassification = () => {
4335
+ if (!bugClassification)
4336
+ return;
4337
+ for (const summaryPath of missionSummaryPaths) {
4338
+ try {
4339
+ if (!fs6.existsSync(summaryPath))
4340
+ continue;
4341
+ const data = JSON.parse(fs6.readFileSync(summaryPath, "utf8"));
4342
+ data.bug_classification = bugClassification;
4343
+ fs6.writeFileSync(summaryPath, JSON.stringify(data, null, 2));
4344
+ } catch {
4345
+ }
4346
+ }
4347
+ };
3525
4348
  const saveMissionResults = () => {
3526
4349
  if (resultsSaved)
3527
4350
  return;
3528
4351
  resultsSaved = true;
3529
- if (!fs5.existsSync(contextDir))
3530
- fs5.mkdirSync(contextDir, { recursive: true });
4352
+ if (!fs6.existsSync(contextDir))
4353
+ fs6.mkdirSync(contextDir, { recursive: true });
3531
4354
  const finalSuccess = aiMarkedSuccess && !hasCriticalError;
3532
4355
  try {
3533
- const memoryFile2 = path5.join(contextDir, "memoria-arcality.json");
4356
+ const memoryFile2 = path6.join(contextDir, "memoria-arcality.json");
3534
4357
  let memories = [];
3535
- if (fs5.existsSync(memoryFile2)) {
3536
- const content = fs5.readFileSync(memoryFile2, "utf8").trim();
4358
+ if (fs6.existsSync(memoryFile2)) {
4359
+ const content = fs6.readFileSync(memoryFile2, "utf8").trim();
3537
4360
  if (content) {
3538
4361
  try {
3539
4362
  memories = JSON.parse(content);
@@ -3604,10 +4427,11 @@ function writeBatchMissionResult(payload) {
3604
4427
  error: hasCriticalError,
3605
4428
  fail_reason: failReason,
3606
4429
  fail_reasoning: failReasoning,
4430
+ bug_classification: bugClassification || null,
3607
4431
  timestamp: Date.now()
3608
4432
  };
3609
4433
  memories.push(missionData);
3610
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4434
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3611
4435
  if (process.env.DEBUG)
3612
4436
  console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${cleanSteps.length} pasos limpios (${rawSteps.length - cleanSteps.length} corruptos filtrados)`);
3613
4437
  } catch (err) {
@@ -3624,22 +4448,29 @@ function writeBatchMissionResult(payload) {
3624
4448
  error: hasCriticalError,
3625
4449
  fail_reason: failReason,
3626
4450
  fail_reasoning: failReasoning,
4451
+ bug_classification: bugClassification || null,
3627
4452
  usage: totalMissionUsage,
3628
4453
  target: process.env.BASE_URL || "",
3629
4454
  target_path: process.env.TARGET_PATH || "",
3630
4455
  report_dir: process.env.REPORTS_DIR || "",
3631
4456
  timestamp: timestampIso
3632
4457
  };
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
- );
4458
+ const logPath = path6.join(contextDir, `agent-log-${timestampMs}.json`);
4459
+ fs6.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
4460
+ missionSummaryPaths.add(logPath);
4461
+ const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
4462
+ if (!runDomainDir) {
4463
+ throw new Error("Missing ARCALITY_RUN_DOMAIN_DIR. Arcality must run through the CLI wrapper so durable logs stay inside the user project .arcality directory.");
4464
+ }
4465
+ const durableLogsDir = path6.join(runDomainDir, "logs");
4466
+ fs6.mkdirSync(durableLogsDir, { recursive: true });
4467
+ const durableLogPath = path6.join(durableLogsDir, `mission-log-${timestampMs}.json`);
4468
+ fs6.writeFileSync(durableLogPath, JSON.stringify(summaryPayload, null, 2));
4469
+ missionSummaryPaths.add(durableLogPath);
3642
4470
  writeBatchMissionResult(summaryPayload);
4471
+ if (runDomainDir) {
4472
+ missionSummaryPaths.add(path6.join(runDomainDir, "mission-result.json"));
4473
+ }
3643
4474
  try {
3644
4475
  const historyLine = JSON.stringify({
3645
4476
  ts: timestampIso,
@@ -3649,12 +4480,12 @@ function writeBatchMissionResult(payload) {
3649
4480
  fail_reason: failReason,
3650
4481
  report_dir: process.env.REPORTS_DIR || ""
3651
4482
  }) + "\n";
3652
- fs5.appendFileSync(
3653
- path5.join(contextDir, "arcality-history.log"),
4483
+ fs6.appendFileSync(
4484
+ path6.join(contextDir, "arcality-history.log"),
3654
4485
  historyLine
3655
4486
  );
3656
- fs5.appendFileSync(
3657
- path5.join(durableLogsDir, "arcality-history.log"),
4487
+ fs6.appendFileSync(
4488
+ path6.join(durableLogsDir, "arcality-history.log"),
3658
4489
  historyLine
3659
4490
  );
3660
4491
  } catch (e) {
@@ -3667,9 +4498,11 @@ function writeBatchMissionResult(payload) {
3667
4498
  const pid = process.env.ARCALITY_PROJECT_ID || "";
3668
4499
  const apiUrl = process.env.ARCALITY_API_URL || "";
3669
4500
  const apiKey = process.env.ARCALITY_API_KEY || "";
3670
- if (!pid || !apiUrl || !apiKey) {
4501
+ const orgId = process.env.ARCALITY_ORG_ID || "";
4502
+ if (!pid || !apiUrl || !apiKey || !orgId) {
4503
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva omitida por configuraci\xF3n incompleta.`);
3671
4504
  if (process.env.DEBUG)
3672
- console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}' \u2192 NO persiste.`);
4505
+ console.warn(`[CollectiveMemory] \u26A0\uFE0F Vars faltantes \u2014 project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? "OK" : "MISSING"}' org_id='${orgId || "MISSING"}' \u2192 NO persiste.`);
3673
4506
  return;
3674
4507
  }
3675
4508
  if (collectiveMemoryPersisted && !isFailed) {
@@ -3816,8 +4649,7 @@ function writeBatchMissionResult(payload) {
3816
4649
  const matches = promptKeywords.filter((kw) => patternKeywords.some((pkw) => pkw.includes(kw) || kw.includes(pkw))).length;
3817
4650
  const isVectorMatch = true;
3818
4651
  if (isVectorMatch) {
3819
- if (process.env.DEBUG)
3820
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
4652
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
3821
4653
  reusedPatternId = bestMatch.id || "unknown";
3822
4654
  let patternJsonObj = {};
3823
4655
  try {
@@ -3849,11 +4681,11 @@ function writeBatchMissionResult(payload) {
3849
4681
  timestamp: Date.now()
3850
4682
  };
3851
4683
  try {
3852
- const memoryFile2 = path5.join(contextDir, "memoria-agente.json");
4684
+ const memoryFile2 = path6.join(contextDir, "memoria-agente.json");
3853
4685
  let memories = [];
3854
- if (fs5.existsSync(memoryFile2)) {
4686
+ if (fs6.existsSync(memoryFile2)) {
3855
4687
  try {
3856
- memories = JSON.parse(fs5.readFileSync(memoryFile2, "utf8"));
4688
+ memories = JSON.parse(fs6.readFileSync(memoryFile2, "utf8"));
3857
4689
  } catch {
3858
4690
  memories = [];
3859
4691
  }
@@ -3865,14 +4697,14 @@ function writeBatchMissionResult(payload) {
3865
4697
  if (existingIdx < 0) {
3866
4698
  if (resolvedStepsData.length > 0) {
3867
4699
  memories.push(localMemoryData);
3868
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4700
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3869
4701
  console.log(`>>ARCALITY_STATUS>> \u2728 Memoria sincronizada en memoria-agente.json (${resolvedStepsData.length} pasos). Arcality entrar\xE1 en MODO GU\xCDA.`);
3870
4702
  } else {
3871
4703
  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
4704
  }
3873
4705
  } else if (!existingHasSteps && resolvedStepsData.length > 0) {
3874
4706
  memories[existingIdx] = localMemoryData;
3875
- fs5.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
4707
+ fs6.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3876
4708
  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
4709
  } else {
3878
4710
  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.`);
@@ -4054,6 +4886,35 @@ Detalles de la excepci\xF3n en consola: ${jsError}`;
4054
4886
  }
4055
4887
  if (isFinished)
4056
4888
  break;
4889
+ try {
4890
+ const hiddenResult = await readHiddenPortalResult(page);
4891
+ if (hiddenResult) {
4892
+ const hiddenStatus = hiddenResult.status.toLowerCase();
4893
+ const hiddenMessage = hiddenResult.message || hiddenResult.raw;
4894
+ if (/^(failed|error|invalid|rejected)$/.test(hiddenStatus)) {
4895
+ console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Resultado oculto detectado: "${hiddenStatus}" - ${hiddenMessage.substring(0, 120)}`);
4896
+ hasCriticalError = true;
4897
+ failReason = "portal_hidden_result_error";
4898
+ failReasoning = `El portal public\xF3 un resultado interno oculto indicando fallo: "${hiddenMessage}". Arcality detuvo la misi\xF3n para evitar loops sobre una operaci\xF3n ya rechazada.`;
4899
+ aiMarkedSuccess = false;
4900
+ isFinished = true;
4901
+ history.push(`Turno ${stepCount}: \u{1F6D1} [HIDDEN_RESULT_ERROR] El portal devolvi\xF3 status="${hiddenStatus}" con mensaje "${hiddenMessage.substring(0, 200)}".`);
4902
+ saveMissionResults();
4903
+ break;
4904
+ }
4905
+ if (/^(success|succeeded|ok|completed)$/.test(hiddenStatus)) {
4906
+ console.log(`>>ARCALITY_STATUS>> \u2728 Resultado oculto de \xE9xito detectado: "${hiddenStatus}"`);
4907
+ aiMarkedSuccess = true;
4908
+ hasCriticalError = false;
4909
+ isFinished = true;
4910
+ history.push(`Turno ${stepCount}: \u2705 [HIDDEN_RESULT_SUCCESS] El portal devolvi\xF3 status="${hiddenStatus}".`);
4911
+ await persistCollectiveMemory();
4912
+ saveMissionResults();
4913
+ break;
4914
+ }
4915
+ }
4916
+ } catch {
4917
+ }
4057
4918
  let hasVisibleError = false;
4058
4919
  try {
4059
4920
  const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message, .mat-snack-bar-container, [class*="alert"], [class*="toast"]').all();
@@ -4142,7 +5003,16 @@ ${patternContext}` : prompt;
4142
5003
  saveMissionResults();
4143
5004
  break;
4144
5005
  } else {
4145
- throw err;
5006
+ const errMsg = err?.message || String(err);
5007
+ console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Error terminal del motor IA: ${errMsg}`);
5008
+ aiMarkedSuccess = false;
5009
+ hasCriticalError = true;
5010
+ failReason = "agent_runtime_error";
5011
+ failReasoning = errMsg;
5012
+ isFinished = true;
5013
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AGENT RUNTIME ERROR] ${errMsg}`);
5014
+ saveMissionResults();
5015
+ break;
4146
5016
  }
4147
5017
  }
4148
5018
  if (response.usage) {
@@ -4198,7 +5068,7 @@ ${patternContext}` : prompt;
4198
5068
  const loopType = isClickLoop ? "click" : "fill";
4199
5069
  const uniqueTargets = isClickLoop ? uniqueClicks : uniqueFills;
4200
5070
  console.warn(`
4201
- \u26A0 [OUROBOROS SUBSYSTEM: Repetitive ${loopType} loop anomaly detected]:`);
5071
+ \u26A0 Bucle repetitivo de ${loopType} detectado:`);
4202
5072
  console.warn(` \xDAltimas acciones: ${lastActions.join(" \u2192 ")}`);
4203
5073
  response.thought = `\u{1F6D1} ERROR: Bucle repetitivo en ${loopType} sobre: ${Array.from(uniqueTargets).join(", ")}.`;
4204
5074
  response.finish = true;
@@ -4216,7 +5086,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4216
5086
  const lastSixHistory = history.slice(-6);
4217
5087
  const consecutiveWaits = lastSixHistory.filter((h) => h.includes('wait en "')).length;
4218
5088
  if (consecutiveWaits >= 3) {
4219
- const blankEvidencePath = path5.join(contextDir, `portal-blank-state-${Date.now()}.png`);
5089
+ const blankEvidencePath = path6.join(contextDir, `portal-blank-state-${Date.now()}.png`);
4220
5090
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
4221
5091
  });
4222
5092
  const blankUrl = page.url();
@@ -4315,7 +5185,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4315
5185
  }
4316
5186
  console.log(`
4317
5187
  \u{1F6D1} [AUTO-STOP] Bloqueando click repetido en "${desc}" (${sameActionCount} veces). Forzando re-evaluaci\xF3n.`);
4318
- history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Si es un dropdown/combobox que no abre sus opciones, usa send_keyboard_event con key "ArrowDown" para navegar opciones y "Enter" para seleccionar. NO vuelvas a hacer click en el mismo elemento.`);
5188
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Si es un dropdown/combobox que no abre sus opciones, usa send_keyboard_event con key "ArrowDown" y luego "Enter". Si es un input enmascarado o un c\xF3digo/identificador, usa type_sequentially con el valor completo despu\xE9s de UN solo enfoque. NO vuelvas a hacer click en el mismo elemento.`);
4319
5189
  await page.waitForTimeout(800);
4320
5190
  break;
4321
5191
  }
@@ -4431,12 +5301,17 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4431
5301
  } else {
4432
5302
  await page.waitForTimeout(500);
4433
5303
  }
4434
- } else if (step.action === "fill" && loc && step.value) {
5304
+ } else if (step.action === "fill" && loc) {
4435
5305
  const inputType = await loc.evaluate((el) => el.tagName === "INPUT" ? (el.type || "").toLowerCase() : null).catch(() => null);
4436
5306
  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);
5307
+ const rawUploadValue = String(step.value || "placeholder:image/banner");
5308
+ console.log(` \u{1F4C2} Detectado input de archivo. Subiendo: "${rawUploadValue}"`);
5309
+ const fileVal = rawUploadValue.replace(/^"|"$/g, "").trim();
5310
+ const desc = step.description || "file upload";
5311
+ const resolvedAsset = resolveUploadAsset(fileVal, desc);
5312
+ await loc.setInputFiles(resolvedAsset);
5313
+ } else if (!step.value) {
5314
+ console.log(` \u26A0\uFE0F Acci\xF3n fill sin valor para un campo no-file. Se omite para evitar corrupci\xF3n de estado.`);
4440
5315
  } else if (inputType === "date" || inputType === "time" || inputType === "datetime-local") {
4441
5316
  console.log(` \u{1F39B}\uFE0F Detectado input nativo (${inputType}). Usando estrategia segura para: "${step.value}"`);
4442
5317
  try {
@@ -4488,7 +5363,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4488
5363
  const panelCount = Math.min(panels.length, 6);
4489
5364
  if (panelCount >= 2 && emptyPanelCount >= panelCount * 0.6) {
4490
5365
  const blankUrl = page.url();
4491
- const blankEvidencePath = path5.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
5366
+ const blankEvidencePath = path6.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
4492
5367
  await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
4493
5368
  });
4494
5369
  console.error(`
@@ -4772,8 +5647,8 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
4772
5647
  console.log("\u{1F916} [SMART-YAML] Generating mission card...");
4773
5648
  const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
4774
5649
  if (smartYaml) {
4775
- const yamlPath = path5.join(contextDir, "last-mission-smart.yaml");
4776
- fs5.writeFileSync(yamlPath, smartYaml);
5650
+ const yamlPath = path6.join(contextDir, "last-mission-smart.yaml");
5651
+ fs6.writeFileSync(yamlPath, smartYaml);
4777
5652
  console.log(`>>ARCALITY_STATUS>> \u{1F4DD} Smart Mission Card generated.`);
4778
5653
  await testInfo.attach("smart_mission_card", {
4779
5654
  body: Buffer.from(smartYaml, "utf-8"),
@@ -4827,12 +5702,39 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
4827
5702
  if (!aiMarkedSuccess) {
4828
5703
  if (process.env.DEBUG)
4829
5704
  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}`);
5705
+ bugClassification = await agent.classifyFailure(prompt, failReason || "unknown_failure", failReasoning, history);
5706
+ console.log(`>>ARCALITY_STATUS>> Bug classification: ${bugClassification}`);
5707
+ updateSavedMissionClassification();
5708
+ await testInfo.attach("bug_classification", {
5709
+ body: Buffer.from(bugClassification, "utf-8"),
5710
+ contentType: "text/plain"
5711
+ });
5712
+ try {
5713
+ const { createAdoTask: createAdoTask2 } = await Promise.resolve().then(() => (init_arcalityClient(), arcalityClient_exports));
5714
+ const shortPrompt = prompt.length > 90 ? prompt.substring(0, 87).trimEnd() + "..." : prompt;
5715
+ const adoTitle = `[Arcality QA] ${shortPrompt}`;
5716
+ const adoDescription = [
5717
+ `**Bug detectado automaticamente por Arcality QA**`,
5718
+ ``,
5719
+ `**Clasificacion:** ${bugClassification}`,
5720
+ `**Fail reason:** ${failReason || "unknown_failure"}`,
5721
+ failReasoning ? `**Analisis:** ${failReasoning}` : `**Analisis:** No disponible.`,
5722
+ `**Mision:** ${prompt}`,
5723
+ `**Historial reciente:**`,
5724
+ history.slice(-8).map((h) => `- ${h}`).join("\n"),
5725
+ `**Fecha:** ${(/* @__PURE__ */ new Date()).toISOString()}`
5726
+ ].join("\n");
5727
+ await createAdoTask2(adoTitle, adoDescription, { classification: bugClassification }).catch(() => null);
5728
+ } catch {
5729
+ }
4830
5730
  await persistCollectiveMemory(true).catch(() => {
4831
5731
  });
4832
5732
  let finalErrorMsg = `Misi\xF3n no completada o finalizada con errores.
4833
5733
 
4834
5734
  `;
4835
5735
  finalErrorMsg += `======================================================
5736
+ `;
5737
+ finalErrorMsg += `Bug classification: ${bugClassification}
4836
5738
  `;
4837
5739
  finalErrorMsg += `\u{1F3AF} TIPO DE FALLO: ${failReason}
4838
5740
  `;
@@ -4848,15 +5750,15 @@ ${failReasoning}
4848
5750
  if (process.env.DEBUG)
4849
5751
  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
5752
  }
4851
- if (fs5.existsSync(contextDir)) {
5753
+ if (fs6.existsSync(contextDir)) {
4852
5754
  try {
4853
- const files = fs5.readdirSync(contextDir);
5755
+ const files = fs6.readdirSync(contextDir);
4854
5756
  for (const file of files) {
4855
5757
  if (file.endsWith(".json") || file.endsWith(".png")) {
4856
- fs5.unlinkSync(path5.join(contextDir, file));
5758
+ fs6.unlinkSync(path6.join(contextDir, file));
4857
5759
  }
4858
5760
  }
4859
- fs5.rmSync(contextDir, { recursive: true, force: true });
5761
+ fs6.rmSync(contextDir, { recursive: true, force: true });
4860
5762
  console.log(`>>ARCALITY_STATUS>> \u{1F9F9} Carpeta context eliminada (Memoria ef\xEDmera completada).`);
4861
5763
  } catch (err) {
4862
5764
  console.warn(`No se pudo limpiar la carpeta context: ${err}`);