Klick auf die rote Fahne am PO oeffnet ein Modal im Stil der Board-Spalten-Modals mit allen offenen Tickets externer User (Repo, Nummer mit Link, Titel, Typ, Beschreibung) plus der jeweiligen Spalte, spaltenuebergreifend nach created_at sortiert, neuestes zuerst. Die Fahne liegt dafuer ueber der Trefferflaeche des PO-Platzes und stoppt die Klick-Weitergabe, damit nicht zusaetzlich die Projektverwaltung aufgeht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1040 lines
42 KiB
JavaScript
1040 lines
42 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,
|
|
filterEventsForToday,
|
|
buildSystemModalContent,
|
|
systemsWithoutHostConfig,
|
|
formatSystemCheckTime,
|
|
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,
|
|
isAgentReportingBoardVisit,
|
|
DEFAULT_RELOAD_INTERVAL_SECONDS,
|
|
resolveReloadIntervalSeconds,
|
|
nextCountdownSeconds,
|
|
countExternalOpenTickets,
|
|
filterExternalOpenTickets,
|
|
buildExternalTicketModalContent,
|
|
INTERNAL_TICKET_AUTHORS,
|
|
} 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 liefert Ticket-Nummern fuer Zettel, nicht fuer den Stapel (Issue #91)", () => {
|
|
const tickets = Array.from({ length: 7 }, (_, i) => ({
|
|
column_name: "bereit",
|
|
source: "codeberg",
|
|
external_ref: String(i + 1),
|
|
}));
|
|
tickets[1] = { column_name: "bereit", source: "api" };
|
|
const bereit = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "bereit");
|
|
|
|
assert.deepEqual(bereit.noteNumbers, ["#1", null, "#3", "#4"]);
|
|
assert.equal(bereit.stackCount, 3);
|
|
});
|
|
|
|
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");
|
|
// Issue #102: Modal zeigt neuestes Ticket zuerst, also id 2 (zuletzt in der Eingabe) vor id 1.
|
|
assert.equal(content.items[0].link, null);
|
|
assert.equal(content.items[1].link, "https://codeberg.org/fds2610/monitor/issues/42");
|
|
});
|
|
|
|
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" },
|
|
]);
|
|
// Issue #102: Modal zeigt neuestes Ticket zuerst, also id 2 (zuletzt in der Eingabe) vor id 1.
|
|
assert.equal(content.items[0].link, "https://codeberg.org/fds2610/nc-gantt/issues/9");
|
|
assert.equal(content.items[1].link, "https://codeberg.org/fds2610/Steuerkram/issues/3");
|
|
});
|
|
|
|
test("buildTicketModalContent zeigt das neueste Ticket zuerst, absteigend nach Alter (Issue #102)", () => {
|
|
const content = buildTicketModalContent("backlog", [
|
|
{ id: 1, title: "aeltestes" },
|
|
{ id: 2, title: "mittel" },
|
|
{ id: 3, title: "neuestes" },
|
|
]);
|
|
assert.deepEqual(
|
|
content.items.map((item) => item.id),
|
|
[3, 2, 1]
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// 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("filterEventsForToday behaelt nur Events vom aktuellen Kalendertag (lokale Zeitzone, F16)", () => {
|
|
const now = new Date("2026-07-27T10:00:00");
|
|
const events = [
|
|
{ id: "heute-frueh", occurred_at: "2026-07-27T00:05:00" },
|
|
{ id: "heute-spaet", occurred_at: "2026-07-27T23:55:00" },
|
|
{ id: "gestern", occurred_at: "2026-07-26T23:59:00" },
|
|
{ id: "morgen", occurred_at: "2026-07-28T00:00:00" },
|
|
{ id: "ungueltig", occurred_at: "nicht-valide" },
|
|
];
|
|
const filtered = filterEventsForToday(events, now);
|
|
assert.deepEqual(
|
|
filtered.map((e) => e.id),
|
|
["heute-frueh", "heute-spaet"]
|
|
);
|
|
});
|
|
|
|
test("filterEventsForToday liefert eine leere Liste, wenn kein Event von heute ist", () => {
|
|
const now = new Date("2026-07-27T10:00:00");
|
|
const events = [{ id: "gestern", occurred_at: "2026-07-26T10:00:00" }];
|
|
assert.deepEqual(filterEventsForToday(events, now), []);
|
|
});
|
|
|
|
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("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 heute",
|
|
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("transcribing"), { location: AGENT_LOCATIONS.DESK, pose: "working" });
|
|
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("isAgentReportingBoardVisit erkennt eigene Meldung des Agenten (Issue #76)", () => {
|
|
assert.equal(isAgentReportingBoardVisit("walking_to_board"), true);
|
|
assert.equal(isAgentReportingBoardVisit("interacting_with_board"), true);
|
|
assert.equal(isAgentReportingBoardVisit("walking_back"), false);
|
|
assert.equal(isAgentReportingBoardVisit("idle"), false);
|
|
assert.equal(isAgentReportingBoardVisit("working"), false);
|
|
assert.equal(isAgentReportingBoardVisit(undefined), false);
|
|
});
|
|
|
|
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(), []);
|
|
});
|
|
|
|
test("buildTicketModalContent liefert Repo und Ticket-Nummer fuer die Ueberschrift (Issue #88)", () => {
|
|
const content = buildTicketModalContent("backlog", [
|
|
{ id: 1, title: "A", source: "codeberg", external_ref: "3", repo: "fds2610/Steuerkram" },
|
|
{ id: 2, title: "B", source: "api", external_ref: null },
|
|
]);
|
|
// Issue #102: Modal zeigt neuestes Ticket zuerst, also id 2 (zuletzt in der Eingabe) vor id 1.
|
|
assert.equal(content.items[0].repo, null);
|
|
assert.equal(content.items[0].number, null);
|
|
assert.equal(content.items[1].repo, "fds2610/Steuerkram");
|
|
assert.equal(content.items[1].number, "3");
|
|
});
|
|
|
|
test("buildTicketModalContent kuerzt Beschreibungen auf 10 Zeilen mit '...' (Issue #88)", () => {
|
|
const lines = Array.from({ length: 12 }, (_, i) => `Zeile ${i + 1}`);
|
|
// Issue #102: Modal zeigt neuestes Ticket zuerst, also kommt "exact" (id 2) vor "long" (id 1).
|
|
const [exact, long] = buildTicketModalContent("backlog", [
|
|
{ id: 1, title: "A", description: lines.join("\n") },
|
|
{ id: 2, title: "B", description: lines.slice(0, 10).join("\n") },
|
|
]).items;
|
|
assert.equal(long.description, `${lines.slice(0, 10).join("\n")}\n...`);
|
|
assert.equal(exact.description, lines.slice(0, 10).join("\n"));
|
|
});
|
|
|
|
test("Pull-Request-Tickets bekommen 'PR #n' als Nummer, Streifen-Markierung und einen /pulls/-Link", () => {
|
|
const tickets = [
|
|
{ id: 1, column_name: "fertig", source: "codeberg", repo: "fds2610/monitor", external_ref: "7", title: "PR", is_pull_request: 1 },
|
|
{ id: 2, column_name: "fertig", source: "codeberg", repo: "fds2610/monitor", external_ref: "8", title: "Issue", is_pull_request: 0 },
|
|
];
|
|
const fertig = buildBoardColumnLayout(
|
|
aggregateTicketsByColumn(tickets),
|
|
groupTicketsByColumn(tickets)
|
|
).find((c) => c.key === "fertig");
|
|
|
|
assert.deepEqual(fertig.noteNumbers, ["PR #7", "#8"]);
|
|
assert.deepEqual(fertig.notePullRequests, [true, false]);
|
|
|
|
const items = buildTicketModalContent("fertig", tickets).items;
|
|
const byNumber = Object.fromEntries(items.map((i) => [i.number, i]));
|
|
assert.equal(byNumber["7"].link, "https://codeberg.org/fds2610/monitor/pulls/7");
|
|
assert.equal(byNumber["7"].isPullRequest, true);
|
|
assert.equal(byNumber["8"].link, "https://codeberg.org/fds2610/monitor/issues/8");
|
|
});
|
|
|
|
test("countExternalOpenTickets zaehlt nur offene Tickets externer User (Issue #113)", () => {
|
|
const tickets = [
|
|
{ column_name: "backlog", author: "jemand" },
|
|
{ column_name: "fertig", author: "extern-pr", is_pull_request: 1 },
|
|
{ column_name: "in_arbeit", author: "fds2610" },
|
|
{ column_name: "backlog", author: "FDS2610-Bot" },
|
|
{ column_name: "done", author: "jemand" },
|
|
{ column_name: "backlog", author: null },
|
|
{ column_name: "backlog" },
|
|
];
|
|
assert.equal(countExternalOpenTickets(tickets), 2);
|
|
assert.equal(countExternalOpenTickets([]), 0);
|
|
assert.equal(countExternalOpenTickets(), 0);
|
|
assert.deepEqual(INTERNAL_TICKET_AUTHORS, ["fds2610", "fds2610-bot"]);
|
|
});
|
|
|
|
test("buildExternalTicketModalContent listet offene Tickets externer User wie ein Spalten-Modal (Issue #117)", () => {
|
|
const tickets = [
|
|
{ id: 1, column_name: "backlog", author: "jemand", title: "Alt", source: "codeberg", external_ref: "3", repo: "fds2610/monitor", created_at: "2026-10-01T10:00:00+02:00", type: "bug", color: "#ff0000" },
|
|
{ id: 2, column_name: "fertig", author: "extern-pr", title: "PR", source: "codeberg", external_ref: "9", repo: "fds2610/andere", is_pull_request: 1, created_at: "2026-10-05T10:00:00+02:00" },
|
|
{ id: 3, column_name: "in_arbeit", author: "fds2610", title: "Intern", created_at: "2026-10-06T10:00:00+02:00" },
|
|
{ id: 4, column_name: "done", author: "jemand", title: "Erledigt", created_at: "2026-10-07T10:00:00+02:00" },
|
|
{ id: 5, column_name: "qa", author: "jemand", title: "Neu", source: "api", created_at: "2026-10-08T10:00:00+02:00" },
|
|
];
|
|
|
|
assert.deepEqual(filterExternalOpenTickets(tickets).map((t) => t.id), [1, 2, 5]);
|
|
|
|
const content = buildExternalTicketModalContent(tickets);
|
|
assert.equal(content.title, "Externe Tickets");
|
|
assert.equal(typeof content.emptyText, "string");
|
|
assert.deepEqual(content.items.map((item) => item.id), [5, 2, 1]);
|
|
assert.deepEqual(content.items.map((item) => item.column), ["QA", "Fertig", "Backlog"]);
|
|
|
|
const [api, pr, issue] = content.items;
|
|
assert.equal(api.link, null);
|
|
assert.equal(api.number, null);
|
|
assert.equal(pr.isPullRequest, true);
|
|
assert.equal(pr.link, "https://codeberg.org/fds2610/andere/pulls/9");
|
|
assert.equal(issue.link, "https://codeberg.org/fds2610/monitor/issues/3");
|
|
assert.equal(issue.repo, "fds2610/monitor");
|
|
assert.equal(issue.type, "bug");
|
|
assert.equal(issue.color, "#ff0000");
|
|
|
|
assert.deepEqual(buildExternalTicketModalContent().items, []);
|
|
});
|