import { Pool } from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { todayIso } from "@/lib/format"; import { resolveReportingLines, type OmHolder, type OmUnit } from "@/lib/om-reporting"; // Die Berichtslinien-Regel existiert zweimal: als om_reporting_lines() in // der Datenbank und als resolveReportingLines() im Anwendungscode. Zwei // Fassungen derselben Regel driften auseinander, und die Abweichung fällt // niemandem auf — im Organigramm stünde einfach eine andere Führungskraft // als im Export. Also über den gesamten Bestand gegeneinanderhalten. // // Eigene Verbindung statt der Zugriffsschicht: geprüft wird die Regel, nicht // die Berechtigung. Mit dem Zeilenschutz dazwischen liefe der Vergleich über // eine gefilterte Teilmenge und bewiese nichts. const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 2 }); describe.skipIf(!process.env.DATABASE_URL)("om_reporting_lines stimmt mit resolveReportingLines überein", () => { const asOf = todayIso(); afterAll(async () => { await pool.end(); }); type DbZeile = { employee_id: string; formal_manager_id: string | null; acting_manager_id: string | null }; async function fromDatabase(): Promise { const { rows } = await pool.query("select * from om_reporting_lines($1)", [asOf]); return rows; } async function fromTypeScript() { const { rows: units } = await pool.query<{ id: string; parent_id: string | null }>( "select id, parent_id from org_units" ); // Dieselbe Gültigkeitsprüfung wie in der Datenbankfunktion: halboffen, // das Ende ausgeschlossen. Sie steht hier ausgeschrieben und nicht als // Aufruf einer gemeinsamen Hilfsfunktion — sonst prüfte der Test die // Regel gegen sich selbst. const { rows: besetzungen } = await pool.query<{ employee_id: string; position_id: string; org_unit_id: string; is_chief: boolean; }>( `select a.employee_id, a.position_id, p.org_unit_id, p.is_chief from position_assignments a join om_positions p on p.id = a.position_id where a.valid_from <= $1 and (a.valid_to is null or a.valid_to > $1) and p.valid_from <= $1 and (p.valid_to is null or p.valid_to > $1)`, [asOf] ); const { rows: personen } = await pool.query<{ id: string; karenz_start_date: string | null; karenz_return_date: string | null; }>("select id, karenz_start_date, karenz_return_date from employees"); const abwesend = new Map( personen.map((e) => [ e.id, Boolean( e.karenz_start_date && e.karenz_start_date <= asOf && (!e.karenz_return_date || asOf < e.karenz_return_date) ), ]) ); const omUnits: OmUnit[] = units.map((u) => ({ id: u.id, parentId: u.parent_id })); const holders: OmHolder[] = besetzungen.map((a) => ({ employeeId: a.employee_id, positionId: a.position_id, orgUnitId: a.org_unit_id, isChief: a.is_chief, absent: abwesend.get(a.employee_id) ?? false, })); return resolveReportingLines(omUnits, holders); } it("liefert für jede Person dieselbe formale und tatsächliche Führungskraft", async () => { const [db, ts] = await Promise.all([fromDatabase(), fromTypeScript()]); expect(db.length).toBe(ts.length); expect(db.length).toBeGreaterThan(0); const tsById = new Map(ts.map((l) => [l.employeeId, l])); const abweichungen = db .map((row) => { const mine = tsById.get(row.employee_id); if (!mine) return `${row.employee_id}: fehlt in der TypeScript-Fassung`; if (mine.actingManagerId !== row.acting_manager_id) return `${row.employee_id}: acting DB=${row.acting_manager_id} TS=${mine.actingManagerId}`; if (mine.formalManagerId !== row.formal_manager_id) return `${row.employee_id}: formal DB=${row.formal_manager_id} TS=${mine.formalManagerId}`; return null; }) .filter(Boolean); expect(abweichungen.slice(0, 10)).toEqual([]); }); it("gibt genau einer Person keine Führungskraft — der obersten Leitung", async () => { const db = await fromDatabase(); expect(db.filter((r) => r.acting_manager_id === null)).toHaveLength(1); }); it("erzeugt keine Berichtslinie auf sich selbst", async () => { const db = await fromDatabase(); expect(db.filter((r) => r.acting_manager_id === r.employee_id)).toEqual([]); }); it("lässt jede Berichtslinie an der obersten Leitung enden", async () => { // Ein Ring in den abgeleiteten Linien wäre im Organigramm ein Teilbaum, // der nie gerendert wird — und niemand würde es merken. const db = await fromDatabase(); const managerOf = new Map(db.map((r) => [r.employee_id, r.acting_manager_id])); for (const start of db) { const gesehen = new Set(); let cur: string | null = start.employee_id; while (cur && !gesehen.has(cur)) { gesehen.add(cur); cur = managerOf.get(cur) ?? null; } expect(cur, `Ring in der Berichtslinie ab ${start.employee_id}`).toBeNull(); } }); });