Merge branch 'review/slice-1-recheck'

This commit is contained in:
Leonid Pershin
2026-08-20 14:41:29 +03:00
4 changed files with 169 additions and 1 deletions
+47
View File
@@ -2,6 +2,53 @@
Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md). Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md).
## Перепроверка среза 1 (после одежды / сессии / общения)
- **Фазы:** 04
- **Проверен на:** `f6bb4c7`, 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. Оболочка и карта ## Срез 1. Оболочка и карта
- **Фазы:** 04 - **Фазы:** 04
+1 -1
View File
@@ -20,7 +20,7 @@
| Срез | Проверен на | Журнал | | Срез | Проверен на | Журнал |
| --- | --- | --- | | --- | --- | --- |
| Срез 1. Оболочка и карта | `418a4ea` | [01-shell](01-shell/reviewed.md) | | Срез 1. Оболочка и карта | `f6bb4c7` | [01-shell](01-shell/reviewed.md) |
| Срез 2. Люди | `bdb1253` | [02-people](02-people/reviewed.md) | | Срез 2. Люди | `bdb1253` | [02-people](02-people/reviewed.md) |
| Срез 3. Штат | `bdb1253` | [03-staffing](03-staffing/reviewed.md) | | Срез 3. Штат | `bdb1253` | [03-staffing](03-staffing/reviewed.md) |
| Срез 4. Расписание | `bdb1253` | [04-schedule](04-schedule/reviewed.md) | | Срез 4. Расписание | `bdb1253` | [04-schedule](04-schedule/reviewed.md) |
@@ -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<HTMLButtonElement>('.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', () => { describe('GameScreen location talk', () => {
it('shows talk partners from the directory on the location list, not from the frame', () => { it('shows talk partners from the directory on the location list, not from the frame', () => {
setLocale('ru'); setLocale('ru');
@@ -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] [Fact]
public async Task Dump_ReturnsPeopleNodesAndTimetable() public async Task Dump_ReturnsPeopleNodesAndTimetable()
{ {