Files
manus-dashboard/src/backend/test/app-registry.test.js
Santinova Deployment a6c1dab3d7
All checks were successful
Validation Dashboard / Tests backend et build frontend (push) Successful in 40s
fix: rafraîchir les manifests du dashboard après déploiement
2026-10-24 19:38:44 +00:00

147 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Tests de non-régression du registre d'applications.
* Le registre doit faire de app.json l'unique source de vérité des applications déployées.
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { discoverApps, refreshApps, refreshAppsIfStale } = require('../src/app-registry');
function writeManifest(basePath, directory, manifest) {
const appPath = path.join(basePath, directory);
fs.mkdirSync(appPath, { recursive: true });
fs.writeFileSync(path.join(appPath, 'app.json'), JSON.stringify(manifest), 'utf8');
}
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'app-registry-'));
try {
writeManifest(sandbox, 'contacts', {
id: 'itinova-contacts',
name: 'Itinova Contacts',
category: 'ITINOVA',
containerName: 'itinova-contacts',
image: 'images/itinova-contacts.png',
ai: {
engines: [{ provider: 'Google Gemini', model: 'gemini-2.5-flash', role: 'Assistant conversationnel', billing: 'free', source: 'referential' }],
activeConfigurationPath: '/api/ai/active-configuration',
},
urls: {
recette: 'https://contacts-itinova.recette.santinova-soft.org',
prod: 'https://contacts-itinova.santinova-soft.org',
},
});
writeManifest(sandbox, 'dashboard', {
id: 'manus-dashboard',
name: 'Dashboard',
category: 'INFRA',
urls: {
recette: 'https://dashboard-santinova.recette.santinova-soft.org',
prod: 'https://dashboard-santinova.santinova-soft.org',
},
});
writeManifest(sandbox, 'falc', {
id: 'falc-generator',
name: 'FALC Generator',
category: 'ITINOVA',
urls: { recette: 'https://falc-itinova.recette.santinova-soft.org' },
});
writeManifest(sandbox, 'invalide', {
id: 'app-invalide',
name: 'Application invalide',
category: 'INCONNUE',
urls: { recette: 'https://invalide.recette.santinova-soft.org' },
});
writeManifest(sandbox, 'ia-invalide', {
id: 'app-ia-invalide',
name: 'Application IA invalide',
category: 'ITINOVA',
ai: { engines: [{ provider: 'Google Gemini' }] },
urls: { recette: 'https://ia-invalide.recette.santinova-soft.org' },
});
writeManifest(sandbox, 'tarif-ia-invalide', {
id: 'app-tarif-ia-invalide',
name: 'Tarif IA invalide',
category: 'ITINOVA',
ai: { engines: [{ provider: 'Google Gemini', model: 'gemini-2.5-flash', billing: 'inconnu' }] },
urls: { recette: 'https://tarif-ia-invalide.recette.santinova-soft.org' },
});
const discovery = discoverApps({ appsBasePath: sandbox, environment: 'recette' });
assert.equal(discovery.apps.length, 3, 'un manifeste invalide doit être exclu');
assert.equal(discovery.errors.length, 3, 'une erreur explicite doit être retournée pour chaque manifeste invalide');
assert.ok(discovery.errors.some((error) => error.error.includes('billing doit être free ou paid')));
const app = discovery.apps.find((candidate) => candidate.id === 'itinova-contacts');
assert.equal(app.directory, 'contacts', 'le dossier doit être déduit du chemin du manifeste');
assert.equal(app.healthCheckUrl, app.urls.recette, 'la vérification doit cibler l’environnement courant');
assert.equal(app.containerName, 'itinova-contacts');
assert.deepEqual(app.ai, {
engines: [{ provider: 'Google Gemini', model: 'gemini-2.5-flash', role: 'Assistant conversationnel', billing: 'free', source: 'referential' }],
activeConfigurationPath: '/api/ai/active-configuration',
}, 'la métadonnée IA descriptive, tarifaire, de provenance et de configuration active doit être conservée');
const falc = discovery.apps.find((candidate) => candidate.id === 'falc-generator');
assert.deepEqual(falc.ai, {
engines: [
{ provider: 'Manus Forge', model: 'modèle par défaut non explicité', role: 'Transformation FALC', billing: 'paid', source: 'application' },
{ provider: 'Manus Forge', model: 'gemini-3.1-pro-preview', role: 'Récits de vie', billing: 'paid', source: 'application' },
],
}, 'le catalogue IA doit compléter un manifeste historique sans moteur déclaré');
const dashboardRecette = discovery.apps.find((candidate) => candidate.id === 'manus-dashboard');
assert.equal(dashboardRecette.name, 'Dashboard Recette');
const productionDiscovery = discoverApps({ appsBasePath: sandbox, environment: 'prod' });
const dashboardProduction = productionDiscovery.apps.find((candidate) => candidate.id === 'manus-dashboard');
assert.equal(dashboardProduction.name, 'Dashboard Production');
const config = {
appsBasePath: sandbox,
environment: 'recette',
apps: [{ id: 'itinova-contacts', name: 'ancienne valeur statique' }],
};
const refresh = refreshApps(config);
assert.equal(refresh.apps.length, 3, 'les applications non déployées ne doivent pas rester en cache');
assert.equal(
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
'Itinova Contacts',
'app.json doit prévaloir sur toute définition statique'
);
// Un déploiement met à jour app.json : le dashboard doit le détecter sans
// redémarrage, mais sans relire les manifests à chaque requête.
refreshApps(config, console, 1_000);
writeManifest(sandbox, 'contacts', {
id: 'itinova-contacts',
name: 'Itinova Contacts mis à jour',
category: 'ITINOVA',
urls: {
recette: 'https://contacts-itinova.recette.santinova-soft.org',
prod: 'https://contacts-itinova.santinova-soft.org',
},
});
const cachedRefresh = refreshAppsIfStale(config, { now: 1_999, maxAgeMs: 1_000 });
assert.equal(cachedRefresh.refreshed, false, 'le cache reste utilisable avant son expiration');
assert.equal(
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
'Itinova Contacts',
'le cache ne doit pas relire les manifests avant son expiration'
);
const expiredRefresh = refreshAppsIfStale(config, { now: 2_000, maxAgeMs: 1_000 });
assert.equal(expiredRefresh.refreshed, true, 'le manifeste est relu lorsque le cache expire');
assert.equal(
config.apps.find((candidate) => candidate.id === 'itinova-contacts').name,
'Itinova Contacts mis à jour',
'la modification de manifeste doit apparaître sans redémarrage du dashboard'
);
console.log('OK app-registry');
} finally {
fs.rmSync(sandbox, { recursive: true, force: true });
}