@arcadialdev/arcality 2.4.37 → 2.4.49
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 +7 -4
- package/scripts/gen-and-run.mjs +179 -55
- package/scripts/init.mjs +8 -3
- package/src/EvidenceUploader.mjs +45 -0
- package/src/KnowledgeService.ts +13 -5
- package/src/arcalityClient.mjs +53 -9
- package/src/configLoader.mjs +2 -1
- package/src/index.ts +19 -1
- package/src/services/collectiveMemoryService.ts +98 -7
- package/tests/_helpers/ArcalityReporter.js +729 -706
- package/tests/_helpers/agentic-runner.bundle.spec.js +888 -98
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcadialdev/arcality",
|
|
3
|
-
"version": "2.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.4.49",
|
|
4
|
+
"description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "node scripts/gen-and-run.mjs",
|
|
@@ -51,9 +51,11 @@
|
|
|
51
51
|
"@types/figlet": "^1.7.0",
|
|
52
52
|
"@types/node": "^25.0.9",
|
|
53
53
|
"@types/prompts": "^2.4.9",
|
|
54
|
-
"esbuild": "^0.20.2"
|
|
54
|
+
"esbuild": "^0.20.2",
|
|
55
|
+
"typescript": "^6.0.3"
|
|
55
56
|
},
|
|
56
57
|
"dependencies": {
|
|
58
|
+
"@azure/storage-blob": "^12.31.0",
|
|
57
59
|
"@clack/prompts": "^1.0.1",
|
|
58
60
|
"@inquirer/prompts": "^8.2.0",
|
|
59
61
|
"@playwright/test": "^1.57.0",
|
|
@@ -62,9 +64,10 @@
|
|
|
62
64
|
"dotenv": "^17.2.3",
|
|
63
65
|
"figlet": "^1.9.4",
|
|
64
66
|
"js-yaml": "^4.1.1",
|
|
67
|
+
"mime-types": "^3.0.2",
|
|
65
68
|
"node-fetch": "^3.3.2",
|
|
66
69
|
"prompts": "^2.4.2",
|
|
67
70
|
"terminal-image": "^1.1.0",
|
|
68
71
|
"tsx": "^4.21.0"
|
|
69
72
|
}
|
|
70
|
-
}
|
|
73
|
+
}
|
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
|
|
|
34
34
|
const ORIGINAL_CWD = process.cwd();
|
|
35
35
|
|
|
36
36
|
// ── Load arcality.config if present ──
|
|
37
|
-
|
|
37
|
+
let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
38
38
|
if (projectConfig) {
|
|
39
39
|
injectConfigToEnv(projectConfig);
|
|
40
40
|
}
|
|
@@ -324,6 +324,9 @@ function run(cmd, args, options = {}) {
|
|
|
324
324
|
env,
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
// Exponer el proceso hijo para que el cancelHandler pueda matarlo
|
|
328
|
+
if (options.onSpawn) options.onSpawn(p);
|
|
329
|
+
|
|
327
330
|
let outputBuffer = '';
|
|
328
331
|
const handleData = (data) => {
|
|
329
332
|
const str = data.toString();
|
|
@@ -467,7 +470,6 @@ async function main() {
|
|
|
467
470
|
...(savedMissions.length ? [{ label: '🚀 Launch Saved Mission (.yaml)', value: 'launch_saved' }] : []),
|
|
468
471
|
...(yamlOutputFiles.length ? [{ label: '♻️ Reuse YAML + Auto-Guide (if prev. run exists)', value: 'reuse_yaml' }] : []),
|
|
469
472
|
...(rootYamlFiles.length ? [{ label: '📂 Choose Root YAML (Templates)', value: 'yaml_list' }] : []),
|
|
470
|
-
{ label: '📋 View Current Configuration', value: 'view_config' },
|
|
471
473
|
{ label: '🔄 Reconfigure (arcality init)', value: 'reconfigure' },
|
|
472
474
|
{ label: '❌ Exit', value: 'exit' }
|
|
473
475
|
];
|
|
@@ -484,28 +486,6 @@ async function main() {
|
|
|
484
486
|
|
|
485
487
|
if (action === 'agent') {
|
|
486
488
|
agentMode = true;
|
|
487
|
-
} else if (action === 'view_config') {
|
|
488
|
-
if (projectConfig) {
|
|
489
|
-
note(
|
|
490
|
-
chalk.bold.white('📋 Project: ') + chalk.cyan(projectConfig.project?.name) + '\n' +
|
|
491
|
-
chalk.bold.white('🌐 Base URL: ') + chalk.cyan(projectConfig.project?.baseUrl) + '\n' +
|
|
492
|
-
chalk.bold.white('🛠️ Framework: ') + chalk.magenta(projectConfig.project?.frameworkDetected || 'N/A') + '\n' +
|
|
493
|
-
chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId) + '\n' +
|
|
494
|
-
chalk.bold.white('🏢 Org ID: ') + chalk.gray(projectConfig.organizationId || 'N/A') + '\n' +
|
|
495
|
-
chalk.bold.white('👤 User: ') + chalk.cyan(projectConfig.auth?.username) + '\n' +
|
|
496
|
-
chalk.bold.white('📂 YAML Dir: ') + chalk.yellow(projectConfig.runtime?.yamlOutputDir) + '\n' +
|
|
497
|
-
chalk.bold.white('♻️ Reuse YAMLs: ') + chalk.cyan(projectConfig.runtime?.reuseSuccessfulYamls ? '✅' : '❌') + '\n' +
|
|
498
|
-
chalk.bold.white('📦 Version: ') + chalk.yellow(projectConfig.meta?.arcalityVersion || 'N/A'),
|
|
499
|
-
'arcality.config'
|
|
500
|
-
);
|
|
501
|
-
} else {
|
|
502
|
-
note(
|
|
503
|
-
chalk.yellow('No arcality.config found.') + '\n' +
|
|
504
|
-
chalk.gray('Run `arcality init` to configure this project.'),
|
|
505
|
-
'No Configuration'
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
continue;
|
|
509
489
|
} else if (action === 'reconfigure') {
|
|
510
490
|
// Launch arcality init in the user's project directory
|
|
511
491
|
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
@@ -519,7 +499,20 @@ async function main() {
|
|
|
519
499
|
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
|
|
520
500
|
child.on('error', reject);
|
|
521
501
|
});
|
|
522
|
-
|
|
502
|
+
|
|
503
|
+
// ── REFRESH CONFIG ──
|
|
504
|
+
// Recargar el archivo que init.mjs acaba de escribir
|
|
505
|
+
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
506
|
+
if (projectConfig) {
|
|
507
|
+
injectConfigToEnv(projectConfig);
|
|
508
|
+
}
|
|
509
|
+
setupEnvironment();
|
|
510
|
+
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// Mostrar el error claramente antes de que console.clear() lo borre
|
|
513
|
+
console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
|
|
514
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
515
|
+
}
|
|
523
516
|
continue;
|
|
524
517
|
} else if (action === 'launch_saved') {
|
|
525
518
|
const sel = await select({
|
|
@@ -620,6 +613,14 @@ async function main() {
|
|
|
620
613
|
const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
|
|
621
614
|
|
|
622
615
|
const validation = await validateApiKey();
|
|
616
|
+
if (process.env.DEBUG || process.argv.includes('--debug')) {
|
|
617
|
+
console.log(chalk.gray(`[DEBUG] Validation result: ${JSON.stringify(validation, null, 2)}`));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const detectedOrgId = validation.organization_id || validation.organizationId || validation.OrganizationId;
|
|
621
|
+
if (detectedOrgId) {
|
|
622
|
+
process.env.ARCALITY_ORG_ID = detectedOrgId;
|
|
623
|
+
}
|
|
623
624
|
if (!validation.valid) {
|
|
624
625
|
const errorMessages = {
|
|
625
626
|
'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
|
|
@@ -636,7 +637,7 @@ async function main() {
|
|
|
636
637
|
|
|
637
638
|
// Show plan info
|
|
638
639
|
const modeLabel = validation.mode === 'mock' ? chalk.gray('(local)') : chalk.green('(live)');
|
|
639
|
-
const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit ||
|
|
640
|
+
const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
|
|
640
641
|
const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
|
|
641
642
|
note(
|
|
642
643
|
chalk.bold.white('✅ Valid API Key ') + modeLabel + '\n' +
|
|
@@ -653,9 +654,13 @@ async function main() {
|
|
|
653
654
|
if (!selectedProjectId) {
|
|
654
655
|
// If no project ID from config, try to create/select from backend
|
|
655
656
|
try {
|
|
657
|
+
const projController = new AbortController();
|
|
658
|
+
const projTimeout = setTimeout(() => projController.abort(), 5000);
|
|
656
659
|
const projRes = await fetch(`${apiBase}/api/v1/projects`, {
|
|
657
|
-
headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' }
|
|
660
|
+
headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' },
|
|
661
|
+
signal: projController.signal
|
|
658
662
|
});
|
|
663
|
+
clearTimeout(projTimeout);
|
|
659
664
|
|
|
660
665
|
if (projRes.ok) {
|
|
661
666
|
const data = await projRes.json();
|
|
@@ -735,6 +740,63 @@ async function main() {
|
|
|
735
740
|
|
|
736
741
|
const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
|
|
737
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
|
|
747
|
+
let userCanceledMission = false;
|
|
748
|
+
const cancelHandler = async () => {
|
|
749
|
+
if (userCanceledMission) return; // Evitar doble disparo
|
|
750
|
+
userCanceledMission = true;
|
|
751
|
+
|
|
752
|
+
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.'));
|
|
754
|
+
|
|
755
|
+
// 1. Escribir un agent-log de cancelación para que el reporte lo refleje
|
|
756
|
+
try {
|
|
757
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
758
|
+
if (ctxDir && fs.existsSync(ctxDir)) {
|
|
759
|
+
const cancelLog = {
|
|
760
|
+
prompt,
|
|
761
|
+
history: ['[CANCELADO POR USUARIO] La prueba fue detenida manualmente antes de completarse.'],
|
|
762
|
+
steps: 0,
|
|
763
|
+
success: false,
|
|
764
|
+
error: true,
|
|
765
|
+
fail_reason: 'user_canceled',
|
|
766
|
+
canceled_by_user: true,
|
|
767
|
+
timestamp: new Date().toISOString()
|
|
768
|
+
};
|
|
769
|
+
fs.writeFileSync(path.join(ctxDir, `agent-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
|
|
770
|
+
}
|
|
771
|
+
} catch { /* silencioso */ }
|
|
772
|
+
|
|
773
|
+
// 2. Llamar endMission con estado 'canceled'
|
|
774
|
+
try {
|
|
775
|
+
const { endMission: endM } = await import('../src/arcalityClient.mjs');
|
|
776
|
+
await endM(mission.mission_id, 'canceled', null, 'user_canceled');
|
|
777
|
+
console.log(chalk.gray(' >> Misión registrada como "cancelada" en el servidor.'));
|
|
778
|
+
} catch { /* silencioso si el backend no está disponible */ }
|
|
779
|
+
|
|
780
|
+
// 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
|
|
781
|
+
// a que el agente-log y endMission terminen antes del kill.
|
|
782
|
+
// NO llamar process.exit() aquí: el finally del try/catch principal
|
|
783
|
+
// se encarga de generar el reporte visual y abrirlo en el browser.
|
|
784
|
+
if (activeChildProcess) {
|
|
785
|
+
try {
|
|
786
|
+
if (process.platform === 'win32') {
|
|
787
|
+
spawn('taskkill', ['/pid', activeChildProcess.pid, '/f', '/t'], { stdio: 'ignore', windowsHide: true });
|
|
788
|
+
} else {
|
|
789
|
+
activeChildProcess.kill('SIGTERM');
|
|
790
|
+
}
|
|
791
|
+
} catch { /* silencioso */ }
|
|
792
|
+
}
|
|
793
|
+
// El bloque finally del try/catch de run() tomará el control
|
|
794
|
+
// a partir de aquí (el proceso hijo ya terminó → run() rechaza → catch → finally).
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
process.once('SIGINT', cancelHandler);
|
|
798
|
+
process.once('SIGTERM', cancelHandler);
|
|
799
|
+
|
|
738
800
|
// Build env for the runner — merge arcality.config values.
|
|
739
801
|
// ARCALITY_API_URL must be explicit — it comes from the library's internal config,
|
|
740
802
|
// not from the user's project .env, so we inject it directly via getApiUrl().
|
|
@@ -745,7 +807,8 @@ async function main() {
|
|
|
745
807
|
...process.env,
|
|
746
808
|
ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
|
|
747
809
|
ARCALITY_PROJECT_ID: finalProjectId,
|
|
748
|
-
ARCALITY_MISSION_ID: mission.mission_id || '', // ← Propagate mission_id to every proxy call
|
|
810
|
+
ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
|
|
811
|
+
ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
|
|
749
812
|
BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
|
|
750
813
|
LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
|
|
751
814
|
LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
|
|
@@ -758,7 +821,8 @@ async function main() {
|
|
|
758
821
|
};
|
|
759
822
|
|
|
760
823
|
if (finalProjectId) {
|
|
761
|
-
console.log(chalk.gray(
|
|
824
|
+
console.log(chalk.gray(`>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
|
|
825
|
+
console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
|
|
762
826
|
}
|
|
763
827
|
|
|
764
828
|
// ── Resolve execution paths ──
|
|
@@ -798,16 +862,20 @@ async function main() {
|
|
|
798
862
|
'--headed',
|
|
799
863
|
'--config', 'playwright.config.js'
|
|
800
864
|
], {
|
|
801
|
-
cwd: PROJECT_ROOT,
|
|
865
|
+
cwd: PROJECT_ROOT,
|
|
802
866
|
env: mergedEnv,
|
|
803
|
-
onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); }
|
|
867
|
+
onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
|
|
868
|
+
onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
|
|
804
869
|
});
|
|
870
|
+
|
|
871
|
+
// Eliminar handlers SIGINT ahora que el proceso terminó normalmente
|
|
872
|
+
process.off('SIGINT', cancelHandler);
|
|
873
|
+
process.off('SIGTERM', cancelHandler);
|
|
874
|
+
|
|
875
|
+
if (userCanceledMission) return; // El handler ya tomó el control
|
|
805
876
|
if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
|
|
806
877
|
else console.log(chalk.green('✅ Mission completed successfully.'));
|
|
807
878
|
|
|
808
|
-
// ── End Mission ──
|
|
809
|
-
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
810
|
-
|
|
811
879
|
let finalUsage = undefined;
|
|
812
880
|
try {
|
|
813
881
|
const ctxDir = process.env.CONTEXT_DIR;
|
|
@@ -822,6 +890,27 @@ async function main() {
|
|
|
822
890
|
}
|
|
823
891
|
} catch(e) {}
|
|
824
892
|
|
|
893
|
+
// ── Upload Evidence ──
|
|
894
|
+
const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
895
|
+
const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
|
|
899
|
+
if (sasData && sasData.sasUrl) {
|
|
900
|
+
if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
|
|
901
|
+
const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
|
|
902
|
+
|
|
903
|
+
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
904
|
+
|
|
905
|
+
await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
|
|
906
|
+
if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
|
|
907
|
+
}
|
|
908
|
+
} catch (e) {
|
|
909
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
910
|
+
if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Evidence upload failed.'));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ── End Mission ──
|
|
825
914
|
await endMission(mission.mission_id, 'success', finalUsage);
|
|
826
915
|
|
|
827
916
|
// ── Ping Project ──
|
|
@@ -873,31 +962,66 @@ async function main() {
|
|
|
873
962
|
}
|
|
874
963
|
}
|
|
875
964
|
} catch (e) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
965
|
+
// Eliminar handlers SIGINT en el path de error también
|
|
966
|
+
process.off('SIGINT', cancelHandler);
|
|
967
|
+
process.off('SIGTERM', cancelHandler);
|
|
968
|
+
|
|
969
|
+
if (userCanceledMission) {
|
|
970
|
+
// El cancelHandler ya registró la cancelación en el servidor.
|
|
971
|
+
// Solo mostramos un mensaje y dejamos que el finally genere el reporte.
|
|
972
|
+
console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
|
|
973
|
+
} else {
|
|
974
|
+
s.stop(chalk.red(`❌ Mission could not be completed.`));
|
|
975
|
+
console.error(chalk.red(`\nError Details: ${e.message}`));
|
|
976
|
+
if (e.stack && process.argv.includes('--debug')) {
|
|
977
|
+
console.error(chalk.gray(e.stack));
|
|
978
|
+
}
|
|
881
979
|
|
|
882
|
-
|
|
883
|
-
try {
|
|
884
|
-
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
885
|
-
let finalUsage = undefined;
|
|
980
|
+
// Si NO fue cancelación de usuario, cerrar la misión como fallida en el servidor
|
|
886
981
|
try {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
892
|
-
const
|
|
893
|
-
const
|
|
894
|
-
|
|
982
|
+
let finalUsage = undefined;
|
|
983
|
+
let failReason = "timeout_or_crash";
|
|
984
|
+
try {
|
|
985
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
986
|
+
if (fs.existsSync(ctxDir)) {
|
|
987
|
+
const files = fs.readdirSync(ctxDir);
|
|
988
|
+
const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
|
|
989
|
+
if (logFiles.length > 0) {
|
|
990
|
+
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
991
|
+
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
992
|
+
finalUsage = logData.usage;
|
|
993
|
+
failReason = logData.fail_reason || failReason;
|
|
994
|
+
}
|
|
895
995
|
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
996
|
+
} catch(e) {}
|
|
997
|
+
|
|
998
|
+
// ── Upload Evidence ──
|
|
999
|
+
const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
1000
|
+
const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
|
|
1004
|
+
if (sasData && sasData.sasUrl) {
|
|
1005
|
+
if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
|
|
1006
|
+
const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
|
|
1007
|
+
|
|
1008
|
+
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
1009
|
+
|
|
1010
|
+
await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
|
|
1011
|
+
if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
|
|
1012
|
+
}
|
|
1013
|
+
} catch (e) {}
|
|
1014
|
+
|
|
1015
|
+
await endMission(mission.mission_id, 'failed', finalUsage, failReason);
|
|
1016
|
+
} catch { }
|
|
1017
|
+
}
|
|
900
1018
|
} finally {
|
|
1019
|
+
// Si fue cancelación de usuario, dar un breve margen para que el cancelHandler
|
|
1020
|
+
// async (endMission, escritura de log) termine antes de generar el reporte.
|
|
1021
|
+
if (userCanceledMission) {
|
|
1022
|
+
await new Promise(r => setTimeout(r, 800));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
901
1025
|
const sRep = spinner();
|
|
902
1026
|
sRep.start('📊 Generating report...');
|
|
903
1027
|
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
package/scripts/init.mjs
CHANGED
|
@@ -143,8 +143,12 @@ async function main() {
|
|
|
143
143
|
401: '❌ Invalid API Key. Please check your key and try again.',
|
|
144
144
|
403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
|
|
145
145
|
};
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const msg = errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`;
|
|
147
|
+
s.stop(chalk.red(msg));
|
|
148
|
+
console.log(chalk.yellow('\n Press Enter to return to menu...'));
|
|
149
|
+
// Salida limpia (código 0) para que el menú padre no interprete esto como crash
|
|
150
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
151
|
+
process.exit(0);
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
const data = await res.json();
|
|
@@ -162,7 +166,8 @@ async function main() {
|
|
|
162
166
|
} catch (err) {
|
|
163
167
|
s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
|
|
164
168
|
console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
|
|
165
|
-
|
|
169
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
170
|
+
process.exit(0);
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
// ── Step 3: Project details ──
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ContainerClient } from '@azure/storage-blob';
|
|
4
|
+
import mime from 'mime-types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subes la evidencia de prueba iterativamente a Azure Blob Storage usando una URL de SAS.
|
|
8
|
+
* @param {string} reportsDir Directorio local de reportes.
|
|
9
|
+
* @param {string} sasUrl URL de SAS para el ContainerClient.
|
|
10
|
+
* @param {string} basePath Path base dentro del contenedor de Azure.
|
|
11
|
+
*/
|
|
12
|
+
export async function uploadEvidence(reportsDir, sasUrl, basePath) {
|
|
13
|
+
if (!fs.existsSync(reportsDir)) return;
|
|
14
|
+
|
|
15
|
+
const containerClient = new ContainerClient(sasUrl);
|
|
16
|
+
|
|
17
|
+
async function walkAndUpload(currentDir, relativePath = '') {
|
|
18
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
19
|
+
const uploadPromises = [];
|
|
20
|
+
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
23
|
+
const entryRelativePath = path.posix.join(relativePath, entry.name);
|
|
24
|
+
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
uploadPromises.push(walkAndUpload(fullPath, entryRelativePath));
|
|
27
|
+
} else {
|
|
28
|
+
// Blob path en Azure: basePath/ruta_relativa_del_archivo
|
|
29
|
+
const blobPath = `${basePath}/${entryRelativePath}`;
|
|
30
|
+
const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
|
|
31
|
+
|
|
32
|
+
const contentType = mime.lookup(entry.name) || 'application/octet-stream';
|
|
33
|
+
|
|
34
|
+
uploadPromises.push(
|
|
35
|
+
blockBlobClient.uploadFile(fullPath, {
|
|
36
|
+
blobHTTPHeaders: { blobContentType: contentType }
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await Promise.all(uploadPromises);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await walkAndUpload(reportsDir);
|
|
45
|
+
}
|
package/src/KnowledgeService.ts
CHANGED
|
@@ -41,12 +41,14 @@ export class KnowledgeService {
|
|
|
41
41
|
private apiBase: string;
|
|
42
42
|
private apiKey: string;
|
|
43
43
|
private projectId: string | null = null;
|
|
44
|
+
private orgId: string | null = null;
|
|
44
45
|
|
|
45
46
|
private constructor() {
|
|
46
47
|
// API URL is internal-only, loaded from the tool's .env via dotenv
|
|
47
48
|
this.apiBase = process.env.ARCALITY_API_URL || 'https://arcalityqadev.arcadial.lat';
|
|
48
49
|
this.apiKey = process.env.ARCALITY_API_KEY || '';
|
|
49
50
|
this.projectId = process.env.ARCALITY_PROJECT_ID || null;
|
|
51
|
+
this.orgId = process.env.ARCALITY_ORG_ID || null;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -140,7 +142,7 @@ export class KnowledgeService {
|
|
|
140
142
|
if (res.status === 200) {
|
|
141
143
|
const data = await res.json();
|
|
142
144
|
if (data.status === 'skipped_duplicate_dom') {
|
|
143
|
-
|
|
145
|
+
// console.log(chalk.gray(`[Knowledge] Ingesta omitida (Duplicado).`));
|
|
144
146
|
} else {
|
|
145
147
|
console.log(chalk.cyan(`[Knowledge] Ingesta exitosa (Project: ${payload.project_id})`));
|
|
146
148
|
}
|
|
@@ -158,10 +160,13 @@ export class KnowledgeService {
|
|
|
158
160
|
if (!pid || pid === 'undefined') return null;
|
|
159
161
|
try {
|
|
160
162
|
const pathUrl = new URL(url).pathname;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
let contextUrl = `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`;
|
|
164
|
+
if (this.orgId) {
|
|
165
|
+
contextUrl += `&organization_id=${this.orgId}`;
|
|
166
|
+
}
|
|
167
|
+
const res = await this.fetchWithTimeout(contextUrl, {
|
|
168
|
+
headers: { 'x-api-key': this.apiKey }
|
|
169
|
+
});
|
|
165
170
|
if (!res.ok) return null;
|
|
166
171
|
return await res.json();
|
|
167
172
|
} catch (e: any) {
|
|
@@ -185,6 +190,7 @@ export class KnowledgeService {
|
|
|
185
190
|
'x-api-key': this.apiKey
|
|
186
191
|
},
|
|
187
192
|
body: JSON.stringify({
|
|
193
|
+
organization_id: this.orgId,
|
|
188
194
|
project_id: pid,
|
|
189
195
|
rule_type: "UI_UX", // Default según especificación
|
|
190
196
|
title,
|
|
@@ -210,6 +216,7 @@ export class KnowledgeService {
|
|
|
210
216
|
'x-api-key': this.apiKey
|
|
211
217
|
},
|
|
212
218
|
body: JSON.stringify({
|
|
219
|
+
organization_id: this.orgId,
|
|
213
220
|
project_id: pid,
|
|
214
221
|
field_identifier: identifier,
|
|
215
222
|
field_type: type,
|
|
@@ -235,6 +242,7 @@ export class KnowledgeService {
|
|
|
235
242
|
'x-api-key': this.apiKey
|
|
236
243
|
},
|
|
237
244
|
body: JSON.stringify({
|
|
245
|
+
organization_id: this.orgId,
|
|
238
246
|
project_id: pid,
|
|
239
247
|
doc_type: docType,
|
|
240
248
|
title,
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
|
|
8
8
|
|
|
9
9
|
// ── Backend config (internal, not user-configurable) ──
|
|
10
|
-
const DAILY_MISSION_LIMIT =
|
|
10
|
+
const DAILY_MISSION_LIMIT = 35;
|
|
11
11
|
const USAGE_FILE = path.join(CONFIG_DIR, 'usage.json');
|
|
12
12
|
|
|
13
13
|
// ═══════════════════════════════════════════════════════
|
|
@@ -128,22 +128,31 @@ export async function validateApiKey() {
|
|
|
128
128
|
// ── LIVE mode: Backend available ──
|
|
129
129
|
if (apiBase) {
|
|
130
130
|
try {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
131
133
|
const res = await fetch(`${apiBase}/api/v1/auth/validate`, {
|
|
132
134
|
method: 'POST',
|
|
133
135
|
headers: {
|
|
134
136
|
'Content-Type': 'application/json',
|
|
135
137
|
'x-api-key': key
|
|
136
|
-
}
|
|
138
|
+
},
|
|
139
|
+
signal: controller.signal
|
|
137
140
|
});
|
|
141
|
+
clearTimeout(timeout);
|
|
138
142
|
|
|
139
143
|
if (res.status === 401) return { valid: false, error: 'invalid_api_key', mode: 'live' };
|
|
140
144
|
if (res.status === 403) return { valid: false, error: 'plan_expired', mode: 'live' };
|
|
141
145
|
if (!res.ok) return { valid: false, error: 'server_error', mode: 'live' };
|
|
142
146
|
|
|
143
147
|
const data = await res.json();
|
|
148
|
+
if (process.env.DEBUG || process.argv.includes('--debug')) {
|
|
149
|
+
console.log(`[DEBUG] Auth validation response keys:`, Object.keys(data));
|
|
150
|
+
console.log(`[DEBUG] Auth validation response:`, JSON.stringify(data, null, 2));
|
|
151
|
+
}
|
|
144
152
|
return { ...data, valid: true, mode: 'live' };
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
|
|
155
|
+
console.warn(`⚠️ Backend not available (${apiBase}): ${reason}. Using local mode.`);
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -175,6 +184,8 @@ export async function startMission(prompt, targetUrl) {
|
|
|
175
184
|
try {
|
|
176
185
|
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
177
186
|
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
178
189
|
const res = await fetch(`${apiBase}/api/v1/missions/start`, {
|
|
179
190
|
method: 'POST',
|
|
180
191
|
headers: {
|
|
@@ -186,8 +197,10 @@ export async function startMission(prompt, targetUrl) {
|
|
|
186
197
|
prompt_hash: simpleHash(prompt),
|
|
187
198
|
target_url: targetUrl,
|
|
188
199
|
project_id: projectId || undefined,
|
|
189
|
-
})
|
|
200
|
+
}),
|
|
201
|
+
signal: controller.signal
|
|
190
202
|
});
|
|
203
|
+
clearTimeout(timeout);
|
|
191
204
|
|
|
192
205
|
if (res.status === 429) {
|
|
193
206
|
const data = await res.json();
|
|
@@ -196,8 +209,9 @@ export async function startMission(prompt, targetUrl) {
|
|
|
196
209
|
if (!res.ok) return { allowed: false, error: 'server_error' };
|
|
197
210
|
|
|
198
211
|
return { allowed: true, ...(await res.json()) };
|
|
199
|
-
} catch {
|
|
200
|
-
|
|
212
|
+
} catch (e) {
|
|
213
|
+
const reason = e?.name === 'AbortError' ? 'timeout (5s)' : (e?.message || 'unknown');
|
|
214
|
+
console.warn(`⚠️ startMission failed (${apiBase}): ${reason}. Falling back to mock mode.`);
|
|
201
215
|
}
|
|
202
216
|
}
|
|
203
217
|
|
|
@@ -236,7 +250,7 @@ export async function startMission(prompt, targetUrl) {
|
|
|
236
250
|
* @param {'success'|'failed'|'cancelled'} result
|
|
237
251
|
* @param {object} usage - Optional usage statistics directly from the AI
|
|
238
252
|
*/
|
|
239
|
-
export async function endMission(missionId, result, usage = null) {
|
|
253
|
+
export async function endMission(missionId, result, usage = null, failReason = null) {
|
|
240
254
|
const apiBase = getEffectiveApiBase();
|
|
241
255
|
if (!apiBase || !missionId) return { ok: true };
|
|
242
256
|
|
|
@@ -248,7 +262,7 @@ export async function endMission(missionId, result, usage = null) {
|
|
|
248
262
|
'Content-Type': 'application/json',
|
|
249
263
|
'x-api-key': key
|
|
250
264
|
},
|
|
251
|
-
body: JSON.stringify({ result, usage })
|
|
265
|
+
body: JSON.stringify({ result, usage, failReason })
|
|
252
266
|
});
|
|
253
267
|
} catch { }
|
|
254
268
|
return { ok: true };
|
|
@@ -266,3 +280,33 @@ function simpleHash(str) {
|
|
|
266
280
|
}
|
|
267
281
|
return 'ph_' + Math.abs(hash).toString(36);
|
|
268
282
|
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Obtiene el Token SAS y el path base para subir evidencias.
|
|
286
|
+
* @param {string} missionId
|
|
287
|
+
* @param {string} projectId
|
|
288
|
+
*/
|
|
289
|
+
export async function getEvidenceSasToken(missionId, projectId) {
|
|
290
|
+
const apiBase = getEffectiveApiBase();
|
|
291
|
+
if (!apiBase || !missionId || !projectId) return null;
|
|
292
|
+
|
|
293
|
+
const key = getEffectiveApiKey();
|
|
294
|
+
if (!key) return null;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(`${apiBase}/api/v1/missions/${missionId}/evidence/sas`, {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: {
|
|
300
|
+
'Content-Type': 'application/json',
|
|
301
|
+
'Authorization': `Bearer ${key}`
|
|
302
|
+
},
|
|
303
|
+
body: JSON.stringify({ projectId })
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!res.ok) return null;
|
|
307
|
+
|
|
308
|
+
return await res.json();
|
|
309
|
+
} catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/configLoader.mjs
CHANGED
|
@@ -18,7 +18,8 @@ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
|
18
18
|
* @returns {string}
|
|
19
19
|
*/
|
|
20
20
|
export function getApiUrl() {
|
|
21
|
-
return
|
|
21
|
+
// return 'https://arcalityqadev.arcadial.lat';
|
|
22
|
+
return 'https://arcalityqadev.arcadial.lat';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|