Merge branch 'review/slice-7-a-recheck'

This commit is contained in:
Leonid Pershin
2026-08-20 15:57:12 +03:00
7 changed files with 182 additions and 137 deletions
+43 -97
View File
@@ -5,49 +5,31 @@ using System.Text.Json.Nodes;
namespace HSchool.AppHost.Tests;
/// <summary>
/// Phase 25 goldens. The folders stay historic (format 2, no country): that shape must not start.
/// Load tests stamp today's format, country and owner onto a copy so missing-field defaults and
/// seed-equals-id still have a host assertion after later slices raised <c>CanStart</c>.
/// Phase 25 goldens stay historic (format 2, <c>nameSetId: Slavic</c>). Phase 29 lifts that to
/// Russia in memory; the file is not rewritten until the school saves itself. Slice 6 recheck
/// treated format 2 as unstartable — that gate contradicted the lift, so these tests load.
/// </summary>
[Collection(AppHostCollection.Name)]
public class GoldenSaveTests(AppHostFixture fixture)
{
[Fact]
public async Task HistoricFormat2Golden_LeavesTheSchoolUnstartedAndTheFileUntouched()
public async Task CurrentFormatSave_LoadsTheSamePeopleAndTime()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
await InstallAsync(client, "current");
try
{
InstallFolder(directory, "current");
var path = Path.Combine(directory, "1.json");
var before = File.ReadAllBytes(path);
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);
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);
reload.EnsureSuccessStatusCode();
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();
}
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"));
}
[Fact]
@@ -55,84 +37,46 @@ public class GoldenSaveTests(AppHostFixture fixture)
{
using var client = fixture.App.CreateHttpClient("server");
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);
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();
}
Assert.Contains("\"items\"", File.ReadAllText(Path.Combine(directory, "1.people.json")), StringComparison.Ordinal);
}
private static async Task InstallRunnableAsync(
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)
private async Task InstallAsync(HttpClient client, string folder)
{
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
foreach (var file in Directory.EnumerateFiles(source))
{
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))
?? throw new InvalidOperationException($"Save {path} parsed to nothing.");
node["format"] = 3;
node["countryId"] = "Russia";
node["climatePresetId"] = "TemperateContinental";
node["owner"] = SchoolApiTests.TestUserName;
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);
}
}
node["owner"] = userName;
File.WriteAllText(
path,
node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
}
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
@@ -164,6 +108,8 @@ public class GoldenSaveTests(AppHostFixture fixture)
.ToArray();
}
private sealed record SessionResponse(string UserName);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
@@ -130,9 +130,9 @@ public class DressGeneratorTests
roster.Families,
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);
Assert.False(DressGenerator.NeedsDressing(dressed, ApplicantPool.Empty));
Assert.False(DressGenerator.NeedsDressing(catalog, dressed, ApplicantPool.Empty));
Assert.Equal(ItemsSnapshot(roster), ItemsSnapshot(dressed));
}
@@ -180,6 +180,34 @@ public class DressGeneratorTests
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) =>
string.Join(
'\n',