import { describe, expect, it } from "vitest"; import { ROLL_EPSILON, groupStrips, type StripGrouping } from "./slotStrips"; /** Минимальный слот для группировки. tn задаём как локальную ISO-строку (как отдаёт бэкенд). */ type S = { satelliteId: number; slotNumber: number; cycle: number; tn: string; tk: string; roll: number; }; const keyOf = (s: S) => `${s.satelliteId}:${s.slotNumber}:${s.cycle}`; /** Слот на минуту mm (от базовой даты) с длительностью 30 c. cycle ≈ «виток» (в границу не входит). */ function slot(satelliteId: number, slotNumber: number, roll: number, mm: number, cycle = 1): S { const tn = `2026-06-17T10:${String(mm).padStart(2, "0")}:00`; const tk = `2026-06-17T10:${String(mm).padStart(2, "0")}:30`; return { satelliteId, slotNumber, cycle, tn, tk, roll }; } /** Разбиение на полосы как множество множеств ключей — для сравнения, не зависящего от имён PolosaId. */ function partition(g: StripGrouping): string[][] { const byStrip = new Map(); for (const [key, strip] of g.idByKey) { const arr = byStrip.get(strip) ?? []; arr.push(key); byStrip.set(strip, arr); } return [...byStrip.values()].map((keys) => keys.slice().sort()).sort((a, b) => (a[0]! < b[0]! ? -1 : 1)); } describe("groupStrips", () => { it("пустой вход → пустая разметка", () => { const g = groupStrips([], keyOf); expect(g.idByKey.size).toBe(0); expect(g.membersOf.size).toBe(0); }); it("один слот → своя полоса", () => { const g = groupStrips([slot(1, 1, 5, 0)], keyOf); expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1"); expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1"]); }); it("веер кренов в один момент → разные полосы; равный крен вдоль времени → одна полоса", () => { // КА 1: два нацеливания (крен 36.5 и 39.5, шаг 3° > ε), каждое снято в моменты mm=0 и mm=1. const slots = [ slot(1, 1, 36.5, 0), slot(1, 2, 39.5, 0), slot(1, 3, 36.5, 1), slot(1, 4, 39.5, 1), ]; expect(partition(groupStrips(slots, keyOf))).toEqual([ ["1:1:1", "1:3:1"], // крен 36.5 вдоль времени ["1:2:1", "1:4:1"], // крен 39.5 вдоль времени ]); }); it("смена витка (cycle) внутри одного крена и каденции НЕ рвёт полосу", () => { const slots = [ slot(1, 1, 5, 0, 1), slot(1, 2, 5, 1, 1), slot(1, 3, 5, 2, 2), // виток сменился у экватора — но это та же полоса slot(1, 4, 5, 3, 2), ]; expect(partition(groupStrips(slots, keyOf))).toEqual([ ["1:1:1", "1:2:1", "1:3:2", "1:4:2"], ]); }); it("большой разрыв по времени рвёт полосу на две (другой проход)", () => { const slots = [ slot(1, 1, 5, 0), slot(1, 2, 5, 1), slot(1, 3, 5, 2), slot(1, 4, 5, 3), slot(1, 5, 5, 30), // разрыв ≫ каденции (1 мин) → новый проход slot(1, 6, 5, 31), slot(1, 7, 5, 32), ]; expect(partition(groupStrips(slots, keyOf))).toEqual([ ["1:1:1", "1:2:1", "1:3:1", "1:4:1"], ["1:5:1", "1:6:1", "1:7:1"], ]); }); it("разные КА — разные полосы (даже при одном крене и времени)", () => { const slots = [slot(1, 1, 5, 0), slot(2, 1, 5, 0), slot(1, 2, 5, 1), slot(2, 2, 5, 1)]; expect(partition(groupStrips(slots, keyOf))).toEqual([ ["1:1:1", "1:2:1"], ["2:1:1", "2:2:1"], ]); }); it("граница ε по крену: разрыв == 0.5 склеивает, 0.6 рвёт", () => { const same = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON, 1)], keyOf); expect(partition(same)).toEqual([["1:1:1", "1:2:1"]]); const broken = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON + 0.1, 1)], keyOf); expect(partition(broken)).toEqual([["1:1:1"], ["1:2:1"]]); }); it("устойчивость к порядку входа: разбиение одинаково при перемешивании", () => { const slots = [slot(1, 1, 5, 0), slot(1, 2, 5, 1), slot(1, 3, 20, 0), slot(2, 1, 5, 0)]; const a = partition(groupStrips(slots, keyOf)); const b = partition(groupStrips([...slots].reverse(), keyOf)); const c = partition(groupStrips([slots[2]!, slots[0]!, slots[3]!, slots[1]!], keyOf)); expect(b).toEqual(a); expect(c).toEqual(a); }); it("PolosaId = polosa:${ключ первого по времени члена}; membersOf — в порядке времени", () => { const g = groupStrips([slot(1, 2, 5, 1), slot(1, 1, 5, 0)], keyOf); expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1"); expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1"); expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1", "1:2:1"]); }); it("NaN tn → синглтон без падения, не клеит и не рвёт валидных соседей", () => { const broken: S = { satelliteId: 1, slotNumber: 9, cycle: 1, tn: "битый", tk: "битый", roll: 5 }; const slots = [slot(1, 1, 5, 0), broken, slot(1, 2, 5, 1)]; const g = groupStrips(slots, keyOf); expect(g.idByKey.get("1:9:1")).toBe("polosa:1:9:1"); expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1"); expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1"); }); });