Update reviewed phase details, enhance presence tests for hiring and assigning teachers during lessons, and fix timetable handling in the School class. Added new tests for presence management and ensured proper campus staffing behavior during lesson transitions.
ci / server (push) Failing after 4m0s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-20 02:47:24 +03:00
parent 2833a09799
commit cbe739a4e6
4 changed files with 159 additions and 22 deletions
+52 -5
View File
@@ -519,15 +519,16 @@ Calendar+обеды 20, планировщик 9, TimetableApi 6.
## Срез 5. Жизнь школы
- **Фазы:** 1821
- **Проверен на:** `5a00ad7`, 2026-08-19 (первичная проверка — `21d79cb`). Фаза 21 закрыта за час до проверки, `core` правился
параллельно — проверка велась в отдельном worktree на коммите, не на рабочем дереве
- **Проверен на:** `450495f`, 2026-08-20 (первичная проверка — `21d79cb`; предыдущая перепроверка — `5a00ad7`)
- **Пути:** `src/HSchool.Ai`, `src/HSchool.Simulation/{PresenceSystem,ActivitySystem,LessonLearningSystem,School}.cs`,
`src/HSchool.Protocol` (кадр `Presence`, пропуск в кадре часов),
`src/HSchool.Server/Game/PresenceFrame.cs`, `src/HSchool.Client/src/ui/gameScreen.ts`,
`src/HSchool.Server/mods/core/defs/{actions,behavior,needs}`, `tests/HSchool.Ai.Tests`,
`tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests}.cs`
- **Итог:** все 32 теста из списков четырёх фаз на месте и проходят; тестов не дописано, зато
измерен прожитый учебный день — и он показал две вещи, которых списки не ловят
`tests/HSchool.Simulation.Tests/{PresenceTests,DecisionTests,ActivityTests,NeedDecayTests,LunchTests}.cs`,
`tests/HSchool.AppHost.Tests/GameSocketTests.cs`
- **Итог:** все 32 теста из списков четырёх фаз на месте; дописаны 2 теста явки после смены таблицы,
`SetTimetable` сбрасывает планы дня, хостовый тест ждёт `Clock.Running` после снятия паузы.
Голод из первого прохода уже закрыт данными. AppHost не гонялся — Aspire держит DLL.
Что подтверждено:
@@ -606,6 +607,52 @@ Calendar+обеды 20, планировщик 9, TimetableApi 6.
ходьба, действия и решения не тронуты, `decayPerHour` и `needGain` тоже, поэтому измеренная выше
картина с голодом остаётся в силе: вопрос «чем закрывать голод» по-прежнему открыт.
### Перепроверка на `450495f`, 2026-08-20
Дрейф после `5a00ad7` — обеденные смены, веса в `BehaviorDef` (фаза 24), `Decision.cs`,
`PresenceSystem` (очередь после действий). Обещания фаз 18–21 целы; протокол остался v7.
Что подтверждено повторно:
- **Фаза 18.** Все 13 тестов списка на месте в `WalkingTests` / `PresenceTests`. Пустая школа
без учителей в рабочее окно не пропускается. `EnsurePlans` пересобирал планы только при смене
дня или численности — после пина или назначения предмета без найма `AppearAt` оставался `null`.
- **Фаза 19.** Раскладка присутствия с обеих сторон (`Presence_RoundTrips`, `protocol.test.ts`),
снимок карты без людей, пропуск в учебное время отбрасывается. Кадр часов несёт разрешение на
пропуск; клиент его не считает.
- **Фаза 20.** Декей голода, обед, стулья, ночь вне школы, неизвестная нужда, детерминизм.
`SleepAndHunger_ReturnToMaxOffCampus` и `AfterASchoolDay_NobodyIsLeftStarving` закрывают
открытый в первом проходе голод.
- **Фаза 21.** Восемь тестов списка плюс обеденные веса в `DecisionPlannerTests`. `HSchool.Ai`
без Arch/ASP.NET/`DateTime.Now`, `Simulation` без сокетов.
Исправлено:
- `School.SetTimetable` обнуляет `PlanDay`. Иначе `EnsurePlans` считает планы актуальными,
пока не сменятся день или численность, и после назначения математики в 10:20 никто не приходит:
вчерашний план собран по пустой таблице, `AppearAt == null`. Найм это маскировал (численность
растёт), пин и назначение — нет.
- `OpeningASchoolDuringAMathLesson_...` после снятия паузы сразу ждал людей в кадре. Присутствие
уходит и на паузе (пустые люди, подписи урока из таблицы), и 40 кадров бюджета уходили в эти
кадры до обработки `SetRunning`. Теперь тест ждёт `clock.Running`, потом людей. Сам хост
с Aspire в этой сессии не гонялся (MSB3021).
Дописано:
- `HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus` — найм на паузе в 10:20, потолок
64 решения, шаг 20 Гц, как у работника.
- `AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus` — назначение без смены численности;
без сброса `PlanDay` кампус остаётся пустым.
Закрыто из прошлого прохода:
- **Голод.** Уже закрыто 2026-08-19 (`restoredOffCampus`, стулья 112, смены). Повторно зелёные
`NeedDecayTests.SleepAndHunger_ReturnToMaxOffCampus` и `LunchTests.AfterASchoolDay_NobodyIsLeftStarving`.
- **`OpeningASchoolDuringAMathLesson_...`.** Это была явка плюс чтение паузных кадров, не флак
нагрузки. Симуляция после правки зелёная; хостовый прогон — когда Aspire отпустят DLL.
Ai 29, Presence+Decision+Activity+NeedDecay+Lunch 31, Presence_RoundTrips 1.
## Что из журнала закрыто, 2026-08-19
Ревью нашло семь пунктов; шесть закрыты в этот же день, седьмой снят как несуществующий.
+4
View File
@@ -231,6 +231,10 @@ public sealed class School : IDisposable
ArgumentNullException.ThrowIfNull(timetable);
Timetable = timetable;
TimetableDirty = false;
// Duty rooms and AppearAt come from this table. EnsurePlans only rebuilds when the day
// or the roster size changes, so a pin or an assign would otherwise keep yesterday's
// empty-staff plans and leave the campus empty.
PlanDay = null;
LastDecisionSlot = null;
foreach (var id in Roster?.People.Select(person => person.Id) ?? [])
{
@@ -212,6 +212,10 @@ public class GameSocketTests(AppHostFixture fixture)
// one person made this test fail under load.
await SendAsync(socket, buffer =>
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: true)));
// Presence still goes out while paused (empty people, lesson labels from the table).
// Waiting for people first drains those frames and can spend the 40-frame budget
// before the resume even lands. Clock.Running is the signal that time is moving.
await ReceiveClockWhereAsync(socket, clock => clock.Running);
presence = await ReceivePresenceWhereAsync(socket, frame => frame.People.Count > 0);
Assert.All(presence.People, person => Assert.False(string.IsNullOrWhiteSpace(person.NodeId)));
Assert.Contains(presence.People, person => directory.People.Any(row => row.Id == person.Id));
+99 -17
View File
@@ -224,11 +224,87 @@ public class PresenceTests
[Fact]
public void HireAndAssignMath_DuringLessonThree_PutsSomeoneOnCampus()
{
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
using var school = OpenEmpty(start);
school.Tick(0.2d, 5d);
Assert.True(school.IsCampusEmpty());
using var school = UnstaffedDuringLessonThree();
HireMathTeacher(school);
Assert.True(school.IsCampusEmpty(), "paused ticks must not walk anyone in");
school.Clock.IsRunning = true;
Assert.True(SomeoneWalksIn(school), $"nobody on campus after mid-lesson hire at {school.Clock.Time:HH:mm}");
}
[Fact]
public void AssigningMath_AfterDayPlansWereBuilt_PutsSomeoneOnCampus()
{
using var school = UnstaffedDuringLessonThree();
var catalog = school.Catalog!;
var map = school.Map!;
var hired = Staffing.Hire(
catalog,
map,
school.Roster!,
school.Applicants!,
school.Applicants!.Applicants[0].Person.Id,
Staffing.TeacherPosition,
Cap);
Assert.Equal(StaffingError.None, hired.Error);
school.ApplyStaffing(hired.Roster, hired.Pool);
school.SetTimetable(SchoolTimetables.Build(catalog, map, hired.Roster, null, weekDays: 5));
school.Clock.IsRunning = true;
school.Tick(1d / 20d, 5d);
school.Clock.IsRunning = false;
Assert.True(school.IsCampusEmpty(), "a teacher with no subjects still has no reason to come");
var assigned = Staffing.AssignSubject(
catalog,
hired.Roster,
hired.Pool,
hired.Roster.People.Single(person => person.IsStaff).Id,
"Mathematics",
Cap);
Assert.Equal(StaffingError.None, assigned.Error);
school.ApplyStaffing(assigned.Roster, assigned.Pool);
school.SetTimetable(SchoolTimetables.Build(catalog, map, assigned.Roster, null, weekDays: 5));
school.Clock.IsRunning = true;
Assert.True(
SomeoneWalksIn(school),
"stale day plans after an assign that did not change headcount left the campus empty");
}
[Fact]
public void Simulation_DoesNotReferenceSockets()
{
var names = typeof(School).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
}
private static School UnstaffedDuringLessonThree()
{
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
var school = OpenEmpty(start);
school.ConfigurePresence(weekDays: 5, maxDecisionsPerTick: 64);
school.SetTimetable(SchoolTimetables.Build(school.Catalog!, school.Map!, school.Roster!, null, weekDays: 5));
var step = 1d / 20d;
for (var i = 0; i < 40; i++)
{
school.Tick(step, 5d);
}
Assert.True(school.IsCampusEmpty());
school.Clock.IsRunning = false;
for (var i = 0; i < 10; i++)
{
school.Tick(step, 5d);
}
return school;
}
private static void HireMathTeacher(School school)
{
var catalog = school.Catalog!;
var map = school.Map!;
var hired = Staffing.Hire(
@@ -242,25 +318,31 @@ public class PresenceTests
Assert.Equal(StaffingError.None, hired.Error);
school.ApplyStaffing(hired.Roster, hired.Pool);
var assigned = Staffing.AssignSubject(catalog, hired.Roster, hired.Pool, hired.Roster.People.Single(person => person.IsStaff).Id, "Mathematics", Cap);
var assigned = Staffing.AssignSubject(
catalog,
hired.Roster,
hired.Pool,
hired.Roster.People.Single(person => person.IsStaff).Id,
"Mathematics",
Cap);
Assert.Equal(StaffingError.None, assigned.Error);
school.ApplyStaffing(assigned.Roster, assigned.Pool);
school.SetTimetable(SchoolTimetables.Build(catalog, map, assigned.Roster, null, weekDays: 5));
for (var i = 0; i < 20; i++)
{
school.Tick(0.2d, 5d);
}
Assert.Contains(school.CapturePresence(), row => row.NodeId is not null);
}
[Fact]
public void Simulation_DoesNotReferenceSockets()
private static bool SomeoneWalksIn(School school)
{
var names = typeof(School).Assembly.GetReferencedAssemblies().Select(assembly => assembly.Name!);
Assert.DoesNotContain(names, name => name.Contains("Sockets", StringComparison.OrdinalIgnoreCase));
Assert.DoesNotContain(names, name => name.Contains("AspNet", StringComparison.OrdinalIgnoreCase));
var step = 1d / 20d;
for (var i = 0; i < 400; i++)
{
school.Tick(step, 5d);
if (school.CapturePresence().Any(row => row.NodeId is not null))
{
return true;
}
}
return false;
}
private static (School School, string Homeroom, string PupilId) StaffedFirstFloorClass()