Merge branch 'review/slice-7-a-recheck'
This commit is contained in:
@@ -5,53 +5,58 @@
|
|||||||
## Срез 7. Этап A — данные и гардероб
|
## Срез 7. Этап A — данные и гардероб
|
||||||
|
|
||||||
- **Фазы:** 29–31
|
- **Фазы:** 29–31
|
||||||
- **Проверен на:** `d5547b7`, 2026-08-20
|
- **Проверен на:** `5df341f`, 2026-08-20 — **перепроверка** после `d5547b7`
|
||||||
- **Пути:** `src/HSchool.Content/{CountryDef,ApparelDefs,CatalogLoader}.cs`,
|
- **Пути:** `src/HSchool.Content/{ApparelDefs,CatalogLoader}.cs`,
|
||||||
`src/HSchool.People/{DressGenerator,CountryClimate,RosterJson}.cs`,
|
`src/HSchool.People/{DressGenerator,CountryClimate,RosterJson}.cs`,
|
||||||
`src/HSchool.Server/{Game/SchoolStore.cs,Api/{ModEndpoints,SchoolEndpoints}.cs,Api/PeopleModels.cs}`,
|
`src/HSchool.Server/{Game/SchoolStore.cs,Game/SchoolWorker.Tick.cs,Api/{ModEndpoints,SchoolEndpoints}.cs,Api/PeopleModels.cs}`,
|
||||||
`src/HSchool.Client/src/ui/{createSchoolDialog.ts,personCard.ts}`,
|
`src/HSchool.Client/src/ui/{createSchoolDialog.ts,personCard.ts,personCardApparel.ts,personCardCarry.ts}`,
|
||||||
`tests/HSchool.Content.Tests/{CountryDefTests,ApparelDefTests,VanillaCoreTests}.cs`,
|
`tests/HSchool.Content.Tests/{CountryDefTests,ApparelDefTests,VanillaCoreTests}.cs`,
|
||||||
`tests/HSchool.People.Tests/{DressGeneratorTests,GoldenRosterTests}.cs`,
|
`tests/HSchool.People.Tests/{DressGeneratorTests,GoldenRosterTests}.cs`,
|
||||||
`tests/HSchool.AppHost.Tests/{SchoolApiTests,InventoryApiTests,GoldenSaveTests,ServiceabilityTests}.cs`,
|
`tests/HSchool.AppHost.Tests/{SchoolApiTests,InventoryApiTests,GoldenSaveTests,ServiceabilityTests}.cs`,
|
||||||
`src/HSchool.Client/src/ui/createSchoolDialog.test.ts`
|
`src/HSchool.Client/src/ui/createSchoolDialog.test.ts`
|
||||||
- **Итог:** все 17 тестов из списков фаз на месте; исправлен дубликат `peopleTabOverview` в
|
- **Итог:** обещания 29–31 после дрейфа (сессия, владельцы, UpperUnderwear, сплит карточки)
|
||||||
`strings.ts` (ломал `npm run build`); одно замечание рядом про вкладки карточки
|
в целом держатся; снятый в `660340c` подъём `nameSetId: Slavic` возвращён; золотой сейв с
|
||||||
|
абстрактным `Underwear` снова одевается. B–C не трогался.
|
||||||
|
|
||||||
Что подтверждено:
|
Что подтверждено:
|
||||||
|
|
||||||
- **29:** `CountryDef`/`ClimatePresetDef`; `namesets/` убран; ваниль `Russia` с двумя пресетами;
|
- **29:** `CountryDef`/`ClimatePresetDef`; `namesets/` нет; ваниль `Russia` с двумя пресетами;
|
||||||
каталог отдаёт `countries`; create принимает `countryId`, `unknown-country` на чужую страну;
|
каталог отдаёт `countries`, не `nameSets`; create — `countryId`, `unknown-country`;
|
||||||
пресет от сида (`CreateSchool_WithTheSameSeed_GetsTheSameClimatePreset`); `nameSetId: Slavic`
|
пресет от сида (`CreateSchool_WithTheSameSeed_GetsTheSameClimatePreset`); формат 3 без
|
||||||
поднимается через `UpgradeOlderSave` (`GoldenSaveTests`, `legacy-no-native/`); формат без
|
`countryId` не стартует (`ServiceabilityTests`); диалог — страны и языки;
|
||||||
страны не стартует (`ServiceabilityTests`); диалог — страны и языки (`createSchoolDialog.test.ts`);
|
золотой ростер (`GoldenRosterTests`). `UpgradeOlderSave` снова в `SchoolStore`: format 2
|
||||||
золотой ростер обновлён (`GoldenRosterTests`).
|
`nameSetId: Slavic` поднимается как `Russia` + первый пресет в памяти, файл не
|
||||||
- **30:** поля `ThingDef`/`ColorDef`, слои как роли, мебель без `layers`; ванильная палитра и
|
переписывается, пока школа сама не сохранится (`GoldenSaveTests`, `legacy-no-native/`).
|
||||||
одежда в `VanillaCoreTests`; локали ru/en (`EveryVanillaLabel_ExistsInBothLocales`); учебник
|
- **30:** поля `ThingDef`/`ColorDef`, слои включая `UpperUnderwear`; мебель без `layers`;
|
||||||
один def без `subject` на def.
|
каталог редактора без футболки/учебника; ваниль и локали (`VanillaCoreTests`); учебник —
|
||||||
- **31:** `InventoryItem` с `def`/`color`/`condition`/`location`/`subject`; `DressGenerator`
|
один def без предмета на def.
|
||||||
детерминирован (`DressGeneratorTests`, `InventoryApiTests.SameSeed_YieldsTheSameShirt`); слои
|
- **31:** `InventoryItem`; `DressGenerator` от `ApparelSalt`; слои без пересечения, платье,
|
||||||
без пересечения, платье на два слоя; пол/яркость/учебник/родители/соискатели/масса/Hauling;
|
пол/яркость/учебник/родители/соискатели/масса/`Hauling`; карточка worn+carried, список —
|
||||||
карточка отдаёт worn+carried, список — нет (`InventoryApiTests`); старый `people.json` без
|
нет; пустой `people.json` одевается на reload. Протокол этапа A не тронут;
|
||||||
items одевается на reload (`PeopleJsonWithoutInventory_IsDressedOnReloadAndRewritten`);
|
`HSchool.People`/`Content` без ASP.NET.
|
||||||
`HSchool.People`/`HSchool.Content` без ASP.NET; протокол не тронут.
|
|
||||||
|
|
||||||
Дописано:
|
Дописано:
|
||||||
|
|
||||||
- (нет новых тестов — списки фаз закрыты существующими)
|
- `GoldenSaveTests.CurrentFormatSave_LoadsTheSamePeopleAndTime` — возвращён (снят в фазе 39)
|
||||||
|
- `GoldenSaveTests.SaveWithoutNativeLanguage_LoadsWithoutReshuffling` — Slavic-сейв стартует, одежда дописывается
|
||||||
|
- `DressGeneratorTests.AbstractUnderwearInASave_IsRedressedOnEnsure`
|
||||||
|
|
||||||
Исправлено:
|
Исправлено:
|
||||||
|
|
||||||
- Дубликат ключа `peopleTabOverview` в `strings.ts` (ru/en): vitest не ловит TS2300/TS1117,
|
- `660340c` выкинул `NameSetId`/`UpgradeOlderSave` и сузил `CanStart` до format == 3: старые
|
||||||
`npm run build` падал; ключ перенесён, `peopleTabPortrait` рядом с остальными вкладками.
|
сейвы с `nameSetId: Slavic` не стартовали. Подъём возвращён; `CanStart` — format ≤ 3 и
|
||||||
- Повтор `using HSchool.Content` в `SchoolStore.cs` (CS0105).
|
непустой `countryId`.
|
||||||
|
- После `UpperUnderwear` золотой `people.json` носил абстрактный `Underwear`, и
|
||||||
|
`RequireKnownApparel` не пускал школу. `NeedsDressing`/`EnsureDressed` переодевают
|
||||||
|
неизвестный или абстрактный def тем же сидом.
|
||||||
|
|
||||||
Замечено рядом:
|
Замечено рядом:
|
||||||
|
|
||||||
- Карточка уже с вкладками (`personCard.ts`, фаза 31 обещала один свиток — позже по
|
- Карточка с вкладками — этап C (фаза 36); сплит в `personCardApparel.ts` / `personCardCarry.ts`.
|
||||||
`../../design/07-inventory/inventory.md`); портрет/SwarmUI-строки в `strings.ts` — не этап A.
|
- `ResolveClimatePreset` на reload без сохранённого пресета берёт первый пресет страны.
|
||||||
- `ResolveClimatePreset` в `SchoolWorker` при reload без сохранённого пресета берёт первый
|
- POST `nameSetId` по-прежнему игнорируется (поля нет на create); пустой `countryId` на create
|
||||||
пресет страны (`schoolSeed: 0`, `rollIfOmitted: false`) — по дизайну для legacy; create
|
берёт первую страну каталога — это create, не сейв.
|
||||||
кидает пресет из `GameLoopService` до spawn.
|
- Этап B–C в этом проходе не проверялся.
|
||||||
|
|
||||||
## Срез 7. Этап B–C — климат, жизнь одежды и правила
|
## Срез 7. Этап B–C — климат, жизнь одежды и правила
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
| Срез 4. Расписание | `01ef277` | [04-schedule](04-schedule/reviewed.md) |
|
| Срез 4. Расписание | `01ef277` | [04-schedule](04-schedule/reviewed.md) |
|
||||||
| Срез 5. Жизнь школы | `1118c59` | [05-ai](05-ai/reviewed.md) |
|
| Срез 5. Жизнь школы | `1118c59` | [05-ai](05-ai/reviewed.md) |
|
||||||
| Срез 6. Фундамент | `1c63357` | [06-foundation](06-foundation/reviewed.md) |
|
| Срез 6. Фундамент | `1c63357` | [06-foundation](06-foundation/reviewed.md) |
|
||||||
| Срез 7. Этап A — данные и гардероб | `d5547b7` | [07-inventory](07-inventory/reviewed.md) |
|
| Срез 7. Этап A — данные и гардероб | `5df341f` | [07-inventory](07-inventory/reviewed.md) |
|
||||||
| Срез 7. Этап B–C — климат, жизнь одежды и правила | `5dcdee6` | [07-inventory](07-inventory/reviewed.md) |
|
| Срез 7. Этап B–C — климат, жизнь одежды и правила | `5dcdee6` | [07-inventory](07-inventory/reviewed.md) |
|
||||||
| Срез 8. Сессия и темп | `1c3ae9a` | [08-session](08-session/reviewed.md) |
|
| Срез 8. Сессия и темп | `1c3ae9a` | [08-session](08-session/reviewed.md) |
|
||||||
| Срез 9. Этапы A–B — мнения и разговоры | `b38d0bb` | [09-social](09-social/reviewed.md) |
|
| Срез 9. Этапы A–B — мнения и разговоры | `b38d0bb` | [09-social](09-social/reviewed.md) |
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public static class DressGenerator
|
|||||||
int? pupilYear,
|
int? pupilYear,
|
||||||
int age)
|
int age)
|
||||||
{
|
{
|
||||||
if (person.Items.Count > 0)
|
if (person.Items.Count > 0 && ItemsUsable(catalog, person.Items))
|
||||||
{
|
{
|
||||||
return person;
|
return person;
|
||||||
}
|
}
|
||||||
@@ -130,11 +130,12 @@ public static class DressGenerator
|
|||||||
return pool with { Applicants = applicants };
|
return pool with { Applicants = applicants };
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool NeedsDressing(Roster roster, ApplicantPool? pool)
|
public static bool NeedsDressing(DefCatalog catalog, Roster roster, ApplicantPool? pool)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(catalog);
|
||||||
foreach (var person in roster.People)
|
foreach (var person in roster.People)
|
||||||
{
|
{
|
||||||
if (person.Items.Count == 0)
|
if (person.Items.Count == 0 || !ItemsUsable(catalog, person.Items))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ public static class DressGenerator
|
|||||||
|
|
||||||
foreach (var applicant in pool.Applicants)
|
foreach (var applicant in pool.Applicants)
|
||||||
{
|
{
|
||||||
if (applicant.Person.Items.Count == 0)
|
if (applicant.Person.Items.Count == 0 || !ItemsUsable(catalog, applicant.Person.Items))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -156,6 +157,19 @@ public static class DressGenerator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ItemsUsable(DefCatalog catalog, IReadOnlyList<InventoryItem> items)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (!catalog.Things.TryGetValue(item.Def, out var def) || def.Abstract)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds textbooks for subjects the pupil just became old enough for. Existing clothes stay.
|
/// Adds textbooks for subjects the pupil just became old enough for. Existing clothes stay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ internal sealed class SchoolSave
|
|||||||
|
|
||||||
public string? ClimatePresetId { get; init; }
|
public string? ClimatePresetId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Legacy field. Format 2 and older; mapped to <see cref="CountryId"/> on load.</summary>
|
||||||
|
public string? NameSetId { get; init; }
|
||||||
|
|
||||||
public string? NativeLanguage { get; init; }
|
public string? NativeLanguage { get; init; }
|
||||||
|
|
||||||
public IReadOnlyList<PresenceSnapshot>? Presence { get; init; }
|
public IReadOnlyList<PresenceSnapshot>? Presence { get; init; }
|
||||||
@@ -155,6 +158,11 @@ internal sealed class SchoolStore
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (save.Format < CurrentFormat)
|
||||||
|
{
|
||||||
|
save = UpgradeOlderSave(save);
|
||||||
|
}
|
||||||
|
|
||||||
// Claimed last, so a file rejected above does not reserve an id a good file needs.
|
// Claimed last, so a file rejected above does not reserve an id a good file needs.
|
||||||
if (!claimed.TryAdd(save.Id, path))
|
if (!claimed.TryAdd(save.Id, path))
|
||||||
{
|
{
|
||||||
@@ -178,6 +186,7 @@ internal sealed class SchoolStore
|
|||||||
Map = save.Map,
|
Map = save.Map,
|
||||||
CountryId = save.CountryId,
|
CountryId = save.CountryId,
|
||||||
ClimatePresetId = save.ClimatePresetId,
|
ClimatePresetId = save.ClimatePresetId,
|
||||||
|
NameSetId = save.NameSetId,
|
||||||
NativeLanguage = save.NativeLanguage,
|
NativeLanguage = save.NativeLanguage,
|
||||||
Presence = save.Presence,
|
Presence = save.Presence,
|
||||||
DressRules = save.DressRules,
|
DressRules = save.DressRules,
|
||||||
@@ -196,9 +205,52 @@ internal sealed class SchoolStore
|
|||||||
return saves;
|
return saves;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Older and newer files stay on disk for the menu to delete; they never start.</summary>
|
/// <summary>
|
||||||
|
/// Format 2 stored a name set. Slavic becomes Russia; any other id is tried as a country
|
||||||
|
/// (the example pack kept its defName). Climate is filled on the worker from the country's
|
||||||
|
/// first preset so the file is not rewritten until the school saves itself.
|
||||||
|
/// </summary>
|
||||||
|
internal static SchoolSave UpgradeOlderSave(SchoolSave save)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(save.CountryId))
|
||||||
|
{
|
||||||
|
return save;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countryId = save.NameSetId switch
|
||||||
|
{
|
||||||
|
"Slavic" or null or "" => "Russia",
|
||||||
|
_ => save.NameSetId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new SchoolSave
|
||||||
|
{
|
||||||
|
Format = save.Format,
|
||||||
|
Id = save.Id,
|
||||||
|
Name = save.Name,
|
||||||
|
GameTime = save.GameTime,
|
||||||
|
Running = save.Running,
|
||||||
|
SpeedIndex = save.SpeedIndex,
|
||||||
|
ModIds = save.ModIds,
|
||||||
|
Map = save.Map,
|
||||||
|
CountryId = countryId,
|
||||||
|
ClimatePresetId = save.ClimatePresetId,
|
||||||
|
NameSetId = save.NameSetId,
|
||||||
|
NativeLanguage = save.NativeLanguage,
|
||||||
|
Presence = save.Presence,
|
||||||
|
DressRules = save.DressRules,
|
||||||
|
SpeechRules = save.SpeechRules,
|
||||||
|
Owner = save.Owner,
|
||||||
|
PortraitSettings = save.PortraitSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Newer files, and current-format files without a country, stay on disk for the menu to
|
||||||
|
/// delete; they never start. Format 2 name-set saves are lifted first, then this gate.
|
||||||
|
/// </summary>
|
||||||
public static bool CanStart(SchoolSave save) =>
|
public static bool CanStart(SchoolSave save) =>
|
||||||
save.Format == CurrentFormat && !string.IsNullOrWhiteSpace(save.CountryId);
|
save.Format <= CurrentFormat && !string.IsNullOrWhiteSpace(save.CountryId);
|
||||||
|
|
||||||
public void Save(SchoolSave save)
|
public void Save(SchoolSave save)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ internal sealed partial class SchoolWorker
|
|||||||
generated = true;
|
generated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DressGenerator.NeedsDressing(roster, applicants))
|
if (DressGenerator.NeedsDressing(catalog, roster, applicants))
|
||||||
{
|
{
|
||||||
roster = DressGenerator.EnsureRoster(catalog, roster, seed, school.Clock.Time);
|
roster = DressGenerator.EnsureRoster(catalog, roster, seed, school.Clock.Time);
|
||||||
applicants = DressGenerator.EnsurePool(catalog, applicants, roster, seed, school.Clock.Time);
|
applicants = DressGenerator.EnsurePool(catalog, applicants, roster, seed, school.Clock.Time);
|
||||||
|
|||||||
@@ -5,49 +5,31 @@ using System.Text.Json.Nodes;
|
|||||||
namespace HSchool.AppHost.Tests;
|
namespace HSchool.AppHost.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 25 goldens. The folders stay historic (format 2, no country): that shape must not start.
|
/// Phase 25 goldens stay historic (format 2, <c>nameSetId: Slavic</c>). Phase 29 lifts that to
|
||||||
/// Load tests stamp today's format, country and owner onto a copy so missing-field defaults and
|
/// Russia in memory; the file is not rewritten until the school saves itself. Slice 6 recheck
|
||||||
/// seed-equals-id still have a host assertion after later slices raised <c>CanStart</c>.
|
/// treated format 2 as unstartable — that gate contradicted the lift, so these tests load.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Collection(AppHostCollection.Name)]
|
[Collection(AppHostCollection.Name)]
|
||||||
public class GoldenSaveTests(AppHostFixture fixture)
|
public class GoldenSaveTests(AppHostFixture fixture)
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HistoricFormat2Golden_LeavesTheSchoolUnstartedAndTheFileUntouched()
|
public async Task CurrentFormatSave_LoadsTheSamePeopleAndTime()
|
||||||
{
|
{
|
||||||
using var client = fixture.App.CreateHttpClient("server");
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
await SchoolApiTests.ResetAsync(client);
|
await SchoolApiTests.ResetAsync(client);
|
||||||
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
|
await InstallAsync(client, "current");
|
||||||
|
|
||||||
try
|
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
||||||
{
|
var school = Assert.Single(state.Schools);
|
||||||
InstallFolder(directory, "current");
|
Assert.Equal(1, school.Id);
|
||||||
var path = Path.Combine(directory, "1.json");
|
Assert.Equal(1, school.Seed);
|
||||||
var before = File.ReadAllBytes(path);
|
Assert.Equal("Золотая", school.Name);
|
||||||
|
Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime);
|
||||||
|
Assert.False(school.Running);
|
||||||
|
|
||||||
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
var people = await PeopleAsync(client, school.Id);
|
||||||
reload.EnsureSuccessStatusCode();
|
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
|
||||||
|
Assert.Contains(people, person => person.Roles.Contains("student"));
|
||||||
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
|
||||||
Assert.Empty(state.Schools);
|
|
||||||
var broken = Assert.Single(state.Others);
|
|
||||||
Assert.Equal(1, broken.Id);
|
|
||||||
Assert.Equal("Золотая", broken.Name);
|
|
||||||
Assert.Equal(1, broken.Seed);
|
|
||||||
|
|
||||||
using var people = await client.GetAsync(
|
|
||||||
$"/api/schools/{broken.Id}/people?pageSize=1",
|
|
||||||
TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, people.StatusCode);
|
|
||||||
|
|
||||||
Assert.Equal(before, File.ReadAllBytes(path));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
DeleteSchoolFiles(directory, 1);
|
|
||||||
using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
|
||||||
cleanup.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -55,84 +37,46 @@ public class GoldenSaveTests(AppHostFixture fixture)
|
|||||||
{
|
{
|
||||||
using var client = fixture.App.CreateHttpClient("server");
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
await SchoolApiTests.ResetAsync(client);
|
await SchoolApiTests.ResetAsync(client);
|
||||||
|
await InstallAsync(client, "legacy-no-native");
|
||||||
|
|
||||||
|
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
||||||
|
var school = Assert.Single(state.Schools);
|
||||||
|
Assert.Equal(1, school.Id);
|
||||||
|
Assert.Equal(1, school.Seed);
|
||||||
|
Assert.Equal("Золотая", school.Name);
|
||||||
|
|
||||||
|
var people = await PeopleAsync(client, school.Id);
|
||||||
|
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
|
||||||
|
|
||||||
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
|
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
|
||||||
|
Assert.Contains("\"items\"", File.ReadAllText(Path.Combine(directory, "1.people.json")), StringComparison.Ordinal);
|
||||||
try
|
|
||||||
{
|
|
||||||
await InstallRunnableAsync(client, directory, "legacy-no-native", includeNative: false);
|
|
||||||
|
|
||||||
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
|
||||||
var school = Assert.Single(state.Schools);
|
|
||||||
Assert.Equal(1, school.Id);
|
|
||||||
Assert.Equal(1, school.Seed);
|
|
||||||
Assert.Equal("Золотая", school.Name);
|
|
||||||
|
|
||||||
var people = await PeopleAsync(client, school.Id);
|
|
||||||
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
|
|
||||||
Assert.Contains(people, person => person.Roles.Contains("student"));
|
|
||||||
Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
DeleteSchoolFiles(directory, 1);
|
|
||||||
using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
|
||||||
cleanup.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task InstallRunnableAsync(
|
private async Task InstallAsync(HttpClient client, string folder)
|
||||||
HttpClient client,
|
|
||||||
string directory,
|
|
||||||
string folder,
|
|
||||||
bool includeNative)
|
|
||||||
{
|
|
||||||
InstallFolder(directory, folder);
|
|
||||||
StampRunnable(Path.Combine(directory, "1.json"), includeNative);
|
|
||||||
|
|
||||||
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
|
||||||
reload.EnsureSuccessStatusCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void InstallFolder(string directory, string folder)
|
|
||||||
{
|
{
|
||||||
|
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
|
||||||
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
|
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
|
||||||
foreach (var file in Directory.EnumerateFiles(source))
|
foreach (var file in Directory.EnumerateFiles(source))
|
||||||
{
|
{
|
||||||
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
|
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var session = await client.GetFromJsonAsync<SessionResponse>("/api/session", TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(session);
|
||||||
|
StampOwner(Path.Combine(directory, "1.json"), session.UserName);
|
||||||
|
|
||||||
|
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||||
|
reload.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StampRunnable(string path, bool includeNative)
|
private static void StampOwner(string path, string userName)
|
||||||
{
|
{
|
||||||
var node = JsonNode.Parse(File.ReadAllText(path))
|
var node = JsonNode.Parse(File.ReadAllText(path))
|
||||||
?? throw new InvalidOperationException($"Save {path} parsed to nothing.");
|
?? throw new InvalidOperationException($"Save {path} parsed to nothing.");
|
||||||
node["format"] = 3;
|
node["owner"] = userName;
|
||||||
node["countryId"] = "Russia";
|
File.WriteAllText(
|
||||||
node["climatePresetId"] = "TemperateContinental";
|
path,
|
||||||
node["owner"] = SchoolApiTests.TestUserName;
|
node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
|
||||||
node.AsObject().Remove("nameSetId");
|
|
||||||
if (includeNative)
|
|
||||||
{
|
|
||||||
node["nativeLanguage"] = "RussianLanguage";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
node.AsObject().Remove("nativeLanguage");
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllText(path, node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DeleteSchoolFiles(string directory, int id)
|
|
||||||
{
|
|
||||||
foreach (var name in new[] { $"{id}.json", $"{id}.people.json", $"{id}.timetable.json" })
|
|
||||||
{
|
|
||||||
var path = Path.Combine(directory, name);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
|
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
|
||||||
@@ -164,6 +108,8 @@ public class GoldenSaveTests(AppHostFixture fixture)
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record SessionResponse(string UserName);
|
||||||
|
|
||||||
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
|
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
|
||||||
|
|
||||||
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
|
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ public class DressGeneratorTests
|
|||||||
roster.Families,
|
roster.Families,
|
||||||
roster.Classes);
|
roster.Classes);
|
||||||
|
|
||||||
Assert.True(DressGenerator.NeedsDressing(stripped, ApplicantPool.Empty));
|
Assert.True(DressGenerator.NeedsDressing(catalog, stripped, ApplicantPool.Empty));
|
||||||
var dressed = DressGenerator.EnsureRoster(catalog, stripped, Fixtures.SchoolSeed, Fixtures.AsOf);
|
var dressed = DressGenerator.EnsureRoster(catalog, stripped, Fixtures.SchoolSeed, Fixtures.AsOf);
|
||||||
Assert.False(DressGenerator.NeedsDressing(dressed, ApplicantPool.Empty));
|
Assert.False(DressGenerator.NeedsDressing(catalog, dressed, ApplicantPool.Empty));
|
||||||
Assert.Equal(ItemsSnapshot(roster), ItemsSnapshot(dressed));
|
Assert.Equal(ItemsSnapshot(roster), ItemsSnapshot(dressed));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +180,34 @@ public class DressGeneratorTests
|
|||||||
Assert.All(roster.People, person => Assert.True(person.Skills.ContainsKey("Hauling")));
|
Assert.All(roster.People, person => Assert.True(person.Skills.ContainsKey("Hauling")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AbstractUnderwearInASave_IsRedressedOnEnsure()
|
||||||
|
{
|
||||||
|
var catalog = Fixtures.Catalog();
|
||||||
|
var roster = Fixtures.Generate(Fixtures.Classrooms(4));
|
||||||
|
var broken = new Roster(
|
||||||
|
roster.People.Select(person =>
|
||||||
|
{
|
||||||
|
var items = person.Items.Select(item =>
|
||||||
|
item.Def.Equals("Briefs", StringComparison.Ordinal)
|
||||||
|
|| item.Def.Equals("Boxers", StringComparison.Ordinal)
|
||||||
|
|| item.Def.Equals("Panties", StringComparison.Ordinal)
|
||||||
|
? item with { Def = "Underwear" }
|
||||||
|
: item).ToArray();
|
||||||
|
return person with { Items = items };
|
||||||
|
}).ToArray(),
|
||||||
|
roster.Families,
|
||||||
|
roster.Classes);
|
||||||
|
|
||||||
|
Assert.True(DressGenerator.NeedsDressing(catalog, broken, ApplicantPool.Empty));
|
||||||
|
var dressed = DressGenerator.EnsureRoster(catalog, broken, Fixtures.SchoolSeed, Fixtures.AsOf);
|
||||||
|
Assert.False(DressGenerator.NeedsDressing(catalog, dressed, ApplicantPool.Empty));
|
||||||
|
Assert.Equal(ItemsSnapshot(roster), ItemsSnapshot(dressed));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
dressed.People.SelectMany(person => person.Items),
|
||||||
|
item => item.Def.Equals("Underwear", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
private static string ItemsSnapshot(Roster roster) =>
|
private static string ItemsSnapshot(Roster roster) =>
|
||||||
string.Join(
|
string.Join(
|
||||||
'\n',
|
'\n',
|
||||||
|
|||||||
Reference in New Issue
Block a user