@arcadialdev/arcality 2.4.29 → 2.4.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.29",
3
+ "version": "2.4.31",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,9 +18,9 @@
18
18
  "bin/",
19
19
  "scripts/",
20
20
  "src/",
21
- "tests/_helpers/",
22
21
  ".agents/",
23
- "public/",
22
+ "tests/_helpers/agentic-runner.bundle.spec.js",
23
+ "tests/_helpers/ArcalityReporter.js",
24
24
  "playwright.config.js",
25
25
  "README.md"
26
26
  ],
@@ -586,7 +586,7 @@ async function main() {
586
586
 
587
587
  if (!discoverPath) {
588
588
  const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
589
- const promptMsg = knownBaseUrl
589
+ const promptMsg = knownBaseUrl
590
590
  ? `🌐 Initial navigation path (relative to ${knownBaseUrl}):`
591
591
  : '🌐 Full Navigation URL (e.g., http://localhost:3000/):';
592
592
 
@@ -623,7 +623,7 @@ async function main() {
623
623
  if (!validation.valid) {
624
624
  const errorMessages = {
625
625
  'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
626
- 'invalid_format': '🔑 Invalid API Key. It must start with "arc_k_"',
626
+ 'invalid_format': '🔑 Invalid API Key. It must start with "arc_"',
627
627
  'invalid_api_key': '🔑 API Key not recognized by the server',
628
628
  'plan_expired': '💳 Your plan has expired'
629
629
  };
package/scripts/init.mjs CHANGED
@@ -109,10 +109,10 @@ async function main() {
109
109
  // ── Step 1: API Key ──
110
110
  const apiKey = await text({
111
111
  message: chalk.cyan('🔑 Enter your Arcality API Key:'),
112
- placeholder: 'arc_k_live_xxxxx',
112
+ placeholder: 'arc_live_xxxxx',
113
113
  validate: (v) => {
114
114
  if (!v || !v.trim()) return 'API Key is required.';
115
- if (!v.startsWith('arc_k_')) return 'API Key must start with "arc_k_"';
115
+ if (!v.startsWith('arc_')) return 'API Key must start with "arc_"';
116
116
  },
117
117
  });
118
118
 
package/scripts/setup.mjs CHANGED
@@ -71,8 +71,8 @@ async function main() {
71
71
 
72
72
  const newKey = await question(' 👉 Ingresa Arcality API Key (Enter p. mantener): ');
73
73
  if (newKey) {
74
- if (!newKey.startsWith('arc_k_')) {
75
- console.log(' ❌ La API Key debe iniciar con "arc_k_"');
74
+ if (!newKey.startsWith('arc_')) {
75
+ console.log(' ❌ La API Key debe iniciar con "arc_"');
76
76
  process.exit(1);
77
77
  }
78
78
  config.api_key = newKey;
@@ -119,7 +119,7 @@ export async function validateApiKey() {
119
119
  return { valid: false, error: 'no_api_key', mode: 'mock' };
120
120
  }
121
121
 
122
- if (!key.startsWith('arc_k_')) {
122
+ if (!key.startsWith('arc_')) {
123
123
  return { valid: false, error: 'invalid_format', mode: 'mock' };
124
124
  }
125
125
 
@@ -161,7 +161,7 @@ export async function validateApiKey() {
161
161
 
162
162
  /**
163
163
  * Requests to start a mission.
164
- * @param {string} prompt - The mission prompt (only a hash is sent, never the raw text)
164
+ * @param {string} prompt - The mission prompt
165
165
  * @param {string} targetUrl - Target portal URL
166
166
  */
167
167
  export async function startMission(prompt, targetUrl) {
@@ -182,6 +182,7 @@ export async function startMission(prompt, targetUrl) {
182
182
  'x-api-key': key
183
183
  },
184
184
  body: JSON.stringify({
185
+ prompt: prompt,
185
186
  prompt_hash: simpleHash(prompt),
186
187
  target_url: targetUrl,
187
188
  project_id: projectId || undefined,
@@ -939,7 +939,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
939
939
  const role = htmlEl.getAttribute("role") || "";
940
940
  const text = (htmlEl.innerText || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || "").trim().replace(/\s+/g, " ");
941
941
  const textLower = text.toLowerCase();
942
- const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\xF3") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("existe") || textLower.includes("inv\xE1lido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido")) && text.length < 200;
942
+ const isCriticalFailure = (textLower.includes("error") || textLower.includes("fall\xF3") || textLower.includes("no se puede") || textLower.includes("ya existe") || textLower.includes("existe") || textLower.includes("inv\xE1lido") || textLower.includes("incorrecto") || textLower.includes("ligado") || textLower.includes("depende") || textLower.includes("duplicado") || textLower.includes("repetido") || /* Validaciones ZOD / Wizard de campos de formulario */
943
+ textLower.includes("debe ser mayor") || textLower.includes("debe ser menor") || textLower.includes("debe ser igual") || textLower.includes("debe ser mayor o igual") || textLower.includes("debe ser menor o igual") || /* ZOD date messages (Spanish) */
944
+ textLower.includes("debe ser posterior") || textLower.includes("debe ser anterior") || textLower.includes("debe ser una fecha") || textLower.includes("fecha debe ser") || textLower.includes("posterior o igual a la fecha") || textLower.includes("anterior o igual a la fecha") || textLower.includes("posterior a la fecha") || textLower.includes("anterior a la fecha") || textLower.includes("fecha") && (textLower.includes("actual") || textLower.includes("pasada") || textLower.includes("futura") || textLower.includes("requerida") || textLower.includes("inv\xE1lida") || textLower.includes("incorrecta") || textLower.includes("posterior") || textLower.includes("anterior")) || /* ZOD number/range messages */
945
+ textLower.includes("debe ser un n\xFAmero") || textLower.includes("debe ser positivo") || textLower.includes("debe ser negativo") || textLower.includes("m\xEDnimo") && textLower.includes("caracteres") || textLower.includes("m\xE1ximo") && textLower.includes("caracteres") || textLower.includes("al menos") || textLower.includes("como m\xE1ximo") || /* Generic ZOD/form required */
946
+ textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower.includes("requerido") || textLower.includes("obligatorio") || textLower.includes("no puede estar vac\xEDo") || textLower.includes("no puede ser") || textLower.includes("no es v\xE1lido") || textLower.includes("no es correcto") || textLower.includes("recuerda llenar") || textLower.includes("completa los campos") || textLower.includes("campos obligatorios") || textLower.includes("formato incorrecto") || textLower.includes("formato inv\xE1lido") || textLower.includes("valor no permitido")) && text.length < 250;
943
947
  const isCriticalSuccess = (textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") && text.length < 100) && text.length < 200;
944
948
  const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
945
949
  if (!isCriticalFeedback && (tag === "span" || tag === "p" || tag === "label" || tag === "i" || tag === "svg" || tag === "img" || tag === "div")) {
@@ -1445,6 +1449,13 @@ These are NOT optional. You MUST use these tools in these situations:
1445
1449
  4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
1446
1450
  5. **MENUS & DROPDOWNS**: After clicking a toggle/menu, WAIT for the next turn. Never click the same menu icon twice in a row.
1447
1451
  6. **ERROR HANDLING**: If you see '\u{1F6D1} [ERROR_CR\xCDTICO]', STOP everything. Analyze the error, fix the data, then retry.
1452
+ 6b. **DUPLICATE / REPEATED VALUE ERROR \u2014 MANDATORY PROTOCOL**:
1453
+ - If HISTORY contains a line with "ERROR VISIBLE EN PANTALLA" or "ERROR POST-GUARDADO" with words like "repetido", "duplicado", "ya existe", or "already exists":
1454
+ 1. You MUST identify WHICH field caused the duplication (usually the main name/title field).
1455
+ 2. You MUST use \`fill\` to REPLACE that field's value with a NEW unique value. Add a numeric suffix like \`_${(/* @__PURE__ */ new Date()).getMinutes()}${(/* @__PURE__ */ new Date()).getSeconds()}\` or a short random number.
1456
+ 3. ONLY AFTER changing the value, attempt to save again.
1457
+ 4. **NEVER attempt to save with the same value that was already rejected.** This is FORBIDDEN.
1458
+ 5. If you cannot determine which field is the duplicate, use \`capture_console_errors\` to get hints from the application.
1448
1459
  7. **DATA UNIQUENESS**: When generating test data, ALWAYS include a unique suffix (timestamp, counter). Never use "Test123" or "Prueba" alone.
1449
1460
  8. **FINISH CORRECTLY**: Use \`finish: true\` ONLY when the mission objective is FULLY achieved.
1450
1461
  - **BEWARE**: If you see a record in a table, confirm it is the one you JUST created (e.g., check timestamp or log message). Do NOT assume an existing record is your success.
@@ -1453,7 +1464,34 @@ These are NOT optional. You MUST use these tools in these situations:
1453
1464
  10. **STUCK DETECTION**: If history shows 3+ similar actions without progress, use \`report_inability_to_proceed\`.
1454
1465
  11. **NEGATIVE TESTING**: If mission expects an error (e.g., "verifica que no se pueda guardar sin campos"), and you see that error, the mission is SUCCESS. Use \`finish: true\`.
1455
1466
  12. **GUIDE CONFLICT**: If SUCCESS MEMORY disagrees with what you see on screen, ABANDON the memory and trust your eyes.
1456
- 13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.`,
1467
+ 13. **NATIVE CONTROLS**: For \`<input type="time">\`, \`<input type="date">\`, or similar native pickers, NEVER use \`fill\` repeatedly. Use \`interact_native_control\` or \`send_keyboard_event\` instead. If \`fill\` fails once, switch strategy immediately.
1468
+
1469
+ # WIZARD & MULTI-STEP FORMS (CRITICAL)
1470
+ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 Step 3):
1471
+
1472
+ 1. **VALIDATION ERROR ON SAVE = STEP HAS INVALID DATA**: If you see a \u{1F6D1} [ERROR_CR\xCDTICO] after clicking Save/Next and you are on step N, it means a PREVIOUS step contains bad data. You MUST navigate BACK to that step.
1473
+ - Look for a "Back" / "Anterior" / "Periodo" / step-number button and click it to return.
1474
+ - NEVER retry the Save button without first fixing the invalid data in the previous step.
1475
+
1476
+ 2. **DATE VALIDATION ERRORS \u2014 MANDATORY PROTOCOL (ZOD-AWARE)**:
1477
+ - ZOD generates Spanish validation messages such as:
1478
+ - "La fecha de inicio debe ser posterior o igual a la fecha actual."
1479
+ - "La fecha debe ser una fecha futura."
1480
+ - "Debe ser posterior a la fecha de inicio."
1481
+ - "El valor debe ser mayor o igual a X."
1482
+ - ANY \u{1F6D1} [ERROR_CR\xCDTICO] containing FECHA + (POSTERIOR/ANTERIOR/ACTUAL/PASADA/FUTURA) is a **ZOD DATE VALIDATION ERROR**.
1483
+ - **RECOVERY PROTOCOL** (follow in order, NEVER skip):
1484
+ 1. STOP \u2014 do NOT click Save/Siguiente again with the same data.
1485
+ 2. Identify which wizard step contains the failing date field (check HISTORY \u2014 usually Step 1 or Step 2).
1486
+ 3. Click the Back/Anterior/step-number button to return to that step.
1487
+ 4. Locate the date input using \`validate_element_state\` with 'has_value' to confirm its current (bad) value.
1488
+ 5. Clear and re-fill the date using \`interact_native_control\` with control_type \`'date'\` and a value \u2265 TODAY in YYYY-MM-DD format.
1489
+ 6. TODAY: \`${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}\` \u2014 use this exact expression to compute the date dynamically.
1490
+ 7. After fixing ALL date fields, advance through each wizard step again.
1491
+
1492
+ 3. **NEVER LOOP ON THE SAME STEP**: If you've been on the same wizard step for 3+ turns without advancing, go back one step and verify the data is correct before trying to advance again.
1493
+
1494
+ 4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm.`,
1457
1495
  cache_control: { type: "ephemeral" }
1458
1496
  }
1459
1497
  ];
@@ -1536,7 +1574,7 @@ ${history.slice(-25).join("\n") || "None"}`
1536
1574
  headers,
1537
1575
  signal: controller.signal,
1538
1576
  body: JSON.stringify({
1539
- model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
1577
+ model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
1540
1578
  max_tokens: 4096,
1541
1579
  system: systemPromptBlocks,
1542
1580
  tools: rawTools.map((t) => ({
@@ -1581,7 +1619,7 @@ ${history.slice(-25).join("\n") || "None"}`
1581
1619
  const textResponse = message.content.find((c) => c.type === "text")?.text || "";
1582
1620
  if (toolCalls.length === 0) {
1583
1621
  console.log(`(${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
1584
- return { thought: textResponse || "No action decided", actions: [] };
1622
+ return { thought: textResponse || "No action decided", actions: [], usage: data.usage };
1585
1623
  }
1586
1624
  const toolResults = [];
1587
1625
  let uiActionCall = null;
@@ -1640,7 +1678,8 @@ ${history.slice(-25).join("\n") || "None"}`
1640
1678
 
1641
1679
  \u{1F4A1} SUGERENCIA: ${toolInput.prompt_suggestion}`,
1642
1680
  actions: [],
1643
- finish: true
1681
+ finish: true,
1682
+ usage: data.usage
1644
1683
  };
1645
1684
  } else if (toolName === "validate_element_state") {
1646
1685
  const { idx, validations } = toolInput;
@@ -2147,7 +2186,8 @@ ${report}` });
2147
2186
  }
2148
2187
  return { ...a, value: finalValue, selector: c?.selector, frameIdx: c?.frameIdx, description: c?.name, type: c?.type };
2149
2188
  }),
2150
- finish: input.finish
2189
+ finish: input.finish,
2190
+ usage: data.usage
2151
2191
  };
2152
2192
  }
2153
2193
  }
@@ -2178,7 +2218,7 @@ ${report}` });
2178
2218
  method: "POST",
2179
2219
  headers,
2180
2220
  body: JSON.stringify({
2181
- model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
2221
+ model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2182
2222
  max_tokens: 300,
2183
2223
  system: "Eres un Senior QA personalizador de reportes. Tu tarea es resumir en una sola frase corta, profesional y amigable por qu\xE9 una misi\xF3n de testing fue exitosa bas\xE1ndote en el historial de pasos. Usa un tono de victoria. No menciones detalles t\xE9cnicos como 'selectores' o 'DOM'. El resumen debe empezar con algo como '\xA1Misi\xF3n cumplida! ...'",
2184
2224
  messages: [
@@ -2226,7 +2266,7 @@ ${history.join("\n")}` }
2226
2266
  method: "POST",
2227
2267
  headers,
2228
2268
  body: JSON.stringify({
2229
- model: process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022",
2269
+ model: process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest",
2230
2270
  max_tokens: 1e3,
2231
2271
  system: `You are an elite QA Engineer. Your task is to transform a raw execution history into a clean, human-friendly English YAML Mission Card.
2232
2272
 
@@ -2409,6 +2449,7 @@ function captureValidationRule(errorMessage, context) {
2409
2449
  let stepsDataBackup = [];
2410
2450
  const maxSteps = 30;
2411
2451
  let lastSeenSuccessToast = "";
2452
+ let accumulatedCost = 0;
2412
2453
  const saveMissionResults = () => {
2413
2454
  if (resultsSaved)
2414
2455
  return;
@@ -2612,6 +2653,22 @@ function captureValidationRule(errorMessage, context) {
2612
2653
  stepsDataBackup = [...stepsData];
2613
2654
  stepsData.length = 0;
2614
2655
  }
2656
+ try {
2657
+ const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message, .mat-snack-bar-container').all();
2658
+ for (const el of errorEls) {
2659
+ if (await el.isVisible().catch(() => false)) {
2660
+ const errTxt = await el.innerText().catch(() => "");
2661
+ if (errTxt && failureKeywords.test(errTxt.toLowerCase())) {
2662
+ const isDuplicateError = /repetido|duplicado|ya existe|already exists/i.test(errTxt);
2663
+ const correctionHint = isDuplicateError ? `\u{1F6D1} ACCI\xD3N REQUERIDA: El valor que ingresaste ya existe en el sistema. DEBES regresar al campo que caus\xF3 el error y cambiar su valor por uno \xDANICO (agrega un sufijo como _${Date.now().toString().slice(-4)} o un n\xFAmero aleatorio). NO intentes guardar sin cambiar el valor primero.` : `\u{1F6D1} ACCI\xD3N REQUERIDA: El sistema mostr\xF3 un error de validaci\xF3n. Analiza el error y corrige el dato antes de reintentar.`;
2664
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR VISIBLE EN PANTALLA: "${errTxt.trim().substring(0, 200)}". ${correctionHint}`);
2665
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error inyectado en historial: "${errTxt.substring(0, 80)}"`);
2666
+ break;
2667
+ }
2668
+ }
2669
+ }
2670
+ } catch {
2671
+ }
2615
2672
  }
2616
2673
  if (response) {
2617
2674
  const guideActionDesc = response.actions?.[0] ? `${response.actions[0].action} en "${response.actions[0].description || ""}"`.toLowerCase() : "";
@@ -2629,6 +2686,21 @@ function captureValidationRule(errorMessage, context) {
2629
2686
  stepCount++;
2630
2687
  console.log(`>>ARCALITY_STATUS>> \u23F3 Turno IA ${stepCount} de ${maxSteps} (Gu\xEDa us\xF3 ${guideStepCount} turnos gratis)...`);
2631
2688
  response = await agent.askIA(prompt, history, stepCount);
2689
+ if (response.usage) {
2690
+ const inputs = response.usage.input_tokens || 0;
2691
+ const outputs = response.usage.output_tokens || 0;
2692
+ const cost = inputs / 1e6 * 3 + outputs / 1e6 * 15;
2693
+ accumulatedCost += cost;
2694
+ if (accumulatedCost >= 6) {
2695
+ console.warn(`
2696
+ \u{1F6D1} [AUTO-STOP] Capacidad m\xE1xima de exploraci\xF3n t\xE9cnica alcanzada para esta prueba.`);
2697
+ response.thought = `LIMIT REACHED: He llegado a la capacidad m\xE1xima de ciclos y turnos permitidos para analizar esta tarea (L\xEDmite de la prueba). Detendr\xE9 esta misi\xF3n para evitar sobrecarga del sistema.`;
2698
+ response.finish = true;
2699
+ response.actions = [];
2700
+ aiMarkedSuccess = false;
2701
+ hasCriticalError = true;
2702
+ }
2703
+ }
2632
2704
  }
2633
2705
  console.log(`\u{1F9E0} Pensamiento: ${response.thought}`);
2634
2706
  if (!response.finish && history.length >= 6) {
@@ -2771,6 +2843,9 @@ function captureValidationRule(errorMessage, context) {
2771
2843
  } else if (failureKeywords.test(fbText.toLowerCase())) {
2772
2844
  console.log(`>>ARCALITY_STATUS>> \u{1F4DA} [BUSINESS RULE] Error de validaci\xF3n capturado: "${fbText.substring(0, 80)}"`);
2773
2845
  captureValidationRule(fbText.trim(), `Acci\xF3n: click en "${desc}" en ${page.url()}`);
2846
+ const isDuplicateError = /repetido|duplicado|ya existe|already exists/i.test(fbText);
2847
+ const correctionHint = isDuplicateError ? `CORRECCI\xD3N OBLIGATORIA: El valor que ingresaste ya existe. Ve al campo que lo caus\xF3 y c\xE1mbialo por uno \xFAnico (a\xF1ade _${Date.now().toString().slice(-4)} como sufijo).` : `CORRECCI\xD3N OBLIGATORIA: Corrige el campo se\xF1alado antes de intentar guardar de nuevo.`;
2848
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR POST-GUARDADO: "${fbText.trim().substring(0, 200)}". ${correctionHint}`);
2774
2849
  }
2775
2850
  }
2776
2851
  }
Binary file
Binary file
package/public/logo.png DELETED
Binary file
@@ -1,63 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
-
4
- const baseDir = 'c:/Users/apulido/OneDrive - Block Networks/Documentos/Arcadial Projects/Char.Dev/out/qmsDev';
5
- const nestedDir = path.join(baseDir, 'context', 'qmsDev');
6
- const targetContextDir = path.join(baseDir, 'context');
7
- const targetMissionsDir = path.join(baseDir, 'missions');
8
-
9
- function cleanup() {
10
- if (!fs.existsSync(nestedDir)) {
11
- console.log('Nested directory not found, skipping.');
12
- return;
13
- }
14
-
15
- const files = fs.readdirSync(nestedDir);
16
-
17
- files.forEach(file => {
18
- const filePath = path.join(nestedDir, file);
19
- if (file.startsWith('agent-log-')) {
20
- console.log(`Moving ${file} to context/`);
21
- fs.renameSync(filePath, path.join(targetContextDir, file));
22
- } else if (file === 'memoria-agente.json') {
23
- const targetMemory = path.join(targetContextDir, 'memoria-agente.json');
24
- if (fs.existsSync(targetMemory)) {
25
- console.log('Merging memoria-agente.json...');
26
- const mainMemory = JSON.parse(fs.readFileSync(targetMemory, 'utf8'));
27
- const nestedMemory = JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
- const combined = [...mainMemory, ...nestedMemory];
29
- fs.writeFileSync(targetMemory, JSON.stringify(combined, null, 2));
30
- fs.unlinkSync(filePath);
31
- } else {
32
- console.log('Moving memoria-agente.json...');
33
- fs.renameSync(filePath, targetMemory);
34
- }
35
- } else if (file === 'saved_missions') {
36
- const missions = fs.readdirSync(filePath);
37
- missions.forEach(m => {
38
- if (m.endsWith('.yaml') || m.endsWith('.yml')) {
39
- console.log(`Moving mission ${m} to missions/`);
40
- fs.renameSync(path.join(filePath, m), path.join(targetMissionsDir, m));
41
- }
42
- });
43
- fs.rmdirSync(filePath, { recursive: true });
44
- } else if (fs.statSync(filePath).isFile()) {
45
- console.log(`Moving other file ${file} to context/`);
46
- fs.renameSync(filePath, path.join(targetContextDir, file));
47
- }
48
- });
49
-
50
- // Try to remove the empty nested dir
51
- try {
52
- if (fs.readdirSync(nestedDir).length === 0) {
53
- fs.rmdirSync(nestedDir);
54
- console.log('Removed empty nested directory.');
55
- } else {
56
- console.warn('Nested directory not empty, keeping it.');
57
- }
58
- } catch (e) {
59
- console.error('Error removing nested dir:', e.message);
60
- }
61
- }
62
-
63
- cleanup();
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env node
2
- import 'dotenv/config';
3
- import fs from 'node:fs';
4
- import { spawn } from 'node:child_process';
5
- import path from 'node:path';
6
-
7
- const targetPath = process.argv[2] || '/';
8
- const isWin = process.platform === 'win32';
9
- const cmd = isWin ? 'npx.cmd' : 'npx';
10
- const testFile = 'tests/_helpers/discover-view.spec.ts';
11
-
12
- function parseDotEnvFile(p) {
13
- try {
14
- const raw = fs.readFileSync(p, 'utf8');
15
- const lines = raw.split(/\r?\n/);
16
- const out = {};
17
- for (const line of lines) {
18
- const trimmed = line.trim();
19
- if (!trimmed || trimmed.startsWith('#')) continue;
20
- const idx = trimmed.indexOf('=');
21
- if (idx === -1) continue;
22
- let k = trimmed.slice(0, idx).trim();
23
- let v = trimmed.slice(idx + 1).trim();
24
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
25
- out[k] = v;
26
- }
27
- return out;
28
- } catch {
29
- return {};
30
- }
31
- }
32
-
33
- const fileEnv = parseDotEnvFile(path.join(process.cwd(), '.env'));
34
- const env = { ...process.env, ...fileEnv, TARGET_PATH: targetPath };
35
-
36
- const args = ['playwright', 'test', testFile, '--project=chromium'];
37
- // Debug printing removed to avoid leaking placeholders; command is spawned directly
38
-
39
- const child = spawn(cmd, args, {
40
- stdio: 'inherit',
41
- shell: isWin,
42
- env,
43
- });
44
-
45
- child.on('exit', (code) => {
46
- process.exit(code ?? 0);
47
- });
48
-
49
- child.on('error', (err) => {
50
- console.error('Failed to run playwright:', err);
51
- process.exit(1);
52
- });
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env node
2
- import 'dotenv/config';
3
- import fs from 'node:fs';
4
- import { spawn } from 'node:child_process';
5
- import path from 'node:path';
6
-
7
- const targetPath = process.argv[2] || '/';
8
- const isWin = process.platform === 'win32';
9
- const cmd = isWin ? 'npx.cmd' : 'npx';
10
-
11
- // Use POSIX-style forward slashes so Playwright reliably matches the file on all platforms
12
- const testFile = 'tests/_helpers/extract-view.spec.ts';
13
-
14
- function parseDotEnvFile(p) {
15
- try {
16
- const raw = fs.readFileSync(p, 'utf8');
17
- const lines = raw.split(/\r?\n/);
18
- const out = {};
19
- for (const line of lines) {
20
- const trimmed = line.trim();
21
- if (!trimmed || trimmed.startsWith('#')) continue;
22
- const idx = trimmed.indexOf('=');
23
- if (idx === -1) continue;
24
- let k = trimmed.slice(0, idx).trim();
25
- let v = trimmed.slice(idx + 1).trim();
26
- if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
27
- out[k] = v;
28
- }
29
- return out;
30
- } catch {
31
- return {};
32
- }
33
- }
34
-
35
- const fileEnv = parseDotEnvFile(path.join(process.cwd(), '.env'));
36
- const env = { ...process.env, ...fileEnv, TARGET_PATH: targetPath };
37
-
38
- const args = ['playwright', 'test', testFile, '--project=chromium', '--headed'];
39
-
40
- // Debug printing removed to avoid leaking placeholders; command is spawned directly
41
-
42
- const child = spawn(cmd, args, {
43
- stdio: 'inherit',
44
- shell: isWin,
45
- env,
46
- });
47
-
48
- child.on('exit', (code) => {
49
- // DESACTIVADO POR PETICIÓN DE USUARIO: Evitar pantallas extras/consolas
50
- /*
51
- try {
52
- const showCmd = isWin ? 'npx.cmd' : 'npx';
53
- spawn(showCmd, ['playwright', 'show-report'], { stdio: 'ignore', shell: isWin, detached: true });
54
- } catch (e) {
55
- // ignore
56
- }
57
- */
58
- process.exit(code ?? 0);
59
- });
60
-
61
- child.on('error', (err) => {
62
- console.error('Failed to run playwright:', err);
63
- process.exit(1);
64
- });
@@ -1,157 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
- const PROJECT_ROOT = path.join(__dirname, "..");
8
-
9
- function migrate() {
10
- const outDirs = fs.readdirSync(PROJECT_ROOT).filter(f => f.startsWith('out_') && fs.statSync(path.join(PROJECT_ROOT, f)).isDirectory());
11
- const centralOut = path.join(PROJECT_ROOT, 'out');
12
-
13
- if (!fs.existsSync(centralOut)) fs.mkdirSync(centralOut);
14
-
15
- console.log(`🚀 Starting migration of ${outDirs.length} legacy output directories...`);
16
-
17
- for (const oldDirName of outDirs) {
18
- const oldDirPath = path.join(PROJECT_ROOT, oldDirName);
19
- const children = fs.readdirSync(oldDirPath);
20
-
21
- // Try to find a config name
22
- let configName = oldDirName.replace('out_', '');
23
- const configSubDir = children.find(c => fs.statSync(path.join(oldDirPath, c)).isDirectory());
24
-
25
- if (configSubDir) {
26
- configName = configSubDir;
27
- console.log(`📦 Found configuration folder "${configName}" inside ${oldDirName}`);
28
- }
29
-
30
- const newConfigPath = path.join(centralOut, configName);
31
- const contextPath = path.join(newConfigPath, 'context');
32
- const missionsPath = path.join(newConfigPath, 'missions');
33
- const reportsPath = path.join(newConfigPath, 'reports');
34
-
35
- [contextPath, missionsPath, reportsPath].forEach(p => fs.mkdirSync(p, { recursive: true }));
36
-
37
- // Move files
38
- children.forEach(child => {
39
- const oldChildPath = path.join(oldDirPath, child);
40
- const isDir = fs.statSync(oldChildPath).isDirectory();
41
-
42
- if (child === configSubDir) {
43
- // Move internals of config subDir to context/projects or similar?
44
- // For now, let's just move the whole folder to context
45
- moveFolder(oldChildPath, path.join(contextPath, child));
46
- } else if (child.startsWith('agent-log') || child.includes('memory') || child.includes('memoria') || child.includes('components-')) {
47
- moveFile(oldChildPath, path.join(contextPath, child));
48
- } else if (child.endsWith('.yaml') || child.endsWith('.yml')) {
49
- moveFile(oldChildPath, path.join(missionsPath, child));
50
- } else if (child === 'saved_missions') {
51
- const subMissions = fs.readdirSync(oldChildPath);
52
- subMissions.forEach(sm => moveFile(path.join(oldChildPath, sm), path.join(missionsPath, sm)));
53
- fs.rmdirSync(oldChildPath, { recursive: true });
54
- } else {
55
- // Default to context
56
- if (isDir) moveFolder(oldChildPath, path.join(contextPath, child));
57
- else moveFile(oldChildPath, path.join(contextPath, child));
58
- }
59
- });
60
-
61
- // Remove the old out_ directory if empty
62
- if (fs.readdirSync(oldDirPath).length === 0) {
63
- fs.rmdirSync(oldDirPath);
64
- console.log(`✅ Cleaned up ${oldDirName}`);
65
- } else {
66
- console.warn(`⚠️ ${oldDirName} not empty after migration.`);
67
- }
68
- }
69
-
70
- // Move tests/projects folders
71
- const projectsDir = path.join(PROJECT_ROOT, 'tests', 'projects');
72
- if (fs.existsSync(projectsDir)) {
73
- const projects = fs.readdirSync(projectsDir).filter(f => fs.statSync(path.join(projectsDir, f)).isDirectory());
74
- console.log(`📁 Migrating ${projects.length} project models from tests/projects...`);
75
- for (const pName of projects) {
76
- const oldPPath = path.join(projectsDir, pName);
77
- // We assume project name matches or is related to a config
78
- // If out/[pName] exists, move there
79
- const targetConfigPath = path.join(centralOut, pName);
80
- const targetContextPath = path.join(targetConfigPath, 'context');
81
- if (!fs.existsSync(targetContextPath)) fs.mkdirSync(targetContextPath, { recursive: true });
82
-
83
- moveFolder(oldPPath, path.join(targetContextPath, pName));
84
- console.log(`✅ Moved project model "${pName}" to out/${pName}/context/`);
85
- }
86
- // If empty, remove tests/projects
87
- if (fs.readdirSync(projectsDir).length === 0) {
88
- fs.rmdirSync(projectsDir);
89
- }
90
- }
91
-
92
- // Move loose files in central out/ to out/Default/context/
93
- const looseFiles = fs.readdirSync(centralOut).filter(f => fs.statSync(path.join(centralOut, f)).isFile());
94
- if (looseFiles.length > 0) {
95
- console.log(`📄 Migrating ${looseFiles.length} loose files from out/ to out/Default/context/...`);
96
- const defaultContext = path.join(centralOut, 'Default', 'context');
97
- if (!fs.existsSync(defaultContext)) fs.mkdirSync(defaultContext, { recursive: true });
98
- looseFiles.forEach(f => moveFile(path.join(centralOut, f), path.join(defaultContext, f)));
99
- }
100
-
101
- // Move global reports
102
- const globalReports = ['tests-report', 'playwright-internal-report', 'test-results'];
103
- for (const gr of globalReports) {
104
- const grPath = path.join(PROJECT_ROOT, gr);
105
- if (fs.existsSync(grPath)) {
106
- const activeConfig = process.env.ACTIVE_CONFIG || 'Default';
107
- const targetDir = path.join(centralOut, activeConfig, 'reports', gr);
108
- fs.mkdirSync(path.dirname(targetDir), { recursive: true });
109
- moveFolder(grPath, targetDir);
110
- console.log(`📊 Moved global report "${gr}" to out/${activeConfig}/reports/`);
111
- }
112
- }
113
-
114
- console.log('✨ Migration completed successfully.');
115
- }
116
-
117
- function moveFile(src, dest) {
118
- if (!fs.existsSync(src)) return;
119
- try {
120
- fs.renameSync(src, dest);
121
- } catch (e) {
122
- if (e.code === 'EXDEV') {
123
- fs.copyFileSync(src, dest);
124
- fs.unlinkSync(src);
125
- } else {
126
- console.error(`Error moving ${src}:`, e.message);
127
- }
128
- }
129
- }
130
-
131
- function moveFolder(src, dest) {
132
- if (!fs.existsSync(src)) return;
133
- try {
134
- if (fs.existsSync(dest)) {
135
- // Merge content if exists
136
- const files = fs.readdirSync(src);
137
- files.forEach(f => {
138
- const s = path.join(src, f);
139
- const d = path.join(dest, f);
140
- if (fs.statSync(s).isDirectory()) moveFolder(s, d);
141
- else moveFile(s, d);
142
- });
143
- fs.rmdirSync(src, { recursive: true });
144
- } else {
145
- fs.renameSync(src, dest);
146
- }
147
- } catch (e) {
148
- if (e.code === 'EXDEV') {
149
- fs.cpSync(src, dest, { recursive: true });
150
- fs.rmSync(src, { recursive: true });
151
- } else {
152
- console.error(`Error moving folder ${src}:`, e.message);
153
- }
154
- }
155
- }
156
-
157
- migrate();