Wer einen Server aus der Ping-Ueberwachung nehmen wollte, musste sich bisher auf den Rechner mit check_fds.sh einloggen und die hosts.conf editieren - und die Kachel blieb danach trotzdem im System-Monitor stehen, weil ein einmal gemeldetes System nirgends wieder geloescht werden konnte. Die Liste steht jetzt in der Datenbank und ist ueber ein Zahnrad neben der Ueberschrift "Systeme" pflegbar, analog zum Zahnrad am PO-Platz (Issue #27). Jeder Host gehoert dem OPs-Agenten (Issue #66), der ihn anpingt: API und Ping-Check laufen nicht zwangslaeufig auf demselben Rechner, die API kann die hosts.conf also nicht selbst schreiben. check_fds.sh holt sie stattdessen bei jedem Lauf mit seinem Agenten-Key ab und schreibt daraus die lokale Datei; ist die API nicht erreichbar, pingt es unveraendert nach der zuletzt geschriebenen Liste weiter. Beim ersten Lauf laedt der Check seine bestehende hosts.conf hoch, damit die bereits ueberwachten Server nicht von Hand nachgetragen werden muessen. Dass die API das nur einmal annimmt (Marker in host_config_imports) und "noch nie konfiguriert" per 204 von "im Frontend geleert" per 200 unterscheidet, ist noetig, damit ein Ping-Rechner mit alter lokaler Datei die dort geloeschten Hosts nicht wieder anlegt. Loeschen raeumt mit auf: Ein entfernter Host nimmt seine Kachel mit - beide Ids, denn der status.txt-Import benennt ihn nach dem Hostnamen, die REST-Meldung nach dessen Slug. Kacheln ohne zugehoerigen Host (Selbstmelder oder Reste laengst entfernter Hosts) stehen im selben Dialog unter "Weitere Kacheln im System-Monitor" und sind dort einzeln loeschbar - dafuer gibt es neu DELETE /api/v1/systems/{id}. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vNjbfr8M3HSfWMee92GTE
896 lines
35 KiB
JavaScript
896 lines
35 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
BOARD_COLUMNS,
|
|
buildBoardColumnLayout,
|
|
buildDeskLayout,
|
|
buildSystemTiles,
|
|
buildEventLines,
|
|
MAX_SYSTEM_TILES,
|
|
MAX_EVENT_LINES,
|
|
DEFAULT_DESK_COUNT,
|
|
NOTE_STACK_LAYERS,
|
|
aggregateTicketsByColumn,
|
|
groupTicketsByColumn,
|
|
buildTicketModalContent,
|
|
formatEventTime,
|
|
mapEventForDisplay,
|
|
formatEventLineText,
|
|
truncateToWidth,
|
|
EVENT_LINE_CHAR_WIDTH,
|
|
buildEventModalContent,
|
|
buildSystemModalContent,
|
|
systemsWithoutHostConfig,
|
|
formatSystemCheckTime,
|
|
buildKpiSparklinePoints,
|
|
buildKpiTiles,
|
|
buildKpiModalContent,
|
|
buildAgentCountKpis,
|
|
buildHooksKpi,
|
|
MAX_KPI_TILES,
|
|
AGENT_LOCATIONS,
|
|
resolveAgentVisualState,
|
|
mapAgentsToDesks,
|
|
normalizeAgentForDisplay,
|
|
isAgentPresent,
|
|
isAgentVisible,
|
|
agentProblemSeverity,
|
|
countAgentProblems,
|
|
agentsWithProblems,
|
|
pickIdleActivity,
|
|
AnimationQueue,
|
|
DOOR_VISIT_TYPES,
|
|
DOOR_VISIT_STAGES,
|
|
detectDoorVisits,
|
|
resolveDoorVisitVisual,
|
|
advanceDoorVisitStage,
|
|
PO_AGENT_ID,
|
|
PO_DISPLAY_NAME,
|
|
PO_VISIT_STAGES,
|
|
detectUnassignedTicketChanges,
|
|
advancePoVisitStage,
|
|
applyPendingTicketMoves,
|
|
detectAgentOwnedTicketChanges,
|
|
hasAgentReachedBoard,
|
|
DEFAULT_RELOAD_INTERVAL_SECONDS,
|
|
resolveReloadIntervalSeconds,
|
|
nextCountdownSeconds,
|
|
} from "../../html/js/office-scene.js";
|
|
|
|
test("buildBoardColumnLayout liefert alle Spalten in API.md-Reihenfolge", () => {
|
|
const layout = buildBoardColumnLayout({});
|
|
assert.deepEqual(
|
|
layout.map((c) => c.key),
|
|
BOARD_COLUMNS.map((c) => c.key)
|
|
);
|
|
});
|
|
|
|
test("buildBoardColumnLayout zeigt Tickets bis einschliesslich 5 einzeln an", () => {
|
|
const layout = buildBoardColumnLayout({ bereit: 5 });
|
|
const bereit = layout.find((c) => c.key === "bereit");
|
|
assert.equal(bereit.visibleNotes, 5);
|
|
assert.equal(bereit.stackCount, 0);
|
|
});
|
|
|
|
test("buildBoardColumnLayout stapelt ab dem 5. Ticket (F3): aelteste 4 einzeln + Rest als Stapel", () => {
|
|
const layout = buildBoardColumnLayout({ backlog: 8 });
|
|
const backlog = layout.find((c) => c.key === "backlog");
|
|
assert.equal(backlog.visibleNotes, 4);
|
|
assert.equal(backlog.stackCount, 4);
|
|
assert.equal(backlog.count, 8);
|
|
});
|
|
|
|
test("buildBoardColumnLayout stellt die Spalte done immer als Stapel dar (F2), unabhaengig von der Anzahl", () => {
|
|
const wenige = buildBoardColumnLayout({ done: 2 }).find((c) => c.key === "done");
|
|
assert.equal(wenige.visibleNotes, 0);
|
|
assert.equal(wenige.stackCount, 2);
|
|
|
|
const viele = buildBoardColumnLayout({ done: 42 }).find((c) => c.key === "done");
|
|
assert.equal(viele.visibleNotes, 0);
|
|
assert.equal(viele.stackCount, 42);
|
|
});
|
|
|
|
test("buildBoardColumnLayout behandelt fehlende/negative Zaehler als 0", () => {
|
|
const layout = buildBoardColumnLayout({ qa: -3 });
|
|
const qa = layout.find((c) => c.key === "qa");
|
|
assert.equal(qa.count, 0);
|
|
assert.equal(qa.visibleNotes, 0);
|
|
});
|
|
|
|
test("buildBoardColumnLayout faerbt jeden Zettel in der Farbe seines Projekts (Issue #27)", () => {
|
|
const tickets = [
|
|
{ column_name: "bereit", color: "#fbca04" },
|
|
{ column_name: "bereit", color: "#28a745" },
|
|
{ column_name: "bereit", color: "#fbca04" },
|
|
];
|
|
const bereit = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "bereit");
|
|
|
|
assert.deepEqual(bereit.noteColors, ["#fbca04", "#28a745", "#fbca04"]);
|
|
});
|
|
|
|
test("buildBoardColumnLayout laesst Zettel ohne Projektfarbe ungefaerbt", () => {
|
|
const tickets = [{ column_name: "qa" }, { column_name: "qa", color: "#0366d6" }];
|
|
const qa = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "qa");
|
|
|
|
assert.deepEqual(qa.noteColors, [null, "#0366d6"]);
|
|
});
|
|
|
|
test("buildBoardColumnLayout ohne Ticketliste (Demo-Modus) laesst alle Zettel ungefaerbt", () => {
|
|
const bereit = buildBoardColumnLayout({ bereit: 2 }).find((c) => c.key === "bereit");
|
|
assert.deepEqual(bereit.noteColors, [null, null]);
|
|
assert.deepEqual(bereit.stackColors, []);
|
|
});
|
|
|
|
test("buildBoardColumnLayout zeigt im Stapel die verschiedenen Projektfarben der gestapelten Tickets", () => {
|
|
// 6 Tickets: die aeltesten 4 einzeln, die restlichen 2 im Stapel - dessen Farben stammen
|
|
// ausschliesslich aus diesen beiden.
|
|
const tickets = [
|
|
...Array.from({ length: 4 }, () => ({ column_name: "backlog", color: "#fbca04" })),
|
|
{ column_name: "backlog", color: "#28a745" },
|
|
{ column_name: "backlog", color: "#0366d6" },
|
|
];
|
|
const backlog = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "backlog");
|
|
|
|
assert.equal(backlog.stackCount, 2);
|
|
// Dritter Zettel: zyklische Wiederholung statt Standardfarbe - siehe stackLayerColors().
|
|
assert.deepEqual(backlog.stackColors, ["#28a745", "#0366d6", "#28a745"]);
|
|
});
|
|
|
|
test("buildBoardColumnLayout laesst einen Stapel ganz ohne Projektfarben im Standard", () => {
|
|
const tickets = Array.from({ length: 7 }, () => ({ column_name: "backlog" }));
|
|
const backlog = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "backlog");
|
|
|
|
assert.equal(backlog.stackCount, 3);
|
|
assert.deepEqual(backlog.stackColors, []);
|
|
});
|
|
|
|
test("buildBoardColumnLayout zeigt im Stapel jede Projektfarbe nur einmal und hoechstens NOTE_STACK_LAYERS", () => {
|
|
const tickets = ["#fbca04", "#fbca04", "#28a745", "#0366d6", "#ec4899"].map((color) => ({
|
|
column_name: "done",
|
|
color,
|
|
}));
|
|
const done = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "done");
|
|
|
|
assert.equal(done.stackCount, 5);
|
|
assert.equal(done.stackColors.length, NOTE_STACK_LAYERS);
|
|
assert.deepEqual(done.stackColors, ["#fbca04", "#28a745", "#0366d6"]);
|
|
});
|
|
|
|
test("buildDeskLayout erzeugt genau DEFAULT_DESK_COUNT Schreibtische", () => {
|
|
const desks = buildDeskLayout([]);
|
|
assert.equal(desks.length, DEFAULT_DESK_COUNT);
|
|
assert.ok(desks.every((d) => d.agent === null));
|
|
});
|
|
|
|
test("buildDeskLayout ordnet Agenten der Reihe nach Schreibtischen zu und ignoriert Ueberschuss", () => {
|
|
const agents = Array.from({ length: DEFAULT_DESK_COUNT + 2 }, (_, i) => ({
|
|
name: `Agent${i}`,
|
|
role: "entwickler",
|
|
}));
|
|
const desks = buildDeskLayout(agents);
|
|
assert.equal(desks.length, DEFAULT_DESK_COUNT);
|
|
assert.equal(desks[0].agent.name, "Agent0");
|
|
assert.equal(desks[DEFAULT_DESK_COUNT - 1].agent.name, `Agent${DEFAULT_DESK_COUNT - 1}`);
|
|
});
|
|
|
|
test("buildDeskLayout ergaenzt fehlende pose auf idle", () => {
|
|
const desks = buildDeskLayout([{ name: "Alice", role: "entwickler" }], 1);
|
|
assert.equal(desks[0].agent.pose, "idle");
|
|
});
|
|
|
|
test("buildSystemTiles sortiert Fehler vor ok und kappt bei MAX_SYSTEM_TILES", () => {
|
|
const systems = Array.from({ length: MAX_SYSTEM_TILES + 3 }, (_, i) => ({
|
|
id: `s${i}`,
|
|
status: i === MAX_SYSTEM_TILES ? "error" : "ok",
|
|
}));
|
|
const tiles = buildSystemTiles(systems);
|
|
assert.equal(tiles.length, MAX_SYSTEM_TILES);
|
|
assert.equal(tiles[0].status, "error");
|
|
});
|
|
|
|
test("buildEventLines kappt bei MAX_EVENT_LINES und behaelt die Reihenfolge bei", () => {
|
|
const events = Array.from({ length: MAX_EVENT_LINES + 4 }, (_, i) => ({ id: i }));
|
|
const lines = buildEventLines(events);
|
|
assert.equal(lines.length, MAX_EVENT_LINES);
|
|
assert.equal(lines[0].id, 0);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Phase 3 - Kanban-Board live
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("aggregateTicketsByColumn zaehlt Tickets je Spalte und ignoriert unbekannte Spalten", () => {
|
|
const counts = aggregateTicketsByColumn([
|
|
{ column_name: "backlog" },
|
|
{ column_name: "backlog" },
|
|
{ column_name: "qa" },
|
|
{ column_name: "unbekannt" },
|
|
]);
|
|
assert.equal(counts.backlog, 2);
|
|
assert.equal(counts.qa, 1);
|
|
assert.equal(counts.done, 0);
|
|
assert.equal(Object.keys(counts).length, BOARD_COLUMNS.length);
|
|
});
|
|
|
|
test("groupTicketsByColumn gruppiert und behaelt die Reihenfolge je Spalte bei", () => {
|
|
const groups = groupTicketsByColumn([
|
|
{ id: 1, column_name: "backlog" },
|
|
{ id: 2, column_name: "done" },
|
|
{ id: 3, column_name: "backlog" },
|
|
]);
|
|
assert.deepEqual(
|
|
groups.backlog.map((t) => t.id),
|
|
[1, 3]
|
|
);
|
|
assert.deepEqual(
|
|
groups.done.map((t) => t.id),
|
|
[2]
|
|
);
|
|
assert.deepEqual(groups.qa, []);
|
|
});
|
|
|
|
test("buildTicketModalContent traegt Titel/Spaltenlabel und baut Codeberg-Links nur fuer source=codeberg", () => {
|
|
const content = buildTicketModalContent("bereit", [
|
|
{ id: 1, title: "A", description: "", source: "codeberg", external_ref: "42" },
|
|
{ id: 2, title: "B", description: "", source: "api", external_ref: null },
|
|
]);
|
|
assert.equal(content.title, "Bereit");
|
|
assert.equal(content.items[0].link, "https://codeberg.org/fds2610/monitor/issues/42");
|
|
assert.equal(content.items[1].link, null);
|
|
});
|
|
|
|
test("buildTicketModalContent respektiert benutzerdefinierten owner/repo", () => {
|
|
const content = buildTicketModalContent(
|
|
"backlog",
|
|
[{ id: 1, title: "A", source: "codeberg", external_ref: "7" }],
|
|
{ codebergOwner: "acme", codebergRepo: "widgets" }
|
|
);
|
|
assert.equal(content.items[0].link, "https://codeberg.org/acme/widgets/issues/7");
|
|
});
|
|
|
|
test("buildTicketModalContent baut den Link aus dem repo-Feld des Tickets, nicht aus opts (mehrere Codeberg-Projekte, F26)", () => {
|
|
const content = buildTicketModalContent("backlog", [
|
|
{ id: 1, title: "A", source: "codeberg", external_ref: "3", repo: "fds2610/Steuerkram", color: "#28a745" },
|
|
{ id: 2, title: "B", source: "codeberg", external_ref: "9", repo: "fds2610/nc-gantt", color: "#0366d6" },
|
|
]);
|
|
assert.equal(content.items[0].link, "https://codeberg.org/fds2610/Steuerkram/issues/3");
|
|
assert.equal(content.items[1].link, "https://codeberg.org/fds2610/nc-gantt/issues/9");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Phase 4 - Monitor-Wand live
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("formatEventTime liefert HH:MM und faengt ungueltige Zeitstempel ab", () => {
|
|
assert.match(formatEventTime("2026-07-27T10:05:00Z"), /^\d{2}:\d{2}$/);
|
|
assert.equal(formatEventTime("nicht-valide"), "--:--");
|
|
});
|
|
|
|
test("mapEventForDisplay uebersetzt ein rohes Event in die Wand-Zeilendarstellung", () => {
|
|
const line = mapEventForDisplay({
|
|
occurred_at: "2026-07-27T10:05:00Z",
|
|
name: "Ping",
|
|
message: "Host down",
|
|
severity: "error",
|
|
});
|
|
assert.equal(line.system, "Ping");
|
|
assert.equal(line.message, "Host down");
|
|
assert.equal(line.severity, "error");
|
|
assert.match(line.time, /^\d{2}:\d{2}$/);
|
|
});
|
|
|
|
test("formatEventLineText setzt Zeit, System und Meldung zur Monitorzeile zusammen", () => {
|
|
assert.equal(
|
|
formatEventLineText({ time: "14:32", system: "Datenbank", message: "Verbindung weg" }),
|
|
"14:32 Datenbank: Verbindung weg"
|
|
);
|
|
});
|
|
|
|
test("truncateToWidth laesst passende Zeilen unveraendert", () => {
|
|
const value = "14:32 DB: weg";
|
|
assert.equal(truncateToWidth(value, value.length * EVENT_LINE_CHAR_WIDTH), value);
|
|
});
|
|
|
|
test("truncateToWidth kuerzt zu lange Zeilen auf die verfuegbare Breite inkl. Ellipse", () => {
|
|
const value = "14:32 Datenbank: Verbindung fehlgeschlagen";
|
|
const truncated = truncateToWidth(value, 10 * EVENT_LINE_CHAR_WIDTH);
|
|
assert.equal(truncated.length, 10);
|
|
assert.ok(truncated.endsWith("…"));
|
|
assert.ok(value.startsWith(truncated.slice(0, -1)));
|
|
});
|
|
|
|
test("truncateToWidth kommt mit entarteten Breiten klar", () => {
|
|
assert.equal(truncateToWidth("abc", 0), "");
|
|
assert.equal(truncateToWidth("abc", -5), "");
|
|
assert.equal(truncateToWidth("abc", EVENT_LINE_CHAR_WIDTH), "…");
|
|
});
|
|
|
|
test("buildEventModalContent zeigt die volle Liste (kein MAX_EVENT_LINES-Limit)", () => {
|
|
const events = Array.from({ length: MAX_EVENT_LINES + 3 }, (_, i) => ({
|
|
occurred_at: "2026-07-27T10:00:00Z",
|
|
name: `Event${i}`,
|
|
message: "m",
|
|
severity: "warning",
|
|
system_id: "srv1",
|
|
}));
|
|
const content = buildEventModalContent(events);
|
|
assert.equal(content.items.length, events.length);
|
|
assert.equal(content.items[0].systemId, "srv1");
|
|
});
|
|
|
|
test("buildSystemModalContent uebernimmt Status/Detail je System", () => {
|
|
const content = buildSystemModalContent([
|
|
{ id: "srv1", name: "Webserver", status: "ok", type: "host", detail: null, last_check_at: "t" },
|
|
]);
|
|
assert.equal(content.items[0].status, "ok");
|
|
assert.equal(content.items[0].detail, "");
|
|
assert.equal(content.items[0].lastCheckAt, null);
|
|
});
|
|
|
|
test("buildSystemModalContent formatiert einen gueltigen last_check_at (Issue #57)", () => {
|
|
const content = buildSystemModalContent([
|
|
{ id: "srv1", name: "Webserver", status: "ok", type: "host", detail: "1.2.3.4", last_check_at: "2026-08-20 18:40" },
|
|
]);
|
|
assert.match(content.items[0].lastCheckAt, /^\d{2}\.\d{2}\.\d{2}, \d{2}:\d{2}$/);
|
|
});
|
|
|
|
test("formatSystemCheckTime akzeptiert ISO-8601 (API) und 'YYYY-MM-DD HH:MM' (Legacy-status.txt)", () => {
|
|
assert.match(formatSystemCheckTime("2026-07-27T10:00:00Z"), /^\d{2}\.\d{2}\.\d{2}, \d{2}:\d{2}$/);
|
|
assert.match(formatSystemCheckTime("2026-08-20 18:40"), /^\d{2}\.\d{2}\.\d{2}, \d{2}:\d{2}$/);
|
|
});
|
|
|
|
test("formatSystemCheckTime liefert null fuer fehlende/ungueltige Werte", () => {
|
|
assert.equal(formatSystemCheckTime(null), null);
|
|
assert.equal(formatSystemCheckTime(undefined), null);
|
|
assert.equal(formatSystemCheckTime(""), null);
|
|
assert.equal(formatSystemCheckTime("nicht-valide"), null);
|
|
});
|
|
|
|
test("buildKpiSparklinePoints liefert leeren String ohne Historie und einen Punkt pro Messwert sonst", () => {
|
|
assert.equal(buildKpiSparklinePoints([]), "");
|
|
const points = buildKpiSparklinePoints([{ value: 3 }, { value: 1 }, { value: 2 }], 100, 20);
|
|
assert.equal(points.split(" ").length, 3);
|
|
});
|
|
|
|
test("buildKpiSparklinePoints behandelt konstante Werte ohne Division durch 0", () => {
|
|
const points = buildKpiSparklinePoints([{ value: 5 }, { value: 5 }], 100, 20);
|
|
assert.equal(points.split(" ").length, 2);
|
|
assert.ok(points.length > 0);
|
|
});
|
|
|
|
test("buildKpiTiles kappt bei MAX_KPI_TILES", () => {
|
|
const kpis = Array.from({ length: MAX_KPI_TILES + 2 }, (_, i) => ({
|
|
key: `k${i}`,
|
|
current_value: i,
|
|
history: [],
|
|
}));
|
|
const tiles = buildKpiTiles(kpis);
|
|
assert.equal(tiles.length, MAX_KPI_TILES);
|
|
});
|
|
|
|
test("buildKpiModalContent zeigt alle KPIs (kein Limit) inkl. Historie", () => {
|
|
const content = buildKpiModalContent([
|
|
{ key: "open_tickets", label: "Offene Tickets", unit: "count", current_value: 4, history: [{ value: 4 }] },
|
|
]);
|
|
assert.equal(content.items.length, 1);
|
|
assert.equal(content.items[0].label, "Offene Tickets");
|
|
});
|
|
|
|
test("buildAgentCountKpis liefert Anzahl registrierter und online-Agenten (Issue #39)", () => {
|
|
const kpis = buildAgentCountKpis([
|
|
{ id: 1, is_present: true },
|
|
{ id: 2, is_present: false },
|
|
{ id: 3, is_present: true },
|
|
]);
|
|
assert.equal(kpis.length, 3);
|
|
assert.deepEqual(kpis[0], { key: "agents_total", label: "Agenten", unit: "", current_value: 3, history: [] });
|
|
assert.deepEqual(kpis[1], { key: "agents_online", label: "Online", unit: "", current_value: 2, history: [] });
|
|
assert.deepEqual(kpis[2], {
|
|
key: "agents_problem",
|
|
label: "Problem",
|
|
unit: "",
|
|
current_value: 0,
|
|
history: [],
|
|
severity: null,
|
|
});
|
|
});
|
|
|
|
test("buildAgentCountKpis ignoriert leere Schreibtisch-Plaetze (null) und faellt ohne is_present auf anwesend zurueck", () => {
|
|
const kpis = buildAgentCountKpis([{ id: 1 }, null, { id: 2 }, null]);
|
|
assert.equal(kpis[0].current_value, 2);
|
|
assert.equal(kpis[1].current_value, 2);
|
|
});
|
|
|
|
test("buildAgentCountKpis liefert 0/0 ohne Agenten", () => {
|
|
const kpis = buildAgentCountKpis([]);
|
|
assert.equal(kpis[0].current_value, 0);
|
|
assert.equal(kpis[1].current_value, 0);
|
|
});
|
|
|
|
test("buildHooksKpi liefert einen KPI-foermigen Eintrag mit der uebergebenen Anzahl (Issue #38)", () => {
|
|
assert.deepEqual(buildHooksKpi(12), {
|
|
key: "hooks_received",
|
|
label: "Hooks",
|
|
unit: "",
|
|
current_value: 12,
|
|
history: [],
|
|
});
|
|
assert.equal(buildHooksKpi(0).current_value, 0);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Phase 5 - Agenten-Zustandsmaschine & Animation
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("resolveAgentVisualState bildet bekannte Zustaende auf Ort/Pose ab", () => {
|
|
assert.deepEqual(resolveAgentVisualState("working"), { location: AGENT_LOCATIONS.DESK, pose: "working" });
|
|
assert.deepEqual(resolveAgentVisualState("walking_to_board"), {
|
|
location: AGENT_LOCATIONS.BOARD,
|
|
pose: "walking",
|
|
});
|
|
assert.deepEqual(resolveAgentVisualState("at_coffee_corner"), {
|
|
location: AGENT_LOCATIONS.COFFEE_CORNER,
|
|
pose: "coffee",
|
|
});
|
|
});
|
|
|
|
test("resolveAgentVisualState faellt bei unbekanntem/fehlendem Zustand auf idle-am-Schreibtisch zurueck", () => {
|
|
assert.deepEqual(resolveAgentVisualState("was-auch-immer"), { location: AGENT_LOCATIONS.DESK, pose: "idle" });
|
|
assert.deepEqual(resolveAgentVisualState(undefined), { location: AGENT_LOCATIONS.DESK, pose: "idle" });
|
|
});
|
|
|
|
test("mapAgentsToDesks platziert Agenten anhand von desk_index, nicht Array-Reihenfolge", () => {
|
|
const desks = mapAgentsToDesks(
|
|
[
|
|
{ id: 1, desk_index: 2 },
|
|
{ id: 2, desk_index: 0 },
|
|
],
|
|
4
|
|
);
|
|
assert.equal(desks[0].id, 2);
|
|
assert.equal(desks[2].id, 1);
|
|
assert.equal(desks[1], null);
|
|
assert.equal(desks[3], null);
|
|
});
|
|
|
|
test("mapAgentsToDesks ignoriert Agenten ohne Schreibtisch (desk_index null, Rolle user)", () => {
|
|
const desks = mapAgentsToDesks([{ id: 1, desk_index: null, role: "user" }], 2);
|
|
assert.ok(desks.every((d) => d === null));
|
|
});
|
|
|
|
test("isAgentPresent liest die Praesenz direkt aus dem is_present-Feld der API (Issue #20)", () => {
|
|
assert.equal(isAgentPresent({ is_present: true }), true);
|
|
assert.equal(isAgentPresent({ is_present: false }), false);
|
|
});
|
|
|
|
test("isAgentPresent faellt bei fehlendem is_present-Feld defensiv auf anwesend zurueck", () => {
|
|
assert.equal(isAgentPresent({ id: 1 }), true);
|
|
assert.equal(isAgentPresent(), true);
|
|
});
|
|
|
|
test("mapAgentsToDesks laesst den Schreibtisch eines Agenten frei, der laut is_present nicht mehr anwesend ist", () => {
|
|
const desks = mapAgentsToDesks([{ id: 1, desk_index: 0, is_present: false }], 2);
|
|
assert.equal(desks[0], null);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Problemzustaende: abgelaufene Claude-Anmeldung (auth_expiring/auth_expired)
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("agentProblemSeverity ordnet den beiden Auth-Zustaenden einen Schweregrad zu", () => {
|
|
assert.equal(agentProblemSeverity({ state: "auth_expiring" }), "warning");
|
|
assert.equal(agentProblemSeverity({ state: "auth_expired" }), "error");
|
|
});
|
|
|
|
test("agentProblemSeverity meldet fuer Normalbetrieb und unbekannte Zustaende kein Problem", () => {
|
|
assert.equal(agentProblemSeverity({ state: "working" }), null);
|
|
assert.equal(agentProblemSeverity({ state: "was-auch-immer" }), null);
|
|
assert.equal(agentProblemSeverity({ id: 1 }), null);
|
|
assert.equal(agentProblemSeverity(null), null);
|
|
});
|
|
|
|
test("resolveAgentVisualState laesst blockierte Agenten am Schreibtisch, mit eigener Pose", () => {
|
|
assert.deepEqual(resolveAgentVisualState("auth_expired"), {
|
|
location: AGENT_LOCATIONS.DESK,
|
|
pose: "auth_expired",
|
|
});
|
|
assert.deepEqual(resolveAgentVisualState("auth_expiring"), {
|
|
location: AGENT_LOCATIONS.DESK,
|
|
pose: "auth_expiring",
|
|
});
|
|
});
|
|
|
|
test("isAgentVisible haelt einen blockierten Agenten sichtbar, obwohl er nicht mehr praesent ist", () => {
|
|
const blocked = { id: 1, is_present: false, state: "auth_expired" };
|
|
assert.equal(isAgentPresent(blocked), false);
|
|
assert.equal(isAgentVisible(blocked), true);
|
|
});
|
|
|
|
test("isAgentVisible blendet einen abwesenden Agenten ohne Problemzustand weiterhin aus", () => {
|
|
assert.equal(isAgentVisible({ id: 1, is_present: false, state: "idle" }), false);
|
|
assert.equal(isAgentVisible({ id: 1, is_present: true, state: "working" }), true);
|
|
});
|
|
|
|
test("mapAgentsToDesks behaelt einen blockierten Agenten auf seinem Platz, auch ohne frische Meldung", () => {
|
|
const desks = mapAgentsToDesks(
|
|
[
|
|
{ id: 1, desk_index: 0, is_present: false, state: "auth_expired" },
|
|
{ id: 2, desk_index: 1, is_present: false, state: "idle" },
|
|
],
|
|
2
|
|
);
|
|
assert.equal(desks[0].id, 1);
|
|
assert.equal(desks[1], null);
|
|
});
|
|
|
|
test("countAgentProblems zaehlt beide Schweregrade und ignoriert leere Plaetze", () => {
|
|
assert.equal(
|
|
countAgentProblems([
|
|
{ id: 1, state: "auth_expired" },
|
|
{ id: 2, state: "working" },
|
|
null,
|
|
{ id: 3, state: "auth_expiring" },
|
|
]),
|
|
2
|
|
);
|
|
assert.equal(countAgentProblems([]), 0);
|
|
});
|
|
|
|
test("agentsWithProblems liefert die betroffenen Agenten fuer den Banner", () => {
|
|
const problems = agentsWithProblems([
|
|
{ id: 1, display_name: "Alice", state: "auth_expired" },
|
|
{ id: 2, display_name: "Bob", state: "working" },
|
|
]);
|
|
assert.equal(problems.length, 1);
|
|
assert.equal(problems[0].display_name, "Alice");
|
|
});
|
|
|
|
test("buildAgentCountKpis zaehlt einen blockierten Agenten als Problem, nicht als online", () => {
|
|
const kpis = buildAgentCountKpis([
|
|
{ id: 1, is_present: true, state: "working" },
|
|
{ id: 2, is_present: false, state: "auth_expired" },
|
|
]);
|
|
assert.equal(kpis[0].current_value, 2, "registriert");
|
|
assert.equal(kpis[1].current_value, 1, "online");
|
|
assert.equal(kpis[2].current_value, 1, "Problem");
|
|
assert.equal(kpis[2].severity, "error");
|
|
});
|
|
|
|
test("buildKpiTiles reicht den Schweregrad durch und laesst gemeldete KPIs neutral", () => {
|
|
const tiles = buildKpiTiles([
|
|
{ key: "agents_problem", label: "Problem", current_value: 2, history: [], severity: "error" },
|
|
{ key: "open_tickets", label: "Offene Tickets", current_value: 4, history: [] },
|
|
]);
|
|
assert.equal(tiles[0].severity, "error");
|
|
assert.equal(tiles[1].severity, null);
|
|
});
|
|
|
|
test("normalizeAgentForDisplay leitet name/pose/location aus display_name/state ab", () => {
|
|
const normalized = normalizeAgentForDisplay({ id: 1, display_name: "Alice", state: "working" });
|
|
assert.equal(normalized.name, "Alice");
|
|
assert.equal(normalized.pose, "working");
|
|
assert.equal(normalized.location, AGENT_LOCATIONS.DESK);
|
|
});
|
|
|
|
test("pickIdleActivity nutzt den injizierten Zufallsgenerator deterministisch", () => {
|
|
assert.equal(pickIdleActivity(() => 0), "reading_newspaper");
|
|
assert.equal(pickIdleActivity(() => 0.999), "talking_to_colleague");
|
|
});
|
|
|
|
test("AnimationQueue fuehrt Tasks mit Mindestabstand aus (Warteschlange bei Buerst-Events, F9)", () => {
|
|
let now = 0;
|
|
const queue = new AnimationQueue({ minIntervalMs: 100, now: () => now });
|
|
const ran = [];
|
|
queue.enqueue(() => ran.push("a"));
|
|
queue.enqueue(() => ran.push("b"));
|
|
queue.enqueue(() => ran.push("c"));
|
|
|
|
queue.processDue();
|
|
assert.deepEqual(ran, ["a"]);
|
|
assert.equal(queue.size, 2);
|
|
|
|
now = 50;
|
|
queue.processDue();
|
|
assert.deepEqual(ran, ["a"]);
|
|
|
|
now = 120;
|
|
queue.processDue();
|
|
assert.deepEqual(ran, ["a", "b"]);
|
|
|
|
now = 500;
|
|
queue.processDue();
|
|
assert.deepEqual(ran, ["a", "b", "c"]);
|
|
assert.equal(queue.size, 0);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Phase 6 - Externe Rollen & Tueren (F2)
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("detectDoorVisits erkennt ein neues, per Codeberg-Sync angelegtes Backlog-Ticket", () => {
|
|
const visits = detectDoorVisits([], [{ id: 1, column_name: "backlog", source: "codeberg" }]);
|
|
assert.deepEqual(visits, [{ type: DOOR_VISIT_TYPES.NEW_BACKLOG_TICKET, ticketId: 1 }]);
|
|
});
|
|
|
|
test("detectDoorVisits ignoriert neue Backlog-Tickets, die nicht per Codeberg-Sync kamen", () => {
|
|
const visits = detectDoorVisits([], [{ id: 1, column_name: "backlog", source: "api" }]);
|
|
assert.deepEqual(visits, []);
|
|
});
|
|
|
|
test("detectDoorVisits ignoriert neue Tickets ausserhalb von Backlog (von einer Rolle angelegt)", () => {
|
|
const visits = detectDoorVisits([], [{ id: 1, column_name: "bereit", source: "api" }]);
|
|
assert.deepEqual(visits, []);
|
|
});
|
|
|
|
test("detectDoorVisits erkennt ein Ticket, das die Spalte fertig erreicht", () => {
|
|
const before = [{ id: 5, column_name: "qa", source: "api" }];
|
|
const after = [{ id: 5, column_name: "fertig", source: "api" }];
|
|
assert.deepEqual(detectDoorVisits(before, after), [
|
|
{ type: DOOR_VISIT_TYPES.TICKET_READY_FOR_REVIEW, ticketId: 5 },
|
|
]);
|
|
});
|
|
|
|
test("detectDoorVisits erkennt ein Ticket, das geschlossen (done) wird", () => {
|
|
const before = [{ id: 7, column_name: "testing", source: "api" }];
|
|
const after = [{ id: 7, column_name: "done", source: "api" }];
|
|
assert.deepEqual(detectDoorVisits(before, after), [{ type: DOOR_VISIT_TYPES.TICKET_DONE, ticketId: 7 }]);
|
|
});
|
|
|
|
test("detectDoorVisits ignoriert Tickets ohne Spaltenwechsel", () => {
|
|
const before = [{ id: 1, column_name: "in_arbeit", source: "api" }];
|
|
const after = [{ id: 1, column_name: "in_arbeit", source: "api" }];
|
|
assert.deepEqual(detectDoorVisits(before, after), []);
|
|
});
|
|
|
|
test("detectDoorVisits verarbeitet mehrere gleichzeitige Anlaesse in einem Diff", () => {
|
|
const before = [
|
|
{ id: 1, column_name: "qa", source: "api" },
|
|
{ id: 2, column_name: "testing", source: "api" },
|
|
];
|
|
const after = [
|
|
{ id: 1, column_name: "fertig", source: "api" },
|
|
{ id: 2, column_name: "done", source: "api" },
|
|
{ id: 3, column_name: "backlog", source: "codeberg" },
|
|
];
|
|
const visits = detectDoorVisits(before, after);
|
|
assert.equal(visits.length, 3);
|
|
assert.deepEqual(visits, [
|
|
{ type: DOOR_VISIT_TYPES.TICKET_READY_FOR_REVIEW, ticketId: 1 },
|
|
{ type: DOOR_VISIT_TYPES.TICKET_DONE, ticketId: 2 },
|
|
{ type: DOOR_VISIT_TYPES.NEW_BACKLOG_TICKET, ticketId: 3 },
|
|
]);
|
|
});
|
|
|
|
test("resolveDoorVisitVisual bildet jede Stage auf Ort/Pose ab", () => {
|
|
assert.deepEqual(resolveDoorVisitVisual("walking_to_board"), {
|
|
location: AGENT_LOCATIONS.BOARD,
|
|
pose: "walking",
|
|
});
|
|
assert.deepEqual(resolveDoorVisitVisual("interacting_with_board"), {
|
|
location: AGENT_LOCATIONS.BOARD,
|
|
pose: "interacting",
|
|
});
|
|
assert.deepEqual(resolveDoorVisitVisual("leaving"), { location: AGENT_LOCATIONS.DOOR, pose: "walking" });
|
|
});
|
|
|
|
test("resolveDoorVisitVisual faellt bei unbekannter Stage auf die erste Stage zurueck", () => {
|
|
assert.deepEqual(resolveDoorVisitVisual("unbekannt"), resolveDoorVisitVisual(DOOR_VISIT_STAGES[0]));
|
|
});
|
|
|
|
test("advanceDoorVisitStage durchlaeuft alle Stages und liefert am Ende null", () => {
|
|
let stage = DOOR_VISIT_STAGES[0];
|
|
const seen = [stage];
|
|
for (let i = 0; i < DOOR_VISIT_STAGES.length; i++) {
|
|
stage = advanceDoorVisitStage(stage);
|
|
if (stage !== null) seen.push(stage);
|
|
}
|
|
assert.deepEqual(seen, DOOR_VISIT_STAGES);
|
|
assert.equal(advanceDoorVisitStage(DOOR_VISIT_STAGES[DOOR_VISIT_STAGES.length - 1]), null);
|
|
});
|
|
|
|
test("advanceDoorVisitStage liefert bei unbekannter Stage null", () => {
|
|
assert.equal(advanceDoorVisitStage("unbekannt"), null);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// Product Owner (Issue #19)
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
test("PO_AGENT_ID/PO_DISPLAY_NAME sind stabile Konstanten (der PO heisst schlicht 'PO')", () => {
|
|
assert.equal(PO_AGENT_ID, "po");
|
|
assert.equal(PO_DISPLAY_NAME, "PO");
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges erkennt einen Statuswechsel nach 'bereit' (Label automate)", () => {
|
|
const before = [{ id: 19, column_name: "backlog" }];
|
|
const after = [{ id: 19, column_name: "bereit" }];
|
|
assert.deepEqual(detectUnassignedTicketChanges(before, after), [{ ticketId: 19, column: "bereit" }]);
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges ignoriert Wechsel nach fertig/done (schon Sache der Rolle user)", () => {
|
|
const before = [
|
|
{ id: 1, column_name: "qa" },
|
|
{ id: 2, column_name: "testing" },
|
|
];
|
|
const after = [
|
|
{ id: 1, column_name: "fertig" },
|
|
{ id: 2, column_name: "done" },
|
|
];
|
|
assert.deepEqual(detectUnassignedTicketChanges(before, after), []);
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges ignoriert neu angelegte Tickets (schon Sache von detectDoorVisits)", () => {
|
|
const visits = detectUnassignedTicketChanges([], [{ id: 1, column_name: "bereit" }]);
|
|
assert.deepEqual(visits, []);
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges ignoriert Tickets ohne Spaltenwechsel", () => {
|
|
const before = [{ id: 1, column_name: "in_arbeit" }];
|
|
const after = [{ id: 1, column_name: "in_arbeit" }];
|
|
assert.deepEqual(detectUnassignedTicketChanges(before, after), []);
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges ignoriert Wechsel von/zu in_arbeit (Label wip - Sache des Agenten, Issue #32)", () => {
|
|
const before = [
|
|
{ id: 1, column_name: "bereit" },
|
|
{ id: 2, column_name: "in_arbeit" },
|
|
{ id: 3, column_name: "in_arbeit" },
|
|
];
|
|
const after = [
|
|
{ id: 1, column_name: "in_arbeit" },
|
|
{ id: 2, column_name: "testing" },
|
|
{ id: 3, column_name: "backlog" },
|
|
];
|
|
assert.deepEqual(detectUnassignedTicketChanges(before, after), []);
|
|
});
|
|
|
|
test("detectUnassignedTicketChanges verarbeitet mehrere gleichzeitige Wechsel", () => {
|
|
const before = [
|
|
{ id: 1, column_name: "backlog" },
|
|
{ id: 2, column_name: "backlog" },
|
|
{ id: 3, column_name: "qa" },
|
|
];
|
|
const after = [
|
|
{ id: 1, column_name: "bereit" },
|
|
{ id: 2, column_name: "in_arbeit" },
|
|
{ id: 3, column_name: "fertig" },
|
|
];
|
|
assert.deepEqual(detectUnassignedTicketChanges(before, after), [{ ticketId: 1, column: "bereit" }]);
|
|
});
|
|
|
|
test("detectAgentOwnedTicketChanges erkennt Wechsel nach in_arbeit inkl. zustaendigem Agenten (Issue #61)", () => {
|
|
const before = [{ id: 1, column_name: "bereit", assignee_agent_id: null }];
|
|
const after = [{ id: 1, column_name: "in_arbeit", assignee_agent_id: 7 }];
|
|
assert.deepEqual(detectAgentOwnedTicketChanges(before, after), [{ ticketId: 1, agentId: 7 }]);
|
|
});
|
|
|
|
test("detectAgentOwnedTicketChanges erkennt Wechsel weg von in_arbeit und nutzt den vorherigen Agenten, falls das Ticket schon unassigned ist (Issue #61)", () => {
|
|
const before = [{ id: 1, column_name: "in_arbeit", assignee_agent_id: 7 }];
|
|
const after = [{ id: 1, column_name: "testing", assignee_agent_id: null }];
|
|
assert.deepEqual(detectAgentOwnedTicketChanges(before, after), [{ ticketId: 1, agentId: 7 }]);
|
|
});
|
|
|
|
test("detectAgentOwnedTicketChanges ignoriert Wechsel ohne in_arbeit-Beteiligung (Issue #61)", () => {
|
|
const before = [{ id: 1, column_name: "qa", assignee_agent_id: 7 }];
|
|
const after = [{ id: 1, column_name: "fertig", assignee_agent_id: 7 }];
|
|
assert.deepEqual(detectAgentOwnedTicketChanges(before, after), []);
|
|
});
|
|
|
|
test("detectAgentOwnedTicketChanges ignoriert Tickets ohne Spaltenwechsel und neu angelegte Tickets (Issue #61)", () => {
|
|
assert.deepEqual(
|
|
detectAgentOwnedTicketChanges(
|
|
[{ id: 1, column_name: "in_arbeit" }],
|
|
[{ id: 1, column_name: "in_arbeit" }],
|
|
),
|
|
[],
|
|
);
|
|
assert.deepEqual(detectAgentOwnedTicketChanges([], [{ id: 1, column_name: "in_arbeit" }]), []);
|
|
});
|
|
|
|
test("hasAgentReachedBoard ist waehrend walking_to_board false, sonst true (Issue #61)", () => {
|
|
assert.equal(hasAgentReachedBoard("walking_to_board"), false);
|
|
assert.equal(hasAgentReachedBoard("interacting_with_board"), true);
|
|
assert.equal(hasAgentReachedBoard("walking_back"), true);
|
|
assert.equal(hasAgentReachedBoard("idle"), true);
|
|
assert.equal(hasAgentReachedBoard(undefined), true);
|
|
});
|
|
|
|
test("advancePoVisitStage durchlaeuft alle Stages und liefert am Ende null", () => {
|
|
let stage = PO_VISIT_STAGES[0];
|
|
const seen = [stage];
|
|
for (let i = 0; i < PO_VISIT_STAGES.length; i++) {
|
|
stage = advancePoVisitStage(stage);
|
|
if (stage !== null) seen.push(stage);
|
|
}
|
|
assert.deepEqual(seen, PO_VISIT_STAGES);
|
|
assert.equal(advancePoVisitStage(PO_VISIT_STAGES[PO_VISIT_STAGES.length - 1]), null);
|
|
});
|
|
|
|
test("advancePoVisitStage liefert bei unbekannter Stage null", () => {
|
|
assert.equal(advancePoVisitStage("unbekannt"), null);
|
|
});
|
|
|
|
test("applyPendingTicketMoves gibt die Liste unveraendert zurueck ohne laufende Besuche (Issue #55)", () => {
|
|
const tickets = [{ id: 1, column_name: "bereit" }];
|
|
assert.equal(applyPendingTicketMoves(tickets, new Map()), tickets);
|
|
assert.equal(applyPendingTicketMoves(tickets, null), tickets);
|
|
});
|
|
|
|
test("applyPendingTicketMoves zeigt ein Ticket mit laufendem Besuch weiter am alten Platz (Issue #55)", () => {
|
|
const oldTicket = { id: 1, column_name: "bereit" };
|
|
const latest = [{ id: 1, column_name: "fertig" }, { id: 2, column_name: "qa" }];
|
|
const pending = new Map([[1, oldTicket]]);
|
|
assert.deepEqual(applyPendingTicketMoves(latest, pending), [oldTicket, { id: 2, column_name: "qa" }]);
|
|
});
|
|
|
|
test("applyPendingTicketMoves blendet ein brandneues Ticket aus, bis die Persona am Board ist (Issue #55)", () => {
|
|
const latest = [{ id: 1, column_name: "backlog" }];
|
|
const pending = new Map([[1, null]]);
|
|
assert.deepEqual(applyPendingTicketMoves(latest, pending), []);
|
|
});
|
|
|
|
test("applyPendingTicketMoves laesst Tickets ohne laufenden Besuch unangetastet, auch wenn andere pending sind (Issue #55)", () => {
|
|
const latest = [{ id: 1, column_name: "fertig" }, { id: 2, column_name: "qa" }];
|
|
const pending = new Map([[1, { id: 1, column_name: "bereit" }]]);
|
|
const result = applyPendingTicketMoves(latest, pending);
|
|
assert.deepEqual(result[1], { id: 2, column_name: "qa" });
|
|
});
|
|
|
|
test("resolveReloadIntervalSeconds uebernimmt gueltige positive Zahlen (auch als String)", () => {
|
|
assert.equal(resolveReloadIntervalSeconds(30), 30);
|
|
assert.equal(resolveReloadIntervalSeconds("45"), 45);
|
|
assert.equal(resolveReloadIntervalSeconds(12.4), 12);
|
|
});
|
|
|
|
test("resolveReloadIntervalSeconds faellt bei fehlendem/ungueltigem Wert auf den Default zurueck", () => {
|
|
assert.equal(resolveReloadIntervalSeconds(undefined), DEFAULT_RELOAD_INTERVAL_SECONDS);
|
|
assert.equal(resolveReloadIntervalSeconds(null), DEFAULT_RELOAD_INTERVAL_SECONDS);
|
|
assert.equal(resolveReloadIntervalSeconds(0), DEFAULT_RELOAD_INTERVAL_SECONDS);
|
|
assert.equal(resolveReloadIntervalSeconds(-5), DEFAULT_RELOAD_INTERVAL_SECONDS);
|
|
assert.equal(resolveReloadIntervalSeconds("nicht-numerisch"), DEFAULT_RELOAD_INTERVAL_SECONDS);
|
|
});
|
|
|
|
test("nextCountdownSeconds zaehlt herunter und springt bei 1 auf das volle Intervall zurueck", () => {
|
|
assert.equal(nextCountdownSeconds(30, 30), 29);
|
|
assert.equal(nextCountdownSeconds(2, 30), 1);
|
|
assert.equal(nextCountdownSeconds(1, 30), 30);
|
|
});
|
|
|
|
test("systemsWithoutHostConfig laesst nur Kacheln ohne konfigurierten Ping-Host uebrig", () => {
|
|
const systems = [
|
|
{ id: "nextcloud-lokal", name: "Nextcloud(lokal)" },
|
|
{ id: "Webserver", name: "Webserver" },
|
|
{ id: "fremd", name: "Selbstmelder" },
|
|
];
|
|
// system_ids kommen so von GET /api/v1/hosts: derselbe Host kann unter beiden Ids melden.
|
|
const hosts = [
|
|
{ hostname: "Nextcloud(lokal)", system_ids: ["Nextcloud(lokal)", "nextcloud-lokal"] },
|
|
{ hostname: "Webserver", system_ids: ["Webserver"] },
|
|
];
|
|
|
|
assert.deepEqual(
|
|
systemsWithoutHostConfig(systems, hosts).map((system) => system.id),
|
|
["fremd"]
|
|
);
|
|
});
|
|
|
|
test("systemsWithoutHostConfig kommt ohne Hosts und ohne system_ids zurecht", () => {
|
|
assert.deepEqual(systemsWithoutHostConfig([{ id: "a" }], []), [{ id: "a" }]);
|
|
assert.deepEqual(systemsWithoutHostConfig([{ id: "a" }], [{ hostname: "a" }]), [{ id: "a" }]);
|
|
assert.deepEqual(systemsWithoutHostConfig(), []);
|
|
});
|