From 1097d8ddf55e5bfb5cf12529ac1c6614228a0cb7 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 21 Aug 2026 02:46:30 +0300 Subject: [PATCH 1/3] Add review tests for notices, scene prompts, and Play lock. Co-authored-by: Cursor --- src/HSchool.Client/src/ui/gameScreen.test.ts | 29 +++++++++++ tests/HSchool.Content.Tests/EventDefTests.cs | 18 +++++++ .../PortraitSceneCollectorTests.cs | 51 +++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/src/HSchool.Client/src/ui/gameScreen.test.ts b/src/HSchool.Client/src/ui/gameScreen.test.ts index 7e8161c..8f5a775 100644 --- a/src/HSchool.Client/src/ui/gameScreen.test.ts +++ b/src/HSchool.Client/src/ui/gameScreen.test.ts @@ -292,6 +292,35 @@ describe('GameScreen view restore', () => { expect(location.search).toContain('tab=people'); }); + it('disables Play while a pausing notice is open', () => { + const screen = new GameScreen({ + onLeave: () => {}, + onSetRunning: () => {}, + onSetSpeed: () => {}, + onSkip: () => {}, + }); + document.body.append(screen.element); + screen.show(school({ running: false })); + + const play = screen.element.querySelector('.clock__controls .button--icon'); + expect(play?.disabled).toBe(false); + + screen.applyNotice({ + type: 'notice', + id: 4, + defName: 'GenerationFailed', + severity: 2, + pause: true, + ttlMs: 0, + personId: 0, + action: 'none', + }); + + expect(play?.disabled).toBe(true); + expect(play?.title).toBe(t('noticePauseLocked')); + expect(screen.element.querySelector('.notice-modal')).not.toBeNull(); + }); + it('keeps a guest on overview even when the URL asks for manage', () => { history.replaceState(null, '', '/?school=3&mode=manage'); const screen = new GameScreen({ diff --git a/tests/HSchool.Content.Tests/EventDefTests.cs b/tests/HSchool.Content.Tests/EventDefTests.cs index e6cd997..5e78cf0 100644 --- a/tests/HSchool.Content.Tests/EventDefTests.cs +++ b/tests/HSchool.Content.Tests/EventDefTests.cs @@ -53,6 +53,24 @@ public class EventDefTests Assert.Contains("fullMoon", ex.Message, StringComparison.Ordinal); } + [Fact] + public void UnknownAction_FailsTheCatalog() + { + var documents = PackDocuments.FromDirectory( + CatalogLoader.CorePackId, + Path.Combine(AppContext.BaseDirectory, "vanilla")) + .Append(PackDocuments.Def( + CatalogLoader.CorePackId, + "events", + "bad-action", + """{ "defName": "BadAct", "severity": "info", "ttlMs": 1, "trigger": "dayStart", "action": "teleport" }""")) + .ToList(); + + var ex = Assert.Throws(() => _loader.Load([CatalogLoader.CorePackId], documents)); + Assert.Contains("action", ex.Message, StringComparison.Ordinal); + Assert.Contains("teleport", ex.Message, StringComparison.Ordinal); + } + private DefCatalog LoadVanilla() { var root = Path.Combine(AppContext.BaseDirectory, "vanilla"); diff --git a/tests/HSchool.Server.Tests/PortraitSceneCollectorTests.cs b/tests/HSchool.Server.Tests/PortraitSceneCollectorTests.cs index e44c106..10967fc 100644 --- a/tests/HSchool.Server.Tests/PortraitSceneCollectorTests.cs +++ b/tests/HSchool.Server.Tests/PortraitSceneCollectorTests.cs @@ -1,4 +1,5 @@ using HSchool.Content; +using HSchool.Server.Api; using HSchool.Server.Game; using HSchool.Simulation; @@ -101,6 +102,56 @@ public class PortraitSceneCollectorTests Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("= 0 && sceneAt > clothesAt); + + var chairs = PortraitSceneCollector.Collect( + catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget); + var (without, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: chairs); + Assert.DoesNotContain("blackboard", without, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("chair", without, StringComparison.OrdinalIgnoreCase); + } + + private static PersonCardResponse SampleWornCard() => + new( + "f0.c0", + "Maria Ivanova", + "Ivanova", + "Maria", + "", + true, + 12, + new DateTime(2000, 3, 14, 0, 0, 0, DateTimeKind.Utc), + ["student"], + 5, + "A", + "class-1", + null, + null, + [new LabeledStatResponse("HairColor", "Hair colour", "Black")], + [], + [], + [], + null, + null, + new PersonFamilyResponse([], [], [], []), + [new WornItemResponse("Shirt", "Shirt", "White", "White", [new DefLabelResponse(ApparelLayers.Top, "Top")], 1f, "new", [ApparelLayers.Underwear])], + [], + 0f, + 0f); + private DefCatalog LoadCatalog() { return _loader.Load( From a544d34cb599b3068ed6999690ac10986a4f3ae7 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 21 Aug 2026 02:48:50 +0300 Subject: [PATCH 2/3] Record slice 11 stages B-D (notices, scene, generate button) as reviewed. Co-authored-by: Cursor --- docs/phases/11-events/reviewed.md | 39 +++++++++++++++++++++++++++++++ docs/phases/reviewed.md | 1 + docs/protocol.md | 4 ++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/phases/11-events/reviewed.md b/docs/phases/11-events/reviewed.md index b5b7e7e..923a009 100644 --- a/docs/phases/11-events/reviewed.md +++ b/docs/phases/11-events/reviewed.md @@ -33,3 +33,42 @@ HTTP: `GET /api/settings/swarmui` и create копируют два Krea-id (т - Пример JSON у `GET .../portrait/prompt` в `protocol.md` всё ещё с `cinematic photo`, `age 12` и `(low quality, worst quality:1.4)` — это HTTP промпта/сцены (этапы C–D), не каталог. Здесь не правился: соседний проход трогает `protocol.md`. - Хостовый GET/create на Krea-id не перегонялся в этом worktree. - Этапы B–D (тосты, пауза, сборщик сцены, `generateImage`) — не этот проход. + +## Срез 11. Этапы B–D — лента, сцена и кнопка + +- **Фазы:** 64–67 +- **Проверен на:** `HASH_PLACEHOLDER`, 2026-08-21 +- **Пути:** `src/HSchool.Content/EventDefs.cs`, `src/HSchool.Content/EventDefValidator.cs`, `src/HSchool.Content/PromptContribution.cs`, `src/HSchool.Content/PromptContributionValidator.cs`, `src/HSchool.Simulation/EventSystem.cs`, `src/HSchool.Simulation/WorldEvent.cs`, `src/HSchool.Protocol/ProtocolCodec.cs`, `src/HSchool.Client/src/net/protocol.ts`, `docs/protocol.md`, `src/HSchool.Server/Game/NoticeBoard.cs`, `src/HSchool.Server/Game/PortraitSceneCollector.cs`, `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/GenerationFailedPoster.cs`, `src/HSchool.Client/src/ui/noticeToasts.ts`, `src/HSchool.Client/src/ui/noticeModals.ts`, `src/HSchool.Client/src/ui/gameScreen.ts` +- **Итог:** обещания 64–67 совпадают с кодом и тестами. Сокет v10 (`0x87` / `0x08`), info не в сейве, sticky паузит и переживает Open, сцена после одежды с бюджетом 6/2/2, кнопка `generateImage` — хозяин, успех не снимает паузу. Дописаны тесты на Play lock, неизвестный `action` и сборку «доска после одежды». Пример JSON у `GET .../portrait/prompt` приведён к прозе и сцене (замечание этапа A). Каталог Krea / состав моделей не ревьюился. Хостовые `GameSocketTests` / `NoticeImageApiTests` в этом проходе не гонялись (общий AppHost / живой сервер). + +### 64. События и info-тост + +`EventDef`: severity / pause / ttlMs / trigger / action, локаль как у других def. Ваниль `DayStarted` / `LessonStarted` — info, TTL 8000, `none`. `EventSystem` на крае рабочего утра и на смене периода урока — один факт на школу, не по классам; повторный тик в ту же минуту пуст. Работник сопоставляет def и шлёт `0x87`. Клиент — тост, `t(defName)`, клик → `0x08`, TTL гасит локально, колонки нет. Info не в сейве и не на Open. Круглый трип и байтовая раскладка на C# и TS, `PROTOCOL_VERSION` = 10. + +`dotnet test tests/HSchool.Content.Tests --filter FullyQualifiedName~EventDefTests`: 3 зелёных (включая неизвестный action). `HSchool.Simulation.Tests` `WorldEventTests` и `HSchool.Protocol.Tests` `Notice_*` на месте, в этом проходе не перегонялись. + +### 65. Варнинг и пауза + +`Notice` с `pause` ставит `running=false`; `SetRunning(true)` игнор, пока sticky не пуст. Dismiss последнего не Play. Потолок 8, девятый не эмитится. Sticky в сейве, info нет. Dismiss pausing — хозяин (сокет `CanManage`, HTTP `403` `not-owner`). Модал без TTL, очередь из одного. Play серый, пока модал открыт. + +### 66. Сцена в промпте + +Блок `prompt` на Thing / Room / Territory / осадках; 121 символ и вес < 0 — ошибка загрузки. Ваниль: доска и снег непустые, `Chair` молчит. Сборщик на работнике: узел + fill + worn/held + погода только на улице, после одежды, вес ↓ затем `defName`. Бюджет в `SimulationOptions` (6 / 2 / 2). LoRA в `loras`, embed как ``. Сокет не бампили. `GET .../portrait/prompt` и Generate делят `PortraitPromptBuilder.Build` + `LookupBuildInput`. + +### 67. Картинка с уведомления + +`GenerationFailed` — error, pause, `generateImage`. Эмит из `PortraitService` при Unavailable/TimedOut, не при успехе и не при пустом personId. HTTP `POST .../notices/{id}/generate` — хозяин, тот же сборщик, kind с доски или `full`. Без personId — `400` `notice-cannot-generate`, Swarm не зовётся. Успех паузу не снимает. Кнопка только при action и personId ≠ 0; тост дня без кнопки. + +Дописано: + +- `UnknownAction_FailsTheCatalog` — `action: teleport` валит загрузку +- `CollectThenBuild_PutsBlackboardAfterClothing_ChairRoomDoesNot` — фрагмент доски после одежды; стул не попадает +- `disables Play while a pausing notice is open` — Play `disabled`, title `noticePauseLocked` + +Код фаз не чинился, кроме примера JSON в `protocol.md` (устаревшие `age 12` / cinematic / `(word:1.4)`). Абзац каталога Krea не трогался. + +### Открытое + +- `generateImage` на info-тосте (не sticky): кнопка на клиенте есть, HTTP ищет только sticky → `404` `unknown-notice`. Ваниль так не делает (кнопка на `GenerationFailed`). Валидатор не требует `pause` у `generateImage`. +- Хостовые `GameSocketTests` (утро, пауза, гость, F5) и `NoticeImageApiTests` в этом worktree не гонялись: живой сервер пользователя, общий AppHost. +- Локаль тоста при смене языка: `NoticeToasts.localize` обновляет текст, не подпись кнопки «создать картинку» (тост живёт 8 с). diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 883ed57..0173a08 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -38,3 +38,4 @@ | Портреты (вне очереди) 62 | `d093ea3` | [off-queue](off-queue/reviewed.md) | | Вне очереди. DX 49/54/55 | `ee452b2` | [off-queue](off-queue/reviewed.md) | | Срез 11. Этап A — только Krea 2 | `1002aeb` | [11-events](11-events/reviewed.md) | +| Срез 11. Этапы B–D — лента, сцена и кнопка | `HASH_PLACEHOLDER` | [11-events](11-events/reviewed.md) | diff --git a/docs/protocol.md b/docs/protocol.md index 0713aab..7b4eec9 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -521,8 +521,8 @@ Empty `prompt` blocks are silent. Unknown school is `404` `unknown-school`; unkn ```json { "kind": "avatar", - "positive": "cinematic photo, …, close up, head and shoulders portrait, …, age 12, …", - "negative": "(low quality, worst quality:1.4), …", + "positive": "…, Head and shoulders., A teenage girl, 12 years old, with black hair, wearing a white shirt, a classroom blackboard with chalk notes", + "negative": "…", "promptExtra": null, "presetId": "default", "presetLabel": "Default" From fe8e67e579b842abef448cb5bd45478b2a63cc2e Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 21 Aug 2026 02:49:18 +0300 Subject: [PATCH 3/3] Fill slice 11 B-D review commit hash. Co-authored-by: Cursor --- docs/phases/11-events/reviewed.md | 2 +- docs/phases/reviewed.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/phases/11-events/reviewed.md b/docs/phases/11-events/reviewed.md index 923a009..457075e 100644 --- a/docs/phases/11-events/reviewed.md +++ b/docs/phases/11-events/reviewed.md @@ -37,7 +37,7 @@ HTTP: `GET /api/settings/swarmui` и create копируют два Krea-id (т ## Срез 11. Этапы B–D — лента, сцена и кнопка - **Фазы:** 64–67 -- **Проверен на:** `HASH_PLACEHOLDER`, 2026-08-21 +- **Проверен на:** `a544d34`, 2026-08-21 - **Пути:** `src/HSchool.Content/EventDefs.cs`, `src/HSchool.Content/EventDefValidator.cs`, `src/HSchool.Content/PromptContribution.cs`, `src/HSchool.Content/PromptContributionValidator.cs`, `src/HSchool.Simulation/EventSystem.cs`, `src/HSchool.Simulation/WorldEvent.cs`, `src/HSchool.Protocol/ProtocolCodec.cs`, `src/HSchool.Client/src/net/protocol.ts`, `docs/protocol.md`, `src/HSchool.Server/Game/NoticeBoard.cs`, `src/HSchool.Server/Game/PortraitSceneCollector.cs`, `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/GenerationFailedPoster.cs`, `src/HSchool.Client/src/ui/noticeToasts.ts`, `src/HSchool.Client/src/ui/noticeModals.ts`, `src/HSchool.Client/src/ui/gameScreen.ts` - **Итог:** обещания 64–67 совпадают с кодом и тестами. Сокет v10 (`0x87` / `0x08`), info не в сейве, sticky паузит и переживает Open, сцена после одежды с бюджетом 6/2/2, кнопка `generateImage` — хозяин, успех не снимает паузу. Дописаны тесты на Play lock, неизвестный `action` и сборку «доска после одежды». Пример JSON у `GET .../portrait/prompt` приведён к прозе и сцене (замечание этапа A). Каталог Krea / состав моделей не ревьюился. Хостовые `GameSocketTests` / `NoticeImageApiTests` в этом проходе не гонялись (общий AppHost / живой сервер). diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 0173a08..3c2cbde 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -38,4 +38,4 @@ | Портреты (вне очереди) 62 | `d093ea3` | [off-queue](off-queue/reviewed.md) | | Вне очереди. DX 49/54/55 | `ee452b2` | [off-queue](off-queue/reviewed.md) | | Срез 11. Этап A — только Krea 2 | `1002aeb` | [11-events](11-events/reviewed.md) | -| Срез 11. Этапы B–D — лента, сцена и кнопка | `HASH_PLACEHOLDER` | [11-events](11-events/reviewed.md) | +| Срез 11. Этапы B–D — лента, сцена и кнопка | `a544d34` | [11-events](11-events/reviewed.md) |