Statuswechsel von/zu "in_arbeit" (Label "wip") loest ein Agent selbst aus, wenn er ein Ticket nimmt bzw. abgibt, und meldet die Bewegung ueber seinen eigenen state. detectUnassignedTicketChanges() zaehlte diese Wechsel bislang trotzdem als "niemandem zugeordnet" und liess zusaetzlich den PO dafuer zum Board laufen. Schliesst #32
615 lines
24 KiB
JavaScript
615 lines
24 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,
|
|
buildEventModalContent,
|
|
buildSystemModalContent,
|
|
buildKpiSparklinePoints,
|
|
buildKpiTiles,
|
|
buildKpiModalContent,
|
|
MAX_KPI_TILES,
|
|
AGENT_LOCATIONS,
|
|
resolveAgentVisualState,
|
|
mapAgentsToDesks,
|
|
normalizeAgentForDisplay,
|
|
isAgentPresent,
|
|
pickIdleActivity,
|
|
AnimationQueue,
|
|
DOOR_VISIT_TYPES,
|
|
DOOR_VISIT_STAGES,
|
|
detectDoorVisits,
|
|
resolveDoorVisitVisual,
|
|
advanceDoorVisitStage,
|
|
PO_AGENT_ID,
|
|
PO_DISPLAY_NAME,
|
|
PO_VISIT_STAGES,
|
|
detectUnassignedTicketChanges,
|
|
advancePoVisitStage,
|
|
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("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, "");
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// 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);
|
|
});
|
|
|
|
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("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("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);
|
|
});
|