import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { parseDocx, details } from "../server/parser.js"; import { parseWeeksDocx } from "../server/calendar.js"; import { mergeWeekTables } from "../server/source.js"; import { peopleAndRooms, buildIndex, occurrence, search, dayOf, freeRooms, examsOf, isPhysicalRoom, } from "../server/directory.js"; const fixture = (name) => readFileSync(new URL(`./fixtures/${name}.docx`, import.meta.url)); const calendar = { ...mergeWeekTables([parseWeeksDocx(fixture("weeks"))]), bells: [ { pair: 1, start: "08:30", end: "09:55", halves: [] }, { pair: 2, start: "10:05", end: "11:30", halves: [] }, { pair: 3, start: "11:45", end: "13:10", halves: [] }, ], source: {}, }; const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule }); const fef = doc("fef", parseDocx(fixture("fef-23-ba"), { name: "23-ЛГ_23-БА.docx", faculty: "ФЭФ" })); const rooms = (raw, room = "") => peopleAndRooms({ raw, room, teacher: "" }, "2026"); test("Аудитории: реальные формулировки из Word", () => { assert.deepEqual(rooms("Охрана труда (л.з.) Лемачко С.Н. ауд.205;6,8н.- Горунова С.А.", "205").rooms, ["205"]); const moved = rooms("История (л.) Сумко Е.В. ауд. 432 14.10. -ауд. 333", "432"); assert.deepEqual(moved.rooms, ["432"]); assert.deepEqual(moved.overrides, [{ date: "2026-10-14", room: "333" }]); assert.deepEqual(rooms("Материаловедение (л.) Фруцкий В.А. ауд. 106н 17.10. -210 мк").overrides, [ { date: "2026-10-17", room: "210МК" }, ]); // A typo glues the room to the date: "22417.09." is room 224 and a move on 17 September. const glued = rooms("Информационные технологии (л.) Моняк Т.М. ауд. 22417.09.- ауд.332", "22417"); assert.deepEqual([glued.rooms, glued.overrides[0].room], [["224"], "332"]); assert.deepEqual(rooms("Единый день информирования 10.09;15.10;19.11;17.12", "12").rooms, []); assert.deepEqual(rooms("Информационный час ауд. В0-05", "05").rooms, ["В0-05"]); assert.deepEqual(rooms("Физкультура ауд. 526н325н502н").rooms, ["526Н", "325Н", "502Н"]); assert.deepEqual(rooms("Предмет (л.) Иванов И.И. ауд.129, Петров П.П. ауд. 130").teachers, [ "Иванов И.И.", "Петров П.П.", ]); assert.equal(rooms("Линейная алгебра (л.)\nЗабелендик О.Н.\n200 (ДО)", "200 (ДО)").distance, true); assert.equal(rooms("Сети (л.) Лукьянов А.О. 200ДО").rooms.length, 0); assert.deepEqual(rooms("История (л) Емельянчик О.А. 21/28 (дискозал)").rooms, ["21/28"]); assert.ok(isPhysicalRoom("207Г") && isPhysicalRoom("А1-15") && !isPhysicalRoom("3")); }); test("Занятость по дате учитывает недели, цвет строки и семестр", () => { const idx = buildIndex([fef]); const find = (subject, day) => idx.entries.find((e) => e.lesson.subject === subject && e.lesson.day === day && e.lesson.groups.includes("23-БА")); // Friday 16 October 2026 is week 7: "1-4,6-11н." runs, "12-15н." does not. assert.equal(occurrence(find("PR-менеджмент", 5), "2026-10-16", calendar), "yes"); const later = idx.entries.find((e) => e.lesson.condition === "12-15н." && e.lesson.day === 5); assert.equal(occurrence(later, "2026-10-16", calendar), "no"); assert.equal(occurrence(later, "2026-11-20", calendar), "yes"); // An autumn schedule does not run in March. assert.equal(occurrence(find("PR-менеджмент", 5), "2027-03-05", calendar), "no"); // A spring document without a spring week table: nothing is guessed. const spring = doc("spring", { ...fef.schedule, period: "Весенний семестр" }); const entry = { ...find("PR-менеджмент", 5), doc: spring }; assert.equal(occurrence(entry, "2027-03-05", calendar), "unknown"); }); test("Поиск, день преподавателя и свободные аудитории", () => { const idx = buildIndex([fef]); idx.docs = [fef]; idx.total = 1; idx.failed = []; idx.year = "2026–2027"; assert.equal(search(idx, "селез").teachers[0].name, "Селезнева А.А."); assert.equal(search(idx, "4").teachers.length, 0); const day = dayOf(idx, calendar, { teacher: "Селезнева А.А." }, "2026-10-16"); assert.deepEqual(day.lessons.map((l) => [l.start, l.subject, l.rooms.join()]), [ ["08:30", "Инновационный менеджмент", "333"], ]); const room = dayOf(idx, calendar, { room: "129" }, "2026-10-16"); assert.ok(room.lessons.some((l) => l.subject === "PR-менеджмент")); const free = freeRooms(idx, calendar, "2026-10-16", 2); const freeList = free.free.flatMap((g) => g.rooms); assert.ok(!freeList.includes("129"), "PR-менеджмент занимает 129 на 2-й паре"); assert.ok(freeList.includes("333"), "333 занята только на 1-й паре"); assert.equal(free.coverage.documents, 1); assert.equal(freeRooms(idx, calendar, "2026-10-16", 9), null); }); test("Экзамены, зачёты и консультации распознаются и собираются по группе", () => { assert.deepEqual( ["Экзамен. Финансы организаций Иванов И.И. ауд.129", "Финансы (экз.) Иванов И.И.", "Зачет: Физическая культура Петров П.П.", "Консультация Финансы Иванов И.И."] .map((raw) => [details(raw).type, details(raw).subject]), [ ["Экзамен", "Финансы организаций"], ["Экзамен", "Финансы"], ["Зачёт", "Физическая культура"], ["Консультация", "Финансы"], ], ); assert.equal(details("Психология профессионального консультирования (л.) Сидоров С.С.").type, "Лекция"); const exam = (date, start, type, subject) => ({ id: date + start, day: 1, date, start, end: null, subject, teacher: "Иванов И.И.", room: "129", type, condition: "", groups: ["23-БА"], subgroups: [], variant: "all", raw: `${type} ${subject} Иванов И.И. ауд.129`, }); const session = doc("session", { lessons: [ exam("2027-01-14", "09:00", "Экзамен", "Финансы"), exam("2027-01-13", "15:00", "Консультация", "Финансы"), exam("2027-01-14", "09:00", "Экзамен", "Финансы"), { ...exam("2027-01-20", "09:00", "Экзамен", "Маркетинг"), groups: ["23-ЛГ"] }, ], source: { faculty: "ФЭФ" }, period: "Экзаменационная сессия", }); const idx = buildIndex([fef, session]); const list = examsOf(idx, "23-БА", "ФЭФ"); assert.deepEqual(list.map((e) => [e.date, e.type, e.subject, e.rooms.join()]), [ ["2027-01-13", "Консультация", "Финансы", "129"], ["2027-01-14", "Экзамен", "Финансы", "129"], ]); assert.equal(examsOf(idx, "23-БА", "ФИТ").length, 0); }); test("Поиск группы: любое написание, без учёта фильтров, с фасетами каталога", async () => { const { findGroups, groupKey } = await import("../server/directory.js"); const fit = parseDocx(fixture("fit-daily"), { name: "24-ИТ.docx" }); const docs = [ { id: "fef", file: { id: "fef", name: "23-ЛГ_23-БА.docx", faculty: "ФЭФ", course: "4 курс", form: "Дневная форма" }, schedule: fef.schedule }, { id: "fit", file: { id: "fit", name: "24-ИТ.docx", faculty: "ФИТ", course: "3 курс", form: "Дневная форма" }, schedule: fit }, ]; const idx = { docs, year: "2026–2027" }; assert.equal(groupKey("23 ба"), groupKey("23-БА")); for (const q of ["23-БА", "23 ба", "23ба"]) assert.deepEqual(findGroups(idx, { q }).results.map((r) => r.group), ["23-БА"]); // The query ignores the faculty filter: a student must find their group anywhere. assert.deepEqual( findGroups(idx, { q: "24-ит", faculty: "ФЭФ" }).results.map((r) => r.group), ["24-ИТ-1", "24-ИТ-2"], ); const browse = findGroups(idx, { faculty: "ФИТ" }); assert.deepEqual(browse.results.map((r) => [r.group, r.fileId]), [ ["24-ИТ-1", "fit"], ["24-ИТ-2", "fit"], ]); assert.deepEqual(browse.facets.faculties.map((f) => f.name), ["ФИТ", "ФЭФ"]); assert.deepEqual(browse.facets.courses.map((c) => c.name), ["3 курс"]); assert.equal(findGroups(idx, {}).results.length, 0); assert.equal(findGroups(idx, {}).total, 4); }); test("Выбор группы: зачёты не дублируют группу с основным расписанием", async () => { const { findGroups } = await import("../server/directory.js"); const credits = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" }); const main = { ...credits, period: "Осенний семестр" }; const doc = (id, schedule) => ({ id, file: { id, name: "26-АРХ.docx", faculty: "ИСФ", course: "1 курс" }, schedule }); // With a semester timetable the credits stay in the session countdown, not in the picker. const both = findGroups({ docs: [doc("main", main), doc("credits", credits)] }, { q: "26-АРХ" }); assert.deepEqual(both.results.map((r) => [r.group, r.fileId]), [["26-АРХ", "main"]]); assert.equal(both.total, 1); // A group with only a credit schedule can still open it. const only = findGroups({ docs: [doc("credits", credits)] }, { q: "26-АРХ" }); assert.deepEqual(only.results.map((r) => [r.group, r.fileId, r.period]), [["26-АРХ", "credits", "Зачёты"]]); }); test("Выбор группы: прошедший документ с датами не дублирует основное расписание", async () => { const { findGroups } = await import("../server/directory.js"); // A document dated 8–27 June 2026, like an orientation week, beside the group's semester timetable. const dated = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" }); const main = { ...dated, period: "Осенний семестр", lessons: dated.lessons.map(({ date, ...l }) => l) }; const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule }); const idx = { docs: [doc("main", main), doc("dated", dated)] }; const ids = (today) => findGroups(idx, { q: "23-ИТзс", today }).results.map((r) => r.fileId); assert.deepEqual(ids("2026-10-19"), ["main"]); // Before it is over, both are offered. assert.deepEqual(ids("2026-06-20").sort(), ["dated", "main"]); // Alone, even a finished one stays: it is all the group has. assert.deepEqual(findGroups({ docs: [doc("dated", dated)] }, { q: "23-ИТзс", today: "2026-10-19" }).results.length, 1); });