From c2e01f09d3694bef2122541a51a8a8c07c989483 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 14:53:56 +0300 Subject: [PATCH 1/2] Lock slice-2 filters and the staff-parent intake path later slices left untested. Co-authored-by: Cursor --- .../RosterBrowserTests.cs | 74 ++++++++++++++++++- .../HSchool.People.Tests/YearlyIntakeTests.cs | 37 ++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/tests/HSchool.People.Tests/RosterBrowserTests.cs b/tests/HSchool.People.Tests/RosterBrowserTests.cs index fd8a1a4..adcd296 100644 --- a/tests/HSchool.People.Tests/RosterBrowserTests.cs +++ b/tests/HSchool.People.Tests/RosterBrowserTests.cs @@ -65,10 +65,82 @@ public class RosterBrowserTests Assert.All(page.People, person => Assert.True(person.IsParent)); } + [Fact] + public void FilterBySex_KeepsOnlyThatSex() + { + var roster = Fixtures.Generate(Fixtures.Classrooms(4)); + var page = RosterBrowser.Apply( + roster, + Fixtures.AsOf, + Query(female: true, pageSize: RosterBrowser.MaxPageSize)); + + Assert.NotEmpty(page.People); + Assert.True(page.Total < roster.People.Count); + Assert.All(page.People, person => Assert.True(person.Female)); + } + + [Fact] + public void FilterByLetter_KeepsOnlyThatClassLetter() + { + var roster = Fixtures.Generate(Fixtures.Classrooms(4)); + var letter = roster.Classes[0].Letter; + var page = RosterBrowser.Apply( + roster, + Fixtures.AsOf, + Query(letter: letter, pageSize: RosterBrowser.MaxPageSize)); + var classes = roster.Classes.ToDictionary(schoolClass => schoolClass.Id, StringComparer.Ordinal); + + Assert.NotEmpty(page.People); + Assert.All(page.People, person => + Assert.Equal(letter, person.ClassId is { } classId ? classes[classId].Letter : null)); + } + + [Fact] + public void FilterByAgeRange_KeepsAgesInside() + { + var roster = Fixtures.Generate(Fixtures.Classrooms(4)); + var pupil = roster.People.First(person => person.IsStudent); + var age = pupil.AgeOn(Fixtures.AsOf); + var page = RosterBrowser.Apply( + roster, + Fixtures.AsOf, + Query(ageMin: age, ageMax: age, pageSize: RosterBrowser.MaxPageSize)); + + Assert.Contains(page.People, person => person.Id == pupil.Id); + Assert.All(page.People, person => Assert.Equal(age, person.AgeOn(Fixtures.AsOf))); + } + + [Fact] + public void FilterByPosition_KeepsThatJob() + { + var roster = Fixtures.Generate(Fixtures.Classrooms(4)); + var parent = roster.People.First(person => person.IsParent); + roster = roster with + { + People = roster.People + .Select(person => person.Id.Equals(parent.Id, StringComparison.Ordinal) + ? person with { IsStaff = true, Position = "Librarian" } + : person) + .ToArray(), + }; + + var page = RosterBrowser.Apply( + roster, + Fixtures.AsOf, + Query(position: "Librarian", pageSize: RosterBrowser.MaxPageSize)); + + Assert.Equal(parent.Id, Assert.Single(page.People).Id); + } + private static RosterQuery Query( string? role = null, int? year = null, + string? letter = null, + string? position = null, + bool? female = null, + int? ageMin = null, + int? ageMax = null, int page = 1, int pageSize = RosterBrowser.DefaultPageSize) => - new(role, year, Letter: null, Position: null, Female: null, AgeMin: null, AgeMax: null, PersonSort.Surname, Descending: false, page, pageSize); + new(role, year, letter, position, female, ageMin, ageMax, PersonSort.Surname, Descending: false, page, pageSize); } diff --git a/tests/HSchool.People.Tests/YearlyIntakeTests.cs b/tests/HSchool.People.Tests/YearlyIntakeTests.cs index f5c284d..02a5238 100644 --- a/tests/HSchool.People.Tests/YearlyIntakeTests.cs +++ b/tests/HSchool.People.Tests/YearlyIntakeTests.cs @@ -116,6 +116,43 @@ public class YearlyIntakeTests person => Assert.False(person.IsParent)); } + /// + /// Slice 3 stopped generating staff, so the "unless they work here" branch of the test above + /// is vacuously empty. Hire still marks a parent IsStaff; intake must not drop them + /// with the graduates' civilian co-parent. + /// + [Fact] + public void StaffParentOfOnlyGraduates_StaysAfterIntake() + { + var map = Fixtures.Classrooms(4); + var catalog = Fixtures.Catalog(); + var generated = Fixtures.Generate(map); + var graduating = generated.Classes + .Where(schoolClass => schoolClass.Year == 4) + .SelectMany(schoolClass => schoolClass.PupilIds) + .ToHashSet(StringComparer.Ordinal); + var family = generated.Families.First(candidate => + candidate.ChildIds.Count > 0 + && candidate.ChildIds.All(id => graduating.Contains(id))); + var staffId = family.ParentIds[0]; + var civilians = family.ParentIds.Skip(1).ToArray(); + var before = generated with + { + People = generated.People + .Select(person => person.Id.Equals(staffId, StringComparison.Ordinal) + ? person with { IsStaff = true, Position = "Librarian" } + : person) + .ToArray(), + }; + + var after = YearlyIntake.Apply(catalog, before, Fixtures.SchoolSeed, "Russia", September); + var stayed = Assert.Single(after.People, person => person.Id == staffId); + + Assert.True(stayed.IsStaff); + Assert.False(stayed.IsParent); + Assert.All(civilians, id => Assert.DoesNotContain(after.People, person => person.Id == id)); + } + private static string Snapshot(Roster roster) => string.Join('\n', roster.People.Select(person => $"{person.Id}|{person.FamilyId}|{person.IsStudent}|{person.IsStaff}|{person.IsParent}|{person.ClassId}|{person.Name.Full}|{Skills(person)}")); From 98cb2f0d1dc191926758018c5df3c931dbad283e Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 14:55:11 +0300 Subject: [PATCH 2/2] Record slice 2 recheck after country, clothing and social. Co-authored-by: Cursor --- docs/phases/02-people/reviewed.md | 52 +++++++++++++++++++++++++++++++ docs/phases/reviewed.md | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/phases/02-people/reviewed.md b/docs/phases/02-people/reviewed.md index fee81ea..541b3b0 100644 --- a/docs/phases/02-people/reviewed.md +++ b/docs/phases/02-people/reviewed.md @@ -42,3 +42,55 @@ и проверяет диапазон. - `List_SortsBySurname` требовал ordinal-порядок, сервер ставит Ё на место Е. Совпадали, пока в `core` не было фамилий на Ё. + +## Срез 2. Люди — перепроверка + +- **Фазы:** 5–9 +- **Проверен на:** `c2e01f0`, 2026-08-20 +- **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`, + `src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.cs`, + `src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Game/PersonCardReader.cs`, + `src/HSchool.Server/Game/SchoolWorker.cs`, + `src/HSchool.Client/src/ui/peoplePanel.ts`, `src/HSchool.Client/src/ui/personCard.ts`, + `tests/HSchool.People.Tests`, `tests/HSchool.Content.Tests/PeopleDefTests.cs`, + `tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs`, + `tests/HSchool.AppHost.Tests/PeopleApiTests.cs`, + `tests/HSchool.AppHost.Tests/SchoolSeedTests.cs` +- **Итог:** перепроверка после дрейфа с `bdb1253` (страна, одежда, вкладки карточки, мнения, + речь). Обещания фаз 5–9 целы; код не чинился; дописаны тесты фильтров списка и работающего + родителя на наборе + +Что подтверждено: + +- Дефы, наследование, патч, неизвестная ссылка, склонение, мод со своей страной и чертами. + `GET /api/catalog` отдаёт страны, не наборы имён — так решила фаза 29, не регрессия среза 2. +- Генератор: тот же сид → тот же ростер; 13-я семья не двигает первые двенадцать; отчества + отцовские; полное телосложение зажимает ловкость; 1 и 11 кабинетов собираются; неполные + семьи 5–10% и не все материнские. Arch / ASP.NET / `DateTime.Now` в `HSchool.People` нет. +- Одиннадцать кабинетов-номеров, `Homeroom` не компьютерный класс, сущности в `World`, + `PersistPeople` не с таймера часов. Штат при создании пуст — фаза 11. Нужды в `core` уже + с ненулевым `decayPerHour` — срез 5. +- Список читает опубликованный снимок (`FindPeople`), карточка идёт в мейлбокс с + `TaskCompletionSource`. Фильтры роли/параллели/литеры/должности/пола/возраста живут в + `RosterBrowser`; панель — вкладка рядом с картой, строки через `t(...)`. +- Выпуск старшей имеющейся, вместимость, младший брат в том же роде, reload после набора + (тест прошлого прохода на месте). + +Дописано: + +- `RosterBrowserTests.FilterBySex_KeepsOnlyThatSex` +- `RosterBrowserTests.FilterByLetter_KeepsOnlyThatClassLetter` +- `RosterBrowserTests.FilterByAgeRange_KeepsAgesInside` +- `RosterBrowserTests.FilterByPosition_KeepsThatJob` — фаза 8 обещает эти фильтры; после + дрейфа их гонял только клиентский «отправил query». +- `YearlyIntakeTests.StaffParentOfOnlyGraduates_StaysAfterIntake` — ветка «родитель + выпускников остаётся, если работает здесь» после фазы 11 была пустой: генератор больше + не ставит штат, и старый тест её не касался. + +Исправлено: ничего в коде среза. + +Открытое: + +- Хостовые `PeopleApiTests` / `SchoolSeedTests` в этом проходе не гонялись (Testing policy: + HTTP не менялся). Симуляционный переход 1 сентября и библиотечный набор зелёные. +- `people.json` теперь ещё пишется после разговора и ссоры — это срез 9, не состав среза 2. diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index bab81d1..927c196 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -21,7 +21,7 @@ | Срез | Проверен на | Журнал | | --- | --- | --- | | Срез 1. Оболочка и карта | `d087486` | [01-shell](01-shell/reviewed.md) | -| Срез 2. Люди | `bdb1253` | [02-people](02-people/reviewed.md) | +| Срез 2. Люди | `c2e01f0` | [02-people](02-people/reviewed.md) | | Срез 3. Штат | `bdb1253` | [03-staffing](03-staffing/reviewed.md) | | Срез 4. Расписание | `bdb1253` | [04-schedule](04-schedule/reviewed.md) | | Срез 5. Жизнь школы | `cbe739a` | [05-ai](05-ai/reviewed.md) |