@arcadialdev/arcality 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,12 +66,18 @@ The Arcality flow is designed to be used globally or project-wide via `npx`. Bel
66
66
  | Command | Description | Internal Behavior |
67
67
  | :--- | :--- | :--- |
68
68
  | `npx arcality` | **Interactive Menu**<br/>Launches a visual menu in the terminal to guide the user. | Prompts for the Mission to execute. Ideal for new users who want a guided experience. |
69
- | `npx arcality init` | **Initial Configuration**<br/>Connects the current project to Arcadial. | Prompts for your API Key and Project ID, generates `arcality.config.json`, and validates the portal connection. |
70
- | `npx arcality run` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), and unleashes the AI agent. Ideal for CI/CD. |
71
- | `npx arcality --logs` | **History Viewer**<br/>Displays test performance and tokens spent. | Reads the local `arcality-history.log` and renders a colorful table listing each mission and success status. |
72
-
73
-
74
- ---
69
+ | `npx arcality init` | **Initial Configuration**<br/>Connects the current project to Arcadial. | Prompts for your API Key and Project ID, generates `arcality.config.json`, and validates the portal connection. |
70
+ | `npx arcality run` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), and unleashes the AI agent. Ideal for CI/CD. |
71
+ | `npx arcality logs` | **Mission Logs**<br/>Shows the last 3 executed missions in a concise QA-friendly format. | Reads the local `.arcality/out` mission summaries and displays result, failure reason, last relevant steps, and report path. |
72
+
73
+ ### Parallel collection execution
74
+
75
+ When running a saved collection from the interactive CLI, Arcality can execute multiple missions at the same time. The default is `3` simultaneous missions and the maximum allowed value is `5`.
76
+
77
+ Parallel execution opens multiple independent browser sessions. Arcality cannot validate the tested portal's internal session rules, user lock policies, shared test data constraints, or limits for multiple logins with the same account. If the portal does not support parallel sessions safely, run the collection sequentially with concurrency `1`.
78
+
79
+
80
+ ---
75
81
 
76
82
  ## 🛠️ Internal Architecture
77
83
 
package/bin/arcality.mjs CHANGED
@@ -19,16 +19,17 @@ let isRunAll = false; // CI/CD mode: runs all missions from API headlessly
19
19
 
20
20
  // Buscamos los comandos en cualquier posición para evitar filtros de NPM en Windows
21
21
  rawArgs.forEach(arg => {
22
- if (arg === 'run') isRun = true;
23
- if (arg === 'init') isInit = true;
24
- if (arg === 'setup') isSetup = true;
25
- if (arg === '--logs') isLogs = true;
26
- if (arg === '--run-all') isRunAll = true;
27
- });
22
+ if (arg === 'run') isRun = true;
23
+ if (arg === 'init') isInit = true;
24
+ if (arg === 'setup') isSetup = true;
25
+ if (arg === 'logs') isLogs = true;
26
+ if (arg === '--logs') isLogs = true;
27
+ if (arg === '--run-all') isRunAll = true;
28
+ });
28
29
 
