@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.
- package/README.md +75 -6
- package/bin/arcality.mjs +26 -17
- package/package.json +2 -1
- package/playwright.config.js +22 -21
- package/scripts/gen-and-run.mjs +2610 -2591
- package/scripts/generate.mjs +215 -0
- package/scripts/init.mjs +278 -253
- package/scripts/rebrand-report.mjs +19 -18
- package/src/KnowledgeService.ts +29 -29
- package/src/arcalityClient.mjs +31 -18
- package/src/configLoader.mjs +35 -35
- package/src/configManager.mjs +57 -51
- package/src/services/codebaseAnalyzer.mjs +59 -0
- package/src/services/generatedMissionSchema.mjs +76 -0
- package/src/services/generatedMissionStore.mjs +117 -0
- package/src/services/generationContext.mjs +242 -0
- package/src/services/missionGenerator.mjs +328 -0
- package/src/services/routeDiscovery.mjs +747 -0
- package/src/testRunner.ts +2 -1
- package/tests/_helpers/ArcalityReporter.js +15 -0
- package/tests/_helpers/agentic-runner.bundle.spec.js +1053 -151
|
@@ -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
|
-
|
|
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
|
|
776
|
-
var
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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: ["
|
|
2082
|
+
required: ["idx"]
|
|
1453
2083
|
}
|
|
1454
2084
|
},
|
|
1455
2085
|
{
|
|
1456
2086
|
name: "test_auth_bypass",
|
|
1457
|
-
description: "Attempts to directly navigate to a
|
|
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
|
-
|
|
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: ["
|
|
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
|
|
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 ||
|
|
2183
|
+
const toolsRoot = process.env.ARCALITY_ROOT || path5.join(__dirname, "..", "..");
|
|
1555
2184
|
const possibleDirs = [
|
|
1556
|
-
|
|
1557
|
-
|
|
2185
|
+
path5.join(toolsRoot, ".agent", "skills"),
|
|
2186
|
+
path5.join(toolsRoot, ".agents", "skills")
|
|
1558
2187
|
];
|
|
1559
2188
|
for (const rootDir of possibleDirs) {
|
|
1560
|
-
if (!
|
|
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 =
|
|
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 =
|
|
1571
|
-
if (
|
|
1572
|
-
content =
|
|
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 =
|
|
1576
|
-
content =
|
|
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 =
|
|
1600
|
-
if (
|
|
1601
|
-
const memories = JSON.parse(
|
|
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
|
|
1973
|
-
const
|
|
1974
|
-
const memoryFile =
|
|
1975
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
2048
|
-
|
|
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
|
|
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
|
|
2967
|
+
let backendContextData = null;
|
|
2239
2968
|
try {
|
|
2240
|
-
|
|
2241
|
-
if (
|
|
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 (${
|
|
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
|
|
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
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
-
|
|
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. **
|
|
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:
|
|
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((
|
|
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
|
|
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
|
|
3427
|
-
var
|
|
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 (!
|
|
3459
|
-
|
|
4242
|
+
if (!fs6.existsSync(runDomainDir)) {
|
|
4243
|
+
fs6.mkdirSync(runDomainDir, { recursive: true });
|
|
3460
4244
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
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
|
|
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
|
-
|
|
3484
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
3530
|
-
|
|
4352
|
+
if (!fs6.existsSync(contextDir))
|
|
4353
|
+
fs6.mkdirSync(contextDir, { recursive: true });
|
|
3531
4354
|
const finalSuccess = aiMarkedSuccess && !hasCriticalError;
|
|
3532
4355
|
try {
|
|
3533
|
-
const memoryFile2 =
|
|
4356
|
+
const memoryFile2 = path6.join(contextDir, "memoria-arcality.json");
|
|
3534
4357
|
let memories = [];
|
|
3535
|
-
if (
|
|
3536
|
-
const content =
|
|
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
|
-
|
|
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 =
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
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
|
-
|
|
3653
|
-
|
|
4483
|
+
fs6.appendFileSync(
|
|
4484
|
+
path6.join(contextDir, "arcality-history.log"),
|
|
3654
4485
|
historyLine
|
|
3655
4486
|
);
|
|
3656
|
-
|
|
3657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
4684
|
+
const memoryFile2 = path6.join(contextDir, "memoria-agente.json");
|
|
3853
4685
|
let memories = [];
|
|
3854
|
-
if (
|
|
4686
|
+
if (fs6.existsSync(memoryFile2)) {
|
|
3855
4687
|
try {
|
|
3856
|
-
memories = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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"
|
|
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
|
|
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
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
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 =
|
|
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 =
|
|
4776
|
-
|
|
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 (
|
|
5753
|
+
if (fs6.existsSync(contextDir)) {
|
|
4852
5754
|
try {
|
|
4853
|
-
const files =
|
|
5755
|
+
const files = fs6.readdirSync(contextDir);
|
|
4854
5756
|
for (const file of files) {
|
|
4855
5757
|
if (file.endsWith(".json") || file.endsWith(".png")) {
|
|
4856
|
-
|
|
5758
|
+
fs6.unlinkSync(path6.join(contextDir, file));
|
|
4857
5759
|
}
|
|
4858
5760
|
}
|
|
4859
|
-
|
|
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}`);
|