@arcadialdev/arcality 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { AIAgentHelper, AgentAction } from './ai-agent-helper';
|
|
3
|
+
import { pushKnowledge, pushRule } from '../../src/services/collectiveMemoryService';
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
// Cache de reglas ya enviadas en esta ejecución (evita duplicados por el mismo mensaje)
|
|
9
|
+
const _sessionRuleCache = new Set<string>();
|
|
10
|
+
|
|
11
|
+
/** Guarda un error de validación como regla de negocio (fire-and-forget) */
|
|
12
|
+
function captureValidationRule(errorMessage: string, context: string): void {
|
|
13
|
+
const key = errorMessage.substring(0, 80).toLowerCase().trim();
|
|
14
|
+
if (_sessionRuleCache.has(key)) return; // Ya registrado en esta sesión
|
|
15
|
+
_sessionRuleCache.add(key);
|
|
16
|
+
|
|
17
|
+
pushRule({
|
|
18
|
+
rule_type: 'VALIDATION',
|
|
19
|
+
title: `Restricción detectada: ${errorMessage.substring(0, 80)}`,
|
|
20
|
+
description: `La aplicación rechazó la acción con el mensaje: "${errorMessage.substring(0, 300)}". Contexto: ${context.substring(0, 150)}`,
|
|
21
|
+
severity: 'important'
|
|
22
|
+
}).catch(() => { /* silencioso */ });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
26
|
+
test.setTimeout(1200000); // 20 minutos para que la IA local no se corte
|
|
27
|
+
|
|
28
|
+
const contextDir = process.env.CONTEXT_DIR || 'out';
|
|
29
|
+
const activeConfig = process.env.ACTIVE_CONFIG || 'Default';
|
|
30
|
+
|
|
31
|
+
if (!process.env.BASE_URL && process.env[`${activeConfig}_URL`]) process.env.BASE_URL = process.env[`${activeConfig}_URL`];
|
|
32
|
+
if (!process.env.LOGIN_USER && process.env[`${activeConfig}_USER`]) process.env.LOGIN_USER = process.env[`${activeConfig}_USER`];
|
|
33
|
+
if (!process.env.LOGIN_PASSWORD && process.env[`${activeConfig}_PASS`]) process.env.LOGIN_PASSWORD = process.env[`${activeConfig}_PASS`];
|
|
34
|
+
|
|
35
|
+
await page.context().grantPermissions(['geolocation']);
|
|
36
|
+
await page.context().setGeolocation({ latitude: 19.4326, longitude: -99.1332 });
|
|
37
|
+
|
|
38
|
+
const agent = new AIAgentHelper(page, contextDir, testInfo);
|
|
39
|
+
const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
|
|
40
|
+
const history: string[] = [];
|
|
41
|
+
const urlHistory: string[] = []; // Para rastrear navegación
|
|
42
|
+
const stepsData: any[] = []; // Para la Guía de Éxito
|
|
43
|
+
|
|
44
|
+
// Keywords Globales para el Test (Refinados para evitar falsos positivos)
|
|
45
|
+
// 10. DATA CORRECTION: If you try to save/create a record and an [ERROR_CRÍTICO] message appears (e.g., 'duplicado', 'ya existe', 'repetido', 'incorrecto'), you MUST STOP. Analyze which field caused the error, change its value to something unique, and ONLY THEN try to save again.
|
|
46
|
+
// 11. NEVER IGNORE ERRORS: If you see '🛑 [ERROR_CRÍTICO]', do not assume it will clear upon submission. It is a blocker.
|
|
47
|
+
// 12. SUCCESS VERIFICATION: If your mission is 'Create X', and you see a success message OR you are back on the list/table view, check if X is there. If so, use 'finish: true'. DO NOT START CREATING AGAIN.
|
|
48
|
+
// 13. DUPLICATE ACTION: If your history shows multiple clicks on 'Save' without data changes, you are in a loop. CHANGE THE DATA or use 'report_inability_to_proceed'.
|
|
49
|
+
const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
|
|
50
|
+
const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
|
|
51
|
+
|
|
52
|
+
let aiMarkedSuccess = false;
|
|
53
|
+
let hasCriticalError = false;
|
|
54
|
+
let resultsSaved = false;
|
|
55
|
+
let isFinished = false;
|
|
56
|
+
let stepCount = 0; // Solo cuenta turnos de IA (no de Guía)
|
|
57
|
+
let guideStepCount = 0; // Turnos de Guía de Éxito (NO cuentan contra maxSteps)
|
|
58
|
+
let stepsDataBackup: any[] = []; // Backup para no perder la Guía de Éxito permanentemente
|
|
59
|
+
const maxSteps = 30; // Aumentado para misiones complejas
|
|
60
|
+
let lastSeenSuccessToast = ""; // Para evitar falsos positivos por mensajes persistentes
|
|
61
|
+
|
|
62
|
+
const saveMissionResults = () => {
|
|
63
|
+
if (resultsSaved) return;
|
|
64
|
+
resultsSaved = true;
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(contextDir)) fs.mkdirSync(contextDir, { recursive: true });
|
|
67
|
+
const finalSuccess = aiMarkedSuccess && !hasCriticalError;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const memoryFile = path.join(contextDir, 'memoria-agente.json');
|
|
71
|
+
let memories = [];
|
|
72
|
+
if (fs.existsSync(memoryFile)) {
|
|
73
|
+
const content = fs.readFileSync(memoryFile, 'utf8').trim();
|
|
74
|
+
if (content) {
|
|
75
|
+
try {
|
|
76
|
+
memories = JSON.parse(content);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.warn("⚠️ Memoria corrupta, reiniciando...");
|
|
79
|
+
memories = [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const missionData = {
|
|
85
|
+
prompt,
|
|
86
|
+
steps: history,
|
|
87
|
+
steps_data: stepsData,
|
|
88
|
+
success: finalSuccess,
|
|
89
|
+
error: hasCriticalError,
|
|
90
|
+
timestamp: Date.now()
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
memories.push(missionData);
|
|
94
|
+
fs.writeFileSync(memoryFile, JSON.stringify(memories, null, 2));
|
|
95
|
+
console.log(`>>ARCALITY_STATUS>> 🧠 Memoria guardada: ${stepsData.length} pasos nuevos`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("❌ Falló al guardar memoria:", err);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const logPath = path.join(contextDir, `agent-log-${Date.now()}.json`);
|
|
102
|
+
fs.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError }, null, 2));
|
|
103
|
+
} catch (err) { }
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Persistencia en Memoria Colectiva (se llama desde TODOS los caminos de éxito) ──
|
|
107
|
+
let collectiveMemoryPersisted = false;
|
|
108
|
+
const persistCollectiveMemory = async () => {
|
|
109
|
+
if (collectiveMemoryPersisted) return; // Evitar doble ejecución
|
|
110
|
+
collectiveMemoryPersisted = true;
|
|
111
|
+
|
|
112
|
+
const pid = process.env.ARCALITY_PROJECT_ID || '';
|
|
113
|
+
const apiUrl = process.env.ARCALITY_API_URL || '';
|
|
114
|
+
const apiKey = process.env.ARCALITY_API_KEY || '';
|
|
115
|
+
|
|
116
|
+
if (!pid || !apiUrl || !apiKey) {
|
|
117
|
+
console.warn(`[CollectiveMemory] ⚠️ Vars faltantes — project_id='${pid}' api_url='${apiUrl}' api_key='${apiKey ? 'OK' : 'MISSING'}'`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`🧠 [COLLECTIVE MEMORY] Persistiendo conocimiento (project: ${pid})...`);
|
|
122
|
+
try {
|
|
123
|
+
const stepsNarrative = stepsData
|
|
124
|
+
.map(s => `${s.action_data?.action || 'action'} "${s.componentName}" en ${s.url}`)
|
|
125
|
+
.join(' → ')
|
|
126
|
+
.substring(0, 500);
|
|
127
|
+
|
|
128
|
+
// 1. Flujo completo → PROCESS knowledge
|
|
129
|
+
const knowledgeId = await pushKnowledge({
|
|
130
|
+
doc_type: 'PROCESS',
|
|
131
|
+
title: `Flujo exitoso: ${prompt.substring(0, 100)}`,
|
|
132
|
+
content: stepsNarrative || prompt.substring(0, 500)
|
|
133
|
+
});
|
|
134
|
+
if (knowledgeId) console.log(`🧠 [COLLECTIVE MEMORY] ✅ Knowledge guardado: ${knowledgeId}`);
|
|
135
|
+
else console.warn(`[CollectiveMemory] ⚠️ pushKnowledge retornó null (revisar HTTP status arriba)`);
|
|
136
|
+
|
|
137
|
+
// 2. Patrones de navegación → NAVIGATION rule
|
|
138
|
+
const uniqueUrls = [...new Set(stepsData.map(s => s.url).filter(Boolean))];
|
|
139
|
+
if (uniqueUrls.length > 1) {
|
|
140
|
+
const ruleId = await pushRule({
|
|
141
|
+
rule_type: 'NAVIGATION',
|
|
142
|
+
title: `Flujo de navegación: ${prompt.substring(0, 80)}`,
|
|
143
|
+
description: `Agente navegó ${uniqueUrls.length} páginas: ${uniqueUrls.join(' → ').substring(0, 400)}`,
|
|
144
|
+
severity: 'suggestion'
|
|
145
|
+
});
|
|
146
|
+
if (ruleId) console.log(`🧠 [COLLECTIVE MEMORY] ✅ Regla NAVIGATION guardada: ${ruleId}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Si hubo submit de formulario → UI_UX rule
|
|
150
|
+
const submitStep = stepsData.find(s =>
|
|
151
|
+
s.action_data?.action === 'click' &&
|
|
152
|
+
(s.componentName?.toLowerCase().includes('guardar') ||
|
|
153
|
+
s.componentName?.toLowerCase().includes('save') ||
|
|
154
|
+
s.componentName?.toLowerCase().includes('crear') ||
|
|
155
|
+
s.componentName?.toLowerCase().includes('registrar'))
|
|
156
|
+
);
|
|
157
|
+
if (submitStep) {
|
|
158
|
+
const ruleId2 = await pushRule({
|
|
159
|
+
rule_type: 'UI_UX',
|
|
160
|
+
title: `Patrón de guardado: ${prompt.substring(0, 60)}`,
|
|
161
|
+
description: `Flujo exitoso incluyó guardado/creación. Pasos: ${stepsNarrative.substring(0, 400)}`,
|
|
162
|
+
severity: 'important'
|
|
163
|
+
});
|
|
164
|
+
if (ruleId2) console.log(`🧠 [COLLECTIVE MEMORY] ✅ Regla UI_UX guardada: ${ruleId2}`);
|
|
165
|
+
}
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
console.warn(`[CollectiveMemory] ❌ Error inesperado en persistCollectiveMemory: ${err?.message}`);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
console.log(`\n🤖 [AGENTE] Iniciando misión: "${prompt}"`);
|
|
172
|
+
|
|
173
|
+
// Standard Login
|
|
174
|
+
const base = process.env.BASE_URL || '';
|
|
175
|
+
const target = process.env.TARGET_PATH || '/';
|
|
176
|
+
|
|
177
|
+
if (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD) {
|
|
178
|
+
console.log(">>ARCALITY_STATUS>> 🔑 Realizando login automático...");
|
|
179
|
+
await page.goto(base + '/login');
|
|
180
|
+
try {
|
|
181
|
+
const userInp = page.locator('input[type="email"], input[name="email"], input[name="username"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
|
|
182
|
+
await userInp.waitFor({ state: 'visible', timeout: 10000 });
|
|
183
|
+
|
|
184
|
+
const passInp = page.locator('input[type="password"], input[name="password"], [placeholder*="contraseña" i]').first();
|
|
185
|
+
const subBtn = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Entrar"), [role="button"]:has-text("Entrar")').first();
|
|
186
|
+
|
|
187
|
+
await userInp.click();
|
|
188
|
+
await userInp.fill(process.env.LOGIN_USER || '');
|
|
189
|
+
await passInp.click();
|
|
190
|
+
await passInp.fill(process.env.LOGIN_PASSWORD || '');
|
|
191
|
+
|
|
192
|
+
await page.waitForTimeout(500);
|
|
193
|
+
await subBtn.click();
|
|
194
|
+
|
|
195
|
+
// Esperar a que la URL cambie (salir del login)
|
|
196
|
+
await page.waitForURL(u => !u.href.includes('/login'), { timeout: 15000 }).catch(() => { });
|
|
197
|
+
await page.waitForLoadState('networkidle').catch(() => { });
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
console.warn(` ⚠️ Login automático omitido o ya autenticado.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (target !== '/' && !page.url().includes(target)) {
|
|
203
|
+
await page.goto(base + target).catch(() => { });
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
await page.goto(base + target);
|
|
207
|
+
}
|
|
208
|
+
await page.waitForLoadState('networkidle');
|
|
209
|
+
|
|
210
|
+
// Global Download Listener
|
|
211
|
+
page.on('download', async (download) => {
|
|
212
|
+
aiMarkedSuccess = true;
|
|
213
|
+
isFinished = true;
|
|
214
|
+
saveMissionResults();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// MAIN LOOP
|
|
218
|
+
const maxTotalIterations = maxSteps + 50; // Safety: máximo total de iteraciones (IA + Guía)
|
|
219
|
+
let totalIterations = 0;
|
|
220
|
+
while (!isFinished && stepCount < maxSteps) {
|
|
221
|
+
totalIterations++;
|
|
222
|
+
if (totalIterations > maxTotalIterations) {
|
|
223
|
+
console.error(`>>ARCALITY_STATUS>> 🛑 Safety limit: ${totalIterations} iteraciones totales alcanzadas. Abortando.`);
|
|
224
|
+
hasCriticalError = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
let containsError = false;
|
|
228
|
+
// stepCount se incrementa MÁS ABAJO, solo si el turno fue de IA (no de Guía)
|
|
229
|
+
await page.waitForTimeout(1000);
|
|
230
|
+
|
|
231
|
+
// --- DETECCIÓN PROACTIVA DE ÉXITO (Prevención de redundancia) ---
|
|
232
|
+
// Solo se activa tras el Turno 4 Y si ya hubo un submit (ej: guardar/save) en el historial.
|
|
233
|
+
const currentUrl = page.url();
|
|
234
|
+
const historyStr = history.join(' ').toLowerCase();
|
|
235
|
+
const hadSubmitAction = historyStr.includes('guardar') || historyStr.includes('save') || historyStr.includes('registrar');
|
|
236
|
+
const isMultiStep = prompt.toLowerCase().match(/después|luego|posteriormente|pestaña|tabla|doble clic|arrastra|paso|sección|modal|etapa/i);
|
|
237
|
+
const allowRepeatedNames = prompt.toLowerCase().match(/modal|varias|distintos|diferentes|repetir|guardar dos veces/i) || isMultiStep;
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if (stepCount > 4 && hadSubmitAction && !isMultiStep) {
|
|
241
|
+
// 1. Chequeo por retorno a lista tras guardar
|
|
242
|
+
const isNowOnList = !currentUrl.includes('/new') && !currentUrl.includes('/create') && !currentUrl.includes('/edit') && !currentUrl.includes('/details') && !currentUrl.includes('/alta');
|
|
243
|
+
const wasInForm = historyStr.includes('nueva') || historyStr.includes('crear') || historyStr.includes('alta') || historyStr.includes('/new') || historyStr.includes('/create');
|
|
244
|
+
|
|
245
|
+
if (wasInForm && isNowOnList) {
|
|
246
|
+
// Escanear SOLO elementos pequeños de feedback, no todo el body
|
|
247
|
+
const feedbackEls = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .success-message, .mat-snack-bar-container').all();
|
|
248
|
+
let foundSuccessToast = false;
|
|
249
|
+
for (const el of feedbackEls) {
|
|
250
|
+
if (await el.isVisible().catch(() => false)) {
|
|
251
|
+
const txt = await el.innerText().catch(() => '');
|
|
252
|
+
if (successKeywords.test(txt.toLowerCase())) {
|
|
253
|
+
foundSuccessToast = true;
|
|
254
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Auto-Success: Toast de éxito detectado: "${txt.substring(0, 60)}"`);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Si regresamos a la lista Y detectamos un toast de éxito, es éxito.
|
|
261
|
+
// Ya no confiamos solo en "volver a la lista" para evitar falsos positivos en URLs genéricas.
|
|
262
|
+
if (foundSuccessToast) {
|
|
263
|
+
// Solo es éxito si es un mensaje NUEVO o ha pasado tiempo desde el último submit
|
|
264
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Auto-Success: Regreso a lista Y mensaje de éxito detectado.`);
|
|
265
|
+
aiMarkedSuccess = true;
|
|
266
|
+
isFinished = true;
|
|
267
|
+
saveMissionResults();
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- LIMPIEZA DE MENSAJES DE STATUS PREEXISTENTES (Login) ---
|
|
274
|
+
// Si estamos en el primer turno real post-login, registramos mensajes existentes para ignorarlos
|
|
275
|
+
if (stepCount === 1) {
|
|
276
|
+
const initialToasts = await page.locator('.toast, .alert, [role="status"], [role="alert"]').all();
|
|
277
|
+
for (const t of initialToasts) {
|
|
278
|
+
if (await t.isVisible().catch(() => false)) {
|
|
279
|
+
lastSeenSuccessToast = await t.innerText().catch(() => "");
|
|
280
|
+
if (lastSeenSuccessToast) console.log(`>>ARCALITY_STATUS>> 💡 Nota: Ignorando mensaje preexistente: "${lastSeenSuccessToast.substring(0, 30)}..."`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ----------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
// 0. Detectar errores antes de cualquier otra lógica
|
|
287
|
+
const rawBody = await page.innerText('body').catch(() => "");
|
|
288
|
+
const bodyTxt = rawBody.toLowerCase();
|
|
289
|
+
const hasVisibleError = failureKeywords.test(bodyTxt);
|
|
290
|
+
|
|
291
|
+
// 1. INTENTAR GUÍA (Solo si NO hay errores visibles)
|
|
292
|
+
let response: AgentAction | null = null;
|
|
293
|
+
if (!hasVisibleError) {
|
|
294
|
+
response = await agent.getActionFromGuia(prompt, stepsData.length);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Error detectado en pantalla. Saltando Guía para priorizar corrección.`);
|
|
297
|
+
if (stepsData.length > 0) {
|
|
298
|
+
stepsDataBackup = [...stepsData]; // Guardar backup antes de invalidar
|
|
299
|
+
stepsData.length = 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (response) {
|
|
304
|
+
// GUARD: Invalidate guide if it suggests an action already attempted recently
|
|
305
|
+
const guideActionDesc = response.actions?.[0]
|
|
306
|
+
? `${response.actions[0].action} en "${(response.actions[0] as any).description || ''}"`.toLowerCase()
|
|
307
|
+
: '';
|
|
308
|
+
const recentHistory = history.slice(-4).map(h => h.toLowerCase());
|
|
309
|
+
const isGuideRepeating = guideActionDesc && recentHistory.some(h => h.includes(guideActionDesc.substring(0, 30)));
|
|
310
|
+
|
|
311
|
+
if (isGuideRepeating) {
|
|
312
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Guía invalidada: la acción "${guideActionDesc.substring(0, 50)}" ya fue intentada. Delegando a IA.`);
|
|
313
|
+
response = null; // Force IA to decide
|
|
314
|
+
} else {
|
|
315
|
+
guideStepCount++;
|
|
316
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Guía de éxito: Paso validado (Guía #${guideStepCount}, NO consume turno IA)`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!response) {
|
|
321
|
+
// 2. CONSULTAR IA — ESTE turno SÍ cuenta contra maxSteps
|
|
322
|
+
stepCount++;
|
|
323
|
+
console.log(`>>ARCALITY_STATUS>> ⏳ Turno IA ${stepCount} de ${maxSteps} (Guía usó ${guideStepCount} turnos gratis)...`);
|
|
324
|
+
response = await agent.askIA(prompt, history);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log(`🧠 Pensamiento: ${response.thought}`);
|
|
328
|
+
|
|
329
|
+
// DETECCIÓN DE LOOPS: Si las últimas 6 acciones muestran un patrón repetitivo, forzar terminación
|
|
330
|
+
if (!response.finish && history.length >= 6) {
|
|
331
|
+
const lastSixActions = history.slice(-6);
|
|
332
|
+
const clickActions = lastSixActions.filter(h => h.includes('click en'));
|
|
333
|
+
const fillActions = lastSixActions.filter(h => h.includes('fill en'));
|
|
334
|
+
const uniqueClicks = new Set(clickActions);
|
|
335
|
+
const uniqueFills = new Set(fillActions);
|
|
336
|
+
|
|
337
|
+
// Si hay 4+ clicks y solo 1-2 elementos únicos = LOOP DETECTADO
|
|
338
|
+
const isClickLoop = clickActions.length >= 4 && uniqueClicks.size <= 2;
|
|
339
|
+
// NEW: Si hay 4+ fills y solo 1-2 elementos únicos = LOOP DETECTADO (time picker pattern)
|
|
340
|
+
const isFillLoop = fillActions.length >= 4 && uniqueFills.size <= 2;
|
|
341
|
+
|
|
342
|
+
if (isClickLoop || isFillLoop) {
|
|
343
|
+
const loopType = isClickLoop ? 'click' : 'fill';
|
|
344
|
+
const uniqueTargets = isClickLoop ? uniqueClicks : uniqueFills;
|
|
345
|
+
console.warn(`\n⚠️ [LOOP DETECTOR] Patrón repetitivo de ${loopType} detectado:`);
|
|
346
|
+
console.warn(` Últimas acciones: ${lastSixActions.join(' → ')}`);
|
|
347
|
+
console.warn(` El agente está atrapado. Forzando terminación con sugerencia.`);
|
|
348
|
+
|
|
349
|
+
response.thought = `🛑 ERROR: Me he quedado atrapado en un bucle repetitivo intentando interactuar con: ${Array.from(uniqueTargets).join(', ')}. No puedo continuar la misión de forma autónoma.\n\n💡 REGLA DE QA: He detectado que mi acción de ${loopType} no está cambiando el estado de la página como esperaba. Esto podría ser un bug de la aplicación o una limitación de mi percepción actual.`;
|
|
350
|
+
response.finish = true;
|
|
351
|
+
response.actions = [];
|
|
352
|
+
aiMarkedSuccess = false;
|
|
353
|
+
hasCriticalError = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (response.actions && response.actions.length > 0) {
|
|
358
|
+
for (const step of response.actions) {
|
|
359
|
+
console.log(`>>ARCALITY_STATUS>> 🎯 Acción: ${step.action} en "${(step as any).description || step.selector}"`);
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const frame = (step.frameIdx !== undefined && page.frames()[step.frameIdx]) ? page.frames()[step.frameIdx] : page;
|
|
363
|
+
const loc = step.selector ? frame.locator(step.selector).first() : null;
|
|
364
|
+
|
|
365
|
+
const urlBeforeAction = page.url(); // CAPTURA ANTES DE ACCIÓN
|
|
366
|
+
|
|
367
|
+
if ((step.action === 'click' || step.action === 'double_click') && loc) {
|
|
368
|
+
const desc = ((step as any).description || '').toLowerCase();
|
|
369
|
+
const isMenuTrigger = desc.includes('more_vert') || desc.includes('menu') || desc.includes('icono :') || desc.includes('dots') || desc.includes('opciones');
|
|
370
|
+
|
|
371
|
+
// PROFUNDO: BLOQUEO DE DOBLE-SUBMIT
|
|
372
|
+
// Si ya hicimos click en "Guardar" antes, NO LO VOLVEMOS A HACER. Asumimos que la IA está confundida o verificando.
|
|
373
|
+
const isSubmitAction = desc.includes('guardar') || desc.includes('save') || desc.includes('crear') || desc.includes('registrar') || desc.includes('enviar') || desc.includes('aceptar') || desc.includes('confirmar') || desc.includes('update');
|
|
374
|
+
if (isSubmitAction) {
|
|
375
|
+
// Semantic boost for critical feedback
|
|
376
|
+
const rawBodyText = await page.innerText('body').catch(() => "");
|
|
377
|
+
const textLower = rawBodyText.toLowerCase();
|
|
378
|
+
const isCriticalFailure = textLower.includes('error') || textLower.includes('falló') || textLower.includes('no se puede') || textLower.includes('ya existe') || textLower.includes('existe') || textLower.includes('inválido') || textLower.includes('incorrecto') || textLower.includes('ligado') || textLower.includes('depende') || textLower.includes('duplicado') || textLower.includes('repetido') || textLower.includes('no encontrado') || textLower.includes('no existe') || textLower.includes('obligatorio') || textLower.includes('required');
|
|
379
|
+
const isCriticalSuccess = textLower.includes('exitosamente') || textLower.includes('guardado') || textLower.includes('creado') || textLower.includes('success') || textLower.includes('correctamente') || textLower.includes('misión cumplida');
|
|
380
|
+
const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
|
|
381
|
+
|
|
382
|
+
// Priorizar la detección de fallos/éxitos en elementos pequeños que la IA podría ignorar
|
|
383
|
+
if (!isCriticalFeedback && (step as any).type && ['span', 'p', 'label', 'i', 'svg', 'img'].includes((step as any).type)) {
|
|
384
|
+
const elementText = await loc.innerText().catch(() => "");
|
|
385
|
+
const elementTextLower = elementText.toLowerCase();
|
|
386
|
+
if (failureKeywords.test(elementTextLower)) {
|
|
387
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Feedback de ERROR en elemento pequeño: "${elementTextLower.substring(0, 50)}..."`);
|
|
388
|
+
aiMarkedSuccess = false;
|
|
389
|
+
// If a critical error is detected on a small element, it's a strong signal.
|
|
390
|
+
// We should consider this a critical error for the mission.
|
|
391
|
+
hasCriticalError = true;
|
|
392
|
+
isFinished = true;
|
|
393
|
+
saveMissionResults();
|
|
394
|
+
break; // Break out of the actions loop
|
|
395
|
+
} else {
|
|
396
|
+
const isSuccessMatch = successKeywords.test(elementTextLower);
|
|
397
|
+
if (isSuccessMatch) {
|
|
398
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Éxito visual en elemento pequeño: "${elementTextLower.substring(0, 50)}..."`);
|
|
399
|
+
aiMarkedSuccess = true;
|
|
400
|
+
isFinished = true;
|
|
401
|
+
hasCriticalError = false;
|
|
402
|
+
saveMissionResults();
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const previousSubmit = history.find(h =>
|
|
409
|
+
h.toLowerCase().includes('click') &&
|
|
410
|
+
(h.toLowerCase().includes('guardar') || h.toLowerCase().includes('save') || h.toLowerCase().includes('crear') || h.toLowerCase().includes('registrar'))
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (previousSubmit && !allowRepeatedNames) {
|
|
414
|
+
console.log(`>>ARCALITY_STATUS>> 🛑 Advertencia: Click persistente en botón de acción.`);
|
|
415
|
+
// No matamos el test, solo notificamos al agente en su historia para que lo vea
|
|
416
|
+
history.push(`Turno ${stepCount}: Intentaste hacer click en "${desc}" nuevamente, pero la página sigue mostrando el mismo estado. ¿Hay algún error visible que debas corregir antes?`);
|
|
417
|
+
await page.waitForTimeout(1000);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// PROFUNDO: BLOQUEO DE DOBLE-CLICK CONSECUTIVO
|
|
422
|
+
// Si la acción anterior fue exactamente la misma (mismo elemento), y no hubo un cambio significativo, lo bloqueamos.
|
|
423
|
+
const lastAction = history[history.length - 1];
|
|
424
|
+
if (lastAction && lastAction.includes(`click en "${desc}"`)) {
|
|
425
|
+
const isDotsTrigger = desc.includes('dots') || desc.includes('vert') || desc.includes('menu') || desc.includes('opciones');
|
|
426
|
+
const sameActionCount = history.filter(h => h.includes(`click en "${desc}"`)).length;
|
|
427
|
+
|
|
428
|
+
// Si el prompt permite repeticiones (ej. multi-step con varios 'Guardar'), somos más tolerantes
|
|
429
|
+
const toleranceLimit = allowRepeatedNames ? 3 : 1;
|
|
430
|
+
|
|
431
|
+
if (isDotsTrigger && sameActionCount < 2) {
|
|
432
|
+
console.log(` ⚠️ Re-intentando click en menú "${desc}"...`);
|
|
433
|
+
} else if (allowRepeatedNames && sameActionCount < toleranceLimit) {
|
|
434
|
+
console.log(` ⚠️ Click repetido permitido por contexto de misión en "${desc}"...`);
|
|
435
|
+
} else {
|
|
436
|
+
// FALLBACK: Si estamos estancados, ¿quizás el error ya es visible en algun post?
|
|
437
|
+
const bodyText = await page.innerText('body').catch(() => "");
|
|
438
|
+
if (bodyText.match(/ligado|depende|no se puede/i)) {
|
|
439
|
+
console.log(`\n✨ [ESTANCADO] El agente repite click pero el error ya es visible. Finalizando.`);
|
|
440
|
+
isFinished = true;
|
|
441
|
+
aiMarkedSuccess = true;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
console.log(`\n🛑 [AUTO-STOP] Bloqueando click duplicado en "${desc}". Forzando re-evaluación.`);
|
|
445
|
+
history.push(`Turno ${stepCount}: Bloqueado click repetido en "${desc}" (Evitando bucle)`);
|
|
446
|
+
await page.waitForTimeout(1000);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isMenuTrigger && history.some(h => h.includes(desc) && h.includes('click'))) {
|
|
452
|
+
console.log(` ⚠️ El disparador "${desc}" ya fue clicado. Esperando estabilidad extra...`);
|
|
453
|
+
await page.waitForTimeout(1500);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
await loc.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => { });
|
|
457
|
+
|
|
458
|
+
if (step.action === 'double_click') {
|
|
459
|
+
await loc.dblclick({ timeout: 10000, force: true });
|
|
460
|
+
} else {
|
|
461
|
+
// HACK: Si es cerrar toast y falla, ignoramos
|
|
462
|
+
if (desc.includes('toast') || desc.includes('close')) {
|
|
463
|
+
await loc.click({ timeout: 2000, force: true }).catch(() => console.log(" ⚠️ No se pudo cerrar toast, ignorando..."));
|
|
464
|
+
} else {
|
|
465
|
+
await loc.click({ timeout: 10000, force: true });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isMenuTrigger) {
|
|
470
|
+
console.log(" ⏳ Abriendo menú, esperando 1.5s para renderizado...");
|
|
471
|
+
await page.waitForTimeout(1500);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// INTELIGENCIA DE FINALIZACIÓN POST-GUARDADO
|
|
475
|
+
// Si la acción fue "Guardar" o "Crear", verificamos si hubo navegación exitosa
|
|
476
|
+
const actsLikeSubmit = desc.includes('guardar') || desc.includes('save') || desc.includes('crear') || desc.includes('registrar');
|
|
477
|
+
|
|
478
|
+
if (actsLikeSubmit) {
|
|
479
|
+
console.log(" ⏳ Esperando transición post-guardado...");
|
|
480
|
+
try {
|
|
481
|
+
await page.waitForLoadState('networkidle', { timeout: 8000 });
|
|
482
|
+
} catch (e) { console.log(" ⚠️ Timeout esperando red, continuando..."); }
|
|
483
|
+
|
|
484
|
+
// 1. CHEQUEO POR VISUALIZACIÓN DE ÉXITO Y ERRORES (Solo Toasts/Alertas)
|
|
485
|
+
const postSubmitFeedback = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .success-message, .mat-snack-bar-container').all();
|
|
486
|
+
let foundSuccessPost = false;
|
|
487
|
+
for (const fb of postSubmitFeedback) {
|
|
488
|
+
if (await fb.isVisible().catch(() => false)) {
|
|
489
|
+
const fbText = await fb.innerText().catch(() => '');
|
|
490
|
+
const postSuccessKw = /exitosamente|creado correctamente|guardado correctamente|operación exitosa|registro guardado|saved successfully|created successfully/i;
|
|
491
|
+
if (postSuccessKw.test(fbText)) {
|
|
492
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Éxito visual detectado en toast: "${fbText.substring(0, 60)}"`);
|
|
493
|
+
aiMarkedSuccess = true;
|
|
494
|
+
isFinished = true;
|
|
495
|
+
saveMissionResults();
|
|
496
|
+
foundSuccessPost = true;
|
|
497
|
+
break;
|
|
498
|
+
} else if (failureKeywords.test(fbText.toLowerCase())) {
|
|
499
|
+
// ⭐ ERROR DE VALIDACIÓN DE NEGOCIO — Guardar como regla
|
|
500
|
+
console.log(`>>ARCALITY_STATUS>> 📚 [BUSINESS RULE] Error de validación capturado: "${fbText.substring(0, 80)}"`);
|
|
501
|
+
captureValidationRule(fbText.trim(), `Acción: click en "${desc}" en ${page.url()}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (foundSuccessPost) break;
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
// 2. CHEQUEO POR CAMBIO DE URL (Backup)
|
|
509
|
+
const currentUrl = page.url();
|
|
510
|
+
const isBackOnList = urlBeforeAction !== currentUrl && !currentUrl.includes('/new') && !currentUrl.includes('/edit') && !currentUrl.includes('/create');
|
|
511
|
+
|
|
512
|
+
if (isBackOnList) {
|
|
513
|
+
console.log(`\n✨ [AUTO-SUCCESS] Navegación detectada tras guardar (${urlBeforeAction} -> ${currentUrl}). Confirmando...`);
|
|
514
|
+
await page.waitForTimeout(2000);
|
|
515
|
+
|
|
516
|
+
const postNavAlerts = await page.locator(
|
|
517
|
+
'.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .error-message, .mat-snack-bar-container'
|
|
518
|
+
).all();
|
|
519
|
+
let hasPostNavError = false;
|
|
520
|
+
for (const alertEl of postNavAlerts) {
|
|
521
|
+
if (await alertEl.isVisible().catch(() => false)) {
|
|
522
|
+
const alertTxt = await alertEl.innerText().catch(() => '');
|
|
523
|
+
if (failureKeywords.test(alertTxt.toLowerCase())) {
|
|
524
|
+
hasPostNavError = true;
|
|
525
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Error en alerta post-guardado: "${alertTxt.substring(0, 60)}"`);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!hasPostNavError) {
|
|
532
|
+
aiMarkedSuccess = true;
|
|
533
|
+
isFinished = true;
|
|
534
|
+
await persistCollectiveMemory(); // ← Persistir en TODOS los caminos de éxito
|
|
535
|
+
saveMissionResults();
|
|
536
|
+
break;
|
|
537
|
+
} else {
|
|
538
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Se detectó navegación pero hay un ERROR en alerta/toast visible.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else if (desc.includes('siguiente') || desc.includes('next')) {
|
|
542
|
+
await page.waitForTimeout(1000);
|
|
543
|
+
} else {
|
|
544
|
+
await page.waitForTimeout(500); // Click normal, espera corta
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (step.action === 'fill' && loc && step.value) {
|
|
548
|
+
// DETECCIÓN INTELIGENTE DE INPUT FILE
|
|
549
|
+
const isFile = await loc.evaluate((el: any) => el.tagName === 'INPUT' && el.type === 'file').catch(() => false);
|
|
550
|
+
|
|
551
|
+
if (isFile) {
|
|
552
|
+
console.log(` 📂 Detectado input de archivo. Subiendo: "${step.value}"`);
|
|
553
|
+
const filePath = step.value.replace(/^"|"$/g, '').trim(); // Limpiar comillas si la IA las puso
|
|
554
|
+
await loc.setInputFiles(filePath);
|
|
555
|
+
} else {
|
|
556
|
+
await loc.click({ timeout: 5000 }).catch(() => { });
|
|
557
|
+
await loc.fill(step.value, { timeout: 10000, force: true });
|
|
558
|
+
await loc.press('Tab').catch(() => { }); // Disparar validaciones/change events
|
|
559
|
+
}
|
|
560
|
+
await page.waitForTimeout(500); // Dar un respiro a la UI
|
|
561
|
+
}
|
|
562
|
+
else if (step.action === 'wait') {
|
|
563
|
+
await page.waitForTimeout(3000);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Registrar en historial para la IA
|
|
567
|
+
const descForHistory = ((step as any).description || step.selector || '').substring(0, 80);
|
|
568
|
+
const valStr = step.value ? ` con valor "${step.value}"` : '';
|
|
569
|
+
history.push(`Turno ${stepCount}: ${step.action} en "${descForHistory}"${valStr}`);
|
|
570
|
+
urlHistory.push(page.url());
|
|
571
|
+
|
|
572
|
+
// Registrar en Guía de Éxito (solo si el componente tiene un nombre razonable)
|
|
573
|
+
const compName = ((step as any).description || '').substring(0, 80);
|
|
574
|
+
if (compName.length < 100) { // Skip guide storage for misidentified giant text blobs
|
|
575
|
+
stepsData.push({
|
|
576
|
+
url: urlBeforeAction, // USAR URL CAPTURADA ANTES
|
|
577
|
+
componentName: compName,
|
|
578
|
+
componentType: (step as any).type,
|
|
579
|
+
action_data: { action: step.action, value: step.value },
|
|
580
|
+
finish: response.finish
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
} catch (e: any) {
|
|
585
|
+
console.warn(` ⚠️ Acción fallida: ${e.message}`);
|
|
586
|
+
history.push(`Turno ${stepCount}: ❌ FALLÓ ${step.action} en "${(step as any).description}" - Error: ${e.message}. REGLA MANDATORIA: Debes investigar la causa usando 'inspect_element_details' o 'capture_console_errors' antes de cualquier otro paso.`);
|
|
587
|
+
containsError = true;
|
|
588
|
+
|
|
589
|
+
// Solo matamos si el error es persistente para evitar bucles infinitos
|
|
590
|
+
const errorCount = history.filter(h => h.includes('FALLÓ') && h.includes((step as any).description)).length;
|
|
591
|
+
if (errorCount >= 2) {
|
|
592
|
+
console.error(`>>ARCALITY_STATUS>> 🛑 Demasiados fallos técnicos en este elemento "${(step as any).description}". Abortando misión.`);
|
|
593
|
+
hasCriticalError = true;
|
|
594
|
+
isFinished = true;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// SOLO permitimos finalizar si no hubo errores técnicos en este turno
|
|
602
|
+
if (response.finish) {
|
|
603
|
+
if (!containsError) {
|
|
604
|
+
console.log(">>ARCALITY_STATUS>> ✅ El agente solicitó finalizar misión.");
|
|
605
|
+
isFinished = true;
|
|
606
|
+
|
|
607
|
+
// --- REGISTRO DEL PASO FINAL EN LA GUÍA DE ÉXITO ---
|
|
608
|
+
// Si el agente termina sin acciones (ej. vía create_test_evidence), registramos un paso implícito
|
|
609
|
+
// para que la guía sepa que aquí debe terminar en futuras ejecuciones.
|
|
610
|
+
if (!response.actions || response.actions.length === 0) {
|
|
611
|
+
stepsData.push({
|
|
612
|
+
url: page.url(),
|
|
613
|
+
componentName: "MISSION_END",
|
|
614
|
+
componentType: "FINISH_SIGNAL",
|
|
615
|
+
action_data: { action: 'finish' },
|
|
616
|
+
finish: true
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
// Si hubo acciones, nos aseguramos que la última tenga el flag 'finish'
|
|
620
|
+
if (stepsData.length > 0) {
|
|
621
|
+
stepsData[stepsData.length - 1].finish = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Verificar solo en toasts/alertas VISIBLES, NO en el body completo
|
|
626
|
+
const finishFeedbackEls = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .mat-snack-bar-container, .error-message').all();
|
|
627
|
+
let hasVisibleFailure = false;
|
|
628
|
+
for (const el of finishFeedbackEls) {
|
|
629
|
+
if (await el.isVisible().catch(() => false)) {
|
|
630
|
+
const txt = await el.innerText().catch(() => '');
|
|
631
|
+
if (failureKeywords.test(txt.toLowerCase())) {
|
|
632
|
+
hasVisibleFailure = true;
|
|
633
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Error detectado en toast/alerta: "${txt.substring(0, 60)}"`);
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!hasVisibleFailure) {
|
|
640
|
+
// Marcar éxito: el agente pidió finish y no hay errores visibles
|
|
641
|
+
aiMarkedSuccess = true;
|
|
642
|
+
hasCriticalError = false;
|
|
643
|
+
|
|
644
|
+
// Guardar resultados proactivamente antes de que Playwright cierre el contexto
|
|
645
|
+
saveMissionResults();
|
|
646
|
+
|
|
647
|
+
// Restaurar stepsData si fue invalidada pero la misión se recuperó
|
|
648
|
+
if (stepsData.length === 0 && stepsDataBackup.length > 0) {
|
|
649
|
+
stepsData.push(...stepsDataBackup);
|
|
650
|
+
console.log(`>>ARCALITY_STATUS>> 🔄 Guía de Éxito restaurada (${stepsDataBackup.length} pasos recuperados).`);
|
|
651
|
+
}
|
|
652
|
+
await persistCollectiveMemory(); // ← camino: response.finish sin error
|
|
653
|
+
} else {
|
|
654
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ El agente intentó finalizar pero se detectó un ERROR en alert/toast.`);
|
|
655
|
+
aiMarkedSuccess = false;
|
|
656
|
+
isFinished = false; // Forzar a que el agente vea el error
|
|
657
|
+
history.push(`Turno ${stepCount}: El agente intentó finalizar pero el sistema detectó un error en un toast/alerta visible.`);
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
console.warn(">>ARCALITY_STATUS>> ⚠️ El agente intentó finalizar pero hubo fallos en el turno. Continuando para investigación...");
|
|
661
|
+
isFinished = false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Auto-detección por feedback visual (Toasts, Alertas, o mensajes de estado)
|
|
666
|
+
const feedbackSelector = '.toast, .alert, .message, div[role="status"], .error-message, [class*="alert"], [class*="toast"], .status-message';
|
|
667
|
+
const feedbackLocator = page.locator(feedbackSelector);
|
|
668
|
+
|
|
669
|
+
// Solo autodetectar si estamos más allá del Turno 2
|
|
670
|
+
if (stepCount > 2) {
|
|
671
|
+
const allFeedback = await feedbackLocator.all();
|
|
672
|
+
for (const fb of allFeedback) {
|
|
673
|
+
if (await fb.isVisible()) {
|
|
674
|
+
const rawTxt = await fb.innerText();
|
|
675
|
+
const txt = rawTxt.toLowerCase();
|
|
676
|
+
|
|
677
|
+
if (failureKeywords.test(txt)) {
|
|
678
|
+
console.log(`>>ARCALITY_STATUS>> ⚠️ Feedback de ERROR detectado: "${rawTxt.substring(0, 60)}..."`);
|
|
679
|
+
aiMarkedSuccess = false;
|
|
680
|
+
|
|
681
|
+
// ⭐ ERROR DE VALIDACIÓN / NEGOCIO — Guardar como regla de dominio
|
|
682
|
+
captureValidationRule(rawTxt.trim(), `URL: ${page.url()}`);
|
|
683
|
+
|
|
684
|
+
// CRÍTICO: Si detectamos un error, invalidamos la Guía de Éxito para este run
|
|
685
|
+
if (stepsData.length > 0) {
|
|
686
|
+
console.log(`>>ARCALITY_STATUS>> 🔄 Invalidando Guía de Éxito por feedback de error.`);
|
|
687
|
+
stepsDataBackup = [...stepsData]; // Guardar backup
|
|
688
|
+
stepsData.length = 0;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else if (successKeywords.test(txt)) {
|
|
692
|
+
if (txt !== lastSeenSuccessToast) {
|
|
693
|
+
console.log(`>>ARCALITY_STATUS>> ✨ Éxito visual detectado: "${txt.substring(0, 50)}..."`);
|
|
694
|
+
aiMarkedSuccess = true;
|
|
695
|
+
isFinished = true;
|
|
696
|
+
hasCriticalError = false;
|
|
697
|
+
await persistCollectiveMemory(); // ← camino: toast de éxito
|
|
698
|
+
saveMissionResults();
|
|
699
|
+
break;
|
|
700
|
+
} else {
|
|
701
|
+
console.log(`>>ARCALITY_STATUS>> 💡 Mensaje de éxito persistente ignorado.`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (aiMarkedSuccess) {
|
|
710
|
+
hasCriticalError = false;
|
|
711
|
+
// Si no se persistió aún (ej. ningún camino early de success lo hizo), persistir ahora
|
|
712
|
+
await persistCollectiveMemory();
|
|
713
|
+
|
|
714
|
+
console.log("✍️ [HUMANIZANDO] Generando resumen de éxito para el reporte...");
|
|
715
|
+
const summary = await agent.humanizeMissionResult(prompt, history);
|
|
716
|
+
if (summary) {
|
|
717
|
+
await testInfo.attach('success_summary', {
|
|
718
|
+
body: Buffer.from(summary, 'utf-8'),
|
|
719
|
+
contentType: 'text/plain'
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
console.log("🤖 [SMART-YAML] Generating mission card...");
|
|
724
|
+
const smartYaml = await agent.summarizeMissionToYaml(prompt, history, target);
|
|
725
|
+
if (smartYaml) {
|
|
726
|
+
const yamlPath = path.join(contextDir, 'last-mission-smart.yaml');
|
|
727
|
+
fs.writeFileSync(yamlPath, smartYaml);
|
|
728
|
+
console.log(`>>ARCALITY_STATUS>> 📝 Smart Mission Card generated.`);
|
|
729
|
+
await testInfo.attach('smart_mission_card', {
|
|
730
|
+
body: Buffer.from(smartYaml, 'utf-8'),
|
|
731
|
+
contentType: 'text/yaml'
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
saveMissionResults();
|
|
737
|
+
|
|
738
|
+
if (!aiMarkedSuccess) {
|
|
739
|
+
throw new Error("Misión no completada o finalizada con errores.");
|
|
740
|
+
}
|
|
741
|
+
});
|