diff --git a/docs/phases/01-shell/reviewed.md b/docs/phases/01-shell/reviewed.md index 0004f96..483e41e 100644 --- a/docs/phases/01-shell/reviewed.md +++ b/docs/phases/01-shell/reviewed.md @@ -2,6 +2,53 @@ Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md). +## Перепроверка среза 1 (после одежды / сессии / общения) + +- **Фазы:** 0–4 +- **Проверен на:** `PENDING`, 2026-08-20 +- **Пути:** `src/HSchool.Client`, `src/HSchool.Protocol`, `src/HSchool.Content`, + `src/HSchool.Server/Game`, `src/HSchool.Server/Api/ModEndpoints.cs`, + `src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Net`, + `tests/HSchool.Content.Tests`, `tests/HSchool.Protocol.Tests`, + `tests/HSchool.AppHost.Tests/SchoolApiTests.cs`, `tests/HSchool.AppHost.Tests/GameSocketTests.cs`, + `tests/HSchool.AppHost.Tests/ServiceabilityTests.cs` +- **Итог:** перепроверка; `git log 418a4ea..HEAD` по путям среза непустой (одежда, сессия, общение, + ремесло). Обещания фаз 0–4 целы. Дописаны 3 теста. Код не чинился. Фазы 60 и 61 не закрывались. + +Что подтверждено (после дрейфа): + +- PixiJS нет в `package.json` и в клиентском коде. Дерево карты, пять секций локации, клиентский + фильтр, `localize()`. Оболочка — вкладки Карта/Люди; отказ от ленты событий в фазе 1 и + `design/near-term.md`. +- `SchoolWorker` — `LongRunning`, свой `World`, аккумулятор `MaxCatchUpSteps = 5`. Супервизор + таблицу держит и `World` не трогает (фаза 58 разрезала файл, не поток). Сейв на + create/shutdown/`SaveInterval`; `ReloadFromDisk_RestoresAPausedClock`, + `PausingOneSchool_DoesNotStopAnother`. +- Каталог/карта: JSONC, last-wins, parent/abstract, патчи, связность, пустая комната, отказ без + двора, ванильная раскладка. Нет папки мода — школу не стартовать, файл не трогать + (`SchoolContentUnavailableException`, `persist: false`, `RememberIncompatible`). +- `GET /api/mods`, каталог `?lang=`, create с картой и модами, снимок при OpenSchool. `core` в + диалоге нельзя снять. Кодек, `protocol.ts` и `docs/protocol.md` — один `MapSnapshot` (протокол v9, + бамп поздних срезов, раскладка снимка та же). +- `modal.ts` резолвит промис явно; `.dialog--screen` — `height: fit-content`; + `DefaultStartDate` UTC; `MapSnapshotSize` от сообщения; outbox `DropOldest`; + `WaitToReadAsync` переносится между итерациями; меню патчит `card.update`. + +Дописано на этом проходе: + +- `gameScreen.test.ts` — клик по дереву фильтрует панель локации на клиенте +- `gameScreen.test.ts` — `localize()` обновляет пустые секции локации +- `ServiceabilityTests.MissingModFolder_LeavesTheSchoolUnstartedAndTheFileUntouched` + +Исправлено: + +- ничего: расхождений с дизайном среза 1, которые надо было чинить кодом, не нашлось. + +Открытое: + +- срезы 2–6 в сводке всё ещё на старых коммитах — дрейф после одежды/сессии/общения; не этот проход. +- фазы 60 и 61 (портреты) в работе; ревью их не закрывало. + ## Срез 1. Оболочка и карта - **Фазы:** 0–4 diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index cb3a88c..099a066 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -22,7 +22,7 @@ | Срез | Проверен на | Журнал | | --- | --- | --- | -| Срез 1. Оболочка и карта | `418a4ea` | [01-shell](01-shell/reviewed.md) | +| Срез 1. Оболочка и карта | `PENDING` | [01-shell](01-shell/reviewed.md) | | Срез 2. Люди | `bdb1253` | [02-people](02-people/reviewed.md) | | Срез 3. Штат | `bdb1253` | [03-staffing](03-staffing/reviewed.md) | | Срез 4. Расписание | `bdb1253` | [04-schedule](04-schedule/reviewed.md) | diff --git a/src/HSchool.Client/src/ui/gameScreen.test.ts b/src/HSchool.Client/src/ui/gameScreen.test.ts index d97ea5b..84b6bfc 100644 --- a/src/HSchool.Client/src/ui/gameScreen.test.ts +++ b/src/HSchool.Client/src/ui/gameScreen.test.ts @@ -98,6 +98,82 @@ describe('GameScreen speed buttons', () => { }); }); +describe('GameScreen location tree', () => { + it('filters the location panel on the client when a tree node is clicked', () => { + const screen = new GameScreen({ + onLeave: () => {}, + onSetRunning: () => {}, + onSetSpeed: () => {}, + onSkip: () => {}, + }); + document.body.append(screen.element); + screen.show(school()); + screen.applyMap(1, [ + { + kind: 0, + id: 'yard', + parentId: '', + name: 'Двор', + pupilSlots: 0, + items: [], + positions: [], + }, + { + kind: 3, + id: 'office', + parentId: 'yard', + name: 'Кабинет', + pupilSlots: 0, + items: [{ name: 'Стул', count: 2 }], + positions: ['Директор'], + }, + ]); + + expect(screen.element.querySelector('.panel__name')?.textContent).toBe('Двор'); + const itemsSection = screen.element.querySelector('.panel__section'); + expect(itemsSection?.querySelector('.panel__empty')?.textContent).toBe(t('itemsEmpty')); + + const office = [...screen.element.querySelectorAll('.tree__button')].find( + (button) => button.textContent === 'Кабинет', + ); + expect(office).toBeTruthy(); + office?.click(); + + expect(screen.element.querySelector('.panel__name')?.textContent).toBe('Кабинет'); + expect(office?.classList.contains('tree__button--active')).toBe(true); + const lists = [...screen.element.querySelectorAll('.panel__section .panel__list')].map( + (list) => [...list.querySelectorAll('li')].map((item) => item.textContent ?? ''), + ); + expect(lists.some((entries) => entries.includes('Стул ×2'))).toBe(true); + expect(lists.some((entries) => entries.includes('Директор'))).toBe(true); + }); + + it('relocalizes empty location sections when the language switches', () => { + setLocale('en'); + const screen = new GameScreen({ + onLeave: () => {}, + onSetRunning: () => {}, + onSetSpeed: () => {}, + onSkip: () => {}, + }); + document.body.append(screen.element); + screen.show(school()); + + expect(screen.element.querySelector('.panel__section-title')?.textContent).toBe(t('locationItems')); + expect([...screen.element.querySelectorAll('.panel__empty')].map((node) => node.textContent)).toContain( + t('itemsEmpty'), + ); + + setLocale('ru'); + screen.localize(); + + expect(screen.element.querySelector('.panel__section-title')?.textContent).toBe(t('locationItems')); + expect([...screen.element.querySelectorAll('.panel__empty')].map((node) => node.textContent)).toContain( + t('itemsEmpty'), + ); + }); +}); + describe('GameScreen location talk', () => { it('shows talk partners from the directory on the location list, not from the frame', () => { setLocale('ru'); diff --git a/tests/HSchool.AppHost.Tests/ServiceabilityTests.cs b/tests/HSchool.AppHost.Tests/ServiceabilityTests.cs index 3db08c6..894b259 100644 --- a/tests/HSchool.AppHost.Tests/ServiceabilityTests.cs +++ b/tests/HSchool.AppHost.Tests/ServiceabilityTests.cs @@ -83,6 +83,51 @@ public class ServiceabilityTests(AppHostFixture fixture) } } + [Fact] + public async Task MissingModFolder_LeavesTheSchoolUnstartedAndTheFileUntouched() + { + using var client = fixture.App.CreateHttpClient("server"); + await SchoolApiTests.ResetAsync(client); + var created = await SchoolApiTests.CreateAsync(client, "Без папки мода", TuesdayMorning); + var directory = await SavesDirectoryAsync(client); + var path = Path.Combine(directory, $"{created.Id}.json"); + var node = JsonNode.Parse(File.ReadAllText(path)) + ?? throw new InvalidOperationException($"Save {path} parsed to nothing."); + var mods = node["modIds"] as JsonArray + ?? throw new InvalidOperationException($"Save {path} has no modIds."); + mods.Add("missing-pack-slice1-recheck"); + var patched = node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); + + try + { + await SchoolApiTests.ResetAsync(client); + File.WriteAllText(path, patched); + var before = File.ReadAllBytes(path); + + using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); + reload.EnsureSuccessStatusCode(); + + Assert.Equal(before, File.ReadAllBytes(path)); + + var state = await SchoolApiTests.GetSchoolsAsync(client); + var mine = state.Schools.FirstOrDefault(school => school.Id == created.Id); + var other = state.Others.FirstOrDefault(school => school.Id == created.Id); + Assert.True(mine is not null || other is not null); + Assert.False(mine?.Running ?? other?.Running ?? true); + + using var people = await client.GetAsync( + $"/api/schools/{created.Id}/people?pageSize=1", + TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.NotFound, people.StatusCode); + } + finally + { + DeleteSchoolFiles(directory, created.Id); + using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); + cleanup.EnsureSuccessStatusCode(); + } + } + [Fact] public async Task Dump_ReturnsPeopleNodesAndTimetable() {