@arcadialdev/arcality 2.6.1 → 2.6.2

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.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -740,19 +740,51 @@ async function main() {
740
740
 
741
741
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
742
742
 
743
- // ── SIGINT Handler: Cancelación manual (Ctrl+C / ESC) ──
744
- // Se registra AQUÍ, justo antes del spawn, para que solo aplique durante la ejecución.
745
- // Al cancelar: mata el proceso hijo, guarda estado "canceled", llama endMission y genera reporte.
746
- let activeChildProcess = null; // Referencia al proceso hijo de Playwright
743
+ // ── Cancelación Manual (Ctrl+C) Estrategia bulletproof ──
744
+ // El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
745
+ // que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
746
+ // Clack lo re-registra en cada frame de animación.
747
+ //
748
+ // La solución: interceptar process.exit() antes de que Clack lo llame,
749
+ // y usar prependListener para que NUESTRO handler siempre se ejecute primero.
750
+
751
+ let activeChildProcess = null;
747
752
  let userCanceledMission = false;
748
- const cancelHandler = async () => {
749
- if (userCanceledMission) return; // Evitar doble disparo
753
+ let sigintCount = 0;
754
+ let _missionActive = true; // Mientras true, bloqueamos el process.exit de Clack
755
+
756
+ // Guardar el process.exit original y reemplazarlo temporalmente
757
+ const _origProcessExit = process.exit.bind(process);
758
+ process.exit = (code) => {
759
+ if (_missionActive) {
760
+ // Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
761
+ // Nuestro cancelHandler ya habrá sido invocado vía prependListener.
762
+ // El proceso se cerrará naturalmente cuando el child process termine.
763
+ return;
764
+ }
765
+ _origProcessExit(code);
766
+ };
767
+
768
+ const cancelHandler = () => {
769
+ sigintCount++;
770
+ if (sigintCount > 3) {
771
+ console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
772
+ _missionActive = false;
773
+ _origProcessExit(1);
774
+ return;
775
+ }
776
+
777
+ if (userCanceledMission) {
778
+ console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
779
+ return;
780
+ }
750
781
  userCanceledMission = true;
751
782
 
752
783
  console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
753
- if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.'));
784
+ if (!process.argv.includes('--debug')) {
785
+ try { s.stop(chalk.yellow('⚠️ Misión cancelada.')); } catch { /* ignore */ }
786
+ }
754
787
 
755
- // 1. Escribir un agent-log de cancelación para que el reporte lo refleje
756
788
  try {
757
789
  const ctxDir = process.env.CONTEXT_DIR;
758
790
  if (ctxDir && fs.existsSync(ctxDir)) {
@@ -770,28 +802,15 @@ async function main() {
770
802
  }
771
803
  } catch { /* silencioso */ }
772
804
 
773
- // 2. Registraremos la misión como 'canceled' en el bloque finally
774
- console.log(chalk.gray(' >> Registrando misión como "cancelada"...'));
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).
805
+ console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
806
+ // Playwright también recibe el SIGINT nativamente desde la terminal Windows
807
+ // y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
791
808
  };
792
809
 
793
- process.once('SIGINT', cancelHandler);
794
- process.once('SIGTERM', cancelHandler);
810
+ // prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
811
+ process.prependListener('SIGINT', cancelHandler);
812
+ process.prependListener('SIGTERM', cancelHandler);
813
+
795
814
 
796
815
  // Build env for the runner — merge arcality.config values.
797
816
  // ARCALITY_API_URL must be explicit — it comes from the library's internal config,
@@ -831,23 +850,55 @@ async function main() {
831
850
  const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
832
851
  const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
833
852
 
834
- // Resolve playwright CLI relative to PROJECT_ROOT exactly as Node handles module resolution.
835
- // This prevents the "did not expect test() to be called here" singleton duplication error.
853
+ // Resolve playwright CLI CRITICAL: must use the same @playwright/test instance
854
+ // that the agentic-runner.bundle.spec.js was compiled against. If the user's project
855
+ // has its own @playwright/test, we must use THAT one to avoid the singleton conflict
856
+ // ("Playwright Test did not expect test() to be called here").
857
+ //
858
+ // Resolution order:
859
+ // 1. ORIGINAL_CWD (user's project) — preferred, avoids singleton mismatch
860
+ // 2. PROJECT_ROOT (arcality package) — fallback for clean installs
836
861
  let playwrightCli;
837
862
  try {
838
- // Find the core @playwright/test that the local project uses
839
- const testRunnerPath = require.resolve('@playwright/test', { paths: [PROJECT_ROOT] });
840
- // Target the cli.js file strictly inside its parallel playwright dependency
863
+ const searchPaths = [ORIGINAL_CWD, PROJECT_ROOT];
864
+ let testRunnerPath = null;
865
+ let resolveError = null;
866
+
867
+ for (const searchPath of searchPaths) {
868
+ try {
869
+ testRunnerPath = require.resolve('@playwright/test', { paths: [searchPath] });
870
+ break;
871
+ } catch (e) {
872
+ resolveError = e;
873
+ }
874
+ }
875
+
876
+ if (!testRunnerPath) throw resolveError;
877
+
878
+ // Navigate from @playwright/test/index.js → playwright/cli.js (sibling package)
841
879
  playwrightCli = path.join(testRunnerPath, '..', '..', '..', 'playwright', 'cli.js');
880
+
881
+ // Some versions nest it differently — try alternate location
842
882
  if (!fs.existsSync(playwrightCli)) {
843
- throw new Error(`CLI not found exactly at: ${playwrightCli}`);
883
+ const alt = path.join(testRunnerPath, '..', '..', 'playwright', 'cli.js');
884
+ if (fs.existsSync(alt)) playwrightCli = alt;
885
+ else throw new Error(`Playwright CLI not found. Tried:\n ${playwrightCli}\n ${alt}`);
886
+ }
887
+
888
+ if (process.argv.includes('--debug')) {
889
+ console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
844
890
  }
845
891
  } catch (e) {
846
- console.error(chalk.red('\n[FATAL] Playwright core not found! Make sure playwright is properly installed.'));
892
+ console.error(chalk.red('\n[FATAL] Playwright core not found. Make sure @playwright/test is installed.'));
847
893
  console.error(chalk.red(e.message));
894
+ console.error(chalk.yellow('\nTip: Run `npm install @playwright/test` in your project directory.'));
895
+ _missionActive = false;
896
+ process.exit = _origProcessExit;
848
897
  process.exit(1);
849
898
  }
850
899
 
900
+
901
+ const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando tareas de reporte, por favor espera...'));
851
902
  let missionResult = null;
852
903
  let finalFailReason = null;
853
904
  let finalUsageData = undefined;
@@ -869,9 +920,10 @@ async function main() {
869
920
  onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
870
921
  });
871
922
 
872
- // Eliminar handlers SIGINT ahora que el proceso terminó normalmente
873
- process.off('SIGINT', cancelHandler);
874
- process.off('SIGTERM', cancelHandler);
923
+ process.removeAllListeners('SIGINT');
924
+ process.removeAllListeners('SIGTERM');
925
+ process.on('SIGINT', postProcessSigint);
926
+ process.on('SIGTERM', postProcessSigint);
875
927
 
876
928
  if (userCanceledMission) {
877
929
  missionResult = 'canceled';
@@ -932,15 +984,10 @@ async function main() {
932
984
  }
933
985
  }
934
986
  } catch (e) {
935
- // Eliminar handlers SIGINT en el path de error también
936
- process.off('SIGINT', cancelHandler);
937
- process.off('SIGTERM', cancelHandler);
938
-
987
+ // SIGINT ya fue manejado por cancelHandler solo clasificamos el resultado
939
988
  if (userCanceledMission) {
940
989
  missionResult = 'canceled';
941
990
  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
991
  console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
945
992
  } else {
946
993
  missionResult = 'failed';
@@ -952,14 +999,28 @@ async function main() {
952
999
  }
953
1000
  }
954
1001
  } finally {
1002
+ // Desactivar el bloqueo de process.exit y restaurar el original
1003
+ _missionActive = false;
1004
+ process.exit = _origProcessExit;
1005
+ process.removeListener('SIGINT', cancelHandler);
1006
+ process.removeListener('SIGTERM', cancelHandler);
1007
+
955
1008
  if (userCanceledMission) {
956
1009
  missionResult = 'canceled';
957
1010
  finalFailReason = 'user_canceled';
958
1011
  await new Promise(r => setTimeout(r, 800));
959
1012
  }
960
1013
 
1014
+ process.on('SIGINT', postProcessSigint);
1015
+ process.on('SIGTERM', postProcessSigint);
1016
+
961
1017
  const sRep = spinner();
962
- sRep.start('📊 Generating report...');
1018
+ if (!process.argv.includes('--debug')) {
1019
+ sRep.start('📊 Generating report...');
1020
+ } else {
1021
+ console.log('📊 Generating report...');
1022
+ }
1023
+
963
1024
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
964
1025
 
965
1026
  const reportDir = process.env.REPORTS_DIR || 'tests-report';
@@ -997,7 +1058,13 @@ async function main() {
997
1058
  const actualBasePath = sasData?.basePath || sasData?.base_path;
998
1059
 
999
1060
  if (sasData && actualSasUrl && actualBasePath) {
1000
- if (!process.argv.includes('--debug')) sRep.start('☁️ Uploading evidence to Azure...');
1061
+ if (!process.argv.includes('--debug')) {
1062
+ sRep.start('☁️ Uploading evidence to Azure...');
1063
+ }
1064
+ process.removeAllListeners('SIGINT');
1065
+ process.removeAllListeners('SIGTERM');
1066
+ process.on('SIGINT', postProcessSigint);
1067
+ process.on('SIGTERM', postProcessSigint);
1001
1068
 
1002
1069
  await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
1003
1070
 
@@ -1060,6 +1127,10 @@ async function main() {
1060
1127
  agentMode = false;
1061
1128
  prompt = "";
1062
1129
  firstRun = false;
1130
+
1131
+ // Limpiar handlers del post-proceso
1132
+ process.removeListener('SIGINT', postProcessSigint);
1133
+ process.removeListener('SIGTERM', postProcessSigint);
1063
1134
  }
1064
1135
  }
1065
1136
  firstRun = false;