29
30
  if (isLogs) {
30
- const logsScript = path.join(PACKAGE_ROOT, 'scripts', 'arcality-logs.mjs');
31
- spawn('node', [logsScript], {
31
+ const logsScript = path.join(PACKAGE_ROOT, 'scripts', 'arcality-logs.mjs');
32
+ spawn('node', [logsScript, ...rawArgs.filter(a => a !== 'logs' && a !== '--logs')], {
32
33
  stdio: 'inherit',
33
34
  cwd: process.cwd(),
34
35
  env: { ...process.env, ARCALITY_ROOT: PACKAGE_ROOT }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -6,23 +6,25 @@ const path = require('path');
6
6
  // ── Dynamic reporter: JUnit is added when running in CI (Azure DevOps, GitHub Actions) ──
7
7
  // The JUnit XML file enables native test result publishing in your pipeline dashboard.
8
8
  // In local/interactive mode, the JUnit reporter is skipped — only Arcality's HTML reporter runs.
9
- const reportsDir = process.env.REPORTS_DIR || path.join(__dirname, 'tests-report');
10
- const isCI = process.env.CI === 'true';
9
+ const reportsDir = process.env.REPORTS_DIR || path.join(__dirname, 'tests-report');
10
+ const playwrightOutputDir = process.env.PLAYWRIGHT_OUTPUT_DIR || path.join(reportsDir, '..', 'playwright-output');
11
+ const isCI = process.env.CI === 'true';
12
+
13
+ const reporters = [
14
+ ['line'],
15
+ [path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'), { outputDir: reportsDir }],
16
+ ];
17
+
18
+ if (isCI) {
19
+ reporters.push(['junit', { outputFile: path.join(reportsDir, 'results.xml') }]);
20
+ }
11
21
 
12
- const reporters = [
13
- ['line'],
14
- [path.join(__dirname, 'tests', '_helpers', 'ArcalityReporter.js'), { outputDir: reportsDir }],
15
- ];
16
-
17
- if (isCI) {
18
- reporters.push(['junit', { outputFile: path.join(reportsDir, 'results.xml') }]);
19
- }
20
-
21
- module.exports = defineConfig({
22
- testDir: path.join(__dirname, 'tests', '_helpers'),
23
- testMatch: ['agentic-runner.bundle.spec.js'],
24
- reporter: reporters,
25
- use: {
22
+ module.exports = defineConfig({
23
+ testDir: path.join(__dirname, 'tests', '_helpers'),
24
+ testMatch: ['agentic-runner.bundle.spec.js'],
25
+ reporter: reporters,
26
+ outputDir: playwrightOutputDir,
27
+ use: {
26
28
  // ── Dynamic BASE_URL ──
27
29
  // Priority: CLI --base-url flag → arcality.config baseUrl → BASE_URL env → default
28
30
  // This allows pipelines to inject the target environment dynamically:
@@ -1,93 +1,167 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import chalk from "chalk";
5
- import { fileURLToPath } from 'url';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
10
-
11
- const rootOutDir = path.join(process.cwd(), ".arcality", "out");
12
-
13
- // Buscar el archivo arcality-history.log recursivamente (o tomar el más reciente si hay varios reportes)
14
- let centralLog = null;
15
- const findLogRecursively = (dir) => {
16
- if (!fs.existsSync(dir)) return;
17
- const files = fs.readdirSync(dir);
18
- for (const file of files) {
19
- const fullPath = path.join(dir, file);
20
- if (fs.statSync(fullPath).isDirectory()) {
21
- findLogRecursively(fullPath);
22
- } else if (file === "arcality-history.log") {
23
- // Guardamos el primero que encontremos, asumiendo que es el contexto activo
24
- if (!centralLog) centralLog = fullPath;
25
- }
26
- }
27
- };
28
-
29
- findLogRecursively(rootOutDir);
30
-
31
- console.log(chalk.cyan(`\n🔍 Arcality Logs Viewer`));
32
- console.log(chalk.gray(`=========================\n`));
33
-
34
- if (!centralLog || !fs.existsSync(centralLog)) {
35
- console.log(chalk.yellow(`No se encontró un historial de logs en este proyecto.`));
36
- console.log(chalk.gray(`(Directorio buscado: ${rootOutDir})\n`));
37
- console.log(`Ejecuta algunas misiones de prueba primero usando el comando arcality.\n`);
38
- process.exit(0);
39
- }
40
-
41
- try {
42
- const rawData = fs.readFileSync(centralLog, 'utf8');
43
- const lines = rawData.split('\n').filter(l => l.trim().length > 0);
44
-
45
- if (lines.length === 0) {
46
- console.log(chalk.yellow(`El archivo de historial de misiones está vacío.\n`));
47
- process.exit(0);
48
- }
49
-
50
- console.log(chalk.bold(`Registros encontrados: `) + chalk.cyan(lines.length));
51
- console.log(chalk.gray(`Mostrando los últimos 20 resultados:`));
52
- console.log();
53
-
54
- const recentLines = lines.slice(-20);
55
- let totalInput = 0;
56
- let totalOutput = 0;
57
-
58
- recentLines.forEach((line, index) => {
59
- try {
60
- const data = JSON.parse(line);
61
-
62
- // Icono según resultado
63
- const statusIcon = data.success ? chalk.green('✅') : chalk.red('❌');
64
-
65
- // Fecha bonita
66
- const date = new Date(data.ts).toLocaleString();
67
-
68
- // Contabilizar tokens
69
- const inT = data.usage?.input_tokens || 0;
70
- const caT = data.usage?.cache_creation_input_tokens || 0;
71
- const crT = data.usage?.cache_read_input_tokens || 0;
72
- const totalIn = inT + caT + crT;
73
- const totalOut = data.usage?.output_tokens || 0;
74
-
75
- totalInput += totalIn;
76
- totalOutput += totalOut;
77
-
78
- const costStr = chalk.yellow(`[IN: ${totalIn} | OUT: ${totalOut}]`);
79
- const targetStr = data.target ? chalk.gray(` @ ${data.target}`) : '';
80
-
81
- console.log(`${statusIcon} ${chalk.blue(date)} ${costStr}`);
82
- console.log(` ${chalk.white(data.mission)}${targetStr}`);
83
- console.log(` ${chalk.gray(`Pasos dados: ${data.steps}`)}\n`);
84
- } catch (e) {
85
- console.log(chalk.red(`⚠️ Error parseando línea: ${line.substring(0, 50)}...\n`));
86
- }
87
- });
88
-
89
- console.log(chalk.gray(`--------------------------------------------------\n`));
90
-
91
- } catch (err) {
92
- console.error(chalk.red(`❌ Ocurrió un error al leer los logs:`), err.message);
93
- }
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+
6
+ const MAX_RUNS = 3;
7
+ const rootOutDir = path.join(process.cwd(), ".arcality", "out");
8
+
9
+ function walk(dir, results = []) {
10
+ if (!fs.existsSync(dir)) return results;
11
+
12
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
13
+ const fullPath = path.join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ walk(fullPath, results);
16
+ continue;
17
+ }
18
+
19
+ if (
20
+ /^mission-log-\d+\.json$/.test(entry.name) ||
21
+ /^agent-log-\d+\.json$/.test(entry.name) ||
22
+ entry.name === "mission-result.json"
23
+ ) {
24
+ results.push(fullPath);
25
+ }
26
+ }
27
+
28
+ return results;
29
+ }
30
+
31
+ function readRun(file) {
32
+ try {
33
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
34
+ const stat = fs.statSync(file);
35
+ const parsedTime = Number.isFinite(Number(data.timestamp))
36
+ ? Number(data.timestamp)
37
+ : Date.parse(data.timestamp || data.ts || "");
38
+
39
+ return {
40
+ file,
41
+ data,
42
+ time: Number.isFinite(parsedTime) ? parsedTime : stat.mtimeMs,
43
+ };
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function oneLine(value) {
50
+ return String(value || "").replace(/\s+/g, " ").trim();
51
+ }
52
+
53
+ function truncate(value, max = 180) {
54
+ const text = oneLine(value);
55
+ return text.length > max ? `${text.slice(0, max - 3)}...` : text;
56
+ }
57
+
58
+ function formatDate(time) {
59
+ return new Date(time).toLocaleString("es-MX", {
60
+ dateStyle: "short",
61
+ timeStyle: "short",
62
+ });
63
+ }
64
+
65
+ function formatReason(reason) {
66
+ const reasons = {
67
+ max_steps_reached: "Se alcanzo el limite de pasos configurado.",
68
+ unknown_failure: "La mision termino sin una confirmacion clara de exito.",
69
+ timeout_or_crash: "La ejecucion se detuvo o el navegador fallo.",
70
+ user_canceled: "La mision fue cancelada por el usuario.",
71
+ missing_mission_log: "No se encontro el resumen tecnico de la mision.",
72
+ };
73
+
74
+ return reasons[reason] || reason || "Sin motivo registrado.";
75
+ }
76
+
77
+ function getHistory(data) {
78
+ if (Array.isArray(data.history)) return data.history;
79
+ if (Array.isArray(data.steps)) return data.steps;
80
+ return [];
81
+ }
82
+
83
+ function cleanStep(step) {
84
+ return oneLine(step)
85
+ .replace(/^>>ARCALITY_STATUS>>\s*/i, "")
86
+ .replace(/^Turno\s+\d+:\s*/i, "")
87
+ .replace(/^Paso\s+\d+:\s*/i, "");
88
+ }
89
+
90
+ function reportPathFor(data) {
91
+ const reportDir = data.report_dir || data.reportPath || data.reportsDir || "";
92
+ if (!reportDir) return "";
93
+
94
+ const indexPath = path.join(reportDir, "index.html");
95
+ return fs.existsSync(indexPath) ? indexPath : reportDir;
96
+ }
97
+
98
+ const files = walk(rootOutDir);
99
+ const runs = files
100
+ .map(readRun)
101
+ .filter(Boolean)
102
+ .sort((a, b) => b.time - a.time);
103
+
104
+ const uniqueRuns = [];
105
+ const seen = new Set();
106
+ for (const run of runs) {
107
+ const key = `${run.data.timestamp || run.time}:${run.data.prompt || run.data.mission || ""}`;
108
+ if (seen.has(key)) continue;
109
+ seen.add(key);
110
+ uniqueRuns.push(run);
111
+ if (uniqueRuns.length >= MAX_RUNS) break;
112
+ }
113
+
114
+ console.log(chalk.cyan("\nArcality Logs"));
115
+ console.log(chalk.gray("=============\n"));
116
+
117
+ if (uniqueRuns.length === 0) {
118
+ console.log(chalk.yellow("No se encontraron misiones recientes en este proyecto."));
119
+ console.log(chalk.gray(`Directorio revisado: ${rootOutDir}`));
120
+ console.log("Ejecuta una mision con arcality run y vuelve a consultar arcality logs.\n");
121
+ process.exit(0);
122
+ }
123
+
124
+ console.log(chalk.gray(`Mostrando las ultimas ${uniqueRuns.length} misiones ejecutadas.\n`));
125
+
126
+ uniqueRuns.forEach((run, index) => {
127
+ const { data, time } = run;
128
+ const success = data.success === true;
129
+ const status = success ? chalk.green("EXITOSA") : chalk.red("FALLIDA");
130
+ const mission = data.prompt || data.mission || "Mision sin nombre";
131
+ const targetParts = [data.target, data.target_path].filter(Boolean);
132
+ const target = targetParts.length > 0 ? targetParts.join("") : "";
133
+ const stepsCount = Number.isFinite(Number(data.steps)) ? Number(data.steps) : getHistory(data).length;
134
+ const reportPath = reportPathFor(data);
135
+ const history = getHistory(data)
136
+ .map(cleanStep)
137
+ .filter(Boolean)
138
+ .filter(step => !/uso total de tokens|memoria guardada|patternsearch/i.test(step));
139
+
140
+ console.log(chalk.bold(`${index + 1}. ${status}`) + chalk.gray(` ${formatDate(time)}`));
141
+ console.log(` Mision: ${chalk.white(truncate(mission, 220))}`);
142
+ if (target) console.log(` Inicio: ${chalk.gray(truncate(target, 180))}`);
143
+ console.log(` Pasos: ${chalk.cyan(String(stepsCount || 0))}`);
144
+
145
+ if (!success) {
146
+ console.log(` Motivo: ${chalk.yellow(truncate(formatReason(data.fail_reason), 220))}`);
147
+ if (data.fail_reasoning) {
148
+ console.log(` Analisis: ${chalk.yellow(truncate(data.fail_reasoning, 260))}`);
149
+ }
150
+ }
151
+
152
+ const recentSteps = history.slice(-5);
153
+ if (recentSteps.length > 0) {
154
+ console.log(" Ultimos pasos:");
155
+ for (const step of recentSteps) {
156
+ console.log(` - ${truncate(step, 180)}`);
157
+ }
158
+ } else {
159
+ console.log(` Ultimos pasos: ${chalk.gray("No hay pasos detallados disponibles.")}`);
160
+ }
161
+
162
+ if (reportPath) {
163
+ console.log(` Reporte: ${chalk.gray(reportPath)}`);
164
+ }
165
+
166
+ console.log("");
167
+ });