@arcadialdev/arcality 4.0.2 → 4.1.1
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/.agents/skills/db-validation-evidence/SKILL.md +43 -0
- package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
- package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
- package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
- package/.agents/skills/form-expert/SKILL.md +98 -0
- package/.agents/skills/investigation-protocol/SKILL.md +56 -0
- package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
- package/.agents/skills/modal-master/SKILL.md +46 -0
- package/.agents/skills/native-control-expert/SKILL.md +74 -0
- package/.agents/skills/qa-context-governance/SKILL.md +23 -0
- package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
- package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
- package/README.md +103 -163
- package/bin/arcality.mjs +25 -25
- package/package.json +75 -75
- package/scripts/edit-config.mjs +843 -0
- package/scripts/gen-and-run.mjs +2705 -2609
- package/scripts/generate.mjs +236 -236
- package/scripts/init.mjs +47 -47
- package/src/configManager.mjs +13 -13
- package/src/envSetup.ts +229 -205
- package/src/services/codebaseAnalyzer.mjs +59 -59
- package/src/services/databaseValidationService.mjs +598 -0
- package/src/services/executionEvidenceService.mjs +124 -0
- package/src/services/generatedMissionSchema.mjs +76 -76
- package/src/services/generatedMissionStore.mjs +117 -117
- package/src/services/generationContext.mjs +242 -242
- package/src/services/mcpStdioClient.mjs +204 -0
- package/src/services/missionGenerator.mjs +329 -329
- package/src/services/routeDiscovery.mjs +762 -762
- package/tests/_helpers/ArcalityReporter.js +1342 -1255
- package/tests/_helpers/agentic-runner.bundle.spec.js +1363 -245
- package/.agent/skills/form-expert.md +0 -102
- package/.agent/skills/investigation-protocol.md +0 -61
- package/.agent/skills/modal-master.md +0 -41
- package/.agent/skills/native-control-expert.md +0 -82
|
@@ -39,7 +39,7 @@ __export(asset_resolver_exports, {
|
|
|
39
39
|
syncAssetsFromBackend: () => syncAssetsFromBackend
|
|
40
40
|
});
|
|
41
41
|
function readJsonFile(filePath) {
|
|
42
|
-
const raw =
|
|
42
|
+
const raw = fs3.readFileSync(filePath, "utf8").trim();
|
|
43
43
|
if (!raw)
|
|
44
44
|
return null;
|
|
45
45
|
return JSON.parse(raw);
|
|
@@ -48,7 +48,7 @@ function sanitizeIdPart(value) {
|
|
|
48
48
|
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80);
|
|
49
49
|
}
|
|
50
50
|
function normalizeMimeFromPath(filePath) {
|
|
51
|
-
const ext =
|
|
51
|
+
const ext = path4.extname(filePath || "").toLowerCase();
|
|
52
52
|
if (ext === ".jpg" || ext === ".jpeg")
|
|
53
53
|
return "image/jpeg";
|
|
54
54
|
if (ext === ".webp")
|
|
@@ -72,16 +72,16 @@ function inferAssetType(...values) {
|
|
|
72
72
|
function normalizeAsset(raw, assetsDir = null) {
|
|
73
73
|
const assetId = String(raw.asset_id || raw.assetId || raw.id || "").trim();
|
|
74
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 ?
|
|
76
|
-
const derivedId = filename ? `asset_${sanitizeIdPart(
|
|
75
|
+
const filename = String(raw.filename || raw.fileName || (rawPath ? path4.basename(rawPath) : "") || "").trim();
|
|
76
|
+
const derivedId = filename ? `asset_${sanitizeIdPart(path4.basename(filename, path4.extname(filename)))}` : "";
|
|
77
77
|
const finalId = assetId || derivedId;
|
|
78
78
|
if (!finalId)
|
|
79
79
|
return null;
|
|
80
80
|
let filePath = rawPath || void 0;
|
|
81
|
-
if (filePath && !
|
|
82
|
-
const cwdCandidate =
|
|
83
|
-
const assetDirCandidate = assetsDir ?
|
|
84
|
-
filePath =
|
|
81
|
+
if (filePath && !path4.isAbsolute(filePath)) {
|
|
82
|
+
const cwdCandidate = path4.resolve(process.cwd(), filePath);
|
|
83
|
+
const assetDirCandidate = assetsDir ? path4.resolve(assetsDir, filePath) : "";
|
|
84
|
+
filePath = fs3.existsSync(cwdCandidate) ? cwdCandidate : assetDirCandidate || cwdCandidate;
|
|
85
85
|
}
|
|
86
86
|
const label = String(raw.label || raw.user_label || raw.userLabel || filename || finalId).trim();
|
|
87
87
|
const type = String(raw.type || inferAssetType(label, filename)).trim().toLowerCase();
|
|
@@ -99,14 +99,14 @@ function normalizeAsset(raw, assetsDir = null) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
function getDefaultAssetsDir() {
|
|
102
|
-
return process.env.ARCALITY_ASSETS_DIR ||
|
|
102
|
+
return process.env.ARCALITY_ASSETS_DIR || path4.join(process.cwd(), ".arcality", "assets");
|
|
103
103
|
}
|
|
104
104
|
function loadManifestAssets(assetsDir) {
|
|
105
105
|
const explicitFile = process.env.ARCALITY_ASSETS_FILE;
|
|
106
|
-
const manifestPath = explicitFile ||
|
|
106
|
+
const manifestPath = explicitFile || path4.join(process.cwd(), ".arcality", "assets.json");
|
|
107
107
|
const inlineJson = process.env.ARCALITY_ASSETS_JSON;
|
|
108
108
|
try {
|
|
109
|
-
const parsed = inlineJson ? JSON.parse(inlineJson) :
|
|
109
|
+
const parsed = inlineJson ? JSON.parse(inlineJson) : fs3.existsSync(manifestPath) ? readJsonFile(manifestPath) : null;
|
|
110
110
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.assets) ? parsed.assets : [];
|
|
111
111
|
return list.map((entry) => normalizeAsset(entry, assetsDir)).filter(Boolean);
|
|
112
112
|
} catch (e) {
|
|
@@ -116,13 +116,13 @@ function loadManifestAssets(assetsDir) {
|
|
|
116
116
|
}
|
|
117
117
|
function scanAssetsDir(assetsDir) {
|
|
118
118
|
try {
|
|
119
|
-
if (!
|
|
119
|
+
if (!fs3.existsSync(assetsDir))
|
|
120
120
|
return [];
|
|
121
|
-
return
|
|
122
|
-
const filePath =
|
|
121
|
+
return fs3.readdirSync(assetsDir, { withFileTypes: true }).filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path4.extname(entry.name).toLowerCase())).map((entry) => {
|
|
122
|
+
const filePath = path4.join(assetsDir, entry.name);
|
|
123
123
|
return normalizeAsset({
|
|
124
|
-
id: `asset_${sanitizeIdPart(
|
|
125
|
-
label:
|
|
124
|
+
id: `asset_${sanitizeIdPart(path4.basename(entry.name, path4.extname(entry.name)))}`,
|
|
125
|
+
label: path4.basename(entry.name, path4.extname(entry.name)).replace(/[_-]+/g, " "),
|
|
126
126
|
path: filePath,
|
|
127
127
|
mime: normalizeMimeFromPath(filePath),
|
|
128
128
|
type: inferAssetType(entry.name)
|
|
@@ -247,7 +247,7 @@ function resolveUploadAsset(value, label = "file upload") {
|
|
|
247
247
|
const subtype = raw.startsWith("placeholder:image/") ? raw.substring("placeholder:image/".length).toLowerCase() : "";
|
|
248
248
|
const targetType = subtype || inferAssetType(label, raw);
|
|
249
249
|
const matchingAsset = assets.find((a) => a.type === targetType || a.label.toLowerCase().includes(targetType)) || assets.find((a) => !a.isDefault);
|
|
250
|
-
if (matchingAsset?.filePath &&
|
|
250
|
+
if (matchingAsset?.filePath && fs3.existsSync(matchingAsset.filePath)) {
|
|
251
251
|
console.log(`>>ARCALITY_STATUS>> Usando asset real configurado para el placeholder "${raw}": ${matchingAsset.filePath}`);
|
|
252
252
|
return matchingAsset.filePath;
|
|
253
253
|
}
|
|
@@ -255,14 +255,14 @@ function resolveUploadAsset(value, label = "file upload") {
|
|
|
255
255
|
}
|
|
256
256
|
if (/^arcasset:\/\//i.test(raw) || /^asset_[a-z0-9_-]+$/i.test(raw)) {
|
|
257
257
|
const asset = findAsset(raw);
|
|
258
|
-
if (asset?.filePath &&
|
|
258
|
+
if (asset?.filePath && fs3.existsSync(asset.filePath)) {
|
|
259
259
|
return asset.filePath;
|
|
260
260
|
}
|
|
261
261
|
console.warn(`>>ARCALITY_STATUS>> Asset "${raw}" no tiene archivo local disponible. Usando placeholder.`);
|
|
262
262
|
return createPlaceholderUpload(label, raw);
|
|
263
263
|
}
|
|
264
|
-
const filePath =
|
|
265
|
-
if (
|
|
264
|
+
const filePath = path4.isAbsolute(raw) ? raw : path4.resolve(process.cwd(), raw);
|
|
265
|
+
if (fs3.existsSync(filePath)) {
|
|
266
266
|
return filePath;
|
|
267
267
|
}
|
|
268
268
|
console.warn(`>>ARCALITY_STATUS>> Archivo de upload no encontrado: "${raw}". Usando placeholder.`);
|
|
@@ -278,8 +278,8 @@ async function syncAssetsFromBackend() {
|
|
|
278
278
|
}
|
|
279
279
|
try {
|
|
280
280
|
const assetsDir = getDefaultAssetsDir();
|
|
281
|
-
if (!
|
|
282
|
-
|
|
281
|
+
if (!fs3.existsSync(assetsDir)) {
|
|
282
|
+
fs3.mkdirSync(assetsDir, { recursive: true });
|
|
283
283
|
}
|
|
284
284
|
const url = `${apiUrl}/api/v1/projects/${projectId}/assets`;
|
|
285
285
|
console.log(`>>ARCALITY_STATUS>> \u{1F4E6} Consultando assets configurados del proyecto...`);
|
|
@@ -314,14 +314,14 @@ async function syncAssetsFromBackend() {
|
|
|
314
314
|
continue;
|
|
315
315
|
}
|
|
316
316
|
const prefix = `${assetId}_`;
|
|
317
|
-
const files =
|
|
317
|
+
const files = fs3.existsSync(assetsDir) ? fs3.readdirSync(assetsDir) : [];
|
|
318
318
|
const existingFile = files.find((f) => f.startsWith(prefix));
|
|
319
319
|
let filename = asset.filename || asset.fileName;
|
|
320
320
|
let localFilePath = "";
|
|
321
321
|
let downloadNeeded = true;
|
|
322
322
|
if (existingFile) {
|
|
323
|
-
const candidatePath =
|
|
324
|
-
if (
|
|
323
|
+
const candidatePath = path4.join(assetsDir, existingFile);
|
|
324
|
+
if (fs3.existsSync(candidatePath) && fs3.statSync(candidatePath).size > 0) {
|
|
325
325
|
filename = filename || existingFile.substring(prefix.length);
|
|
326
326
|
localFilePath = candidatePath;
|
|
327
327
|
downloadNeeded = false;
|
|
@@ -342,13 +342,13 @@ async function syncAssetsFromBackend() {
|
|
|
342
342
|
const matFilename = matData.filename || matData.fileName || filename || `${assetId}.png`;
|
|
343
343
|
filename = matFilename;
|
|
344
344
|
const localFilename = `${assetId}_${matFilename}`;
|
|
345
|
-
localFilePath =
|
|
345
|
+
localFilePath = path4.join(assetsDir, localFilename);
|
|
346
346
|
if (downloadUrl) {
|
|
347
347
|
console.log(`>>ARCALITY_STATUS>> \u{1F4E5} Descargando archivo de asset: "${asset.label}"...`);
|
|
348
348
|
const fileRes = await globalThis.fetch(downloadUrl);
|
|
349
349
|
if (fileRes.ok) {
|
|
350
350
|
const buffer = Buffer.from(await fileRes.arrayBuffer());
|
|
351
|
-
|
|
351
|
+
fs3.writeFileSync(localFilePath, buffer);
|
|
352
352
|
console.log(`>>ARCALITY_STATUS>> \u2705 Asset guardado localmente: ${localFilePath}`);
|
|
353
353
|
} else {
|
|
354
354
|
console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al descargar archivo de asset: ${fileRes.status}`);
|
|
@@ -357,7 +357,7 @@ async function syncAssetsFromBackend() {
|
|
|
357
357
|
} else {
|
|
358
358
|
console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al materializar asset: ${matRes.status}`);
|
|
359
359
|
filename = filename || `${assetId}.png`;
|
|
360
|
-
localFilePath =
|
|
360
|
+
localFilePath = path4.join(assetsDir, `${assetId}_${filename}`);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
localAssets.push({
|
|
@@ -373,12 +373,12 @@ async function syncAssetsFromBackend() {
|
|
|
373
373
|
is_default: asset.is_default || asset.isDefault || false
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
|
-
const manifestPath = process.env.ARCALITY_ASSETS_FILE ||
|
|
377
|
-
const manifestDir =
|
|
378
|
-
if (!
|
|
379
|
-
|
|
376
|
+
const manifestPath = process.env.ARCALITY_ASSETS_FILE || path4.join(process.cwd(), ".arcality", "assets.json");
|
|
377
|
+
const manifestDir = path4.dirname(manifestPath);
|
|
378
|
+
if (!fs3.existsSync(manifestDir)) {
|
|
379
|
+
fs3.mkdirSync(manifestDir, { recursive: true });
|
|
380
380
|
}
|
|
381
|
-
|
|
381
|
+
fs3.writeFileSync(manifestPath, JSON.stringify(localAssets, null, 2), "utf8");
|
|
382
382
|
console.log(`>>ARCALITY_STATUS>> \u2705 Inventario local de assets actualizado (${localAssets.length}).`);
|
|
383
383
|
if (process.env.DEBUG) {
|
|
384
384
|
console.log(`[AssetSync] Local manifest updated at ${manifestPath}`);
|
|
@@ -387,11 +387,11 @@ async function syncAssetsFromBackend() {
|
|
|
387
387
|
console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error durante la sincronizaci\xF3n de assets: ${e.message}`);
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
|
-
var
|
|
390
|
+
var fs3, path4, zlib, IMAGE_EXTENSIONS;
|
|
391
391
|
var init_asset_resolver = __esm({
|
|
392
392
|
"tests/_helpers/asset-resolver.ts"() {
|
|
393
|
-
|
|
394
|
-
|
|
393
|
+
fs3 = __toESM(require("fs"));
|
|
394
|
+
path4 = __toESM(require("path"));
|
|
395
395
|
zlib = __toESM(require("zlib"));
|
|
396
396
|
IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
397
397
|
}
|
|
@@ -1072,6 +1072,156 @@ var scrollPageTool = {
|
|
|
1072
1072
|
required: ["direction"]
|
|
1073
1073
|
}
|
|
1074
1074
|
};
|
|
1075
|
+
var validateDatabaseStateTool = {
|
|
1076
|
+
name: "validate_database_state",
|
|
1077
|
+
description: "QA Skill: Validate backend state in PostgreSQL using a safe query_id from the local Arcality database validation catalog. Use this after creating/updating records when the mission asks to corroborate persistence in the database. Supports record existence and exact field-value checks. Never accepts raw SQL.",
|
|
1078
|
+
input_schema: {
|
|
1079
|
+
type: "object",
|
|
1080
|
+
properties: {
|
|
1081
|
+
name: {
|
|
1082
|
+
type: "string",
|
|
1083
|
+
description: "Friendly validation name for the report, e.g. 'cliente_creado_en_bd'."
|
|
1084
|
+
},
|
|
1085
|
+
query_id: {
|
|
1086
|
+
type: "string",
|
|
1087
|
+
description: "Query id from .arcality/db-validations.yaml. Raw SQL is not accepted."
|
|
1088
|
+
},
|
|
1089
|
+
params: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
description: "Named params required by the catalog query, e.g. { name: 'Cliente QA 001' }."
|
|
1092
|
+
},
|
|
1093
|
+
expect: {
|
|
1094
|
+
type: "object",
|
|
1095
|
+
description: "Expected database state. Supports { exists: true/false, fields: { status: 'ACTIVE' } }.",
|
|
1096
|
+
properties: {
|
|
1097
|
+
exists: {
|
|
1098
|
+
type: "boolean",
|
|
1099
|
+
description: "Whether at least one row should exist."
|
|
1100
|
+
},
|
|
1101
|
+
fields: {
|
|
1102
|
+
type: "object",
|
|
1103
|
+
description: "Exact field-value expectations checked against the first returned row."
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
timeout_ms: {
|
|
1108
|
+
type: "number",
|
|
1109
|
+
description: "Optional query timeout in milliseconds."
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
required: ["query_id", "params", "expect"]
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var previewDatabaseValidationTool = {
|
|
1116
|
+
name: "preview_db_validation",
|
|
1117
|
+
description: "QA Skill: Preview an allowlisted PostgreSQL validation query before running a full mission. Use this to verify query_id, params, returned rows, and provider connectivity without requiring an expect block. Never accepts raw SQL.",
|
|
1118
|
+
input_schema: {
|
|
1119
|
+
type: "object",
|
|
1120
|
+
properties: {
|
|
1121
|
+
name: {
|
|
1122
|
+
type: "string",
|
|
1123
|
+
description: "Friendly preview name for the output."
|
|
1124
|
+
},
|
|
1125
|
+
query_id: {
|
|
1126
|
+
type: "string",
|
|
1127
|
+
description: "Query id from .arcality/db-validations.yaml. Raw SQL is not accepted."
|
|
1128
|
+
},
|
|
1129
|
+
params: {
|
|
1130
|
+
type: "object",
|
|
1131
|
+
description: "Named params required by the catalog query."
|
|
1132
|
+
},
|
|
1133
|
+
timeout_ms: {
|
|
1134
|
+
type: "number",
|
|
1135
|
+
description: "Optional query timeout in milliseconds."
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
required: ["query_id"]
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
var registerDbProfileTool = {
|
|
1142
|
+
name: "register_db_profile",
|
|
1143
|
+
description: "QA Skill: Register or validate local database profile metadata without exposing secrets. Use this to confirm the provider, profile name, expected env var, catalog path, timeout defaults, and optional MCP config before running database corroboration.",
|
|
1144
|
+
input_schema: {
|
|
1145
|
+
type: "object",
|
|
1146
|
+
properties: {
|
|
1147
|
+
profile: {
|
|
1148
|
+
type: "string",
|
|
1149
|
+
description: "Logical database profile name, e.g. 'qa_local'."
|
|
1150
|
+
},
|
|
1151
|
+
provider: {
|
|
1152
|
+
enum: ["postgres", "mcp"],
|
|
1153
|
+
description: "Database provider mode to validate."
|
|
1154
|
+
},
|
|
1155
|
+
connection_env: {
|
|
1156
|
+
type: "string",
|
|
1157
|
+
description: "Optional explicit env var name that should hold the connection string."
|
|
1158
|
+
},
|
|
1159
|
+
catalog_path: {
|
|
1160
|
+
type: "string",
|
|
1161
|
+
description: "Optional path to the allowlisted query catalog file."
|
|
1162
|
+
},
|
|
1163
|
+
timeout_ms: {
|
|
1164
|
+
type: "number",
|
|
1165
|
+
description: "Optional timeout default for validations."
|
|
1166
|
+
},
|
|
1167
|
+
mcp_config_path: {
|
|
1168
|
+
type: "string",
|
|
1169
|
+
description: "Optional MCP config path when provider is mcp."
|
|
1170
|
+
},
|
|
1171
|
+
validate_catalog: {
|
|
1172
|
+
type: "boolean",
|
|
1173
|
+
description: "Whether to validate that the catalog file can be loaded.",
|
|
1174
|
+
default: true
|
|
1175
|
+
},
|
|
1176
|
+
persist: {
|
|
1177
|
+
type: "boolean",
|
|
1178
|
+
description: "If true, saves non-secret profile metadata into a local registry file.",
|
|
1179
|
+
default: false
|
|
1180
|
+
},
|
|
1181
|
+
registry_path: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
description: "Optional local registry path used only when persist=true."
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
var summarizeExecutionEvidenceTool = {
|
|
1189
|
+
name: "summarize_execution_evidence",
|
|
1190
|
+
description: "QA Skill: Merge UI outcome, database corroboration, console errors, and attachments into one concise evidence summary object. Use this when closing a mission, preparing a report narrative, or resolving contradictions between visible behavior and backend state.",
|
|
1191
|
+
input_schema: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
properties: {
|
|
1194
|
+
mission_name: {
|
|
1195
|
+
type: "string",
|
|
1196
|
+
description: "Friendly mission name for the summary."
|
|
1197
|
+
},
|
|
1198
|
+
ui_status: {
|
|
1199
|
+
enum: ["passed", "failed", "unknown"],
|
|
1200
|
+
description: "Observed UI outcome status."
|
|
1201
|
+
},
|
|
1202
|
+
ui_summary: {
|
|
1203
|
+
type: "string",
|
|
1204
|
+
description: "Short human summary of what happened in the UI."
|
|
1205
|
+
},
|
|
1206
|
+
expected_business_outcome: {
|
|
1207
|
+
type: "string",
|
|
1208
|
+
description: "Expected business result in plain language."
|
|
1209
|
+
},
|
|
1210
|
+
database_status: {
|
|
1211
|
+
enum: ["passed", "failed", "unknown"],
|
|
1212
|
+
description: "Observed database corroboration status, if already known."
|
|
1213
|
+
},
|
|
1214
|
+
database_validation_name: {
|
|
1215
|
+
type: "string",
|
|
1216
|
+
description: "Optional validation name associated with the DB check."
|
|
1217
|
+
},
|
|
1218
|
+
query_id: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
description: "Optional query_id associated with the DB check."
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1075
1225
|
var qaAdvancedTools = [
|
|
1076
1226
|
validateElementStateTool,
|
|
1077
1227
|
extractTableDataTool,
|
|
@@ -1085,7 +1235,11 @@ var qaAdvancedTools = [
|
|
|
1085
1235
|
sendKeyboardEventTool,
|
|
1086
1236
|
typeSequentiallyTool,
|
|
1087
1237
|
interactNativeControlTool,
|
|
1088
|
-
scrollPageTool
|
|
1238
|
+
scrollPageTool,
|
|
1239
|
+
validateDatabaseStateTool,
|
|
1240
|
+
previewDatabaseValidationTool,
|
|
1241
|
+
registerDbProfileTool,
|
|
1242
|
+
summarizeExecutionEvidenceTool
|
|
1089
1243
|
];
|
|
1090
1244
|
|
|
1091
1245
|
// tests/_helpers/qa-security-tools.ts
|
|
@@ -1200,8 +1354,8 @@ async function scan_sensitive_data_exposure(page) {
|
|
|
1200
1354
|
}
|
|
1201
1355
|
|
|
1202
1356
|
// tests/_helpers/ai-agent-helper.ts
|
|
1203
|
-
var
|
|
1204
|
-
var
|
|
1357
|
+
var fs6 = __toESM(require("fs"));
|
|
1358
|
+
var path7 = __toESM(require("path"));
|
|
1205
1359
|
|
|
1206
1360
|
// src/KnowledgeService.ts
|
|
1207
1361
|
var crypto = __toESM(require("crypto"));
|
|
@@ -2088,6 +2242,822 @@ Local context fingerprint: ${localAnalysis.fingerprint || "N/A"}
|
|
|
2088
2242
|
`;
|
|
2089
2243
|
}
|
|
2090
2244
|
|
|
2245
|
+
// src/services/databaseValidationService.mjs
|
|
2246
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
2247
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
2248
|
+
var import_module = require("module");
|
|
2249
|
+
var import_js_yaml = require("js-yaml");
|
|
2250
|
+
|
|
2251
|
+
// src/services/mcpStdioClient.mjs
|
|
2252
|
+
var import_child_process = require("child_process");
|
|
2253
|
+
var import_path = __toESM(require("path"), 1);
|
|
2254
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
2255
|
+
function isPlainObject(value) {
|
|
2256
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2257
|
+
}
|
|
2258
|
+
function getNestedValue(source, dottedPath) {
|
|
2259
|
+
return String(dottedPath || "").split(".").filter(Boolean).reduce((current, key) => current && Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0, source);
|
|
2260
|
+
}
|
|
2261
|
+
function replaceEnvTokens(value, envSource = process.env) {
|
|
2262
|
+
return String(value).replace(/\$\{([A-Z0-9_]+)\}/gi, (_, envName) => {
|
|
2263
|
+
const resolved = envSource[envName];
|
|
2264
|
+
return resolved === void 0 ? "" : String(resolved);
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
function resolveTemplateValue(value, context = {}, envSource = process.env) {
|
|
2268
|
+
if (typeof value === "string") {
|
|
2269
|
+
const envResolved = replaceEnvTokens(value, envSource);
|
|
2270
|
+
const exactMatch = envResolved.match(/^\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}$/);
|
|
2271
|
+
if (exactMatch) {
|
|
2272
|
+
return getNestedValue(context, exactMatch[1]);
|
|
2273
|
+
}
|
|
2274
|
+
return envResolved.replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (_, tokenPath) => {
|
|
2275
|
+
const resolved = getNestedValue(context, tokenPath);
|
|
2276
|
+
if (resolved === void 0 || resolved === null)
|
|
2277
|
+
return "";
|
|
2278
|
+
if (typeof resolved === "string" || typeof resolved === "number" || typeof resolved === "boolean") {
|
|
2279
|
+
return String(resolved);
|
|
2280
|
+
}
|
|
2281
|
+
return JSON.stringify(resolved);
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
if (Array.isArray(value)) {
|
|
2285
|
+
return value.map((entry) => resolveTemplateValue(entry, context, envSource));
|
|
2286
|
+
}
|
|
2287
|
+
if (isPlainObject(value)) {
|
|
2288
|
+
return Object.fromEntries(
|
|
2289
|
+
Object.entries(value).map(([key, entryValue]) => [key, resolveTemplateValue(entryValue, context, envSource)])
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
return value;
|
|
2293
|
+
}
|
|
2294
|
+
function extractRowsFromMcpToolResult(result, preferredRowsPath = "") {
|
|
2295
|
+
const candidate = preferredRowsPath ? getNestedValue(result, preferredRowsPath) : void 0;
|
|
2296
|
+
if (Array.isArray(candidate))
|
|
2297
|
+
return candidate;
|
|
2298
|
+
if (Array.isArray(result?.rows))
|
|
2299
|
+
return result.rows;
|
|
2300
|
+
const content = Array.isArray(result?.content) ? result.content : [];
|
|
2301
|
+
for (const item of content) {
|
|
2302
|
+
if (!item || item.type !== "text" || typeof item.text !== "string")
|
|
2303
|
+
continue;
|
|
2304
|
+
const text = item.text.trim();
|
|
2305
|
+
if (!text)
|
|
2306
|
+
continue;
|
|
2307
|
+
try {
|
|
2308
|
+
const parsed = JSON.parse(text);
|
|
2309
|
+
if (Array.isArray(parsed))
|
|
2310
|
+
return parsed;
|
|
2311
|
+
if (Array.isArray(parsed?.rows))
|
|
2312
|
+
return parsed.rows;
|
|
2313
|
+
if (preferredRowsPath) {
|
|
2314
|
+
const nested = getNestedValue(parsed, preferredRowsPath);
|
|
2315
|
+
if (Array.isArray(nested))
|
|
2316
|
+
return nested;
|
|
2317
|
+
}
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return [];
|
|
2322
|
+
}
|
|
2323
|
+
function createFrame(message) {
|
|
2324
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
2325
|
+
const header = Buffer.from(`Content-Length: ${body.length}\r
|
|
2326
|
+
\r
|
|
2327
|
+
`, "utf8");
|
|
2328
|
+
return Buffer.concat([header, body]);
|
|
2329
|
+
}
|
|
2330
|
+
function createParser(onMessage) {
|
|
2331
|
+
let buffer = Buffer.alloc(0);
|
|
2332
|
+
return (chunk) => {
|
|
2333
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
2334
|
+
while (true) {
|
|
2335
|
+
const separatorIndex = buffer.indexOf("\r\n\r\n");
|
|
2336
|
+
if (separatorIndex === -1)
|
|
2337
|
+
return;
|
|
2338
|
+
const headerText = buffer.slice(0, separatorIndex).toString("utf8");
|
|
2339
|
+
const headers = headerText.split("\r\n");
|
|
2340
|
+
const lengthHeader = headers.find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
2341
|
+
if (!lengthHeader) {
|
|
2342
|
+
throw new Error("Invalid MCP response: missing Content-Length header.");
|
|
2343
|
+
}
|
|
2344
|
+
const length = Number(lengthHeader.split(":")[1]?.trim() || 0);
|
|
2345
|
+
const bodyStart = separatorIndex + 4;
|
|
2346
|
+
const bodyEnd = bodyStart + length;
|
|
2347
|
+
if (buffer.length < bodyEnd)
|
|
2348
|
+
return;
|
|
2349
|
+
const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
|
|
2350
|
+
buffer = buffer.slice(bodyEnd);
|
|
2351
|
+
onMessage(JSON.parse(body));
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
async function callMcpToolWithConfig(config, toolContext = {}) {
|
|
2356
|
+
if (!config || !config.command) {
|
|
2357
|
+
throw new Error("MCP configuration is missing the command field.");
|
|
2358
|
+
}
|
|
2359
|
+
const timeoutMs = Number(config.timeoutMs || toolContext.timeoutMs || 15e3);
|
|
2360
|
+
const command = resolveTemplateValue(config.command, toolContext);
|
|
2361
|
+
const args = Array.isArray(config.args) ? resolveTemplateValue(config.args, toolContext) : [];
|
|
2362
|
+
const envFromConfig = isPlainObject(config.env) ? resolveTemplateValue(config.env, toolContext) : {};
|
|
2363
|
+
const cwd = config.cwd ? resolveTemplateValue(config.cwd, toolContext) : process.cwd();
|
|
2364
|
+
const toolName = String(resolveTemplateValue(config.toolName || "query", toolContext) || "").trim();
|
|
2365
|
+
const toolArguments = isPlainObject(config.toolArguments) ? resolveTemplateValue(config.toolArguments, toolContext) : {};
|
|
2366
|
+
const child = (0, import_child_process.spawn)(command, args, {
|
|
2367
|
+
cwd: import_path.default.isAbsolute(cwd) ? cwd : import_path.default.join(process.cwd(), cwd),
|
|
2368
|
+
env: { ...process.env, ...envFromConfig },
|
|
2369
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2370
|
+
windowsHide: true
|
|
2371
|
+
});
|
|
2372
|
+
let requestId = 1;
|
|
2373
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2374
|
+
let startupError = "";
|
|
2375
|
+
const parser = createParser((message) => {
|
|
2376
|
+
if (message.id && pending.has(message.id)) {
|
|
2377
|
+
const entry = pending.get(message.id);
|
|
2378
|
+
pending.delete(message.id);
|
|
2379
|
+
if (message.error) {
|
|
2380
|
+
entry.reject(new Error(message.error.message || "Unknown MCP error."));
|
|
2381
|
+
} else {
|
|
2382
|
+
entry.resolve(message.result);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
child.stdout.on("data", (chunk) => parser(chunk));
|
|
2387
|
+
child.stderr.on("data", (chunk) => {
|
|
2388
|
+
startupError += chunk.toString("utf8");
|
|
2389
|
+
});
|
|
2390
|
+
child.on("error", (error) => {
|
|
2391
|
+
for (const [, entry] of pending)
|
|
2392
|
+
entry.reject(error);
|
|
2393
|
+
pending.clear();
|
|
2394
|
+
});
|
|
2395
|
+
const sendRequest = (method, params) => new Promise((resolve2, reject) => {
|
|
2396
|
+
const id = requestId++;
|
|
2397
|
+
pending.set(id, { resolve: resolve2, reject });
|
|
2398
|
+
child.stdin.write(createFrame({ jsonrpc: "2.0", id, method, params }));
|
|
2399
|
+
});
|
|
2400
|
+
const sendNotification = (method, params) => {
|
|
2401
|
+
child.stdin.write(createFrame({ jsonrpc: "2.0", method, params }));
|
|
2402
|
+
};
|
|
2403
|
+
const timeout = new Promise((_, reject) => {
|
|
2404
|
+
setTimeout(() => reject(new Error(`MCP tool call timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
2405
|
+
});
|
|
2406
|
+
try {
|
|
2407
|
+
const result = await Promise.race([
|
|
2408
|
+
(async () => {
|
|
2409
|
+
await sendRequest("initialize", {
|
|
2410
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
2411
|
+
capabilities: {},
|
|
2412
|
+
clientInfo: { name: "arcality", version: "4.1.0" }
|
|
2413
|
+
});
|
|
2414
|
+
sendNotification("notifications/initialized", {});
|
|
2415
|
+
return await sendRequest("tools/call", {
|
|
2416
|
+
name: toolName,
|
|
2417
|
+
arguments: toolArguments
|
|
2418
|
+
});
|
|
2419
|
+
})(),
|
|
2420
|
+
timeout
|
|
2421
|
+
]);
|
|
2422
|
+
return result;
|
|
2423
|
+
} catch (error) {
|
|
2424
|
+
const details = startupError.trim();
|
|
2425
|
+
if (details) {
|
|
2426
|
+
throw new Error(`${error.message} MCP stderr: ${details}`);
|
|
2427
|
+
}
|
|
2428
|
+
throw error;
|
|
2429
|
+
} finally {
|
|
2430
|
+
child.kill();
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// src/services/databaseValidationService.mjs
|
|
2435
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
2436
|
+
var DEFAULT_SAMPLE_LIMIT = 3;
|
|
2437
|
+
var DEFAULT_CATALOG_PATH = import_path2.default.join(".arcality", "db-validations.yaml");
|
|
2438
|
+
var DEFAULT_MCP_CONFIG_PATH = import_path2.default.join(".arcality", "mcp.postgres.json");
|
|
2439
|
+
var SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|secret|token|api[_-]?key|authorization|auth|credential|session|cookie)/i;
|
|
2440
|
+
var SQL_WRITE_PATTERN = /\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|vacuum|copy|call|execute|merge)\b/i;
|
|
2441
|
+
function toEnvKey(value) {
|
|
2442
|
+
return String(value || "").trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2443
|
+
}
|
|
2444
|
+
function isEnabled(value) {
|
|
2445
|
+
return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
|
|
2446
|
+
}
|
|
2447
|
+
function isPlainObject2(value) {
|
|
2448
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2449
|
+
}
|
|
2450
|
+
function normalizeDbValue(value) {
|
|
2451
|
+
if (value instanceof Date)
|
|
2452
|
+
return value.toISOString();
|
|
2453
|
+
if (typeof value === "bigint")
|
|
2454
|
+
return value.toString();
|
|
2455
|
+
if (Buffer.isBuffer(value))
|
|
2456
|
+
return "[binary]";
|
|
2457
|
+
if (Array.isArray(value))
|
|
2458
|
+
return value.map(normalizeDbValue);
|
|
2459
|
+
if (isPlainObject2(value)) {
|
|
2460
|
+
return Object.fromEntries(
|
|
2461
|
+
Object.entries(value).map(([key, entryValue]) => [key, normalizeDbValue(entryValue)])
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
return value;
|
|
2465
|
+
}
|
|
2466
|
+
function normalizeRows(rows) {
|
|
2467
|
+
if (!Array.isArray(rows))
|
|
2468
|
+
return [];
|
|
2469
|
+
return rows.map((row) => normalizeDbValue(row));
|
|
2470
|
+
}
|
|
2471
|
+
function normalizeRedactSet(...sources) {
|
|
2472
|
+
const redact = /* @__PURE__ */ new Set();
|
|
2473
|
+
for (const source of sources) {
|
|
2474
|
+
if (Array.isArray(source)) {
|
|
2475
|
+
for (const field of source) {
|
|
2476
|
+
const name = String(field || "").trim();
|
|
2477
|
+
if (name)
|
|
2478
|
+
redact.add(name);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return redact;
|
|
2483
|
+
}
|
|
2484
|
+
function shouldRedactField(field, redactSet) {
|
|
2485
|
+
return redactSet.has(field) || SENSITIVE_FIELD_PATTERN.test(field);
|
|
2486
|
+
}
|
|
2487
|
+
function redactValue(value) {
|
|
2488
|
+
if (value === void 0 || value === null)
|
|
2489
|
+
return value;
|
|
2490
|
+
return "[REDACTED]";
|
|
2491
|
+
}
|
|
2492
|
+
function redactObject(value, redactSet) {
|
|
2493
|
+
if (!isPlainObject2(value))
|
|
2494
|
+
return value;
|
|
2495
|
+
return Object.fromEntries(
|
|
2496
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
2497
|
+
key,
|
|
2498
|
+
shouldRedactField(key, redactSet) ? redactValue(entryValue) : entryValue
|
|
2499
|
+
])
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
function redactRows(rows, redactSet, limit = DEFAULT_SAMPLE_LIMIT) {
|
|
2503
|
+
return normalizeRows(rows).slice(0, limit).map((row) => redactObject(row, redactSet));
|
|
2504
|
+
}
|
|
2505
|
+
function validateReadOnlySql(sql) {
|
|
2506
|
+
const text = String(sql || "").trim();
|
|
2507
|
+
if (!text) {
|
|
2508
|
+
throw new Error("Database validation query is empty.");
|
|
2509
|
+
}
|
|
2510
|
+
const withoutTrailingSemicolon = text.replace(/;\s*$/, "");
|
|
2511
|
+
if (withoutTrailingSemicolon.includes(";")) {
|
|
2512
|
+
throw new Error("Database validation queries must contain a single statement.");
|
|
2513
|
+
}
|
|
2514
|
+
if (!/^(select|with)\b/i.test(withoutTrailingSemicolon)) {
|
|
2515
|
+
throw new Error("Database validation queries must start with SELECT or WITH.");
|
|
2516
|
+
}
|
|
2517
|
+
if (SQL_WRITE_PATTERN.test(withoutTrailingSemicolon)) {
|
|
2518
|
+
throw new Error("Database validation queries must be read-only.");
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
function resolveConfigPath(configPath, fallbackPath, options = {}) {
|
|
2522
|
+
const projectRoot = options.projectRoot || process.env.ARCALITY_PROJECT_ROOT || process.cwd();
|
|
2523
|
+
return import_path2.default.isAbsolute(configPath) ? configPath : import_path2.default.join(projectRoot, configPath || fallbackPath);
|
|
2524
|
+
}
|
|
2525
|
+
function readJsonOrYamlFile(filePath) {
|
|
2526
|
+
const raw = import_fs.default.readFileSync(filePath, "utf8");
|
|
2527
|
+
if (/\.json$/i.test(filePath)) {
|
|
2528
|
+
return JSON.parse(raw);
|
|
2529
|
+
}
|
|
2530
|
+
return (0, import_js_yaml.load)(raw);
|
|
2531
|
+
}
|
|
2532
|
+
function resolveCatalogPath(options = {}) {
|
|
2533
|
+
const configuredPath = options.catalogPath || process.env.ARCALITY_DB_CATALOG || process.env.ARCALITY_DB_VALIDATIONS_FILE || DEFAULT_CATALOG_PATH;
|
|
2534
|
+
return resolveConfigPath(configuredPath, DEFAULT_CATALOG_PATH, options);
|
|
2535
|
+
}
|
|
2536
|
+
function resolveMcpConfigPath(options = {}) {
|
|
2537
|
+
const configuredPath = options.mcpConfigPath || process.env.ARCALITY_DB_MCP_CONFIG || DEFAULT_MCP_CONFIG_PATH;
|
|
2538
|
+
return resolveConfigPath(configuredPath, DEFAULT_MCP_CONFIG_PATH, options);
|
|
2539
|
+
}
|
|
2540
|
+
function loadDatabaseValidationCatalog(options = {}) {
|
|
2541
|
+
const catalogPath = resolveCatalogPath(options);
|
|
2542
|
+
if (!import_fs.default.existsSync(catalogPath)) {
|
|
2543
|
+
throw new Error(`Database validation catalog not found: ${catalogPath}`);
|
|
2544
|
+
}
|
|
2545
|
+
const raw = readJsonOrYamlFile(catalogPath);
|
|
2546
|
+
const catalog = raw && typeof raw === "object" ? raw : {};
|
|
2547
|
+
const queries = isPlainObject2(catalog.queries) ? catalog.queries : {};
|
|
2548
|
+
return { catalogPath, queries };
|
|
2549
|
+
}
|
|
2550
|
+
function loadDatabaseMcpConfig(options = {}) {
|
|
2551
|
+
const configPath = resolveMcpConfigPath(options);
|
|
2552
|
+
if (!import_fs.default.existsSync(configPath)) {
|
|
2553
|
+
throw new Error(`MCP database config not found: ${configPath}`);
|
|
2554
|
+
}
|
|
2555
|
+
const raw = readJsonOrYamlFile(configPath);
|
|
2556
|
+
if (!isPlainObject2(raw)) {
|
|
2557
|
+
throw new Error(`Invalid MCP database config: ${configPath}`);
|
|
2558
|
+
}
|
|
2559
|
+
return { configPath, config: raw };
|
|
2560
|
+
}
|
|
2561
|
+
function resolveDatabaseProvider(options = {}) {
|
|
2562
|
+
const rawProvider = String(options.provider || process.env.ARCALITY_DB_PROVIDER || "postgres").trim().toLowerCase();
|
|
2563
|
+
if (["mcp", "mcp_postgres", "mcp-postgres"].includes(rawProvider)) {
|
|
2564
|
+
return "mcp";
|
|
2565
|
+
}
|
|
2566
|
+
return "postgres";
|
|
2567
|
+
}
|
|
2568
|
+
function resolveDatabaseConnectionEnv(options = {}) {
|
|
2569
|
+
const profile = String(options.profile || process.env.ARCALITY_DB_PROFILE || "qa_local").trim();
|
|
2570
|
+
const profileKey = toEnvKey(profile);
|
|
2571
|
+
const profileEnvName = profileKey ? `ARCALITY_DB_${profileKey}_URL` : "";
|
|
2572
|
+
const connectionEnvName = options.connectionEnv || process.env.ARCALITY_DB_CONNECTION_ENV || profileEnvName || "ARCALITY_DB_CONNECTION";
|
|
2573
|
+
const connectionString = process.env[connectionEnvName] || process.env.ARCALITY_DB_CONNECTION || process.env.ARCALITY_QA_POSTGRES_URL || "";
|
|
2574
|
+
return {
|
|
2575
|
+
profile,
|
|
2576
|
+
connectionEnvName,
|
|
2577
|
+
hasConnectionString: !!connectionString,
|
|
2578
|
+
connectionString
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
function normalizeTimeoutMs(value) {
|
|
2582
|
+
const parsed = Number(value);
|
|
2583
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
|
|
2584
|
+
}
|
|
2585
|
+
function resolveProfileRegistryPath(options = {}) {
|
|
2586
|
+
const configuredPath = options.registryPath || options.registry_path || import_path2.default.join(".arcality", "db-profiles.json");
|
|
2587
|
+
return resolveConfigPath(configuredPath, import_path2.default.join(".arcality", "db-profiles.json"), options);
|
|
2588
|
+
}
|
|
2589
|
+
function registerDatabaseProfile(profileInput = {}, options = {}) {
|
|
2590
|
+
const profile = String(profileInput.profile || options.profile || process.env.ARCALITY_DB_PROFILE || "qa_local").trim() || "qa_local";
|
|
2591
|
+
const provider = resolveDatabaseProvider({ provider: profileInput.provider || options.provider || process.env.ARCALITY_DB_PROVIDER });
|
|
2592
|
+
const connection = resolveDatabaseConnectionEnv({
|
|
2593
|
+
profile,
|
|
2594
|
+
connectionEnv: profileInput.connection_env || profileInput.connectionEnv || options.connectionEnv
|
|
2595
|
+
});
|
|
2596
|
+
const catalogPath = resolveCatalogPath({
|
|
2597
|
+
...options,
|
|
2598
|
+
catalogPath: profileInput.catalog_path || profileInput.catalogPath
|
|
2599
|
+
});
|
|
2600
|
+
const timeoutMs = normalizeTimeoutMs(profileInput.timeout_ms || profileInput.timeoutMs || options.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS);
|
|
2601
|
+
const warnings = [];
|
|
2602
|
+
const result = {
|
|
2603
|
+
type: "database_profile_registration",
|
|
2604
|
+
profile,
|
|
2605
|
+
provider,
|
|
2606
|
+
connection_env: connection.connectionEnvName,
|
|
2607
|
+
has_connection_string: connection.hasConnectionString,
|
|
2608
|
+
catalog_path: catalogPath,
|
|
2609
|
+
catalog_exists: import_fs.default.existsSync(catalogPath),
|
|
2610
|
+
timeout_ms: timeoutMs,
|
|
2611
|
+
warnings
|
|
2612
|
+
};
|
|
2613
|
+
if (!result.has_connection_string) {
|
|
2614
|
+
warnings.push("Missing PostgreSQL connection string in " + connection.connectionEnvName + ".");
|
|
2615
|
+
}
|
|
2616
|
+
if (!result.catalog_exists) {
|
|
2617
|
+
warnings.push("Database validation catalog not found: " + catalogPath);
|
|
2618
|
+
}
|
|
2619
|
+
if (profileInput.validate_catalog !== false && result.catalog_exists) {
|
|
2620
|
+
try {
|
|
2621
|
+
const catalog = loadDatabaseValidationCatalog({ ...options, catalogPath });
|
|
2622
|
+
result.query_count = Object.keys(catalog.queries || {}).length;
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
result.query_count = 0;
|
|
2625
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (provider === "mcp") {
|
|
2629
|
+
const mcpConfigPath = resolveMcpConfigPath({
|
|
2630
|
+
...options,
|
|
2631
|
+
mcpConfigPath: profileInput.mcp_config_path || profileInput.mcpConfigPath
|
|
2632
|
+
});
|
|
2633
|
+
result.mcp_config_path = mcpConfigPath;
|
|
2634
|
+
result.mcp_config_exists = import_fs.default.existsSync(mcpConfigPath);
|
|
2635
|
+
if (!result.mcp_config_exists) {
|
|
2636
|
+
warnings.push("MCP database config not found: " + mcpConfigPath);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (profileInput.persist === true) {
|
|
2640
|
+
const registryPath = resolveProfileRegistryPath({
|
|
2641
|
+
...options,
|
|
2642
|
+
registryPath: profileInput.registry_path || profileInput.registryPath
|
|
2643
|
+
});
|
|
2644
|
+
let registry = { profiles: {} };
|
|
2645
|
+
if (import_fs.default.existsSync(registryPath)) {
|
|
2646
|
+
try {
|
|
2647
|
+
const existing = JSON.parse(import_fs.default.readFileSync(registryPath, "utf8"));
|
|
2648
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2649
|
+
registry = existing;
|
|
2650
|
+
registry.profiles = registry.profiles && typeof registry.profiles === "object" ? registry.profiles : {};
|
|
2651
|
+
}
|
|
2652
|
+
} catch {
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
registry.profiles[profile] = {
|
|
2656
|
+
provider,
|
|
2657
|
+
connection_env: connection.connectionEnvName,
|
|
2658
|
+
catalog_path: catalogPath,
|
|
2659
|
+
timeout_ms: timeoutMs,
|
|
2660
|
+
...result.mcp_config_path ? { mcp_config_path: result.mcp_config_path } : {}
|
|
2661
|
+
};
|
|
2662
|
+
import_fs.default.mkdirSync(import_path2.default.dirname(registryPath), { recursive: true });
|
|
2663
|
+
import_fs.default.writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf8");
|
|
2664
|
+
result.persisted = true;
|
|
2665
|
+
result.registry_path = registryPath;
|
|
2666
|
+
} else {
|
|
2667
|
+
result.persisted = false;
|
|
2668
|
+
}
|
|
2669
|
+
result.status = warnings.length === 0 ? "ready" : "warning";
|
|
2670
|
+
return result;
|
|
2671
|
+
}
|
|
2672
|
+
function loadPgPool() {
|
|
2673
|
+
const roots = [process.env.ARCALITY_ROOT, process.cwd()].filter(Boolean);
|
|
2674
|
+
for (const root of roots) {
|
|
2675
|
+
try {
|
|
2676
|
+
const requireFromRoot = (0, import_module.createRequire)(import_path2.default.join(root, "package.json"));
|
|
2677
|
+
const pg = requireFromRoot("pg");
|
|
2678
|
+
return pg.Pool;
|
|
2679
|
+
} catch {
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
throw new Error('PostgreSQL driver "pg" is not installed. Run `npm install pg --save` in this project to enable database validation.');
|
|
2683
|
+
}
|
|
2684
|
+
function getQueryEntry(catalog, queryId) {
|
|
2685
|
+
const entry = catalog.queries[queryId];
|
|
2686
|
+
if (!isPlainObject2(entry)) {
|
|
2687
|
+
throw new Error(`Database validation query_id not found in catalog: ${queryId}`);
|
|
2688
|
+
}
|
|
2689
|
+
validateReadOnlySql(entry.sql);
|
|
2690
|
+
return entry;
|
|
2691
|
+
}
|
|
2692
|
+
function buildQueryValues(entry, params) {
|
|
2693
|
+
const paramNames = Array.isArray(entry.params) ? entry.params : [];
|
|
2694
|
+
return paramNames.map((name) => {
|
|
2695
|
+
const key = String(name);
|
|
2696
|
+
if (!Object.prototype.hasOwnProperty.call(params || {}, key)) {
|
|
2697
|
+
throw new Error(`Missing database validation param: ${key}`);
|
|
2698
|
+
}
|
|
2699
|
+
return params[key];
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
function evaluateDatabaseValidationRows(validation, rows, queryEntry = {}) {
|
|
2703
|
+
if (!isPlainObject2(validation)) {
|
|
2704
|
+
throw new Error("Database validation must be an object.");
|
|
2705
|
+
}
|
|
2706
|
+
const normalizedRows = normalizeRows(rows);
|
|
2707
|
+
const expectBlock = isPlainObject2(validation.expect) ? validation.expect : {};
|
|
2708
|
+
const expectsFields = isPlainObject2(expectBlock.fields);
|
|
2709
|
+
const expectedExists = typeof expectBlock.exists === "boolean" ? expectBlock.exists : expectsFields ? true : void 0;
|
|
2710
|
+
const redactSet = normalizeRedactSet(queryEntry.redact, validation.redact);
|
|
2711
|
+
const checks = [];
|
|
2712
|
+
if (typeof expectedExists === "boolean") {
|
|
2713
|
+
const actualExists = normalizedRows.length > 0;
|
|
2714
|
+
checks.push({
|
|
2715
|
+
type: "exists",
|
|
2716
|
+
expected: expectedExists,
|
|
2717
|
+
actual: actualExists,
|
|
2718
|
+
passed: actualExists === expectedExists
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
if (expectsFields) {
|
|
2722
|
+
const firstRow = normalizedRows[0] || {};
|
|
2723
|
+
for (const [field, expected] of Object.entries(expectBlock.fields)) {
|
|
2724
|
+
const hasField = Object.prototype.hasOwnProperty.call(firstRow, field);
|
|
2725
|
+
const actual = hasField ? firstRow[field] : void 0;
|
|
2726
|
+
const passed2 = hasField && actual === normalizeDbValue(expected);
|
|
2727
|
+
const redacted = shouldRedactField(field, redactSet);
|
|
2728
|
+
checks.push({
|
|
2729
|
+
type: "field_exact",
|
|
2730
|
+
field,
|
|
2731
|
+
expected: redacted ? redactValue(expected) : normalizeDbValue(expected),
|
|
2732
|
+
actual: redacted ? redactValue(actual) : actual,
|
|
2733
|
+
passed: passed2
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
const passed = checks.length > 0 && checks.every((check) => check.passed);
|
|
2738
|
+
return {
|
|
2739
|
+
name: String(validation.name || validation.query_id || validation.queryId || "database_validation"),
|
|
2740
|
+
query_id: String(validation.query_id || validation.queryId || ""),
|
|
2741
|
+
status: passed ? "passed" : "failed",
|
|
2742
|
+
passed,
|
|
2743
|
+
row_count: normalizedRows.length,
|
|
2744
|
+
checks,
|
|
2745
|
+
rows_sample: redactRows(normalizedRows, redactSet)
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
async function queryWithTimeout(pool, query, timeoutMs) {
|
|
2749
|
+
let timeoutHandle;
|
|
2750
|
+
const timeout = new Promise((_, reject) => {
|
|
2751
|
+
timeoutHandle = setTimeout(() => {
|
|
2752
|
+
reject(new Error(`Database validation query timed out after ${timeoutMs}ms.`));
|
|
2753
|
+
}, timeoutMs);
|
|
2754
|
+
});
|
|
2755
|
+
try {
|
|
2756
|
+
return await Promise.race([pool.query(query), timeout]);
|
|
2757
|
+
} finally {
|
|
2758
|
+
clearTimeout(timeoutHandle);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
async function queryDatabaseRowsViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
|
|
2762
|
+
const connection = resolveDatabaseConnectionEnv({ profile: validation.profile || options.profile });
|
|
2763
|
+
if (!connection.hasConnectionString) {
|
|
2764
|
+
throw new Error(`Missing PostgreSQL connection string in ${connection.connectionEnvName}.`);
|
|
2765
|
+
}
|
|
2766
|
+
const Pool = loadPgPool();
|
|
2767
|
+
const values = buildQueryValues(queryEntry, validation.params || {});
|
|
2768
|
+
const pool = new Pool({
|
|
2769
|
+
connectionString: connection.connectionString,
|
|
2770
|
+
max: 1,
|
|
2771
|
+
idleTimeoutMillis: 1e3,
|
|
2772
|
+
connectionTimeoutMillis: Math.min(timeoutMs, 1e4),
|
|
2773
|
+
statement_timeout: timeoutMs
|
|
2774
|
+
});
|
|
2775
|
+
try {
|
|
2776
|
+
const result = await queryWithTimeout(pool, { text: queryEntry.sql, values }, timeoutMs);
|
|
2777
|
+
return {
|
|
2778
|
+
rows: result.rows || [],
|
|
2779
|
+
metadata: {
|
|
2780
|
+
provider: "postgres",
|
|
2781
|
+
profile: connection.profile,
|
|
2782
|
+
catalog_path: options.catalogPath || resolveCatalogPath(options),
|
|
2783
|
+
duration_ms: Date.now() - startedAt,
|
|
2784
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
} finally {
|
|
2788
|
+
await pool.end().catch(() => {
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
async function executeDatabaseValidationViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
|
|
2793
|
+
const { rows, metadata } = await queryDatabaseRowsViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options });
|
|
2794
|
+
const evaluated = evaluateDatabaseValidationRows(validation, rows, queryEntry);
|
|
2795
|
+
return {
|
|
2796
|
+
...evaluated,
|
|
2797
|
+
...metadata
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
async function queryDatabaseRowsViaMcp({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
|
|
2801
|
+
const { configPath, config } = loadDatabaseMcpConfig(options);
|
|
2802
|
+
const values = buildQueryValues(queryEntry, validation.params || {});
|
|
2803
|
+
const toolContext = {
|
|
2804
|
+
sql: queryEntry.sql,
|
|
2805
|
+
params: values,
|
|
2806
|
+
paramsNamed: validation.params || {},
|
|
2807
|
+
paramsJson: JSON.stringify(values),
|
|
2808
|
+
queryId: validation.query_id || validation.queryId || "",
|
|
2809
|
+
validationName: validation.name || validation.query_id || validation.queryId || "database_validation",
|
|
2810
|
+
timeoutMs
|
|
2811
|
+
};
|
|
2812
|
+
const result = await callMcpToolWithConfig(config, toolContext);
|
|
2813
|
+
const rows = extractRowsFromMcpToolResult(result, String(config.resultRowsPath || "").trim());
|
|
2814
|
+
return {
|
|
2815
|
+
rows,
|
|
2816
|
+
metadata: {
|
|
2817
|
+
provider: "mcp",
|
|
2818
|
+
mcp_config_path: configPath,
|
|
2819
|
+
mcp_tool_name: String(config.toolName || "query"),
|
|
2820
|
+
catalog_path: options.catalogPath || resolveCatalogPath(options),
|
|
2821
|
+
duration_ms: Date.now() - startedAt,
|
|
2822
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
async function executeDatabaseValidationViaMcp({ validation, queryEntry, timeoutMs, startedAt, options = {} }) {
|
|
2827
|
+
const { rows, metadata } = await queryDatabaseRowsViaMcp({ validation, queryEntry, timeoutMs, startedAt, options });
|
|
2828
|
+
const evaluated = evaluateDatabaseValidationRows(validation, rows, queryEntry);
|
|
2829
|
+
return {
|
|
2830
|
+
...evaluated,
|
|
2831
|
+
...metadata
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
async function executeDatabaseValidation(validation, options = {}) {
|
|
2835
|
+
if (!isEnabled(process.env.ARCALITY_DB_ENABLED) && !options.allowDisabled) {
|
|
2836
|
+
throw new Error("Database validation is disabled. Set ARCALITY_DB_ENABLED=true to use it.");
|
|
2837
|
+
}
|
|
2838
|
+
const queryId = String(validation?.query_id || validation?.queryId || "").trim();
|
|
2839
|
+
if (!queryId) {
|
|
2840
|
+
throw new Error("Database validation requires query_id.");
|
|
2841
|
+
}
|
|
2842
|
+
const startedAt = Date.now();
|
|
2843
|
+
const catalog = loadDatabaseValidationCatalog(options);
|
|
2844
|
+
const queryEntry = getQueryEntry(catalog, queryId);
|
|
2845
|
+
const timeoutMs = Number(validation.timeout_ms || validation.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
2846
|
+
const provider = resolveDatabaseProvider(options);
|
|
2847
|
+
if (provider === "mcp") {
|
|
2848
|
+
return executeDatabaseValidationViaMcp({ validation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
|
|
2849
|
+
}
|
|
2850
|
+
return executeDatabaseValidationViaPostgres({ validation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
|
|
2851
|
+
}
|
|
2852
|
+
function buildDatabaseValidationPreview(validation, rows, queryEntry = {}, metadata = {}) {
|
|
2853
|
+
const redactSet = normalizeRedactSet(queryEntry.redact, validation?.redact);
|
|
2854
|
+
const normalizedRows = normalizeRows(rows);
|
|
2855
|
+
return {
|
|
2856
|
+
type: "database_validation_preview",
|
|
2857
|
+
name: String(validation?.name || validation?.query_id || validation?.queryId || "database_validation_preview"),
|
|
2858
|
+
query_id: String(validation?.query_id || validation?.queryId || ""),
|
|
2859
|
+
params: redactObject(normalizeDbValue(validation?.params || {}), redactSet),
|
|
2860
|
+
row_count: normalizedRows.length,
|
|
2861
|
+
rows_sample: redactRows(normalizedRows, redactSet),
|
|
2862
|
+
query: {
|
|
2863
|
+
description: String(queryEntry.description || ""),
|
|
2864
|
+
params: Array.isArray(queryEntry.params) ? queryEntry.params.map((name) => String(name)) : []
|
|
2865
|
+
},
|
|
2866
|
+
supports_expect: {
|
|
2867
|
+
exists: true,
|
|
2868
|
+
fields: true
|
|
2869
|
+
},
|
|
2870
|
+
...metadata
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
async function previewDatabaseValidation(validation, options = {}) {
|
|
2874
|
+
if (!isEnabled(process.env.ARCALITY_DB_ENABLED) && !options.allowDisabled) {
|
|
2875
|
+
throw new Error("Database validation is disabled. Set ARCALITY_DB_ENABLED=true to use it.");
|
|
2876
|
+
}
|
|
2877
|
+
const queryId = String(validation?.query_id || validation?.queryId || "").trim();
|
|
2878
|
+
if (!queryId) {
|
|
2879
|
+
throw new Error("Database validation preview requires query_id.");
|
|
2880
|
+
}
|
|
2881
|
+
const startedAt = Date.now();
|
|
2882
|
+
const catalog = loadDatabaseValidationCatalog(options);
|
|
2883
|
+
const queryEntry = getQueryEntry(catalog, queryId);
|
|
2884
|
+
const timeoutMs = Number(validation?.timeout_ms || validation?.timeoutMs || process.env.ARCALITY_DB_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
2885
|
+
const provider = resolveDatabaseProvider(options);
|
|
2886
|
+
const normalizedValidation = {
|
|
2887
|
+
...validation,
|
|
2888
|
+
query_id: queryId,
|
|
2889
|
+
name: String(validation?.name || queryId || "database_validation_preview"),
|
|
2890
|
+
params: isPlainObject2(validation?.params) ? validation.params : {}
|
|
2891
|
+
};
|
|
2892
|
+
const execution = provider === "mcp" ? await queryDatabaseRowsViaMcp({ validation: normalizedValidation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } }) : await queryDatabaseRowsViaPostgres({ validation: normalizedValidation, queryEntry, timeoutMs, startedAt, options: { ...options, catalogPath: catalog.catalogPath } });
|
|
2893
|
+
return buildDatabaseValidationPreview(normalizedValidation, execution.rows, queryEntry, execution.metadata);
|
|
2894
|
+
}
|
|
2895
|
+
async function runDatabaseValidations(validations, options = {}) {
|
|
2896
|
+
const list = Array.isArray(validations) ? validations : [];
|
|
2897
|
+
const startedAt = Date.now();
|
|
2898
|
+
const results = [];
|
|
2899
|
+
for (const validation of list) {
|
|
2900
|
+
try {
|
|
2901
|
+
results.push(await executeDatabaseValidation(validation, options));
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
results.push({
|
|
2904
|
+
name: String(validation?.name || validation?.query_id || validation?.queryId || "database_validation"),
|
|
2905
|
+
query_id: String(validation?.query_id || validation?.queryId || ""),
|
|
2906
|
+
status: "failed",
|
|
2907
|
+
passed: false,
|
|
2908
|
+
row_count: 0,
|
|
2909
|
+
checks: [],
|
|
2910
|
+
rows_sample: [],
|
|
2911
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2912
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const passedCount = results.filter((result) => result.passed).length;
|
|
2917
|
+
return {
|
|
2918
|
+
type: "database_validation_report",
|
|
2919
|
+
status: passedCount === results.length ? "passed" : "failed",
|
|
2920
|
+
passed: results.length > 0 && passedCount === results.length,
|
|
2921
|
+
summary: {
|
|
2922
|
+
total: results.length,
|
|
2923
|
+
passed: passedCount,
|
|
2924
|
+
failed: results.length - passedCount
|
|
2925
|
+
},
|
|
2926
|
+
duration_ms: Date.now() - startedAt,
|
|
2927
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2928
|
+
validations: results
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
function parseDatabaseValidationsFromEnv(env = process.env) {
|
|
2932
|
+
const raw = env.ARCALITY_DB_VALIDATIONS_JSON || env.ARCALITY_DB_VALIDATIONS || "";
|
|
2933
|
+
if (!raw.trim())
|
|
2934
|
+
return [];
|
|
2935
|
+
try {
|
|
2936
|
+
const parsed = JSON.parse(raw);
|
|
2937
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2938
|
+
} catch {
|
|
2939
|
+
return [];
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
async function runDatabaseValidationsFromEnv(options = {}) {
|
|
2943
|
+
const validations = parseDatabaseValidationsFromEnv();
|
|
2944
|
+
if (validations.length === 0)
|
|
2945
|
+
return null;
|
|
2946
|
+
return runDatabaseValidations(validations, options);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/services/executionEvidenceService.mjs
|
|
2950
|
+
function normalizeStatus(value) {
|
|
2951
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
2952
|
+
if (normalized === "passed" || normalized === "pass" || normalized === "success")
|
|
2953
|
+
return "passed";
|
|
2954
|
+
if (normalized === "failed" || normalized === "fail" || normalized === "error")
|
|
2955
|
+
return "failed";
|
|
2956
|
+
return "unknown";
|
|
2957
|
+
}
|
|
2958
|
+
function classifyLogs(logs = []) {
|
|
2959
|
+
const entries = Array.isArray(logs) ? logs.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
|
|
2960
|
+
const errors = entries.filter((entry) => entry.includes("[ERROR]") || entry.includes("[NETWORK_ERROR]"));
|
|
2961
|
+
const warnings = entries.filter((entry) => entry.includes("[WARNING]"));
|
|
2962
|
+
return {
|
|
2963
|
+
total: entries.length,
|
|
2964
|
+
error_count: errors.length,
|
|
2965
|
+
warning_count: warnings.length,
|
|
2966
|
+
recent: entries.slice(-10)
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function summarizeAttachments(attachments = []) {
|
|
2970
|
+
const normalized = Array.isArray(attachments) ? attachments.map((att) => ({
|
|
2971
|
+
name: String(att?.name || "").trim(),
|
|
2972
|
+
contentType: String(att?.contentType || "").trim(),
|
|
2973
|
+
path: att?.path ? String(att.path) : void 0
|
|
2974
|
+
})).filter((att) => att.name || att.contentType || att.path) : [];
|
|
2975
|
+
return {
|
|
2976
|
+
total: normalized.length,
|
|
2977
|
+
images: normalized.filter((att) => att.contentType.startsWith("image/")).length,
|
|
2978
|
+
videos: normalized.filter((att) => att.contentType.startsWith("video/")).length,
|
|
2979
|
+
json: normalized.filter((att) => att.contentType === "application/json").length,
|
|
2980
|
+
names: normalized.map((att) => att.name || att.path || "attachment").slice(0, 12)
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function deriveDatabaseStatus(input) {
|
|
2984
|
+
const explicit = normalizeStatus(input?.database_status);
|
|
2985
|
+
if (explicit !== "unknown")
|
|
2986
|
+
return explicit;
|
|
2987
|
+
if (typeof input?.database_report?.passed === "boolean") {
|
|
2988
|
+
return input.database_report.passed ? "passed" : "failed";
|
|
2989
|
+
}
|
|
2990
|
+
return "unknown";
|
|
2991
|
+
}
|
|
2992
|
+
function buildContradictions(uiStatus, dbStatus) {
|
|
2993
|
+
const contradictions = [];
|
|
2994
|
+
if (uiStatus === "passed" && dbStatus === "failed")
|
|
2995
|
+
contradictions.push("ui_passed_but_db_failed");
|
|
2996
|
+
if (uiStatus === "failed" && dbStatus === "passed")
|
|
2997
|
+
contradictions.push("ui_failed_but_db_passed");
|
|
2998
|
+
return contradictions;
|
|
2999
|
+
}
|
|
3000
|
+
function buildRemediationHints(finalStatus, contradictions, consoleSummary) {
|
|
3001
|
+
const hints = [];
|
|
3002
|
+
if (contradictions.includes("ui_passed_but_db_failed")) {
|
|
3003
|
+
hints.push("Revisar la persistencia backend o la confirmacion visual de guardado.");
|
|
3004
|
+
}
|
|
3005
|
+
if (contradictions.includes("ui_failed_but_db_passed")) {
|
|
3006
|
+
hints.push("Revisar si la UI muestra un error falso o si el flujo se completo parcialmente.");
|
|
3007
|
+
}
|
|
3008
|
+
if (consoleSummary.error_count > 0) {
|
|
3009
|
+
hints.push("Inspeccionar errores de consola o red asociados al momento de la falla.");
|
|
3010
|
+
}
|
|
3011
|
+
if (finalStatus === "unknown") {
|
|
3012
|
+
hints.push("Completar evidencia faltante antes de cerrar la conclusion.");
|
|
3013
|
+
}
|
|
3014
|
+
return hints;
|
|
3015
|
+
}
|
|
3016
|
+
function summarizeExecutionEvidence(input = {}, context = {}) {
|
|
3017
|
+
const uiStatus = normalizeStatus(input.ui_status);
|
|
3018
|
+
const dbStatus = deriveDatabaseStatus(input);
|
|
3019
|
+
const consoleSummary = classifyLogs(context.logs || input.console_logs);
|
|
3020
|
+
const attachmentSummary = summarizeAttachments(context.attachments || input.attachments);
|
|
3021
|
+
const contradictions = buildContradictions(uiStatus, dbStatus);
|
|
3022
|
+
let finalStatus = "unknown";
|
|
3023
|
+
if (contradictions.length > 0) {
|
|
3024
|
+
finalStatus = "contradiction";
|
|
3025
|
+
} else if (uiStatus === "failed" || dbStatus === "failed") {
|
|
3026
|
+
finalStatus = "failed";
|
|
3027
|
+
} else if (uiStatus === "passed" || dbStatus === "passed") {
|
|
3028
|
+
finalStatus = "passed";
|
|
3029
|
+
}
|
|
3030
|
+
const remediationHints = buildRemediationHints(finalStatus, contradictions, consoleSummary);
|
|
3031
|
+
const missionName = String(input.mission_name || input.name || "execution_evidence").trim();
|
|
3032
|
+
const expectedOutcome = String(input.expected_business_outcome || "").trim();
|
|
3033
|
+
const uiSummary = String(input.ui_summary || "").trim();
|
|
3034
|
+
const validationName = String(input.database_validation_name || input.database_report?.name || "").trim();
|
|
3035
|
+
const queryId = String(input.query_id || input.database_report?.query_id || "").trim();
|
|
3036
|
+
const summary = finalStatus === "contradiction" ? "La evidencia es contradictoria entre UI y base de datos." : finalStatus === "failed" ? "La ejecucion contiene evidencia suficiente para marcar falla." : finalStatus === "passed" ? "La evidencia disponible respalda un resultado exitoso." : "La ejecucion requiere mas evidencia para una conclusion firme.";
|
|
3037
|
+
return {
|
|
3038
|
+
type: "execution_evidence_summary",
|
|
3039
|
+
mission_name: missionName,
|
|
3040
|
+
final_status: finalStatus,
|
|
3041
|
+
summary,
|
|
3042
|
+
expected_business_outcome: expectedOutcome || void 0,
|
|
3043
|
+
current_url: context.current_url || input.current_url || void 0,
|
|
3044
|
+
ui: {
|
|
3045
|
+
status: uiStatus,
|
|
3046
|
+
summary: uiSummary || void 0
|
|
3047
|
+
},
|
|
3048
|
+
database: {
|
|
3049
|
+
status: dbStatus,
|
|
3050
|
+
validation_name: validationName || void 0,
|
|
3051
|
+
query_id: queryId || void 0
|
|
3052
|
+
},
|
|
3053
|
+
console: consoleSummary,
|
|
3054
|
+
attachments: attachmentSummary,
|
|
3055
|
+
contradictions,
|
|
3056
|
+
remediation_hints: remediationHints,
|
|
3057
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
|
|
2091
3061
|
// tests/_helpers/ai-agent-helper.ts
|
|
2092
3062
|
var qaSecurityTools = [
|
|
2093
3063
|
{
|
|
@@ -2190,11 +3160,11 @@ var AIAgentHelper = class {
|
|
|
2190
3160
|
}
|
|
2191
3161
|
});
|
|
2192
3162
|
this.page.on("pageerror", (exception) => {
|
|
2193
|
-
this.criticalJsError = `Excepci\
|
|
3163
|
+
this.criticalJsError = `Excepci\uFFFDn JS no manejada: ${exception.message}`;
|
|
2194
3164
|
this.logs.push(`[JS_CRASH] ${exception.message}`);
|
|
2195
3165
|
});
|
|
2196
3166
|
this.page.on("crash", () => {
|
|
2197
|
-
this.criticalJsError = `Browser Crash: La pesta\
|
|
3167
|
+
this.criticalJsError = `Browser Crash: La pesta\uFFFDa del navegador colaps\uFFFD inesperadamente.`;
|
|
2198
3168
|
});
|
|
2199
3169
|
}
|
|
2200
3170
|
clearNetworkError() {
|
|
@@ -2204,54 +3174,51 @@ var AIAgentHelper = class {
|
|
|
2204
3174
|
try {
|
|
2205
3175
|
let skillsContext = "";
|
|
2206
3176
|
let loadedCount = 0;
|
|
2207
|
-
const toolsRoot = process.env.ARCALITY_ROOT ||
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
const skillPath = path5.join(rootDir, entry.name, "SKILL.md");
|
|
2224
|
-
if (fs5.existsSync(skillPath)) {
|
|
2225
|
-
content = fs5.readFileSync(skillPath, "utf8");
|
|
2226
|
-
}
|
|
2227
|
-
} else if (entry.name.endsWith(".md")) {
|
|
2228
|
-
const skillPath = path5.join(rootDir, entry.name);
|
|
2229
|
-
content = fs5.readFileSync(skillPath, "utf8");
|
|
3177
|
+
const toolsRoot = process.env.ARCALITY_ROOT || path7.join(__dirname, "..", "..");
|
|
3178
|
+
const rootDir = path7.join(toolsRoot, ".agents", "skills");
|
|
3179
|
+
if (!fs6.existsSync(rootDir)) {
|
|
3180
|
+
return "";
|
|
3181
|
+
}
|
|
3182
|
+
if (loadedCount === 0) {
|
|
3183
|
+
skillsContext += "\nSKILLS INSTALADAS (Metodologias activas):\n";
|
|
3184
|
+
}
|
|
3185
|
+
const entries = fs6.readdirSync(rootDir, { withFileTypes: true });
|
|
3186
|
+
for (const entry of entries) {
|
|
3187
|
+
let content = "";
|
|
3188
|
+
let skillName = entry.name;
|
|
3189
|
+
if (entry.isDirectory()) {
|
|
3190
|
+
const skillPath = path7.join(rootDir, entry.name, "SKILL.md");
|
|
3191
|
+
if (fs6.existsSync(skillPath)) {
|
|
3192
|
+
content = fs6.readFileSync(skillPath, "utf8");
|
|
2230
3193
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
3194
|
+
} else if (entry.name === "SKILL.md") {
|
|
3195
|
+
const skillPath = path7.join(rootDir, entry.name);
|
|
3196
|
+
skillName = path7.basename(rootDir);
|
|
3197
|
+
content = fs6.readFileSync(skillPath, "utf8");
|
|
3198
|
+
}
|
|
3199
|
+
if (content) {
|
|
3200
|
+
skillsContext += `
|
|
2233
3201
|
--- SKILL: ${skillName} ---
|
|
2234
3202
|
${content}
|
|
2235
3203
|
`;
|
|
2236
|
-
|
|
2237
|
-
}
|
|
3204
|
+
loadedCount++;
|
|
2238
3205
|
}
|
|
2239
3206
|
}
|
|
2240
3207
|
if (loadedCount > 0) {
|
|
2241
3208
|
if (process.env.DEBUG)
|
|
2242
|
-
console.log(`>>
|
|
3209
|
+
console.log(`>> [skills] ${loadedCount} habilidades QA inyectadas al cerebro.`);
|
|
2243
3210
|
return skillsContext;
|
|
2244
3211
|
}
|
|
2245
3212
|
} catch (e) {
|
|
2246
|
-
console.warn(`>>ARCALITY_STATUS>>
|
|
3213
|
+
console.warn(`>>ARCALITY_STATUS>> Error cargando habilidades: ${e.message}`);
|
|
2247
3214
|
}
|
|
2248
3215
|
return "";
|
|
2249
3216
|
}
|
|
2250
3217
|
loadMemory(prompt) {
|
|
2251
3218
|
try {
|
|
2252
|
-
const memoryFile =
|
|
2253
|
-
if (
|
|
2254
|
-
const memories = JSON.parse(
|
|
3219
|
+
const memoryFile = path7.join(this.contextDir, "memoria-agente.json");
|
|
3220
|
+
if (fs6.existsSync(memoryFile)) {
|
|
3221
|
+
const memories = JSON.parse(fs6.readFileSync(memoryFile, "utf8"));
|
|
2255
3222
|
const keywords = prompt.toLowerCase().split(" ").filter((w) => w.length > 4);
|
|
2256
3223
|
const relevantSuccess = memories.filter((m) => {
|
|
2257
3224
|
if (!m.success)
|
|
@@ -2265,7 +3232,7 @@ ${content}
|
|
|
2265
3232
|
if (relevantSuccess.length) {
|
|
2266
3233
|
const best = relevantSuccess[0];
|
|
2267
3234
|
memoryContext += `
|
|
2268
|
-
|
|
3235
|
+
?? SUCCESS GUIDANCE FOR THIS MISSION:
|
|
2269
3236
|
The following steps led to SUCCESS for a similar mission ("${best.prompt}"):
|
|
2270
3237
|
${best.steps.slice(0, 15).map((s) => ` - ${s}`).join("\n")}
|
|
2271
3238
|
(Use these steps as a roadmap if the current UI allows it!)
|
|
@@ -2273,7 +3240,7 @@ ${best.steps.slice(0, 15).map((s) => ` - ${s}`).join("\n")}
|
|
|
2273
3240
|
}
|
|
2274
3241
|
if (relevantFailures.length) {
|
|
2275
3242
|
memoryContext += `
|
|
2276
|
-
|
|
3243
|
+
?? PREVIOUS FAILURES TO AVOID:
|
|
2277
3244
|
${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
2278
3245
|
`;
|
|
2279
3246
|
}
|
|
@@ -2325,11 +3292,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
2325
3292
|
const role = htmlEl.getAttribute("role") || "";
|
|
2326
3293
|
const text = (htmlEl.innerText || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || "").trim().replace(/\s+/g, " ");
|
|
2327
3294
|
const textLower = text.toLowerCase();
|
|
2328
|
-
const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\
|
|
3295
|
+
const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\uFFFD") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("inv\uFFFDlido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido") || /* Validaciones ZOD / Wizard de campos de formulario */
|
|
2329
3296
|
textLower.includes("debe ser mayor") || textLower.includes("debe ser menor") || textLower.includes("debe ser igual") || textLower.includes("debe ser mayor o igual") || textLower.includes("debe ser menor o igual") || /* ZOD date messages (Spanish) */
|
|
2330
|
-
textLower.includes("debe ser posterior") || textLower.includes("debe ser anterior") || textLower.includes("debe ser una fecha") || textLower.includes("fecha debe ser") || textLower.includes("posterior o igual a la fecha") || textLower.includes("anterior o igual a la fecha") || textLower.includes("posterior a la fecha") || textLower.includes("anterior a la fecha") || textLower.includes("fecha") && (textLower.includes("actual") || textLower.includes("pasada") || textLower.includes("futura") || textLower.includes("requerida") || textLower.includes("inv\
|
|
2331
|
-
textLower.includes("debe ser un n\
|
|
2332
|
-
textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower === "requerido" || textLower === "obligatorio" || textLower.includes("este campo es requerido") || textLower.includes("este campo es obligatorio") || textLower.includes("no puede estar vac\
|
|
3297
|
+
textLower.includes("debe ser posterior") || textLower.includes("debe ser anterior") || textLower.includes("debe ser una fecha") || textLower.includes("fecha debe ser") || textLower.includes("posterior o igual a la fecha") || textLower.includes("anterior o igual a la fecha") || textLower.includes("posterior a la fecha") || textLower.includes("anterior a la fecha") || textLower.includes("fecha") && (textLower.includes("actual") || textLower.includes("pasada") || textLower.includes("futura") || textLower.includes("requerida") || textLower.includes("inv\uFFFDlida") || textLower.includes("incorrecta") || textLower.includes("posterior") || textLower.includes("anterior")) || /* ZOD number/range messages */
|
|
3298
|
+
textLower.includes("debe ser un n\uFFFDmero") || textLower.includes("debe ser positivo") || textLower.includes("debe ser negativo") || textLower.includes("m\uFFFDnimo") && textLower.includes("caracteres") || textLower.includes("m\uFFFDximo") && textLower.includes("caracteres") || /* Generic ZOD/form required � must be specific, NOT broad instructional text */
|
|
3299
|
+
textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower === "requerido" || textLower === "obligatorio" || textLower.includes("este campo es requerido") || textLower.includes("este campo es obligatorio") || textLower.includes("no puede estar vac\uFFFDo") || textLower.includes("no puede ser") || textLower.includes("no es v\uFFFDlido") || textLower.includes("no es correcto") || textLower.includes("formato incorrecto") || textLower.includes("formato inv\uFFFDlido") || textLower.includes("valor no permitido")) && text.length < 200 && !textLower.includes("recuerda") && !textLower.includes("antes de guardar");
|
|
2333
3300
|
const isCriticalSuccess = (textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") && text.length < 100) && text.length < 200;
|
|
2334
3301
|
const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
|
|
2335
3302
|
const isFormSelection = tag === "input" && (htmlEl.getAttribute("type") === "radio" || htmlEl.getAttribute("type") === "checkbox") || (role === "radio" || role === "checkbox" || role === "switch");
|
|
@@ -2444,8 +3411,8 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
2444
3411
|
let type = "INFO";
|
|
2445
3412
|
const isErrorClass = classLower.includes("error") || classLower.includes("danger") || classLower.includes("fail");
|
|
2446
3413
|
const isError = isCriticalFailure || isErrorClass && text.split(" ").length > 2;
|
|
2447
|
-
const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\
|
|
2448
|
-
const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\
|
|
3414
|
+
const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\uFFFDxito");
|
|
3415
|
+
const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\uFFFDa");
|
|
2449
3416
|
if (tag === "input" || tag === "textarea" || tag === "select" || isFormSelection)
|
|
2450
3417
|
type = "FIELD";
|
|
2451
3418
|
else if (tag === "button" || role === "button" || tag === "a" || role === "link" || isTab)
|
|
@@ -2458,7 +3425,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
2458
3425
|
type = isSuccess ? "SUCCESS_MESSAGE" : isError ? "ERROR_MESSAGE" : "MESSAGE";
|
|
2459
3426
|
}
|
|
2460
3427
|
if (type.includes("MESSAGE")) {
|
|
2461
|
-
const prefix = isError ? "
|
|
3428
|
+
const prefix = isError ? "?? [ERROR_CR\uFFFDTICO] " : isSuccess ? "? [\uFFFDXITO_DETECTADO] " : "[INFO] ";
|
|
2462
3429
|
name = prefix + (name || text || "Mensaje del sistema");
|
|
2463
3430
|
}
|
|
2464
3431
|
if (type === "ACTION" || type === "FIELD") {
|
|
@@ -2510,23 +3477,23 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
2510
3477
|
} catch {
|
|
2511
3478
|
}
|
|
2512
3479
|
}
|
|
2513
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3480
|
+
console.log(`>>ARCALITY_STATUS>> ?? Percepci\uFFFDn profunda encontr\uFFFD ${allComponents.length} componentes`);
|
|
2514
3481
|
const urlForBlankCheck = this.page.url();
|
|
2515
3482
|
const isAuthPage = urlForBlankCheck.includes("/login") || urlForBlankCheck.includes("/auth") || urlForBlankCheck.includes("oauth") || urlForBlankCheck.includes("/#/");
|
|
2516
3483
|
if (allComponents.length < 10 && !isAuthPage) {
|
|
2517
3484
|
const bodyTextLength = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
|
|
2518
3485
|
if (bodyTextLength < 80) {
|
|
2519
3486
|
console.warn(`
|
|
2520
|
-
|
|
3487
|
+
?? [BLANK_STATE] Primera medici\uFFFDn: ${allComponents.length} componentes, ${bodyTextLength} chars. Esperando re-render...`);
|
|
2521
3488
|
await this.page.waitForTimeout(2500);
|
|
2522
3489
|
const bodyTextLength2 = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
|
|
2523
3490
|
const components2 = await this.page.evaluate(() => document.querySelectorAll("a, button, input, select, textarea").length).catch(() => 999);
|
|
2524
3491
|
if (bodyTextLength2 < 80 && components2 < 10) {
|
|
2525
3492
|
console.error(`
|
|
2526
|
-
|
|
2527
|
-
throw new Error(`[BLANK_STATE_CRASH] El portal carg\
|
|
3493
|
+
?? [BLANK_STATE_CRASH] Vista en blanco confirmada tras espera (Comps: ${components2}, Texto: ${bodyTextLength2} chars). URL: ${urlForBlankCheck}`);
|
|
3494
|
+
throw new Error(`[BLANK_STATE_CRASH] El portal carg\uFFFD en blanco. Esto es un fallo de renderizado del portal, no de Arcality.`);
|
|
2528
3495
|
} else {
|
|
2529
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3496
|
+
console.log(`>>ARCALITY_STATUS>> ? [BLANK_STATE] Falsa alarma. P\uFFFDgina recuper\uFFFD contenido tras espera (${components2} comps, ${bodyTextLength2} chars).`);
|
|
2530
3497
|
}
|
|
2531
3498
|
}
|
|
2532
3499
|
}
|
|
@@ -2608,7 +3575,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
2608
3575
|
}
|
|
2609
3576
|
}
|
|
2610
3577
|
} catch (e) {
|
|
2611
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3578
|
+
console.log(`>>ARCALITY_STATUS>> ?? Error en hook de ingesta: ${e.message}`);
|
|
2612
3579
|
}
|
|
2613
3580
|
return {
|
|
2614
3581
|
url,
|
|
@@ -2692,18 +3659,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2692
3659
|
}
|
|
2693
3660
|
}
|
|
2694
3661
|
/**
|
|
2695
|
-
* Busca una
|
|
3662
|
+
* Busca una acci�n sugerida en la memoria SIN usar tokens.
|
|
2696
3663
|
* Compara URL, componentes visuales Y palabras clave del prompt.
|
|
2697
3664
|
*/
|
|
2698
3665
|
async getActionFromGuia(prompt, historyLength) {
|
|
2699
3666
|
try {
|
|
2700
|
-
const
|
|
2701
|
-
const
|
|
2702
|
-
const memoryFile =
|
|
2703
|
-
if (!
|
|
3667
|
+
const fs8 = require("fs");
|
|
3668
|
+
const path9 = require("path");
|
|
3669
|
+
const memoryFile = path9.join(this.contextDir, "memoria-agente.json");
|
|
3670
|
+
if (!fs8.existsSync(memoryFile)) {
|
|
2704
3671
|
return null;
|
|
2705
3672
|
}
|
|
2706
|
-
const memories = JSON.parse(
|
|
3673
|
+
const memories = JSON.parse(fs8.readFileSync(memoryFile, "utf8"));
|
|
2707
3674
|
const state = await this.getPageState();
|
|
2708
3675
|
const currentUrl = this.page.url();
|
|
2709
3676
|
const extractCriticalKeywords = (text) => {
|
|
@@ -2717,8 +3684,8 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2717
3684
|
keywords.add("tercera");
|
|
2718
3685
|
if (lower.includes("cuarta"))
|
|
2719
3686
|
keywords.add("cuarta");
|
|
2720
|
-
if (lower.includes("\
|
|
2721
|
-
keywords.add("\
|
|
3687
|
+
if (lower.includes("\uFFFDltima"))
|
|
3688
|
+
keywords.add("\uFFFDltima");
|
|
2722
3689
|
if (lower.includes("first"))
|
|
2723
3690
|
keywords.add("primera");
|
|
2724
3691
|
if (lower.includes("second"))
|
|
@@ -2728,11 +3695,11 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2728
3695
|
if (lower.includes("fourth"))
|
|
2729
3696
|
keywords.add("cuarta");
|
|
2730
3697
|
if (lower.includes("last"))
|
|
2731
|
-
keywords.add("\
|
|
3698
|
+
keywords.add("\uFFFDltima");
|
|
2732
3699
|
return keywords;
|
|
2733
3700
|
};
|
|
2734
3701
|
const currentKeywords = extractCriticalKeywords(prompt);
|
|
2735
|
-
const currentVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\
|
|
3702
|
+
const currentVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\uFFFDade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => prompt.toLowerCase().includes(v));
|
|
2736
3703
|
const matchingRun = memories.filter((m) => m.success && m.steps_data && m.steps_data[historyLength]).reverse().find((m) => {
|
|
2737
3704
|
const memPrompt = (m.prompt || "").toLowerCase();
|
|
2738
3705
|
const currentPrompt = prompt.toLowerCase();
|
|
@@ -2744,7 +3711,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2744
3711
|
const matchRatio = matches / words.length;
|
|
2745
3712
|
if (matchRatio < 0.85)
|
|
2746
3713
|
return false;
|
|
2747
|
-
const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\
|
|
3714
|
+
const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\uFFFDade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
|
|
2748
3715
|
const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
|
|
2749
3716
|
const hasOppositeIntent = currentVerbs.some((v) => ["elimina", "borra", "delete"].includes(v)) && memVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) || currentVerbs.some((v) => ["crea", "nueva", "create", "new"].includes(v)) && memVerbs.some((v) => ["elimina", "borra", "delete"].includes(v));
|
|
2750
3717
|
if (hasOppositeIntent)
|
|
@@ -2805,21 +3772,21 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2805
3772
|
}
|
|
2806
3773
|
const foundComponent = bestMatchScore >= 40 ? bestMatch : null;
|
|
2807
3774
|
if (!foundComponent && memStep.action_data?.action !== "wait") {
|
|
2808
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3775
|
+
console.log(`>>ARCALITY_STATUS>> ?? Descartado (Componente visual no encontrado: ${memStep.componentName})`);
|
|
2809
3776
|
return null;
|
|
2810
3777
|
}
|
|
2811
3778
|
if (foundComponent && memStep.action_data?.action === "fill") {
|
|
2812
3779
|
const compType = (foundComponent.type || "").toLowerCase();
|
|
2813
3780
|
const compName = (foundComponent.name || "").toLowerCase();
|
|
2814
|
-
const looksLikeDropdown = compType.includes("select") || compType.includes("combobox") || compName.includes("tipo") || compName.includes("type") || compName.includes("categor\
|
|
3781
|
+
const looksLikeDropdown = compType.includes("select") || compType.includes("combobox") || compName.includes("tipo") || compName.includes("type") || compName.includes("categor\uFFFDa") || compName.includes("estado") || compName.includes("frecuencia") || compName.includes("alcance");
|
|
2815
3782
|
if (looksLikeDropdown) {
|
|
2816
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3783
|
+
console.log(`>>ARCALITY_STATUS>> ?? Descartado (fill en dropdown detectado: ${foundComponent.name}). Delegando a IA.`);
|
|
2817
3784
|
return null;
|
|
2818
3785
|
}
|
|
2819
3786
|
}
|
|
2820
3787
|
const memCompName = memStep.componentName || "";
|
|
2821
3788
|
if (memCompName.length > 80) {
|
|
2822
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3789
|
+
console.log(`>>ARCALITY_STATUS>> ?? Descartado (Mensaje enorme o nombre inconsistente: ${memCompName.substring(0, 20)}...)`);
|
|
2823
3790
|
return null;
|
|
2824
3791
|
}
|
|
2825
3792
|
if (!memCompName || memCompName.length < 3) {
|
|
@@ -2829,18 +3796,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2829
3796
|
let actionData = { ...memStep.action_data };
|
|
2830
3797
|
if (actionData.value === "{{secret}}") {
|
|
2831
3798
|
const cName = (foundComponent.name || "").toLowerCase();
|
|
2832
|
-
if (cName.includes("password") || cName.includes("contrase\
|
|
3799
|
+
if (cName.includes("password") || cName.includes("contrase\uFFFDa")) {
|
|
2833
3800
|
actionData.value = "Qa_Test123!";
|
|
2834
3801
|
} else if (cName.includes("email") || cName.includes("correo")) {
|
|
2835
3802
|
actionData.value = `qa_${Date.now()}@test.com`;
|
|
2836
3803
|
} else if (cName.includes("descrip")) {
|
|
2837
|
-
actionData.value = `Descripci\
|
|
3804
|
+
actionData.value = `Descripci\uFFFDn QA Automatizada ${Date.now()}`;
|
|
2838
3805
|
} else {
|
|
2839
3806
|
actionData.value = `QA_Test_${Date.now()}`;
|
|
2840
3807
|
}
|
|
2841
3808
|
}
|
|
2842
3809
|
return {
|
|
2843
|
-
thought: `[GU\
|
|
3810
|
+
thought: `[GU\uFFFDA DE \uFFFDXITO] Reutilizando paso maestro aprendido: ${actionData.action} en ${foundComponent.name}`,
|
|
2844
3811
|
actions: [{
|
|
2845
3812
|
...actionData,
|
|
2846
3813
|
idx: foundComponent.idx,
|
|
@@ -2962,12 +3929,18 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2962
3929
|
};
|
|
2963
3930
|
}
|
|
2964
3931
|
const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
|
|
2965
|
-
|
|
3932
|
+
?? QA CREDENTIALS (Use if login is needed):
|
|
2966
3933
|
- User: ${process.env.LOGIN_USER}
|
|
2967
3934
|
- Password: ${process.env.LOGIN_PASSWORD}
|
|
2968
3935
|
` : "";
|
|
2969
3936
|
const memoryContext = this.loadMemory(prompt);
|
|
2970
3937
|
const skillsContext = this.loadSkills();
|
|
3938
|
+
const databaseValidationEnabled = ["1", "true", "yes", "on"].includes(String(process.env.ARCALITY_DB_ENABLED || "").toLowerCase());
|
|
3939
|
+
const databaseProfile = process.env.ARCALITY_DB_PROFILE || "qa_local";
|
|
3940
|
+
const databaseContext = databaseValidationEnabled ? `
|
|
3941
|
+
# DATABASE VALIDATION CONTEXT
|
|
3942
|
+
PostgreSQL validation is enabled for profile "${databaseProfile}". If the mission asks to corroborate backend persistence or database state, use \`validate_database_state\` after the relevant UI action succeeds. If you need to verify query_id, params, connectivity, or returned rows before asserting, use \`preview_db_validation\`. Use only query_id values from the local catalog; never invent raw SQL.
|
|
3943
|
+
` : "";
|
|
2971
3944
|
let assetContext = "";
|
|
2972
3945
|
try {
|
|
2973
3946
|
const { formatUploadAssetContext: formatUploadAssetContext2 } = (init_asset_resolver(), __toCommonJS(asset_resolver_exports));
|
|
@@ -2980,7 +3953,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2980
3953
|
const runtimeProjectRoot = process.env.ARCALITY_PROJECT_ROOT || process.cwd();
|
|
2981
3954
|
const ctx = getProjectContext(runtimeProjectRoot);
|
|
2982
3955
|
projectInfo = `
|
|
2983
|
-
|
|
3956
|
+
??? PROJECT CONTEXT:
|
|
2984
3957
|
- Name: ${ctx.name}
|
|
2985
3958
|
- Tech Stack: ${ctx.framework}
|
|
2986
3959
|
- Environment: ${process.env.NODE_ENV || "development"}
|
|
@@ -2993,11 +3966,11 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
2993
3966
|
backendContextData = await this.knowledgeService.getContext(state.url);
|
|
2994
3967
|
if (backendContextData && (backendContextData.rules?.length > 0 || backendContextData.fields?.length > 0)) {
|
|
2995
3968
|
if (process.env.DEBUG)
|
|
2996
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3969
|
+
console.log(`>>ARCALITY_STATUS>> ?? Contexto de Memoria Colectiva inyectado (${backendContextData.rules?.length || 0} reglas, ${backendContextData.fields?.length || 0} campos).`);
|
|
2997
3970
|
}
|
|
2998
3971
|
} catch (e) {
|
|
2999
3972
|
if (process.env.DEBUG)
|
|
3000
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
3973
|
+
console.log(`>>ARCALITY_STATUS>> ?? Error en hook de contexto: ${e.message}`);
|
|
3001
3974
|
}
|
|
3002
3975
|
let customUserContext = "";
|
|
3003
3976
|
try {
|
|
@@ -3020,7 +3993,7 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
3020
3993
|
}
|
|
3021
3994
|
}
|
|
3022
3995
|
if (qaContextAnalysis.exists || backendContextData?.rules?.length || backendContextData?.fields?.length) {
|
|
3023
|
-
console.log(`>>ARCALITY_STATUS>> Pol\
|
|
3996
|
+
console.log(`>>ARCALITY_STATUS>> Pol\uFFFDtica efectiva de contexto: local qa-context > memoria backend > heur\uFFFDsticas base.`);
|
|
3024
3997
|
}
|
|
3025
3998
|
this.qaContextStatusLogged = true;
|
|
3026
3999
|
}
|
|
@@ -3035,22 +4008,22 @@ ${JSON.stringify(diagnosticPayload, null, 2)}`
|
|
|
3035
4008
|
{
|
|
3036
4009
|
type: "text",
|
|
3037
4010
|
text: `# IDENTITY
|
|
3038
|
-
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 \
|
|
4011
|
+
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 \uFFFD guessing and blind retries are UNACCEPTABLE.
|
|
3039
4012
|
|
|
3040
4013
|
`
|
|
3041
4014
|
},
|
|
3042
4015
|
{
|
|
3043
4016
|
type: "text",
|
|
3044
4017
|
text: `
|
|
3045
|
-
# METHODOLOGY (MANDATORY \
|
|
4018
|
+
# METHODOLOGY (MANDATORY \uFFFD Follow in order)
|
|
3046
4019
|
Every turn, you MUST follow these 4 phases:
|
|
3047
4020
|
|
|
3048
4021
|
## Phase 1: OBSERVE
|
|
3049
4022
|
- Read ALL components in CURRENT COMPONENTS carefully.
|
|
3050
|
-
- Check the URL \
|
|
3051
|
-
- Read HISTORY \
|
|
3052
|
-
- Look for MESSAGE components \
|
|
3053
|
-
- If you see [INFERRED] in any component name
|
|
4023
|
+
- Check the URL \uFFFD are you on the right page?
|
|
4024
|
+
- Read HISTORY \uFFFD what have you already done? Don't repeat actions.
|
|
4025
|
+
- Look for MESSAGE components \uFFFD errors ([ERROR_CR\uFFFDTICO]) or success messages.
|
|
4026
|
+
- If you see [INFERRED] in any component name ? that name is UNRELIABLE. You MUST call \`inspect_element_details\` on it BEFORE interacting.
|
|
3054
4027
|
|
|
3055
4028
|
## Phase 2: PLAN
|
|
3056
4029
|
- Based on your observations, decide: What is the ONE next meaningful step toward completing the mission?
|
|
@@ -3065,10 +4038,10 @@ Every turn, you MUST follow these 4 phases:
|
|
|
3065
4038
|
- **STEP 1 (MANDATORY)**: Click the dropdown/combobox trigger to OPEN it. Wait for the list to appear.
|
|
3066
4039
|
- **STEP 2 (MANDATORY, SAME or NEXT turn)**: Click the desired option directly (\`[li]\` or \`[option]\`). Do NOT use \`fill\` to type text.
|
|
3067
4040
|
- **STEP 3**: If more fields remain in the same form section, do NOT group them in the same turn. Wait for re-render first.
|
|
3068
|
-
- **NEVER** insert a \`wait\` action between clicking the trigger and clicking the option \
|
|
4041
|
+
- **NEVER** insert a \`wait\` action between clicking the trigger and clicking the option \uFFFD complete both in one turn.
|
|
3069
4042
|
- **STRICTLY FORBIDDEN**: Sending Escape repeatedly. After selecting an option, click the NEXT logical button directly.
|
|
3070
|
-
- For menus: Click menu icon
|
|
3071
|
-
- For \`<input type="date">\` native fields: NEVER use \`fill\`. ALWAYS use \`interact_native_control\` with value in YYYY-MM-DD format. Do this ONCE per field. If it fails, do NOT retry \
|
|
4043
|
+
- For menus: Click menu icon ? WAIT for next turn ? Click option.
|
|
4044
|
+
- For \`<input type="date">\` native fields: NEVER use \`fill\`. ALWAYS use \`interact_native_control\` with value in YYYY-MM-DD format. Do this ONCE per field. If it fails, do NOT retry \uFFFD move on and click Siguiente to see the validation message.
|
|
3072
4045
|
|
|
3073
4046
|
## Phase 4: VERIFY
|
|
3074
4047
|
- After critical actions (SAVE/CREATE/DELETE), verify the result:
|
|
@@ -3087,26 +4060,28 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
3087
4060
|
| Element name has [INFERRED] | \`inspect_element_details\` | BEFORE interacting with it |
|
|
3088
4061
|
| Before clicking SAVE/GUARDAR | \`validate_element_state\` | To confirm button is enabled |
|
|
3089
4062
|
| After successful SAVE | \`extract_table_data\` | To verify record was created |
|
|
4063
|
+
| Mission asks to validate PostgreSQL/backend data | \`validate_database_state\` | AFTER the relevant UI action succeeds, using a query_id from the local catalog |
|
|
4064
|
+
| Need to dry-run a database query_id or inspect returned rows before asserting | \`preview_db_validation\` | BEFORE the mission or before adding expect rules |
|
|
3090
4065
|
| UI is frozen/unresponsive | \`capture_console_errors\` | To check for JS errors |
|
|
3091
4066
|
| Mission mentions a concrete route like '/counter' and current URL is different, or the route has no visible menu item | \`navigate_to_url\` | IMMEDIATELY instead of improvising clicks |
|
|
3092
4067
|
| Bug found in application | \`create_test_evidence\` | To document with annotated screenshot |
|
|
3093
4068
|
| Mission complete | \`create_test_evidence\` | To document the final successful state |
|
|
3094
4069
|
| Modal/popup blocking page | \`send_keyboard_event\` (Escape) | To close the overlay |
|
|
3095
|
-
| Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\
|
|
3096
|
-
| Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \
|
|
4070
|
+
| Masked numeric/code input, OTP/PIN/CLABE/INE identifier, or prompt says "manualmente", "d\uFFFDgito por d\uFFFDgito", "car\uFFFDcter por car\uFFFDcter" | \`type_sequentially\` | After ONE focus attempt on the field; do not keep clicking |
|
|
4071
|
+
| Time/Date/Color picker field | \`interact_native_control\` | INSTEAD of fill \uFFFD native inputs need special handling |
|
|
3097
4072
|
| Element not found on screen | \`scroll_page\` | To reveal off-screen content |
|
|
3098
|
-
| Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \
|
|
3099
|
-
| Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \
|
|
4073
|
+
| Need to close dropdown/menu | \`send_keyboard_event\` (Escape) | IMMEDIATELY after selecting any option \uFFFD MANDATORY |
|
|
4074
|
+
| Dropdown selected but still visually open | \`send_keyboard_event\` (Tab) | If Escape did not close it \uFFFD force field to lose focus |
|
|
3100
4075
|
| Navigate between form sections | \`send_keyboard_event\` (Tab) | To move focus between fields |
|
|
3101
4076
|
|
|
3102
4077
|
# CRITICAL RULES
|
|
3103
4078
|
1. **NEVER BLIND-RETRY**: If an action fails, you MUST investigate with \`inspect_element_details\` before retrying. Repeating the exact same failed action is FORBIDDEN.
|
|
3104
4079
|
2. **NO REPEAT ACTIONS**: Check HISTORY. If you already filled a field or clicked a button, do NOT do it again unless the UI clearly reset OR the button belongs to a different step/modal/section mentioned in the mission (e.g., Save in modal vs Save in main form).
|
|
3105
4080
|
3. **GROUP ACTIONS CAREFULLY**: You MUST group non-conflicting actions in a single turn to save time and tokens.
|
|
3106
|
-
- You can \`fill\` multiple plain text inputs in the SAME turn (Nombre, Descripci\
|
|
4081
|
+
- You can \`fill\` multiple plain text inputs in the SAME turn (Nombre, Descripci\uFFFDn, etc.).
|
|
3107
4082
|
- You CANNOT mix dropdown selection with filling other fields in the same turn (React re-renders invalidate idx).
|
|
3108
|
-
- For \`<input type="date">\` fields: use \`interact_native_control\` ONCE per field \
|
|
3109
|
-
- NEVER insert a \`wait\` action between related dropdown steps (click trigger
|
|
4083
|
+
- For \`<input type="date">\` fields: use \`interact_native_control\` ONCE per field \uFFFD do NOT retry with \`fill\` or \`click\`.
|
|
4084
|
+
- NEVER insert a \`wait\` action between related dropdown steps (click trigger ? click option). It breaks the sequence.
|
|
3110
4085
|
|
|
3111
4086
|
3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
|
|
3112
4087
|
4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
|
|
@@ -3117,8 +4092,8 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
3117
4092
|
- If the dropdown selection was the LAST step in the form, you CAN immediately click Siguiente/Guardar in the same turn.
|
|
3118
4093
|
- **ESCAPE RULE**: You may send Escape ONCE after selecting to help close the dropdown. If you already sent Escape and the form still shows the same state, DO NOT send Escape again. Click \`Siguiente\` or \`Guardar\` directly to proceed.
|
|
3119
4094
|
- **VALIDATION ERRORS**: If Siguiente reports a validation error on a dropdown field, THEN use \`inspect_element_details\` to understand the field's selector and interact with it precisely. Not before.
|
|
3120
|
-
6. **ERROR HANDLING**: If you see '
|
|
3121
|
-
6b. **DUPLICATE / REPEATED VALUE ERROR \
|
|
4095
|
+
6. **ERROR HANDLING**: If you see '?? [ERROR_CR\uFFFDTICO]', STOP everything. Analyze the error, fix the data, then retry.
|
|
4096
|
+
6b. **DUPLICATE / REPEATED VALUE ERROR \uFFFD MANDATORY PROTOCOL**:
|
|
3122
4097
|
- If HISTORY contains a line with "ERROR VISIBLE EN PANTALLA" or "ERROR POST-GUARDADO" with words like "repetido", "duplicado", "ya existe", or "already exists":
|
|
3123
4098
|
1. You MUST identify WHICH field caused the duplication (usually the main name/title field).
|
|
3124
4099
|
2. You MUST use \`fill\` to REPLACE that field's value with a NEW unique value. Add a numeric suffix like \`_${(/* @__PURE__ */ new Date()).getMinutes()}${(/* @__PURE__ */ new Date()).getSeconds()}\` or a short random number.
|
|
@@ -3136,9 +4111,9 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
3136
4111
|
11. **NEGATIVE TESTING**: If mission expects an error (e.g., "verifica que no se pueda guardar sin campos"), and you see that error, the mission is SUCCESS. Use \`finish: true\`.
|
|
3137
4112
|
12. **GUIDE CONFLICT**: If SUCCESS MEMORY disagrees with what you see on screen, ABANDON the memory and trust your eyes.
|
|
3138
4113
|
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.
|
|
3139
|
-
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\
|
|
4114
|
+
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\uFFFDn", 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.
|
|
3140
4115
|
|
|
3141
|
-
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\
|
|
4116
|
+
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\uFFFDmero por n\uFFFDmero", "d\uFFFDgito por d\uFFFDgito", or "car\uFFFDcter por car\uFFFDcter":
|
|
3142
4117
|
- 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").
|
|
3143
4118
|
- Then you MUST use \`type_sequentially\` with the full string. Do NOT simulate manual typing by repeatedly clicking the field.
|
|
3144
4119
|
- 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.
|
|
@@ -3146,53 +4121,53 @@ These are NOT optional. You MUST use these tools in these situations:
|
|
|
3146
4121
|
|
|
3147
4122
|
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"):
|
|
3148
4123
|
- 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>\`).
|
|
3149
|
-
- If no asset matches the required type, use the value \`"placeholder:image/banner"\` \
|
|
4124
|
+
- If no asset matches the required type, use the value \`"placeholder:image/banner"\` \uFFFD the runner will automatically generate a valid placeholder PNG image.
|
|
3150
4125
|
- **NEVER** report a bug or inability to proceed just because you see a file upload field. You ALWAYS have a fallback.
|
|
3151
4126
|
- **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\`.
|
|
3152
4127
|
- 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]\`.
|
|
3153
4128
|
|
|
3154
4129
|
# WIZARD & MULTI-STEP FORMS (CRITICAL)
|
|
3155
|
-
When a form is divided into numbered steps (e.g., Step 1
|
|
4130
|
+
When a form is divided into numbered steps (e.g., Step 1 ? Step 2 ? Step 3):
|
|
3156
4131
|
|
|
3157
|
-
1. **VALIDATION ERROR ON ADVANCE**: If you click "Next/Siguiente" and the UI DOES NOT advance to the next step, AND you see a
|
|
4132
|
+
1. **VALIDATION ERROR ON ADVANCE**: If you click "Next/Siguiente" and the UI DOES NOT advance to the next step, AND you see a ?? [ERROR_CR\uFFFDTICO], you must fix the data on the CURRENT step. DO NOT GO BACK to previous steps simply because you see an error, unless the error explicitly states that a previous step's data is invalid.
|
|
3158
4133
|
|
|
3159
|
-
2. **DATE VALIDATION ERRORS \
|
|
4134
|
+
2. **DATE VALIDATION ERRORS \uFFFD MANDATORY PROTOCOL (ZOD-AWARE)**:
|
|
3160
4135
|
- ZOD generates Spanish validation messages such as:
|
|
3161
4136
|
- "La fecha de inicio debe ser posterior o igual a la fecha actual."
|
|
3162
4137
|
- "La fecha debe ser una fecha futura."
|
|
3163
4138
|
- "Debe ser posterior a la fecha de inicio."
|
|
3164
4139
|
- "El valor debe ser mayor o igual a X."
|
|
3165
|
-
- ANY
|
|
4140
|
+
- ANY ?? [ERROR_CR\uFFFDTICO] containing FECHA + (POSTERIOR/ANTERIOR/ACTUAL/PASADA/FUTURA) is a **ZOD DATE VALIDATION ERROR**.
|
|
3166
4141
|
- **RECOVERY PROTOCOL** (follow in order, NEVER skip):
|
|
3167
|
-
1. STOP \
|
|
4142
|
+
1. STOP \uFFFD do NOT click Save/Siguiente again with the same data.
|
|
3168
4143
|
2. Identify which wizard step contains the failing date field (check HISTORY).
|
|
3169
4144
|
3. If you are NOT on that step, click the Back/Anterior button to return to it.
|
|
3170
4145
|
4. Locate the date input using \`validate_element_state\` with 'has_value' to confirm its current (bad) value.
|
|
3171
|
-
5. Clear and re-fill the date using \`interact_native_control\` with control_type \`'date'\` and a value
|
|
3172
|
-
6. TODAY: \`${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}\` \
|
|
4146
|
+
5. Clear and re-fill the date using \`interact_native_control\` with control_type \`'date'\` and a value = TODAY in YYYY-MM-DD format.
|
|
4147
|
+
6. TODAY: \`${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}\` \uFFFD use this exact expression to compute the date dynamically.
|
|
3173
4148
|
7. After fixing ALL date fields, advance through each wizard step again.
|
|
3174
4149
|
|
|
3175
4150
|
3. **NEVER LOOP ON THE SAME STEP**: If you've been on the same wizard step for 3+ turns without advancing, go back one step and verify the data is correct before trying to advance again.
|
|
3176
4151
|
|
|
3177
4152
|
4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm.
|
|
3178
4153
|
|
|
3179
|
-
5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\
|
|
4154
|
+
5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\uFFFDn", "Resumen", or see a "Terminar" / "Finalizar" button, your objective is ALMOST COMPLETE. Do NOT go back to previous steps just because you see read-only data. Click the final submit button ("Terminar", "Guardar", "Finalizar") to complete the mission.
|
|
3180
4155
|
|
|
3181
|
-
6. **DATE FIELD \
|
|
4156
|
+
6. **DATE FIELD \uFFFD ONE SHOT RULE**: For any \`<input type="date">\`, you get EXACTLY ONE attempt with \`interact_native_control\`. If it does not stick (field shows empty on next perception), do NOT retry the date. Instead: a) use \`send_keyboard_event\` to type the date digit by digit (e.g., "0", "5", "0", "5", "2", "0", "2", "6"), OR b) click \`Siguiente\` and see if ZOD validates it. If ZOD rejects it, THEN and ONLY THEN come back to fix. Looping 3+ times on the same date field is FORBIDDEN.
|
|
3182
4157
|
|
|
3183
|
-
7. **MODAL REWARD COMBOBOX**: In the rewards modal ("Configurar Recompensas del Juego"), the "Tipo de recompensa" dropdown is a \`[div]\` trigger, NOT an \`[input]\`. Always use 2 turns: Turn A = Click the trigger div to open the option list. Turn B = Click the specific option (e.g., \`[li] Puntos\`, \`[li] Cup\
|
|
4158
|
+
7. **MODAL REWARD COMBOBOX**: In the rewards modal ("Configurar Recompensas del Juego"), the "Tipo de recompensa" dropdown is a \`[div]\` trigger, NOT an \`[input]\`. Always use 2 turns: Turn A = Click the trigger div to open the option list. Turn B = Click the specific option (e.g., \`[li] Puntos\`, \`[li] Cup\uFFFDn\`). Do NOT use \`fill\` on this field.
|
|
3184
4159
|
|
|
3185
|
-
8. **MODAL SELECTION PROTOCOL (CRITICAL)**: In selection modals (e.g., "Selecciona cup\
|
|
4160
|
+
8. **MODAL SELECTION PROTOCOL (CRITICAL)**: In selection modals (e.g., "Selecciona cup\uFFFDn"), you MUST select an item BEFORE clicking "Seleccionar" or "Confirmar". To select an item, click on ANY visible element within the item's row: the radio button, the item's name, OR its status label (e.g., the text "Activo" next to a coupon name). Clicking the confirm button without a prior selection WILL FAIL. If you do NOT see a radio button IDX in the component list, click the item's STATUS text (like "Activo") or its NAME text instead \uFFFD this is the EXPECTED behavior for custom UI components.
|
|
3186
4161
|
|
|
3187
|
-
9. **RADIO BUTTON FALLBACK**: If you cannot find a dedicated radio button element for a list item, do NOT loop. Instead: a) Click the item's visible text (name, status, or any text in its row). b) If that doesn't work, try clicking the row container element. c) NEVER use the search box to "select" an item \
|
|
4162
|
+
9. **RADIO BUTTON FALLBACK**: If you cannot find a dedicated radio button element for a list item, do NOT loop. Instead: a) Click the item's visible text (name, status, or any text in its row). b) If that doesn't work, try clicking the row container element. c) NEVER use the search box to "select" an item \uFFFD the search box only filters the list, it does NOT select.
|
|
3188
4163
|
|
|
3189
|
-
10. **DROPDOWN TOGGLE WARNING (CRITICAL)**: Clicking a dropdown/combobox trigger TOGGLES it: 1st click = open, 2nd click = CLOSE, 3rd click = open again. If you clicked a dropdown trigger and do NOT see options in the next turn, do NOT click it again \
|
|
4164
|
+
10. **DROPDOWN TOGGLE WARNING (CRITICAL)**: Clicking a dropdown/combobox trigger TOGGLES it: 1st click = open, 2nd click = CLOSE, 3rd click = open again. If you clicked a dropdown trigger and do NOT see options in the next turn, do NOT click it again \uFFFD you will just close it! Instead, use \`send_keyboard_event\` with "ArrowDown" to open/navigate the dropdown, then "Enter" to select.
|
|
3190
4165
|
|
|
3191
4166
|
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.
|
|
3192
4167
|
|
|
3193
4168
|
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\`.
|
|
3194
4169
|
|
|
3195
|
-
13. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with
|
|
4170
|
+
13. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`?? [ERROR_CR\uFFFDTICO]\` or \`?? 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.`,
|
|
3196
4171
|
cache_control: { type: "ephemeral" }
|
|
3197
4172
|
},
|
|
3198
4173
|
{
|
|
@@ -3202,6 +4177,7 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
|
|
|
3202
4177
|
${prompt}
|
|
3203
4178
|
|
|
3204
4179
|
${projectInfo}
|
|
4180
|
+
${databaseContext}
|
|
3205
4181
|
${skillsContext}
|
|
3206
4182
|
${customUserContext}
|
|
3207
4183
|
${memoryContext}
|
|
@@ -3255,7 +4231,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3255
4231
|
]
|
|
3256
4232
|
}
|
|
3257
4233
|
];
|
|
3258
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4234
|
+
console.log(`>>ARCALITY_STATUS>> ?? Arcality est\uFFFD procesando la visi\uFFFDn...`);
|
|
3259
4235
|
const startTime = Date.now();
|
|
3260
4236
|
let totalUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
3261
4237
|
for (let turn = 0; turn < 10; turn++) {
|
|
@@ -3266,7 +4242,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3266
4242
|
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
3267
4243
|
const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
|
|
3268
4244
|
const displayTurn = stepCount !== void 0 ? stepCount : turn + 1;
|
|
3269
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4245
|
+
console.log(`>>ARCALITY_STATUS>> ?? Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, ciclo interno ${turn + 1}` : ""})`);
|
|
3270
4246
|
const headers = {
|
|
3271
4247
|
"Content-Type": "application/json"
|
|
3272
4248
|
};
|
|
@@ -3306,13 +4282,13 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3306
4282
|
if (response.ok)
|
|
3307
4283
|
break;
|
|
3308
4284
|
const errorText = await response.text();
|
|
3309
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4285
|
+
console.log(`>>ARCALITY_STATUS>> ? API Error ${response.status}: ${errorText.substring(0, 500)}`);
|
|
3310
4286
|
const isTransient = response.status === 529 || response.status === 429 || response.status === 503;
|
|
3311
4287
|
if (isTransient && retryCount < maxRetries) {
|
|
3312
4288
|
retryCount++;
|
|
3313
4289
|
const delay = Math.pow(2, retryCount) * 1e3;
|
|
3314
4290
|
console.log(`
|
|
3315
|
-
>>ARCALITY_STATUS>>
|
|
4291
|
+
>>ARCALITY_STATUS>> ?? Arcality est\uFFFD saturado (${response.status}). Reintentando...`);
|
|
3316
4292
|
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
3317
4293
|
continue;
|
|
3318
4294
|
}
|
|
@@ -3334,7 +4310,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3334
4310
|
const textResponse = message.content.find((c) => c.type === "text")?.text || "";
|
|
3335
4311
|
if (toolCalls.length === 0) {
|
|
3336
4312
|
console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
|
|
3337
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4313
|
+
console.log(`>>ARCALITY_STATUS>> ?? Arcality devolvi\uFFFD texto sin acci\uFFFDn. Reinyectando instrucci\uFFFDn mandatoria...`);
|
|
3338
4314
|
anthropicMessages.push({
|
|
3339
4315
|
role: "user",
|
|
3340
4316
|
content: [{ type: "text", text: `MANDATORY: You MUST call one of the available tools right now. You are NOT allowed to return plain text only. If you are unsure, call 'perform_ui_actions' with the most logical next action based on the mission and history. If you are truly stuck, call 'report_inability_to_proceed'. DO NOT return text without a tool call.` }]
|
|
@@ -3354,7 +4330,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3354
4330
|
} else if (toolName === "inspect_element_details") {
|
|
3355
4331
|
const idx = toolInput.idx;
|
|
3356
4332
|
const c = state.components.find((comp) => comp.idx === idx);
|
|
3357
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4333
|
+
console.log(`>>ARCALITY_STATUS>> ?? Investigando IDX:${idx}...`);
|
|
3358
4334
|
let details = "Element not found or no longer present.";
|
|
3359
4335
|
if (c) {
|
|
3360
4336
|
try {
|
|
@@ -3379,11 +4355,11 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3379
4355
|
}
|
|
3380
4356
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: details });
|
|
3381
4357
|
} else if (toolName === "report_application_bug") {
|
|
3382
|
-
console.error(
|
|
4358
|
+
console.error(`?? [BUG DETECTED] ${toolInput.bug_description}`);
|
|
3383
4359
|
try {
|
|
3384
4360
|
const url = this.page.url();
|
|
3385
4361
|
await this.knowledgeService.saveRule(
|
|
3386
|
-
"Validaci\
|
|
4362
|
+
"Validaci\uFFFDn Detectada (Auto-Learning)",
|
|
3387
4363
|
`Error: ${toolInput.bug_description}. Se esperaba: ${toolInput.expected_behavior}`,
|
|
3388
4364
|
"WARNING",
|
|
3389
4365
|
url
|
|
@@ -3395,9 +4371,9 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3395
4371
|
const shortDesc = toolInput.bug_description.length > 80 ? toolInput.bug_description.substring(0, 80).trimEnd() + "..." : toolInput.bug_description;
|
|
3396
4372
|
const bugTitle = `[QA Bug] ${shortDesc}`;
|
|
3397
4373
|
const bugDescription = [
|
|
3398
|
-
`**Bug detectado autom\
|
|
4374
|
+
`**Bug detectado autom\uFFFDticamente por Arcality QA**`,
|
|
3399
4375
|
``,
|
|
3400
|
-
`**Descripci\
|
|
4376
|
+
`**Descripci\uFFFDn:** ${toolInput.bug_description}`,
|
|
3401
4377
|
`**Comportamiento esperado:** ${toolInput.expected_behavior}`,
|
|
3402
4378
|
`**Comportamiento actual:** ${toolInput.actual_behavior}`,
|
|
3403
4379
|
`**URL afectada:** ${this.page.url()}`,
|
|
@@ -3415,9 +4391,9 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3415
4391
|
const shortDesc = toolInput.bug_description.length > 80 ? toolInput.bug_description.substring(0, 80).trimEnd() + "..." : toolInput.bug_description;
|
|
3416
4392
|
const bugTitle = `[QA Bug] ${shortDesc}`;
|
|
3417
4393
|
const bugDescription = [
|
|
3418
|
-
`**Bug detectado autom\
|
|
4394
|
+
`**Bug detectado autom\uFFFDticamente por Arcality QA**`,
|
|
3419
4395
|
``,
|
|
3420
|
-
`**Descripci\
|
|
4396
|
+
`**Descripci\uFFFDn:** ${toolInput.bug_description}`,
|
|
3421
4397
|
`**Comportamiento esperado:** ${toolInput.expected_behavior}`,
|
|
3422
4398
|
`**Comportamiento actual:** ${toolInput.actual_behavior}`,
|
|
3423
4399
|
`**URL afectada:** ${this.page.url()}`,
|
|
@@ -3430,7 +4406,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3430
4406
|
return {
|
|
3431
4407
|
thought: `DISCULPA: ${toolInput.confusion_reason}
|
|
3432
4408
|
|
|
3433
|
-
|
|
4409
|
+
?? SUGERENCIA: ${toolInput.prompt_suggestion}`,
|
|
3434
4410
|
actions: [],
|
|
3435
4411
|
finish: true,
|
|
3436
4412
|
usage: totalUsage
|
|
@@ -3496,7 +4472,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3496
4472
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
3497
4473
|
} else if (toolName === "extract_table_data") {
|
|
3498
4474
|
const { table_identifier, max_rows = 50 } = toolInput;
|
|
3499
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4475
|
+
console.log(`>>ARCALITY_STATUS>> ?? Extrayendo tabla: ${table_identifier}...`);
|
|
3500
4476
|
try {
|
|
3501
4477
|
const tableData = await this.page.evaluate(({ identifier, limit }) => {
|
|
3502
4478
|
const findTable = () => {
|
|
@@ -3526,6 +4502,107 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3526
4502
|
} catch (e) {
|
|
3527
4503
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error extracting: ${e.message}` });
|
|
3528
4504
|
}
|
|
4505
|
+
} else if (toolName === "validate_database_state") {
|
|
4506
|
+
console.log(`>>ARCALITY_STATUS>> [DB] Validando estado en PostgreSQL: ${toolInput.query_id}...`);
|
|
4507
|
+
try {
|
|
4508
|
+
const report = await executeDatabaseValidation(toolInput);
|
|
4509
|
+
if (this.testInfo) {
|
|
4510
|
+
await this.testInfo.attach("database_validation_report", {
|
|
4511
|
+
body: Buffer.from(JSON.stringify(report, null, 2), "utf-8"),
|
|
4512
|
+
contentType: "application/json"
|
|
4513
|
+
});
|
|
4514
|
+
}
|
|
4515
|
+
const summary = report.passed ? `Database validation passed: ${report.name}` : `Database validation failed: ${report.name}`;
|
|
4516
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `${summary}
|
|
4517
|
+
${JSON.stringify(report, null, 2)}` });
|
|
4518
|
+
} catch (e) {
|
|
4519
|
+
const report = {
|
|
4520
|
+
type: "database_validation_report",
|
|
4521
|
+
status: "failed",
|
|
4522
|
+
passed: false,
|
|
4523
|
+
summary: { total: 1, passed: 0, failed: 1 },
|
|
4524
|
+
validations: [{
|
|
4525
|
+
name: toolInput.name || toolInput.query_id || "database_validation",
|
|
4526
|
+
query_id: toolInput.query_id || "",
|
|
4527
|
+
status: "failed",
|
|
4528
|
+
passed: false,
|
|
4529
|
+
error: e.message
|
|
4530
|
+
}],
|
|
4531
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4532
|
+
};
|
|
4533
|
+
if (this.testInfo) {
|
|
4534
|
+
await this.testInfo.attach("database_validation_report", {
|
|
4535
|
+
body: Buffer.from(JSON.stringify(report, null, 2), "utf-8"),
|
|
4536
|
+
contentType: "application/json"
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database validation error: ${e.message}` });
|
|
4540
|
+
}
|
|
4541
|
+
} else if (toolName === "preview_db_validation") {
|
|
4542
|
+
console.log(`>>ARCALITY_STATUS>> [DB] Preview de validacion PostgreSQL: ${toolInput.query_id}...`);
|
|
4543
|
+
try {
|
|
4544
|
+
const preview = await previewDatabaseValidation(toolInput);
|
|
4545
|
+
if (this.testInfo) {
|
|
4546
|
+
await this.testInfo.attach("database_validation_preview", {
|
|
4547
|
+
body: Buffer.from(JSON.stringify(preview, null, 2), "utf-8"),
|
|
4548
|
+
contentType: "application/json"
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
toolResults.push({
|
|
4552
|
+
type: "tool_result",
|
|
4553
|
+
tool_use_id: toolUse.id,
|
|
4554
|
+
content: `Database validation preview ready: ${preview.name}
|
|
4555
|
+
${JSON.stringify(preview, null, 2)}`
|
|
4556
|
+
});
|
|
4557
|
+
} catch (e) {
|
|
4558
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database validation preview error: ${e.message}` });
|
|
4559
|
+
}
|
|
4560
|
+
} else if (toolName === "register_db_profile") {
|
|
4561
|
+
console.log(`>>ARCALITY_STATUS>> [DB] Registrando o validando perfil local: ${toolInput.profile || process.env.ARCALITY_DB_PROFILE || "qa_local"}...`);
|
|
4562
|
+
try {
|
|
4563
|
+
const profileReport = registerDatabaseProfile(toolInput);
|
|
4564
|
+
if (this.testInfo) {
|
|
4565
|
+
await this.testInfo.attach("database_profile_registration", {
|
|
4566
|
+
body: Buffer.from(JSON.stringify(profileReport, null, 2), "utf-8"),
|
|
4567
|
+
contentType: "application/json"
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
toolResults.push({
|
|
4571
|
+
type: "tool_result",
|
|
4572
|
+
tool_use_id: toolUse.id,
|
|
4573
|
+
content: `Database profile ready: ${profileReport.profile}
|
|
4574
|
+
${JSON.stringify(profileReport, null, 2)}`
|
|
4575
|
+
});
|
|
4576
|
+
} catch (e) {
|
|
4577
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Database profile error: ${e.message}` });
|
|
4578
|
+
}
|
|
4579
|
+
} else if (toolName === "summarize_execution_evidence") {
|
|
4580
|
+
try {
|
|
4581
|
+
const attachments = Array.isArray(this.testInfo?.attachments) ? this.testInfo.attachments.map((att) => ({
|
|
4582
|
+
name: att.name,
|
|
4583
|
+
contentType: att.contentType,
|
|
4584
|
+
path: att.path
|
|
4585
|
+
})) : [];
|
|
4586
|
+
const evidenceSummary = summarizeExecutionEvidence(toolInput, {
|
|
4587
|
+
logs: this.logs,
|
|
4588
|
+
attachments,
|
|
4589
|
+
current_url: this.page.url()
|
|
4590
|
+
});
|
|
4591
|
+
if (this.testInfo) {
|
|
4592
|
+
await this.testInfo.attach("execution_evidence_summary", {
|
|
4593
|
+
body: Buffer.from(JSON.stringify(evidenceSummary, null, 2), "utf-8"),
|
|
4594
|
+
contentType: "application/json"
|
|
4595
|
+
});
|
|
4596
|
+
}
|
|
4597
|
+
toolResults.push({
|
|
4598
|
+
type: "tool_result",
|
|
4599
|
+
tool_use_id: toolUse.id,
|
|
4600
|
+
content: `Execution evidence summary ready: ${evidenceSummary.final_status}
|
|
4601
|
+
${JSON.stringify(evidenceSummary, null, 2)}`
|
|
4602
|
+
});
|
|
4603
|
+
} catch (e) {
|
|
4604
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Execution evidence summary error: ${e.message}` });
|
|
4605
|
+
}
|
|
3529
4606
|
} else if (toolName === "capture_console_errors") {
|
|
3530
4607
|
const logs = this.logs.length > 0 ? this.logs.join("\n") : "No console errors/warnings/network errors captured.";
|
|
3531
4608
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: logs });
|
|
@@ -3551,7 +4628,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3551
4628
|
}
|
|
3552
4629
|
} else if (toolName === "measure_page_performance") {
|
|
3553
4630
|
const { thresholds = {} } = toolInput;
|
|
3554
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4631
|
+
console.log(`>>ARCALITY_STATUS>> ? Midiendo rendimiento de p\uFFFDgina...`);
|
|
3555
4632
|
try {
|
|
3556
4633
|
const perfData = await this.page.evaluate(() => {
|
|
3557
4634
|
const nav = performance.getEntriesByType("navigation")[0];
|
|
@@ -3567,7 +4644,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3567
4644
|
};
|
|
3568
4645
|
});
|
|
3569
4646
|
const results = [
|
|
3570
|
-
|
|
4647
|
+
`?? PERFORMANCE METRICS:`,
|
|
3571
4648
|
` Load Time: ${perfData.load_time_ms ?? "N/A"}ms`,
|
|
3572
4649
|
` DOM Content Loaded: ${perfData.dom_content_loaded_ms ?? "N/A"}ms`,
|
|
3573
4650
|
` First Contentful Paint: ${perfData.first_contentful_paint_ms ?? "N/A"}ms`,
|
|
@@ -3580,7 +4657,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3580
4657
|
}
|
|
3581
4658
|
} else if (toolName === "save_navigation_checkpoint") {
|
|
3582
4659
|
const { checkpoint_name, include_storage = true } = toolInput;
|
|
3583
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4660
|
+
console.log(`>>ARCALITY_STATUS>> ?? Guardando punto de control: "${checkpoint_name}"...`);
|
|
3584
4661
|
try {
|
|
3585
4662
|
const url = this.page.url();
|
|
3586
4663
|
const context = this.page.context();
|
|
@@ -3607,17 +4684,17 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3607
4684
|
sessionStorage = storageData.sessionStorage;
|
|
3608
4685
|
}
|
|
3609
4686
|
this.checkpoints.set(checkpoint_name, { url, storageState, localStorage, sessionStorage });
|
|
3610
|
-
const result =
|
|
4687
|
+
const result = `? Checkpoint "${checkpoint_name}" saved successfully.`;
|
|
3611
4688
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result });
|
|
3612
4689
|
} catch (e) {
|
|
3613
4690
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error saving checkpoint: ${e.message}` });
|
|
3614
4691
|
}
|
|
3615
4692
|
} else if (toolName === "restore_navigation_checkpoint") {
|
|
3616
4693
|
const { checkpoint_name } = toolInput;
|
|
3617
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4694
|
+
console.log(`>>ARCALITY_STATUS>> ?? Restaurando punto de control: "${checkpoint_name}"...`);
|
|
3618
4695
|
const checkpoint = this.checkpoints.get(checkpoint_name);
|
|
3619
4696
|
if (!checkpoint) {
|
|
3620
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4697
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Checkpoint "${checkpoint_name}" not found.` });
|
|
3621
4698
|
} else {
|
|
3622
4699
|
try {
|
|
3623
4700
|
const context = this.page.context();
|
|
@@ -3635,7 +4712,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3635
4712
|
window.sessionStorage.setItem(k, v);
|
|
3636
4713
|
}, { ls: checkpoint.localStorage, ss: checkpoint.sessionStorage });
|
|
3637
4714
|
await this.page.reload({ waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
3638
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4715
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Checkpoint "${checkpoint_name}" restored.` });
|
|
3639
4716
|
} catch (e) {
|
|
3640
4717
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
|
|
3641
4718
|
}
|
|
@@ -3646,7 +4723,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3646
4723
|
if (!el) {
|
|
3647
4724
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
|
|
3648
4725
|
} else {
|
|
3649
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4726
|
+
console.log(`>>ARCALITY_STATUS>> ??? [Security] Probando inyecci\uFFFDn XSS en IDX ${idx}...`);
|
|
3650
4727
|
let payload = "<script>alert(1)</script>";
|
|
3651
4728
|
if (payload_type === "image")
|
|
3652
4729
|
payload = "<img src=x onerror=alert(1)>";
|
|
@@ -3656,7 +4733,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3656
4733
|
const frame = this.page.frames()[el.frameIdx] || this.page.mainFrame();
|
|
3657
4734
|
const locator = frame.locator(el.selector).first();
|
|
3658
4735
|
await locator.fill(payload);
|
|
3659
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\
|
|
4736
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\uFFFDrate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\uFFFD renderizado tal cual o sanitizado.` });
|
|
3660
4737
|
if (this.testInfo)
|
|
3661
4738
|
this.testInfo.attach("security_tool_call", { body: JSON.stringify({ action: "test_xss_injection", target: idx, payload }), contentType: "application/json" });
|
|
3662
4739
|
} catch (e) {
|
|
@@ -3665,7 +4742,7 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3665
4742
|
}
|
|
3666
4743
|
} else if (toolName === "test_auth_bypass") {
|
|
3667
4744
|
const { target_url } = toolInput;
|
|
3668
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4745
|
+
console.log(`>>ARCALITY_STATUS>> ??? [Security] Intentando Auth Bypass en ${target_url}...`);
|
|
3669
4746
|
try {
|
|
3670
4747
|
const baseUrl = new URL(this.page.url()).origin;
|
|
3671
4748
|
const fullUrl = new URL(target_url, baseUrl).toString();
|
|
@@ -3677,15 +4754,15 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
3677
4754
|
URL resultante: ${resultingUrl}.
|
|
3678
4755
|
`;
|
|
3679
4756
|
if (resultingUrl !== fullUrl)
|
|
3680
|
-
resultData += `Posiblemente redirigido (el bypass fall\
|
|
4757
|
+
resultData += `Posiblemente redirigido (el bypass fall\uFFFD o te enviaron a Login).`;
|
|
3681
4758
|
else
|
|
3682
|
-
resultData += `URL se mantuvo igual. \
|
|
4759
|
+
resultData += `URL se mantuvo igual. \uFFFDEsto podr\uFFFDa ser un IDOR o Auth Bypass si ves datos a los que no deber\uFFFDas acceder!`;
|
|
3683
4760
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: resultData });
|
|
3684
4761
|
} catch (e) {
|
|
3685
4762
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error test_auth_bypass: ${e.message}` });
|
|
3686
4763
|
}
|
|
3687
4764
|
} else if (toolName === "scan_sensitive_data_exposure") {
|
|
3688
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4765
|
+
console.log(`>>ARCALITY_STATUS>> ??? [Security] Escaneando datos sensibles...`);
|
|
3689
4766
|
try {
|
|
3690
4767
|
const issues = await scan_sensitive_data_exposure(this.page);
|
|
3691
4768
|
if (issues.length > 0) {
|
|
@@ -3700,7 +4777,7 @@ ${report}` });
|
|
|
3700
4777
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scan_sensitive_data_exposure: ${e.message}` });
|
|
3701
4778
|
}
|
|
3702
4779
|
} else if (toolName === "audit_http_headers") {
|
|
3703
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4780
|
+
console.log(`>>ARCALITY_STATUS>> ??? [Security] Auditando headers HTTP...`);
|
|
3704
4781
|
try {
|
|
3705
4782
|
const issues = await audit_http_headers(this.page);
|
|
3706
4783
|
if (issues.length > 0) {
|
|
@@ -3708,14 +4785,14 @@ ${report}` });
|
|
|
3708
4785
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades de Headers detectadas:
|
|
3709
4786
|
${report}` });
|
|
3710
4787
|
} else {
|
|
3711
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\
|
|
4788
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\uFFFDsicos." });
|
|
3712
4789
|
}
|
|
3713
4790
|
} catch (e) {
|
|
3714
4791
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error audit_http_headers: ${e.message}` });
|
|
3715
4792
|
}
|
|
3716
4793
|
} else if (toolName === "create_test_evidence") {
|
|
3717
4794
|
const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
|
|
3718
|
-
console.log(
|
|
4795
|
+
console.log(`?? [SKILL] Creating test evidence: "${save_as}"...`);
|
|
3719
4796
|
try {
|
|
3720
4797
|
const filename = save_as.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3721
4798
|
if (annotations && annotations.length > 0) {
|
|
@@ -3764,20 +4841,20 @@ ${report}` });
|
|
|
3764
4841
|
contentType: "text/plain"
|
|
3765
4842
|
});
|
|
3766
4843
|
}
|
|
3767
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4844
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Evidence "${filename}" created and attached.` });
|
|
3768
4845
|
if (finish_run) {
|
|
3769
4846
|
this.lastScreenshot = base64Img;
|
|
3770
4847
|
return { thought: `Evidence "${description || filename}" created. Mission complete.`, actions: [], finish: true };
|
|
3771
4848
|
}
|
|
3772
4849
|
} else {
|
|
3773
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4850
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `?? testInfo not available.` });
|
|
3774
4851
|
}
|
|
3775
4852
|
} catch (e) {
|
|
3776
4853
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error creating evidence: ${e.message}` });
|
|
3777
4854
|
}
|
|
3778
4855
|
} else if (toolName === "send_keyboard_event") {
|
|
3779
4856
|
const { key, idx, repeat = 1 } = toolInput;
|
|
3780
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4857
|
+
console.log(`>>ARCALITY_STATUS>> ?? Enviando tecla: "${key}" ${idx !== void 0 ? `(focus en IDX:${idx})` : "(focus actual)"} x${repeat}`);
|
|
3781
4858
|
try {
|
|
3782
4859
|
if (idx !== void 0) {
|
|
3783
4860
|
const c = state.components.find((comp) => comp.idx === idx);
|
|
@@ -3793,13 +4870,13 @@ ${report}` });
|
|
|
3793
4870
|
if (repeat > 1)
|
|
3794
4871
|
await this.page.waitForTimeout(100);
|
|
3795
4872
|
}
|
|
3796
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4873
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Key "${key}" pressed ${repeat} time(s).` });
|
|
3797
4874
|
} catch (e) {
|
|
3798
4875
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error sending key: ${e.message}` });
|
|
3799
4876
|
}
|
|
3800
4877
|
} else if (toolName === "type_sequentially") {
|
|
3801
4878
|
const { idx, text, delay_ms = 80, clear_first = true } = toolInput;
|
|
3802
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4879
|
+
console.log(`>>ARCALITY_STATUS>> ???? Type sequentially IDX:${idx} ? "${text}"`);
|
|
3803
4880
|
const c = state.components.find((comp) => comp.idx === idx);
|
|
3804
4881
|
if (!c) {
|
|
3805
4882
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
|
|
@@ -3830,7 +4907,7 @@ ${report}` });
|
|
|
3830
4907
|
toolResults.push({
|
|
3831
4908
|
type: "tool_result",
|
|
3832
4909
|
tool_use_id: toolUse.id,
|
|
3833
|
-
content:
|
|
4910
|
+
content: `? Sequential typing completed. Current value: "${finalValue}".`
|
|
3834
4911
|
});
|
|
3835
4912
|
} catch (e) {
|
|
3836
4913
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error typing sequentially: ${e.message}` });
|
|
@@ -3839,7 +4916,7 @@ ${report}` });
|
|
|
3839
4916
|
} else if (toolName === "interact_native_control") {
|
|
3840
4917
|
const { idx, control_type, value } = toolInput;
|
|
3841
4918
|
const c = state.components.find((comp) => comp.idx === idx);
|
|
3842
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4919
|
+
console.log(`>>ARCALITY_STATUS>> ??? Control nativo: ${control_type} IDX:${idx} ? "${value}"`);
|
|
3843
4920
|
if (!c) {
|
|
3844
4921
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "Element not found." });
|
|
3845
4922
|
} else {
|
|
@@ -3854,7 +4931,7 @@ ${report}` });
|
|
|
3854
4931
|
await loc.fill(value, { timeout: 3e3 });
|
|
3855
4932
|
const actualValue = await loc.inputValue();
|
|
3856
4933
|
if (actualValue && actualValue !== "") {
|
|
3857
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4934
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? ${control_type} set to "${actualValue}" via fill().` });
|
|
3858
4935
|
break;
|
|
3859
4936
|
}
|
|
3860
4937
|
} catch {
|
|
@@ -3865,12 +4942,12 @@ ${report}` });
|
|
|
3865
4942
|
const digits = value.replace(/[:-T]/g, "");
|
|
3866
4943
|
await this.page.keyboard.type(digits, { delay: 50 });
|
|
3867
4944
|
const finalValue = await loc.inputValue().catch(() => "");
|
|
3868
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4945
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? ${control_type} set via keyboard. Current value: "${finalValue}".` });
|
|
3869
4946
|
break;
|
|
3870
4947
|
}
|
|
3871
4948
|
case "color":
|
|
3872
4949
|
await loc.fill(value, { timeout: 3e3 });
|
|
3873
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4950
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Color set to "${value}".` });
|
|
3874
4951
|
break;
|
|
3875
4952
|
case "range": {
|
|
3876
4953
|
const numVal = parseFloat(value);
|
|
@@ -3884,7 +4961,7 @@ ${report}` });
|
|
|
3884
4961
|
}
|
|
3885
4962
|
}, { sel: c.selector, val: String(numVal) });
|
|
3886
4963
|
});
|
|
3887
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4964
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Range set to ${numVal}.` });
|
|
3888
4965
|
break;
|
|
3889
4966
|
}
|
|
3890
4967
|
case "select": {
|
|
@@ -3898,17 +4975,17 @@ ${report}` });
|
|
|
3898
4975
|
}
|
|
3899
4976
|
}
|
|
3900
4977
|
const selected = await loc.inputValue().catch(() => "unknown");
|
|
3901
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4978
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Option selected. Value: "${selected}".` });
|
|
3902
4979
|
break;
|
|
3903
4980
|
}
|
|
3904
4981
|
case "contenteditable":
|
|
3905
4982
|
await loc.click({ force: true });
|
|
3906
4983
|
await this.page.keyboard.press("Control+a");
|
|
3907
4984
|
await this.page.keyboard.type(value, { delay: 30 });
|
|
3908
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4985
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Content set to "${value}".` });
|
|
3909
4986
|
break;
|
|
3910
4987
|
default:
|
|
3911
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
4988
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Unknown control type: ${control_type}` });
|
|
3912
4989
|
}
|
|
3913
4990
|
} catch (e) {
|
|
3914
4991
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error interacting with native control: ${e.message}` });
|
|
@@ -3916,7 +4993,7 @@ ${report}` });
|
|
|
3916
4993
|
}
|
|
3917
4994
|
} else if (toolName === "scroll_page") {
|
|
3918
4995
|
const { direction, amount = 500, idx } = toolInput;
|
|
3919
|
-
console.log(`>>ARCALITY_STATUS>>
|
|
4996
|
+
console.log(`>>ARCALITY_STATUS>> ?? Scroll: ${direction} ${amount}px ${idx !== void 0 ? `(container IDX:${idx})` : "(page)"}`);
|
|
3920
4997
|
try {
|
|
3921
4998
|
if (idx !== void 0) {
|
|
3922
4999
|
const c = state.components.find((comp) => comp.idx === idx);
|
|
@@ -3962,7 +5039,7 @@ ${report}` });
|
|
|
3962
5039
|
}
|
|
3963
5040
|
}
|
|
3964
5041
|
await this.page.waitForTimeout(300);
|
|
3965
|
-
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content:
|
|
5042
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `? Scrolled ${direction} ${direction === "top" || direction === "bottom" ? "completely" : `${amount}px`}. Page state will refresh on next turn.` });
|
|
3966
5043
|
} catch (e) {
|
|
3967
5044
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
|
|
3968
5045
|
}
|
|
@@ -4001,7 +5078,7 @@ ${report}` });
|
|
|
4001
5078
|
return { thought: "Max internal turns reached", actions: [] };
|
|
4002
5079
|
}
|
|
4003
5080
|
/**
|
|
4004
|
-
* QA Skill: Genera una
|
|
5081
|
+
* QA Skill: Genera una explicaci�n "humana" y amigable de por qu� la misi�n fue exitosa.
|
|
4005
5082
|
*/
|
|
4006
5083
|
async humanizeMissionResult(prompt, history) {
|
|
4007
5084
|
if (!process.env.ANTHROPIC_API_KEY)
|
|
@@ -4027,9 +5104,9 @@ ${report}` });
|
|
|
4027
5104
|
body: JSON.stringify({
|
|
4028
5105
|
model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
|
|
4029
5106
|
max_tokens: 300,
|
|
4030
|
-
system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\
|
|
5107
|
+
system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\uFFFD una misi\uFFFDn de testing fue exitosa bas\uFFFDndote en el historial de pasos. Usa un tono de victoria. No menciones detalles t\uFFFDcnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '\uFFFDMisi\uFFFDn cumplida! ...'",
|
|
4031
5108
|
messages: [
|
|
4032
|
-
{ role: "user", content: `Misi\
|
|
5109
|
+
{ role: "user", content: `Misi\uFFFDn original: ${prompt}
|
|
4033
5110
|
|
|
4034
5111
|
history of steps:
|
|
4035
5112
|
${history.join("\n")}` }
|
|
@@ -4218,8 +5295,8 @@ var SecurityScanner = class {
|
|
|
4218
5295
|
|
|
4219
5296
|
// tests/_helpers/agentic-runner.spec.ts
|
|
4220
5297
|
var import_config = require("dotenv/config");
|
|
4221
|
-
var
|
|
4222
|
-
var
|
|
5298
|
+
var fs7 = __toESM(require("fs"));
|
|
5299
|
+
var path8 = __toESM(require("path"));
|
|
4223
5300
|
var crypto2 = __toESM(require("crypto"));
|
|
4224
5301
|
init_asset_resolver();
|
|
4225
5302
|
var _sessionRuleCache = /* @__PURE__ */ new Set();
|
|
@@ -4282,24 +5359,31 @@ function resolveTargetUrl(baseUrl, currentUrl, targetPath) {
|
|
|
4282
5359
|
const parsedBase = new URL(baseUrl || referenceUrl);
|
|
4283
5360
|
const parsedReference = new URL(referenceUrl);
|
|
4284
5361
|
const normalizedTarget = targetPath.trim();
|
|
5362
|
+
const basePath = parsedBase.pathname.replace(/\/+$/, "");
|
|
5363
|
+
if (normalizedTarget.startsWith("?") || normalizedTarget.startsWith("#")) {
|
|
5364
|
+
return new URL(normalizedTarget, parsedReference).toString();
|
|
5365
|
+
}
|
|
4285
5366
|
if (normalizedTarget.startsWith("/")) {
|
|
4286
|
-
const basePath = parsedBase.pathname.replace(/\/+$/, "");
|
|
4287
5367
|
const targetAlreadyIncludesBase = basePath && (normalizedTarget === basePath || normalizedTarget.startsWith(`${basePath}/`));
|
|
4288
5368
|
const combinedPath = targetAlreadyIncludesBase ? normalizedTarget : `${basePath}${normalizedTarget}`.replace(/\/{2,}/g, "/");
|
|
4289
5369
|
return new URL(`${parsedBase.origin}${combinedPath}`).toString();
|
|
4290
5370
|
}
|
|
4291
|
-
|
|
5371
|
+
if (normalizedTarget.startsWith("./") || normalizedTarget.startsWith("../")) {
|
|
5372
|
+
return new URL(normalizedTarget, parsedReference).toString();
|
|
5373
|
+
}
|
|
5374
|
+
const childPath = `${basePath}/${normalizedTarget}`.replace(/\/{2,}/g, "/");
|
|
5375
|
+
return new URL(`${parsedBase.origin}${childPath}`).toString();
|
|
4292
5376
|
}
|
|
4293
5377
|
function writeBatchMissionResult(payload) {
|
|
4294
5378
|
const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
|
|
4295
5379
|
if (!runDomainDir)
|
|
4296
5380
|
return;
|
|
4297
5381
|
try {
|
|
4298
|
-
if (!
|
|
4299
|
-
|
|
5382
|
+
if (!fs7.existsSync(runDomainDir)) {
|
|
5383
|
+
fs7.mkdirSync(runDomainDir, { recursive: true });
|
|
4300
5384
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
5385
|
+
fs7.writeFileSync(
|
|
5386
|
+
path8.join(runDomainDir, "mission-result.json"),
|
|
4303
5387
|
JSON.stringify(payload, null, 2)
|
|
4304
5388
|
);
|
|
4305
5389
|
} catch {
|
|
@@ -4328,10 +5412,10 @@ function writeBatchMissionResult(payload) {
|
|
|
4328
5412
|
} catch (e) {
|
|
4329
5413
|
console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error al sincronizar assets: ${e.message}`);
|
|
4330
5414
|
}
|
|
4331
|
-
const memoryFile =
|
|
4332
|
-
if (
|
|
5415
|
+
const memoryFile = path8.join(contextDir, "memoria-arcality.json");
|
|
5416
|
+
if (fs7.existsSync(memoryFile)) {
|
|
4333
5417
|
try {
|
|
4334
|
-
|
|
5418
|
+
fs7.unlinkSync(memoryFile);
|
|
4335
5419
|
} catch {
|
|
4336
5420
|
}
|
|
4337
5421
|
}
|
|
@@ -4386,33 +5470,64 @@ function writeBatchMissionResult(payload) {
|
|
|
4386
5470
|
let accumulatedCost = 0;
|
|
4387
5471
|
let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
4388
5472
|
let bugClassification = "";
|
|
5473
|
+
let databaseValidationReport = null;
|
|
4389
5474
|
const missionSummaryPaths = /* @__PURE__ */ new Set();
|
|
4390
|
-
const
|
|
4391
|
-
if (!bugClassification)
|
|
4392
|
-
return;
|
|
5475
|
+
const updateSavedMissionSummaries = () => {
|
|
4393
5476
|
for (const summaryPath of missionSummaryPaths) {
|
|
4394
5477
|
try {
|
|
4395
|
-
if (!
|
|
5478
|
+
if (!fs7.existsSync(summaryPath))
|
|
4396
5479
|
continue;
|
|
4397
|
-
const data = JSON.parse(
|
|
4398
|
-
data.
|
|
4399
|
-
|
|
5480
|
+
const data = JSON.parse(fs7.readFileSync(summaryPath, "utf8"));
|
|
5481
|
+
data.success = aiMarkedSuccess && !hasCriticalError;
|
|
5482
|
+
data.error = hasCriticalError;
|
|
5483
|
+
data.fail_reason = failReason;
|
|
5484
|
+
data.fail_reasoning = failReasoning;
|
|
5485
|
+
if (bugClassification)
|
|
5486
|
+
data.bug_classification = bugClassification;
|
|
5487
|
+
if (databaseValidationReport)
|
|
5488
|
+
data.database_validation = databaseValidationReport;
|
|
5489
|
+
fs7.writeFileSync(summaryPath, JSON.stringify(data, null, 2));
|
|
4400
5490
|
} catch {
|
|
4401
5491
|
}
|
|
4402
5492
|
}
|
|
4403
5493
|
};
|
|
5494
|
+
const updateSavedMissionClassification = updateSavedMissionSummaries;
|
|
5495
|
+
const runPostMissionDatabaseValidations = async () => {
|
|
5496
|
+
if (!aiMarkedSuccess || hasCriticalError || databaseValidationReport)
|
|
5497
|
+
return;
|
|
5498
|
+
const rawValidations = process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || "";
|
|
5499
|
+
if (!rawValidations.trim())
|
|
5500
|
+
return;
|
|
5501
|
+
console.log(`>>ARCALITY_STATUS>> [DB] Ejecutando corroboracion post-mision...`);
|
|
5502
|
+
databaseValidationReport = await runDatabaseValidationsFromEnv();
|
|
5503
|
+
if (!databaseValidationReport)
|
|
5504
|
+
return;
|
|
5505
|
+
await testInfo.attach("database_validation_report", {
|
|
5506
|
+
body: Buffer.from(JSON.stringify(databaseValidationReport, null, 2), "utf-8"),
|
|
5507
|
+
contentType: "application/json"
|
|
5508
|
+
});
|
|
5509
|
+
const summary = databaseValidationReport.summary || { total: 0, passed: 0, failed: 0 };
|
|
5510
|
+
history.push(`Turno ${stepCount}: [DB_VALIDATION] ${summary.passed}/${summary.total} validaciones BD exitosas.`);
|
|
5511
|
+
if (!databaseValidationReport.passed) {
|
|
5512
|
+
aiMarkedSuccess = false;
|
|
5513
|
+
hasCriticalError = true;
|
|
5514
|
+
failReason = "database_validation_failed";
|
|
5515
|
+
failReasoning = `La UI parecia exitosa, pero la corroboracion en PostgreSQL fallo (${summary.failed}/${summary.total} validaciones fallidas). Revisa la seccion Validacion BD del reporte.`;
|
|
5516
|
+
}
|
|
5517
|
+
updateSavedMissionSummaries();
|
|
5518
|
+
};
|
|
4404
5519
|
const saveMissionResults = () => {
|
|
4405
5520
|
if (resultsSaved)
|
|
4406
5521
|
return;
|
|
4407
5522
|
resultsSaved = true;
|
|
4408
|
-
if (!
|
|
4409
|
-
|
|
5523
|
+
if (!fs7.existsSync(contextDir))
|
|
5524
|
+
fs7.mkdirSync(contextDir, { recursive: true });
|
|
4410
5525
|
const finalSuccess = aiMarkedSuccess && !hasCriticalError;
|
|
4411
5526
|
try {
|
|
4412
|
-
const memoryFile2 =
|
|
5527
|
+
const memoryFile2 = path8.join(contextDir, "memoria-arcality.json");
|
|
4413
5528
|
let memories = [];
|
|
4414
|
-
if (
|
|
4415
|
-
const content =
|
|
5529
|
+
if (fs7.existsSync(memoryFile2)) {
|
|
5530
|
+
const content = fs7.readFileSync(memoryFile2, "utf8").trim();
|
|
4416
5531
|
if (content) {
|
|
4417
5532
|
try {
|
|
4418
5533
|
memories = JSON.parse(content);
|
|
@@ -4484,10 +5599,11 @@ function writeBatchMissionResult(payload) {
|
|
|
4484
5599
|
fail_reason: failReason,
|
|
4485
5600
|
fail_reasoning: failReasoning,
|
|
4486
5601
|
bug_classification: bugClassification || null,
|
|
4487
|
-
timestamp: Date.now()
|
|
5602
|
+
timestamp: Date.now(),
|
|
5603
|
+
database_validation: databaseValidationReport
|
|
4488
5604
|
};
|
|
4489
5605
|
memories.push(missionData);
|
|
4490
|
-
|
|
5606
|
+
fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
|
|
4491
5607
|
if (process.env.DEBUG)
|
|
4492
5608
|
console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${cleanSteps.length} pasos limpios (${rawSteps.length - cleanSteps.length} corruptos filtrados)`);
|
|
4493
5609
|
} catch (err) {
|
|
@@ -4509,23 +5625,24 @@ function writeBatchMissionResult(payload) {
|
|
|
4509
5625
|
target: process.env.BASE_URL || "",
|
|
4510
5626
|
target_path: process.env.TARGET_PATH || "",
|
|
4511
5627
|
report_dir: process.env.REPORTS_DIR || "",
|
|
4512
|
-
timestamp: timestampIso
|
|
5628
|
+
timestamp: timestampIso,
|
|
5629
|
+
database_validation: databaseValidationReport
|
|
4513
5630
|
};
|
|
4514
|
-
const logPath =
|
|
4515
|
-
|
|
5631
|
+
const logPath = path8.join(contextDir, `agent-log-${timestampMs}.json`);
|
|
5632
|
+
fs7.writeFileSync(logPath, JSON.stringify(summaryPayload, null, 2));
|
|
4516
5633
|
missionSummaryPaths.add(logPath);
|
|
4517
5634
|
const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR;
|
|
4518
5635
|
if (!runDomainDir) {
|
|
4519
5636
|
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.");
|
|
4520
5637
|
}
|
|
4521
|
-
const durableLogsDir =
|
|
4522
|
-
|
|
4523
|
-
const durableLogPath =
|
|
4524
|
-
|
|
5638
|
+
const durableLogsDir = path8.join(runDomainDir, "logs");
|
|
5639
|
+
fs7.mkdirSync(durableLogsDir, { recursive: true });
|
|
5640
|
+
const durableLogPath = path8.join(durableLogsDir, `mission-log-${timestampMs}.json`);
|
|
5641
|
+
fs7.writeFileSync(durableLogPath, JSON.stringify(summaryPayload, null, 2));
|
|
4525
5642
|
missionSummaryPaths.add(durableLogPath);
|
|
4526
5643
|
writeBatchMissionResult(summaryPayload);
|
|
4527
5644
|
if (runDomainDir) {
|
|
4528
|
-
missionSummaryPaths.add(
|
|
5645
|
+
missionSummaryPaths.add(path8.join(runDomainDir, "mission-result.json"));
|
|
4529
5646
|
}
|
|
4530
5647
|
try {
|
|
4531
5648
|
const historyLine = JSON.stringify({
|
|
@@ -4536,12 +5653,12 @@ function writeBatchMissionResult(payload) {
|
|
|
4536
5653
|
fail_reason: failReason,
|
|
4537
5654
|
report_dir: process.env.REPORTS_DIR || ""
|
|
4538
5655
|
}) + "\n";
|
|
4539
|
-
|
|
4540
|
-
|
|
5656
|
+
fs7.appendFileSync(
|
|
5657
|
+
path8.join(contextDir, "arcality-history.log"),
|
|
4541
5658
|
historyLine
|
|
4542
5659
|
);
|
|
4543
|
-
|
|
4544
|
-
|
|
5660
|
+
fs7.appendFileSync(
|
|
5661
|
+
path8.join(durableLogsDir, "arcality-history.log"),
|
|
4545
5662
|
historyLine
|
|
4546
5663
|
);
|
|
4547
5664
|
} catch (e) {
|
|
@@ -4739,11 +5856,11 @@ function writeBatchMissionResult(payload) {
|
|
|
4739
5856
|
timestamp: Date.now()
|
|
4740
5857
|
};
|
|
4741
5858
|
try {
|
|
4742
|
-
const memoryFile2 =
|
|
5859
|
+
const memoryFile2 = path8.join(contextDir, "memoria-agente.json");
|
|
4743
5860
|
let memories = [];
|
|
4744
|
-
if (
|
|
5861
|
+
if (fs7.existsSync(memoryFile2)) {
|
|
4745
5862
|
try {
|
|
4746
|
-
memories = JSON.parse(
|
|
5863
|
+
memories = JSON.parse(fs7.readFileSync(memoryFile2, "utf8"));
|
|
4747
5864
|
} catch {
|
|
4748
5865
|
memories = [];
|
|
4749
5866
|
}
|
|
@@ -4755,14 +5872,14 @@ function writeBatchMissionResult(payload) {
|
|
|
4755
5872
|
if (existingIdx < 0) {
|
|
4756
5873
|
if (resolvedStepsData.length > 0) {
|
|
4757
5874
|
memories.push(localMemoryData);
|
|
4758
|
-
|
|
5875
|
+
fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
|
|
4759
5876
|
console.log(`>>ARCALITY_STATUS>> \u2728 Memoria sincronizada en memoria-agente.json (${resolvedStepsData.length} pasos). Arcality entrar\xE1 en MODO GU\xCDA.`);
|
|
4760
5877
|
} else {
|
|
4761
5878
|
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).`);
|
|
4762
5879
|
}
|
|
4763
5880
|
} else if (!existingHasSteps && resolvedStepsData.length > 0) {
|
|
4764
5881
|
memories[existingIdx] = localMemoryData;
|
|
4765
|
-
|
|
5882
|
+
fs7.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
|
|
4766
5883
|
console.log(`>>ARCALITY_STATUS>> \u{1F504} Patr\xF3n corrupto (0 pasos) sobreescrito en memoria-agente.json (${resolvedStepsData.length} pasos nuevos). MODO GU\xCDA activado.`);
|
|
4767
5884
|
} else {
|
|
4768
5885
|
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.`);
|
|
@@ -5144,7 +6261,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
|
|
|
5144
6261
|
const lastSixHistory = history.slice(-6);
|
|
5145
6262
|
const consecutiveWaits = lastSixHistory.filter((h) => h.includes('wait en "')).length;
|
|
5146
6263
|
if (consecutiveWaits >= 3) {
|
|
5147
|
-
const blankEvidencePath =
|
|
6264
|
+
const blankEvidencePath = path8.join(contextDir, `portal-blank-state-${Date.now()}.png`);
|
|
5148
6265
|
await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
|
|
5149
6266
|
});
|
|
5150
6267
|
const blankUrl = page.url();
|
|
@@ -5421,7 +6538,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
|
|
|
5421
6538
|
const panelCount = Math.min(panels.length, 6);
|
|
5422
6539
|
if (panelCount >= 2 && emptyPanelCount >= panelCount * 0.6) {
|
|
5423
6540
|
const blankUrl = page.url();
|
|
5424
|
-
const blankEvidencePath =
|
|
6541
|
+
const blankEvidencePath = path8.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
|
|
5425
6542
|
await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
|
|
5426
6543
|
});
|
|
5427
6544
|
console.error(`
|
|
@@ -5643,6 +6760,7 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
|
|
|
5643
6760
|
}
|
|
5644
6761
|
}
|
|
5645
6762
|
}
|
|
6763
|
+
await runPostMissionDatabaseValidations();
|
|
5646
6764
|
if (aiMarkedSuccess) {
|
|
5647
6765
|
hasCriticalError = false;
|
|
5648
6766
|
await persistCollectiveMemory();
|
|
@@ -5705,8 +6823,8 @@ Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(
|
|
|
5705
6823
|
console.log("\u{1F916} [SMART-YAML] Generating mission card...");
|
|
5706
6824
|
const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
|
|
5707
6825
|
if (smartYaml) {
|
|
5708
|
-
const yamlPath =
|
|
5709
|
-
|
|
6826
|
+
const yamlPath = path8.join(contextDir, "last-mission-smart.yaml");
|
|
6827
|
+
fs7.writeFileSync(yamlPath, smartYaml);
|
|
5710
6828
|
console.log(`>>ARCALITY_STATUS>> \u{1F4DD} Smart Mission Card generated.`);
|
|
5711
6829
|
await testInfo.attach("smart_mission_card", {
|
|
5712
6830
|
body: Buffer.from(smartYaml, "utf-8"),
|
|
@@ -5808,15 +6926,15 @@ ${failReasoning}
|
|
|
5808
6926
|
if (process.env.DEBUG)
|
|
5809
6927
|
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}`);
|
|
5810
6928
|
}
|
|
5811
|
-
if (
|
|
6929
|
+
if (fs7.existsSync(contextDir)) {
|
|
5812
6930
|
try {
|
|
5813
|
-
const files =
|
|
6931
|
+
const files = fs7.readdirSync(contextDir);
|
|
5814
6932
|
for (const file of files) {
|
|
5815
6933
|
if (file.endsWith(".json") || file.endsWith(".png")) {
|
|
5816
|
-
|
|
6934
|
+
fs7.unlinkSync(path8.join(contextDir, file));
|
|
5817
6935
|
}
|
|
5818
6936
|
}
|
|
5819
|
-
|
|
6937
|
+
fs7.rmSync(contextDir, { recursive: true, force: true });
|
|
5820
6938
|
console.log(`>>ARCALITY_STATUS>> \u{1F9F9} Carpeta context eliminada (Memoria ef\xEDmera completada).`);
|
|
5821
6939
|
} catch (err) {
|
|
5822
6940
|
console.warn(`No se pudo limpiar la carpeta context: ${err}`);
|