@arcadialdev/arcality 2.6.1 → 2.6.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/package.json
CHANGED
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -27,6 +27,29 @@ import {
|
|
|
27
27
|
// Load global config at startup (injects keys into process.env)
|
|
28
28
|
setupProcessEnv();
|
|
29
29
|
|
|
30
|
+
// ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
|
|
31
|
+
// En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
|
|
32
|
+
// cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
|
|
33
|
+
// Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
|
|
34
|
+
//
|
|
35
|
+
// La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
|
|
36
|
+
// ANTES de que cmd.exe los procese. Al detectar Ctrl+C (byte 0x03) lo convertimos en
|
|
37
|
+
// un SIGINT de Node — cmd.exe nunca lo ve y nunca muestra el prompt.
|
|
38
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
39
|
+
try {
|
|
40
|
+
process.stdin.setRawMode(true);
|
|
41
|
+
process.stdin.resume();
|
|
42
|
+
process.stdin.setEncoding('utf8');
|
|
43
|
+
process.stdin.on('data', (key) => {
|
|
44
|
+
if (key === '\u0003') { // Ctrl+C
|
|
45
|
+
process.emit('SIGINT');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Si falla (ej. en CI sin TTY), continuar sin raw mode
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
54
|
const __dirname = path.dirname(__filename);
|
|
32
55
|
const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
|
|
@@ -317,8 +340,13 @@ function run(cmd, args, options = {}) {
|
|
|
317
340
|
|
|
318
341
|
// Use standard cross-platform spawn. When shell is false, Node's child_process
|
|
319
342
|
// properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
|
|
343
|
+
//
|
|
344
|
+
// CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
|
|
345
|
+
// showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
|
|
346
|
+
// When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
|
|
347
|
+
// the entire process tree before our SIGINT handler can run, bypassing the finally block.
|
|
320
348
|
const p = spawn(cmd, args, {
|
|
321
|
-
stdio: isDebug ? ['
|
|
349
|
+
stdio: isDebug ? ['ignore', 'inherit', 'inherit'] : ['ignore', 'pipe', 'pipe'],
|
|
322
350
|
shell: false,
|
|
323
351
|
cwd,
|
|
324
352
|
env,
|
|
@@ -347,24 +375,26 @@ function run(cmd, args, options = {}) {
|
|
|
347
375
|
}
|
|
348
376
|
};
|
|
349
377
|
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
if (!isDebug && p.stderr) {
|
|
378
|
+
if (p.stdout) p.stdout.on('data', handleData);
|
|
379
|
+
if (p.stderr) {
|
|
354
380
|
p.stderr.on('data', (data) => {
|
|
355
|
-
process.stderr.write(data);
|
|
381
|
+
if (isDebug) process.stderr.write(data);
|
|
382
|
+
else process.stderr.write(data);
|
|
356
383
|
outputBuffer += data.toString();
|
|
357
384
|
});
|
|
358
385
|
}
|
|
359
386
|
|
|
360
|
-
p.on('exit', (code) => {
|
|
387
|
+
p.on('exit', (code, signal) => {
|
|
361
388
|
const lastRunLog = path.join(LOGS_DIR, 'last-run.log');
|
|
362
389
|
try {
|
|
363
390
|
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
364
391
|
fs.writeFileSync(lastRunLog, outputBuffer);
|
|
365
392
|
} catch (e) { }
|
|
366
393
|
|
|
367
|
-
|
|
394
|
+
// code===0 → clean success
|
|
395
|
+
// code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
|
|
396
|
+
// code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
|
|
397
|
+
if (code === 0 || signal !== null || code === 1) resolve();
|
|
368
398
|
else {
|
|
369
399
|
if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
|
|
370
400
|
reject(new Error(`Process exited with code ${code}`));
|
|
@@ -740,19 +770,62 @@ async function main() {
|
|
|
740
770
|
|
|
741
771
|
const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
|
|
742
772
|
|
|
743
|
-
// ──
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
773
|
+
// ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
|
|
774
|
+
// El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
|
|
775
|
+
// que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
|
|
776
|
+
// Clack lo re-registra en cada frame de animación.
|
|
777
|
+
//
|
|
778
|
+
// La solución: interceptar process.exit() antes de que Clack lo llame,
|
|
779
|
+
// y usar prependListener para que NUESTRO handler siempre se ejecute primero.
|
|
780
|
+
|
|
781
|
+
let activeChildProcess = null;
|
|
747
782
|
let userCanceledMission = false;
|
|
748
|
-
|
|
749
|
-
|
|
783
|
+
let sigintCount = 0;
|
|
784
|
+
let _missionActive = true; // Mientras true, bloqueamos el process.exit de Clack
|
|
785
|
+
|
|
786
|
+
// Guardar el process.exit original y reemplazarlo temporalmente
|
|
787
|
+
const _origProcessExit = process.exit.bind(process);
|
|
788
|
+
process.exit = (code) => {
|
|
789
|
+
if (_missionActive) {
|
|
790
|
+
// Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
|
|
791
|
+
// Nuestro cancelHandler ya habrá sido invocado vía prependListener.
|
|
792
|
+
// El proceso se cerrará naturalmente cuando el child process termine.
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
_origProcessExit(code);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const cancelHandler = () => {
|
|
799
|
+
sigintCount++;
|
|
800
|
+
if (sigintCount > 3) {
|
|
801
|
+
console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
|
|
802
|
+
_missionActive = false;
|
|
803
|
+
_origProcessExit(1);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (userCanceledMission) {
|
|
808
|
+
console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
750
811
|
userCanceledMission = true;
|
|
751
812
|
|
|
752
813
|
console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
|
|
753
|
-
if (!process.argv.includes('--debug'))
|
|
814
|
+
if (!process.argv.includes('--debug')) {
|
|
815
|
+
try { s.stop(chalk.yellow('⚠️ Misión cancelada.')); } catch { /* ignore */ }
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Terminar el proceso hijo de Playwright directamente (Windows-safe)
|
|
819
|
+
if (activeChildProcess && !activeChildProcess.killed) {
|
|
820
|
+
try {
|
|
821
|
+
if (process.platform === 'win32') {
|
|
822
|
+
spawn('taskkill', ['/pid', String(activeChildProcess.pid), '/f', '/t'], { stdio: 'ignore', windowsHide: true });
|
|
823
|
+
} else {
|
|
824
|
+
activeChildProcess.kill('SIGTERM');
|
|
825
|
+
}
|
|
826
|
+
} catch { /* silencioso */ }
|
|
827
|
+
}
|
|
754
828
|
|
|
755
|
-
// 1. Escribir un agent-log de cancelación para que el reporte lo refleje
|
|
756
829
|
try {
|
|
757
830
|
const ctxDir = process.env.CONTEXT_DIR;
|
|
758
831
|
if (ctxDir && fs.existsSync(ctxDir)) {
|
|
@@ -770,28 +843,15 @@ async function main() {
|
|
|
770
843
|
}
|
|
771
844
|
} catch { /* silencioso */ }
|
|
772
845
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
// 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
|
|
777
|
-
// a que el agente-log y endMission terminen antes del kill.
|
|
778
|
-
// NO llamar process.exit() aquí: el finally del try/catch principal
|
|
779
|
-
// se encarga de generar el reporte visual y abrirlo en el browser.
|
|
780
|
-
if (activeChildProcess) {
|
|
781
|
-
try {
|
|
782
|
-
if (process.platform === 'win32') {
|
|
783
|
-
spawn('taskkill', ['/pid', activeChildProcess.pid, '/f', '/t'], { stdio: 'ignore', windowsHide: true });
|
|
784
|
-
} else {
|
|
785
|
-
activeChildProcess.kill('SIGTERM');
|
|
786
|
-
}
|
|
787
|
-
} catch { /* silencioso */ }
|
|
788
|
-
}
|
|
789
|
-
// El bloque finally del try/catch de run() tomará el control
|
|
790
|
-
// a partir de aquí (el proceso hijo ya terminó → run() rechaza → catch → finally).
|
|
846
|
+
console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
|
|
847
|
+
// Playwright también recibe el SIGINT nativamente desde la terminal Windows
|
|
848
|
+
// y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
|
|
791
849
|
};
|
|
792
850
|
|
|
793
|
-
|
|
794
|
-
process.
|
|
851
|
+
// prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
|
|
852
|
+
process.prependListener('SIGINT', cancelHandler);
|
|
853
|
+
process.prependListener('SIGTERM', cancelHandler);
|
|
854
|
+
|
|
795
855
|
|
|
796
856
|
// Build env for the runner — merge arcality.config values.
|
|
797
857
|
// ARCALITY_API_URL must be explicit — it comes from the library's internal config,
|
|
@@ -831,27 +891,76 @@ async function main() {
|
|
|
831
891
|
const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
|
|
832
892
|
const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
|
|
833
893
|
|
|
834
|
-
// Resolve playwright CLI
|
|
835
|
-
//
|
|
894
|
+
// Resolve playwright CLI — CRITICAL singleton alignment:
|
|
895
|
+
//
|
|
896
|
+
// @playwright/test uses `playwright` internally. When npm detects a version mismatch
|
|
897
|
+
// between the top-level `playwright` and what `@playwright/test` needs, it installs
|
|
898
|
+
// a NESTED copy at: @playwright/test/node_modules/playwright/
|
|
899
|
+
//
|
|
900
|
+
// Playwright's test runner tracks the active suite via a module-level singleton
|
|
901
|
+
// (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
|
|
902
|
+
// @playwright/test loaded, their singletons are out of sync → "did not expect test()
|
|
903
|
+
// to be called here".
|
|
904
|
+
//
|
|
905
|
+
// Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
|
|
906
|
+
// guaranteed to be the same instance that @playwright/test uses for its internals.
|
|
907
|
+
//
|
|
908
|
+
// Resolution priority:
|
|
909
|
+
// 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
|
|
910
|
+
// 2. <project>/playwright/cli.js ← sibling top-level
|
|
911
|
+
// (repeated for ORIGINAL_CWD and PROJECT_ROOT)
|
|
836
912
|
let playwrightCli;
|
|
837
913
|
try {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
914
|
+
const searchPaths = [ORIGINAL_CWD, PROJECT_ROOT];
|
|
915
|
+
let testRunnerPath = null;
|
|
916
|
+
let resolveError = null;
|
|
917
|
+
|
|
918
|
+
for (const searchPath of searchPaths) {
|
|
919
|
+
try {
|
|
920
|
+
testRunnerPath = require.resolve('@playwright/test', { paths: [searchPath] });
|
|
921
|
+
break;
|
|
922
|
+
} catch (e) {
|
|
923
|
+
resolveError = e;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!testRunnerPath) throw resolveError;
|
|
928
|
+
|
|
929
|
+
const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
|
|
930
|
+
|
|
931
|
+
// Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
|
|
932
|
+
const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
|
|
933
|
+
// Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
|
|
934
|
+
const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
|
|
935
|
+
|
|
936
|
+
if (fs.existsSync(nestedCli)) {
|
|
937
|
+
playwrightCli = nestedCli;
|
|
938
|
+
} else if (fs.existsSync(siblingCli)) {
|
|
939
|
+
playwrightCli = siblingCli;
|
|
940
|
+
} else {
|
|
941
|
+
throw new Error(
|
|
942
|
+
`Playwright CLI not found. Tried:\n (nested) ${nestedCli}\n (sibling) ${siblingCli}`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (process.argv.includes('--debug')) {
|
|
947
|
+
console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
|
|
844
948
|
}
|
|
845
949
|
} catch (e) {
|
|
846
|
-
console.error(chalk.red('\n[FATAL] Playwright core not found
|
|
950
|
+
console.error(chalk.red('\n[FATAL] Playwright core not found. Make sure @playwright/test is installed.'));
|
|
847
951
|
console.error(chalk.red(e.message));
|
|
848
|
-
|
|
952
|
+
console.error(chalk.yellow('\nTip: Run `npm install @playwright/test` in your project directory.'));
|
|
953
|
+
_missionActive = false;
|
|
954
|
+
process.exit = _origProcessExit;
|
|
955
|
+
_origProcessExit(1);
|
|
849
956
|
}
|
|
850
957
|
|
|
958
|
+
const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando tareas de reporte, por favor espera...'));
|
|
851
959
|
let missionResult = null;
|
|
852
960
|
let finalFailReason = null;
|
|
853
961
|
let finalUsageData = undefined;
|
|
854
962
|
let finalReportUrl = null;
|
|
963
|
+
let finalFailReasoning = null;
|
|
855
964
|
|
|
856
965
|
try {
|
|
857
966
|
// No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
|
|
@@ -869,14 +978,19 @@ async function main() {
|
|
|
869
978
|
onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
|
|
870
979
|
});
|
|
871
980
|
|
|
872
|
-
|
|
873
|
-
process.
|
|
874
|
-
process.
|
|
981
|
+
process.removeAllListeners('SIGINT');
|
|
982
|
+
process.removeAllListeners('SIGTERM');
|
|
983
|
+
process.on('SIGINT', postProcessSigint);
|
|
984
|
+
process.on('SIGTERM', postProcessSigint);
|
|
875
985
|
|
|
876
986
|
if (userCanceledMission) {
|
|
877
987
|
missionResult = 'canceled';
|
|
878
988
|
finalFailReason = 'user_canceled';
|
|
879
|
-
|
|
989
|
+
// Detener el spinner explícitamente antes de salir
|
|
990
|
+
if (!process.argv.includes('--debug')) {
|
|
991
|
+
try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
|
|
992
|
+
}
|
|
993
|
+
return; // El handler ya tomó el control, finally se ejecutará
|
|
880
994
|
}
|
|
881
995
|
|
|
882
996
|
missionResult = 'success';
|
|
@@ -932,15 +1046,10 @@ async function main() {
|
|
|
932
1046
|
}
|
|
933
1047
|
}
|
|
934
1048
|
} catch (e) {
|
|
935
|
-
//
|
|
936
|
-
process.off('SIGINT', cancelHandler);
|
|
937
|
-
process.off('SIGTERM', cancelHandler);
|
|
938
|
-
|
|
1049
|
+
// SIGINT ya fue manejado por cancelHandler — solo clasificamos el resultado
|
|
939
1050
|
if (userCanceledMission) {
|
|
940
1051
|
missionResult = 'canceled';
|
|
941
1052
|
finalFailReason = 'user_canceled';
|
|
942
|
-
// El cancelHandler ya registró la cancelación en el servidor.
|
|
943
|
-
// Solo mostramos un mensaje y dejamos que el finally genere el reporte.
|
|
944
1053
|
console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
|
|
945
1054
|
} else {
|
|
946
1055
|
missionResult = 'failed';
|
|
@@ -952,14 +1061,29 @@ async function main() {
|
|
|
952
1061
|
}
|
|
953
1062
|
}
|
|
954
1063
|
} finally {
|
|
1064
|
+
// Desactivar el bloqueo de process.exit y restaurar el original
|
|
1065
|
+
_missionActive = false;
|
|
1066
|
+
process.exit = _origProcessExit;
|
|
1067
|
+
process.removeListener('SIGINT', cancelHandler);
|
|
1068
|
+
process.removeListener('SIGTERM', cancelHandler);
|
|
1069
|
+
|
|
955
1070
|
if (userCanceledMission) {
|
|
956
1071
|
missionResult = 'canceled';
|
|
957
1072
|
finalFailReason = 'user_canceled';
|
|
1073
|
+
finalFailReasoning = 'La misión fue detenida manualmente por el usuario (Ctrl+C) antes de completarse. No se trata de un fallo técnico del sistema ni del portal.';
|
|
958
1074
|
await new Promise(r => setTimeout(r, 800));
|
|
959
1075
|
}
|
|
960
1076
|
|
|
1077
|
+
process.on('SIGINT', postProcessSigint);
|
|
1078
|
+
process.on('SIGTERM', postProcessSigint);
|
|
1079
|
+
|
|
961
1080
|
const sRep = spinner();
|
|
962
|
-
|
|
1081
|
+
if (!process.argv.includes('--debug')) {
|
|
1082
|
+
sRep.start('📊 Generating report...');
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log('📊 Generating report...');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
963
1087
|
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
964
1088
|
|
|
965
1089
|
const reportDir = process.env.REPORTS_DIR || 'tests-report';
|
|
@@ -977,6 +1101,7 @@ async function main() {
|
|
|
977
1101
|
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
978
1102
|
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
979
1103
|
finalUsageData = logData.usage || finalUsageData;
|
|
1104
|
+
finalFailReasoning = logData.fail_reasoning || null;
|
|
980
1105
|
if (missionResult === 'failed' && !finalFailReason) {
|
|
981
1106
|
finalFailReason = logData.fail_reason || "timeout_or_crash";
|
|
982
1107
|
}
|
|
@@ -984,37 +1109,51 @@ async function main() {
|
|
|
984
1109
|
}
|
|
985
1110
|
} catch(e) {}
|
|
986
1111
|
|
|
987
|
-
// ── Upload Evidence ──
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1112
|
+
// ── Upload Evidence (solo si NO fue cancelado por usuario) ──
|
|
1113
|
+
if (missionResult !== 'canceled') {
|
|
1114
|
+
try {
|
|
1115
|
+
const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
1116
|
+
const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
|
|
1117
|
+
|
|
1118
|
+
const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
|
|
1119
|
+
if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
|
|
1120
|
+
|
|
1121
|
+
const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
|
|
1122
|
+
const actualBasePath = sasData?.basePath || sasData?.base_path;
|
|
1123
|
+
|
|
1124
|
+
if (sasData && actualSasUrl && actualBasePath) {
|
|
1125
|
+
if (!process.argv.includes('--debug')) {
|
|
1126
|
+
sRep.start('☁️ Uploading evidence to Azure...');
|
|
1127
|
+
}
|
|
1128
|
+
process.removeAllListeners('SIGINT');
|
|
1129
|
+
process.removeAllListeners('SIGTERM');
|
|
1130
|
+
process.on('SIGINT', postProcessSigint);
|
|
1131
|
+
process.on('SIGTERM', postProcessSigint);
|
|
1132
|
+
|
|
1133
|
+
await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const parsedSas = new URL(actualSasUrl);
|
|
1137
|
+
finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
|
|
1138
|
+
} catch(e) {}
|
|
1139
|
+
|
|
1140
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
|
|
1141
|
+
}
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
1144
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload failed.'));
|
|
1011
1145
|
}
|
|
1146
|
+
} else {
|
|
1147
|
+
console.log(chalk.gray(' >> Skipping evidence upload (mission was canceled).'));
|
|
1148
|
+
}
|
|
1012
1149
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1150
|
+
// ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
|
|
1151
|
+
try {
|
|
1152
|
+
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
1153
|
+
await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning);
|
|
1154
|
+
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
|
|
1015
1155
|
} catch (e) {
|
|
1016
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not
|
|
1017
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload or end mission failed.'));
|
|
1156
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
|
|
1018
1157
|
}
|
|
1019
1158
|
|
|
1020
1159
|
if (globalReportProcess) {
|
|
@@ -1047,9 +1186,19 @@ async function main() {
|
|
|
1047
1186
|
process.env.ARCALITY_API_KEY
|
|
1048
1187
|
);
|
|
1049
1188
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1189
|
+
// Esperar Enter para regresar al menú
|
|
1190
|
+
// Usamos escucha directa de stdin (raw mode) porque @clack/text
|
|
1191
|
+
// no procesa Enter correctamente cuando setRawMode(true) está activo.
|
|
1192
|
+
console.log(chalk.gray('\n Press Enter to return to menu...'));
|
|
1193
|
+
await new Promise(resolve => {
|
|
1194
|
+
const onKey = (key) => {
|
|
1195
|
+
if (key === '\r' || key === '\n' || key === '\u0003') {
|
|
1196
|
+
process.stdin.off('data', onKey);
|
|
1197
|
+
if (key === '\u0003') process.emit('SIGINT'); // Ctrl+C = salir
|
|
1198
|
+
else resolve();
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
process.stdin.on('data', onKey);
|
|
1053
1202
|
});
|
|
1054
1203
|
|
|
1055
1204
|
if (skipMenu) {
|
|
@@ -1060,6 +1209,10 @@ async function main() {
|
|
|
1060
1209
|
agentMode = false;
|
|
1061
1210
|
prompt = "";
|
|
1062
1211
|
firstRun = false;
|
|
1212
|
+
|
|
1213
|
+
// Limpiar handlers del post-proceso
|
|
1214
|
+
process.removeListener('SIGINT', postProcessSigint);
|
|
1215
|
+
process.removeListener('SIGTERM', postProcessSigint);
|
|
1063
1216
|
}
|
|
1064
1217
|
}
|
|
1065
1218
|
firstRun = false;
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -250,7 +250,7 @@ export async function startMission(prompt, targetUrl) {
|
|
|
250
250
|
* @param {'success'|'failed'|'cancelled'} result
|
|
251
251
|
* @param {object} usage - Optional usage statistics directly from the AI
|
|
252
252
|
*/
|
|
253
|
-
export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null) {
|
|
253
|
+
export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null, failReasoning = null) {
|
|
254
254
|
const apiBase = getEffectiveApiBase();
|
|
255
255
|
if (!apiBase || !missionId) return { ok: true };
|
|
256
256
|
|
|
@@ -262,7 +262,7 @@ export async function endMission(missionId, result, usage = null, failReason = n
|
|
|
262
262
|
'Content-Type': 'application/json',
|
|
263
263
|
'x-api-key': key
|
|
264
264
|
},
|
|
265
|
-
body: JSON.stringify({ result, usage, failReason, reportUrl })
|
|
265
|
+
body: JSON.stringify({ result, usage, failReason, failReasoning, reportUrl })
|
|
266
266
|
});
|
|
267
267
|
} catch { }
|
|
268
268
|
return { ok: true };
|
|
@@ -2810,6 +2810,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2810
2810
|
let aiMarkedSuccess = false;
|
|
2811
2811
|
let hasCriticalError = false;
|
|
2812
2812
|
let failReason = "";
|
|
2813
|
+
let failReasoning = "";
|
|
2813
2814
|
let resultsSaved = false;
|
|
2814
2815
|
let isFinished = false;
|
|
2815
2816
|
let stepCount = 0;
|
|
@@ -2902,6 +2903,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2902
2903
|
success: finalSuccess,
|
|
2903
2904
|
error: hasCriticalError,
|
|
2904
2905
|
fail_reason: failReason,
|
|
2906
|
+
fail_reasoning: failReasoning,
|
|
2905
2907
|
timestamp: Date.now()
|
|
2906
2908
|
};
|
|
2907
2909
|
memories.push(missionData);
|
|
@@ -2912,7 +2914,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2912
2914
|
}
|
|
2913
2915
|
try {
|
|
2914
2916
|
const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
|
|
2915
|
-
fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, fail_reason: failReason, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2917
|
+
fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, fail_reason: failReason, fail_reasoning: failReasoning, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2916
2918
|
try {
|
|
2917
2919
|
fs2.appendFileSync(path2.join(contextDir, "arcality-history.log"), JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), mission: prompt.substring(0, 150), success: finalSuccess, steps: stepCount, usage: totalMissionUsage }) + "\n");
|
|
2918
2920
|
} catch (e) {
|
|
@@ -3248,6 +3250,8 @@ ${readableHistory}`;
|
|
|
3248
3250
|
console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error interno del Portal/Red detectado: "${networkError}"`);
|
|
3249
3251
|
hasCriticalError = true;
|
|
3250
3252
|
failReason = "portal_network_error";
|
|
3253
|
+
failReasoning = `El portal web fall\xF3 al intentar comunicarse con su backend (ej. una petici\xF3n XHR o Fetch fall\xF3 o dio timeout). El agente QA no tiene control sobre las ca\xEDdas del servidor.
|
|
3254
|
+
Detalles t\xE9cnicos capturados en la red: ${networkError}`;
|
|
3251
3255
|
isFinished = true;
|
|
3252
3256
|
history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE SISTEMA/RED: ${networkError}. El portal fall\xF3 internamente al ejecutar una petici\xF3n XHR/Fetch.`);
|
|
3253
3257
|
saveMissionResults();
|
|
@@ -3258,6 +3262,8 @@ ${readableHistory}`;
|
|
|
3258
3262
|
console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error Fatal de JavaScript detectado: "${jsError}"`);
|
|
3259
3263
|
hasCriticalError = true;
|
|
3260
3264
|
failReason = "portal_js_crash";
|
|
3265
|
+
failReasoning = `La aplicaci\xF3n web sufri\xF3 una excepci\xF3n no manejada de JavaScript en el navegador, provocando que la p\xE1gina dejara de responder o funcionara de manera err\xE1tica.
|
|
3266
|
+
Detalles de la excepci\xF3n en consola: ${jsError}`;
|
|
3261
3267
|
isFinished = true;
|
|
3262
3268
|
history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE APLICACI\xD3N: ${jsError}. El portal sufri\xF3 una excepci\xF3n no manejada y dej\xF3 de responder.`);
|
|
3263
3269
|
saveMissionResults();
|
|
@@ -3275,6 +3281,7 @@ ${readableHistory}`;
|
|
|
3275
3281
|
console.error(` >> Tipo: portal_internal_error | El agente NO puede corregir errores del servidor.`);
|
|
3276
3282
|
hasCriticalError = true;
|
|
3277
3283
|
failReason = "portal_internal_error";
|
|
3284
|
+
failReasoning = `El servidor devolvi\xF3 un error sist\xE9mico irrecuperable que fue mostrado en la interfaz visual: "${sysErrTxt.trim()}". El agente QA no puede reparar bugs l\xF3gicos o de infraestructura del servidor, por lo que la misi\xF3n fue detenida preventivamente.`;
|
|
3278
3285
|
isFinished = true;
|
|
3279
3286
|
history.push(`Turno ${stepCount}: \u{1F6D1} ERROR SIST\xC9MICO DEL PORTAL: "${sysErrTxt.trim().substring(0, 200)}". Este es un fallo del servidor, no un error de datos del agente. El portal devolvi\xF3 HTTP 200 pero report\xF3 un error interno en la UI.`);
|
|
3280
3287
|
saveMissionResults();
|
|
@@ -3359,7 +3366,24 @@ ${readableHistory}`;
|
|
|
3359
3366
|
const effectivePrompt = patternContext ? `${prompt}
|
|
3360
3367
|
|
|
3361
3368
|
${patternContext}` : prompt;
|
|
3362
|
-
|
|
3369
|
+
try {
|
|
3370
|
+
response = await agent.askIA(effectivePrompt, history, stepCount);
|
|
3371
|
+
} catch (err) {
|
|
3372
|
+
if (err.message?.includes("APPLICATION BUG:")) {
|
|
3373
|
+
const bugDesc = err.message.replace("APPLICATION BUG:", "").trim();
|
|
3374
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Application Bug reportado: ${bugDesc}`);
|
|
3375
|
+
aiMarkedSuccess = false;
|
|
3376
|
+
hasCriticalError = true;
|
|
3377
|
+
failReason = "application_bug";
|
|
3378
|
+
failReasoning = bugDesc;
|
|
3379
|
+
isFinished = true;
|
|
3380
|
+
history.push(`Turno ${stepCount}: \u{1F6D1} [APPLICATION BUG] ${bugDesc}`);
|
|
3381
|
+
saveMissionResults();
|
|
3382
|
+
break;
|
|
3383
|
+
} else {
|
|
3384
|
+
throw err;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3363
3387
|
if (response.usage) {
|
|
3364
3388
|
const inputs = response.usage.input_tokens || 0;
|
|
3365
3389
|
const cacheCreates = response.usage.cache_creation_input_tokens || 0;
|
|
@@ -3381,13 +3405,17 @@ ${patternContext}` : prompt;
|
|
|
3381
3405
|
aiMarkedSuccess = false;
|
|
3382
3406
|
hasCriticalError = true;
|
|
3383
3407
|
failReason = "cost_limit_reached";
|
|
3408
|
+
failReasoning = response.thought;
|
|
3384
3409
|
}
|
|
3385
3410
|
}
|
|
3386
3411
|
}
|
|
3412
|
+
if (response && response.thought && response.thought.includes("DISCULPA:")) {
|
|
3413
|
+
failReasoning = response.thought.replace("DISCULPA:", "").trim();
|
|
3414
|
+
}
|
|
3387
3415
|
console.log(`
|
|
3388
3416
|
======================================================`);
|
|
3389
3417
|
console.log(`\u{1F916} [RAZONAMIENTO DEL AGENTE]`);
|
|
3390
|
-
console.log(`${response.thought}`);
|
|
3418
|
+
console.log(`${response ? response.thought : "N/A"}`);
|
|
3391
3419
|
console.log(`======================================================
|
|
3392
3420
|
`);
|
|
3393
3421
|
if (!response.finish && history.length >= 6) {
|
|
@@ -3417,6 +3445,8 @@ ${patternContext}` : prompt;
|
|
|
3417
3445
|
aiMarkedSuccess = false;
|
|
3418
3446
|
hasCriticalError = true;
|
|
3419
3447
|
failReason = "loop_detected";
|
|
3448
|
+
failReasoning = `El agente QA entr\xF3 en un bucle l\xF3gico intentando ejecutar la misma acci\xF3n repetidamente sin observar cambios significativos en el DOM.
|
|
3449
|
+
Acci\xF3n bloqueada: Bucle de tipo ${loopType} sobre los elementos ${Array.from(uniqueTargets).join(", ")}.`;
|
|
3420
3450
|
} else if ((isClickLoop || isFillLoop) && agentRecovered) {
|
|
3421
3451
|
console.log(`>>ARCALITY_STATUS>> \u{1F504} Loop candidato detectado pero el agente se recuper\xF3 en este turno. Continuando misi\xF3n.`);
|
|
3422
3452
|
}
|
|
@@ -3441,6 +3471,7 @@ ${patternContext}` : prompt;
|
|
|
3441
3471
|
aiMarkedSuccess = false;
|
|
3442
3472
|
hasCriticalError = true;
|
|
3443
3473
|
failReason = "portal_rendering_failure";
|
|
3474
|
+
failReasoning = response.thought;
|
|
3444
3475
|
history.push(`Turno ${stepCount}: \u{1F6D1} [PORTAL_RENDER_FAILURE] La p\xE1gina ${blankUrl} carg\xF3 en blanco (${consecutiveWaits} esperas sin progreso). Los widgets no se renderizaron. Bug del portal detectado por Arcality QA. Evidencia: ${blankEvidencePath}`);
|
|
3445
3476
|
saveMissionResults();
|
|
3446
3477
|
}
|
|
@@ -4027,7 +4058,21 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
|
|
|
4027
4058
|
console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
|
|
4028
4059
|
await persistCollectiveMemory(true).catch(() => {
|
|
4029
4060
|
});
|
|
4030
|
-
|
|
4061
|
+
let finalErrorMsg = `Misi\xF3n no completada o finalizada con errores.
|
|
4062
|
+
|
|
4063
|
+
`;
|
|
4064
|
+
finalErrorMsg += `======================================================
|
|
4065
|
+
`;
|
|
4066
|
+
finalErrorMsg += `\u{1F3AF} TIPO DE FALLO: ${failReason}
|
|
4067
|
+
`;
|
|
4068
|
+
if (failReasoning) {
|
|
4069
|
+
finalErrorMsg += `\u{1F9E0} AN\xC1LISIS DEL AGENTE DE QA:
|
|
4070
|
+
${failReasoning}
|
|
4071
|
+
`;
|
|
4072
|
+
}
|
|
4073
|
+
finalErrorMsg += `======================================================
|
|
4074
|
+
`;
|
|
4075
|
+
throw new Error(finalErrorMsg);
|
|
4031
4076
|
} else {
|
|
4032
4077
|
console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
|
|
4033
4078
|
}
|