@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.
Files changed (97) hide show
  1. package/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
  2. package/.agents/skills/frontend-design/LICENSE.txt +177 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
  5. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  6. package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
  7. package/.agents/skills/playwright-best-practices/README.md +147 -0
  8. package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
  9. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
  10. package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
  11. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
  12. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
  13. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
  14. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
  15. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
  16. package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
  17. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
  18. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
  19. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
  20. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
  21. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
  22. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
  23. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
  24. package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
  25. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
  26. package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
  27. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
  28. package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
  29. package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
  30. package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
  31. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
  32. package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
  33. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
  34. package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
  35. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
  36. package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
  37. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
  38. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
  39. package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
  40. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
  41. package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
  42. package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
  43. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
  44. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
  45. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
  46. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
  47. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
  48. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
  49. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
  50. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
  51. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
  52. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
  53. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
  54. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
  55. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
  56. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
  57. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
  58. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
  59. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
  60. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
  61. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
  62. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
  63. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
  64. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
  65. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
  66. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
  67. package/.env.example +21 -0
  68. package/README.md +30 -0
  69. package/bin/arcality.mjs +86 -0
  70. package/package.json +66 -0
  71. package/playwright.config.ts +12 -0
  72. package/scripts/cleanup-qmsdev.mjs +63 -0
  73. package/scripts/discover-view.mjs +52 -0
  74. package/scripts/extract-view.mjs +64 -0
  75. package/scripts/gen-and-run.mjs +838 -0
  76. package/scripts/init.mjs +290 -0
  77. package/scripts/migrate-to-central-out.mjs +157 -0
  78. package/scripts/postinstall.mjs +63 -0
  79. package/scripts/rebrand-report.mjs +241 -0
  80. package/scripts/setup.mjs +166 -0
  81. package/src/KnowledgeService.ts +239 -0
  82. package/src/arcalityClient.mjs +266 -0
  83. package/src/configLoader.mjs +179 -0
  84. package/src/configManager.mjs +172 -0
  85. package/src/consoleBanner.ts +32 -0
  86. package/src/envSetup.ts +205 -0
  87. package/src/index.ts +25 -0
  88. package/src/projectInspector.ts +42 -0
  89. package/src/services/collectiveMemoryService.ts +178 -0
  90. package/src/testRunner.ts +201 -0
  91. package/tests/_helpers/ArcalityReporter.ts +490 -0
  92. package/tests/_helpers/agentic-runner.spec.ts +741 -0
  93. package/tests/_helpers/ai-agent-helper.ts +1573 -0
  94. package/tests/_helpers/discover-view.spec.ts +238 -0
  95. package/tests/_helpers/extract-view.spec.ts +118 -0
  96. package/tests/_helpers/qa-tools.ts +333 -0
  97. 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
+ });