diff --git a/tests/draco-alertes.test.js b/tests/draco-alertes.test.js index dc5371f..573d149 100644 --- a/tests/draco-alertes.test.js +++ b/tests/draco-alertes.test.js @@ -1,18 +1,61 @@ // Tests unitaires (node:test) du module partagé draco-alertes.js. // Exécutés dans la sandbox : node --test draco-alertes.test.js +// Copie vendangée byte-identique de Projets/dracodev-commons +// ui/alertes/ (chantier 5) — seul le require change selon l'hôte. +// Hôte : Flowdeck // Aucune donnée inventée : les payloads reprennent la forme exacte // des API réelles (Flowdeck /api/notifications, TikTak -// /api/notifications, Kanbante /api/events). +// /api/notifications, Kanbante /api/events). Le payload console +// n'est qu'un EXEMPLE DE FORMAT (voir README commons) : aucun +// endpoint console n'est publié sur agent-commun main au moment +// du chantier. const test = require("node:test"); const assert = require("node:assert/strict"); const t = require("../static/js/draco-alertes.js"); -test("resolveUrls: config explicite > hote -dev > href > defaut prod", () => { +function sampleResults() { + return { + flowdeck: { items: t.normalizeFlowdeck("https://flowdeck.dracodev.net/", { + notifications: [ + { id: 1, ntype: "mention", title: "Mention budget", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" }, + { id: 2, ntype: "reminder", title: "Rappel lu", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" }, + ], unread: 1, + }) }, + tiktak: { items: t.normalizeTiktak("https://tiktak.dracodev.net/", [ + { id: 7, kind: "reminder", title: "Rappel tâche", body: "tache", task_id: 42, is_read: 0, created_at: "2026-10-28T08:00:00" }, + { id: 8, kind: "share", title: "Partage lu", body: "", task_id: null, is_read: 1, created_at: "2026-10-26T08:00:00" }, + ]) }, + kanbante: { items: t.normalizeKanbante("https://kanbante.dracodev.net/", { + events: [ + { id: 9, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "commented", from_column: "", to_column: "", created_at: "2026-10-28T11:50:00" }, + { id: 10, repo_full_name: "Projets/fusion-trois-apps", issue_number: 69, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" }, + ], + }) }, + obsigate: { items: [] }, + console: { items: [], sourceMissing: true }, + }; +} +function allOf(results) { + const all = []; + for (const app of ["obsigate", "flowdeck", "tiktak", "kanbante", "console"]) { + (results[app] && results[app].items || []).forEach((a) => all.push(a)); + } + return all; +} + +test("ordre des onglets : Tout, apps, Console Spark, Historique", () => { + assert.deepEqual(t.TABS, ["tout", "obsigate", "flowdeck", "tiktak", "kanbante", "console", "historique"]); + assert.equal(t.TAB_LABELS.console, "Console Spark"); +}); + +test("resolveUrls: config explicite > hote -dev > href > defaut prod, agent inclus", () => { const prod = t.resolveUrls({ hostname: "tiktak.dracodev.net", tabHrefs: {} }); assert.equal(prod.tiktak, "https://tiktak.dracodev.net/"); + assert.equal(prod.agent, "https://agent.dracodev.net/"); const dev = t.resolveUrls({ hostname: "tiktak-dev.dracodev.net", tabHrefs: {} }); assert.equal(dev.flowdeck, "https://flowdeck-dev.dracodev.net/"); assert.equal(dev.obsigate, "https://og-dev.dracodev.net/"); + assert.equal(dev.agent, "https://agent-dev.dracodev.net/"); const explicit = t.resolveUrls({ hostname: "tiktak-dev.dracodev.net", configUrls: { flowdeck: "https://flowdeck.dracodev.net/" }, @@ -20,9 +63,10 @@ test("resolveUrls: config explicite > hote -dev > href > defaut prod", () => { assert.equal(explicit.flowdeck, "https://flowdeck.dracodev.net/"); const href = t.resolveUrls({ hostname: "example.org", - tabHrefs: { kanbante: "https://kanbante-dev.dracodev.net/" }, + tabHrefs: { kanbante: "https://kanbante-dev.dracodev.net/", agent: "https://agent.dracodev.net/" }, }); assert.equal(href.kanbante, "https://kanbante-dev.dracodev.net/"); + assert.equal(href.agent, "https://agent.dracodev.net/"); const token = t.resolveUrls({ hostname: "x", tabHrefs: { tiktak: "{{URL_TIKTAK}}" } }); assert.equal(token.tiktak, "https://tiktak.dracodev.net/"); assert.equal(t.isDevHost("tiktak-dev.dracodev.net.evil.com"), false); @@ -34,8 +78,7 @@ test("normalizeFlowdeck: non lues actives, lien = base + url", () => { notifications: [ { id: 1, ntype: "mention", title: "Mention", message: "voir page", url: "/collections/3", is_read: 0, created_at: "2026-10-28T10:00:00" }, { id: 2, ntype: "reminder", title: "Rappel", message: "", url: "", is_read: 1, created_at: "2026-10-27T09:00:00" }, - ], - unread: 1, + ], unread: 1, }); assert.equal(items.length, 2); assert.equal(items[0].active, true); @@ -82,3 +125,112 @@ test("sortAlerts + countActive: actives d'abord, date decroissante", () => { assert.equal(sorted[3].date, ""); assert.equal(t.countActive(list), 2); }); + +test("scopeAlerts: Tout et onglets apps = actives seulement ; Historique = fermees seulement", () => { + const r = sampleResults(); + const all = allOf(r); + const tout = t.scopeAlerts(all, "tout", r); + assert.equal(tout.items.length, 2); + assert.ok(tout.items.every((a) => a.active)); + assert.equal(tout.items[0].date, "2026-10-28T10:00:00"); // date decroissante + const fd = t.scopeAlerts(all, "flowdeck", r); + assert.equal(fd.items.length, 1); + assert.equal(fd.items[0].app, "flowdeck"); + const hist = t.scopeAlerts(all, "historique", r); + assert.equal(hist.items.length, 4); // 1 FD lue + 1 TK lue + 2 KB + assert.ok(hist.items.every((a) => !a.active)); + const og = t.scopeAlerts(all, "obsigate", r); + assert.equal(og.items.length, 0); + const cs = t.scopeAlerts(all, "console", r); + assert.equal(cs.items.length, 0); + assert.equal(cs.consoleMissing, true); +}); + +test("scopeAlerts: une section en echec remonte aussi dans Historique (pas avalee)", () => { + const r = sampleResults(); + r.flowdeck = { items: [], error: "Application injoignable depuis cet hôte (TypeError)." }; + const hist = t.scopeAlerts(allOf(r), "historique", r); + assert.equal(hist.errors.length, 1); + assert.equal(hist.errors[0].app, "flowdeck"); + const tout = t.scopeAlerts(allOf(r), "tout", r); + assert.equal(tout.errors.length, 1); +}); + +test("countByTab: compteurs par onglet sur l'echantillon", () => { + const r = sampleResults(); + const c = t.countByTab(allOf(r), r); + assert.deepEqual(c, { + tout: 2, obsigate: 0, flowdeck: 1, tiktak: 1, + kanbante: 0, console: 0, historique: 4, + }); +}); + +test("filterAlerts: titre + contexte, insensible a la casse, sans melanger actifs/historique", () => { + const r = sampleResults(); + const all = allOf(r); + const tout = t.scopeAlerts(all, "tout", r).items; + assert.equal(t.filterAlerts(tout, "budget").length, 1); + assert.equal(t.filterAlerts(tout, "BUDGET").length, 1); + assert.equal(t.filterAlerts(tout, "voir page").length, 1); // contexte (detail) + assert.equal(t.filterAlerts(tout, "tache").length, 1); + // « Rappel lu » est une alerte fermee : absente de Tout meme filtree. + assert.equal(t.filterAlerts(tout, "Rappel lu").length, 0); + const hist = t.scopeAlerts(all, "historique", r).items; + assert.equal(t.filterAlerts(hist, "Rappel lu").length, 1); + assert.equal(t.filterAlerts(tout, "").length, tout.length); +}); + +test("presentation identique: meta Kanbante sans type brut anglais ; date illisible = sans date", () => { + const kb = t.normalizeKanbante("https://kanbante.dracodev.net/", { + events: [{ id: 1, repo_full_name: "R", issue_number: 1, event_type: "moved", from_column: "todo", to_column: "review", created_at: "2026-10-28T12:00:00" }], + })[0]; + assert.ok(!t.metaLine(kb).includes("moved")); + assert.match(t.metaLine(kb), /Kanbante · .* · Déplacée/); + assert.equal(t.displayState({ active: true }), "Active"); + assert.equal(t.displayState({ active: false, app: "flowdeck" }), "Lue"); + const sansDate = { app: "tiktak", active: true, title: "x", detail: "", date: "nimporte-quoi", kind: "", url: "https://x/" }; + assert.match(t.metaLine(sansDate), /sans date/); +}); + +test("hash URL: parse + round-trip, onglet inconnu rejete", () => { + assert.equal(t.parseAlertTab("#alertes=historique"), "historique"); + assert.equal(t.parseAlertTab("#alertes=console"), "console"); + assert.equal(t.parseAlertTab("#alertes"), "tout"); + assert.equal(t.parseAlertTab("#autre"), null); + assert.equal(t.parseAlertTab("#alertes=nimporte"), null); + assert.equal(t.parseAlertTab(""), null); + for (const tab of t.TABS) assert.equal(t.parseAlertTab(t.hashForTab(tab)), tab); + assert.equal(t.hashForTab("nimporte"), "#alertes=tout"); +}); + +test("normalizeConsole (FORMAT EXEMPLE): statuts reels -> actif/ferme, lien console par defaut", () => { + const items = t.normalizeConsole("https://agent.dracodev.net/", { + alerts: [ + { id: "m1", title: "Mission en pause budget", detail: "budget épuisé", status: "pause_budget", created_at: "2026-10-28T09:00:00" }, + { id: "m2", title: "Accès requis", detail: "jeton manquant", status: "acces_requis", created_at: "2026-10-28T09:30:00" }, + { id: "a1", title: "Approbation en attente", status: "en_attente", url: "/approbations", created_at: "2026-10-28T10:00:00" }, + { id: "m3", title: "Mission terminée", status: "terminee", created_at: "2026-10-27T10:00:00" }, + { id: "m4", title: "Sans statut", created_at: "2026-10-27T11:00:00" }, + ], + }); + assert.equal(items.length, 5); + assert.equal(items[0].active, true); + assert.equal(items[1].active, true); + assert.equal(items[2].active, true); + assert.equal(items[2].url, "https://agent.dracodev.net/approbations"); + assert.equal(items[3].active, false); + assert.equal(items[4].active, true); // statut inconnu : expose en active, jamais masque + assert.equal(items[0].url, "https://agent.dracodev.net/"); + const r = sampleResults(); + r.console = { items }; + const c = t.countByTab(allOf(r), r); + assert.equal(c.console, 4); + assert.equal(c.tout, 6); + assert.equal(c.historique, 5); +}); + +test("consoleSourceUrl: null sans configuration (aucune URL devineee)", () => { + assert.equal(t.consoleSourceUrl({}), null); + assert.equal(t.consoleSourceUrl({ consoleUrl: "https://agent-dev.dracodev.net/api/alertes" }), + "https://agent-dev.dracodev.net/api/alertes"); +});