import { describe, expect, it } from "vitest"; import { resolveActingManager, resolveActingManagers, type ChainNode } from "@/lib/acting-manager"; // Nobody's manager_id is rewritten for an absence — the stand-in is derived // here, at read time, so the absent person stays formally in charge and the // arrangement can be shown as temporary. function node(id: string, managerId: string | null, absent = false): ChainNode { return { id, managerId, absent }; } function chain(...nodes: ChainNode[]) { return new Map(nodes.map((n) => [n.id, n])); } describe("resolveActingManager", () => { it("leaves a present manager alone", () => { const ic = node("ic", "lead"); const map = chain(ic, node("lead", "head")); expect(resolveActingManager(ic, map)).toEqual({ actingManagerId: "lead", coveredForId: null }); }); it("rolls up one level when the manager is absent", () => { const ic = node("ic", "lead"); const map = chain(ic, node("lead", "head", true), node("head", "ceo")); expect(resolveActingManager(ic, map)).toEqual({ actingManagerId: "head", coveredForId: "lead" }); }); it("keeps rolling up while each level is absent", () => { const ic = node("ic", "lead"); const map = chain(ic, node("lead", "head", true), node("head", "ceo", true), node("ceo", null)); // coveredForId names the *recorded* manager, which is what the employee // and the org chart know about — not the intermediate level skipped. expect(resolveActingManager(ic, map)).toEqual({ actingManagerId: "ceo", coveredForId: "lead" }); }); it("gives an absent lead the same stand-in as their own reports", () => { // The deputy for an absent person is simply their own acting manager, // which is why no separate deputy field is needed anywhere. const lead = node("lead", "head", true); const map = chain(node("ic", "lead"), lead, node("head", "ceo")); expect(resolveActingManager(lead, map).actingManagerId).toBe("head"); }); it("keeps the recorded manager when everyone above is absent", () => { // Re-rooting the team to the top of the chart would be a bigger // distortion than showing the absent manager, whose absence is labelled // regardless. const ic = node("ic", "lead"); const map = chain(ic, node("lead", "ceo", true), node("ceo", null, true)); expect(resolveActingManager(ic, map)).toEqual({ actingManagerId: "lead", coveredForId: null }); }); it("returns nothing for somebody at the top", () => { const ceo = node("ceo", null); expect(resolveActingManager(ceo, chain(ceo))).toEqual({ actingManagerId: null, coveredForId: null }); }); it("keeps the recorded manager when they are not in the visible set", () => { // A manager who had already left or has not started yet; the snapshot's // own re-rooting deals with that case. const ic = node("ic", "gone"); expect(resolveActingManager(ic, chain(ic))).toEqual({ actingManagerId: "gone", coveredForId: null }); }); it("terminates on a manager cycle instead of looping forever", () => { // Nothing in the schema forbids A -> B -> A. const ic = node("ic", "a"); const map = chain(ic, node("a", "b", true), node("b", "a", true)); expect(() => resolveActingManager(ic, map)).not.toThrow(); expect(resolveActingManager(ic, map)).toEqual({ actingManagerId: "a", coveredForId: null }); }); it("terminates when somebody is their own manager", () => { const ic = node("ic", "self"); const map = chain(ic, node("self", "self", true)); expect(resolveActingManager(ic, map).actingManagerId).toBe("self"); }); }); describe("resolveActingManagers", () => { it("moves a whole team up together", () => { const results = resolveActingManagers([ node("ic1", "lead"), node("ic2", "lead"), node("lead", "head", true), node("head", null), ]); expect(results.get("ic1")).toEqual({ actingManagerId: "head", coveredForId: "lead" }); expect(results.get("ic2")).toEqual({ actingManagerId: "head", coveredForId: "lead" }); // The absent lead stays under their own manager rather than vanishing. expect(results.get("lead")?.actingManagerId).toBe("head"); }); });