Give each school its own people seed so composition no longer follows create order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 00:28:13 +03:00
co-authored by Cursor
parent 0c3eb219c8
commit 48f0b15221
17 changed files with 233 additions and 35 deletions
@@ -16,6 +16,7 @@ public class GoldenSaveTests(AppHostFixture fixture)
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);
@@ -34,6 +35,8 @@ public class GoldenSaveTests(AppHostFixture fixture)
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);
@@ -139,14 +139,14 @@ public class PeopleApiTests(AppHostFixture fixture)
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди семья", Start);
var school = await SchoolApiTests.CreateAsync(client, "Люди семья", Start, seed: 1);
var page = await GetPeopleAsync(client, school.Id, "role=student&year=5&pageSize=10");
// Every fifth-to-tenth family is single-parent on purpose (design/people.md), and the
// roster seed is the school id — which depends on how many schools ran before this test.
// Demanding two parents from whoever happens to sort first therefore fails on some runs;
// look for a pupil who has a mother instead of assuming the first one does.
// Every fifth-to-tenth family is single-parent on purpose (design/people.md). The roster
// seed is no longer the school id, but a given page still mixes complete and single-parent
// houses, so demanding two parents from whoever sorts first fails on some seeds. Look for
// a pupil who has a mother instead of assuming the first one does.
PersonCardResponse? card = null;
foreach (var candidate in page.People)
{
+13 -5
View File
@@ -371,9 +371,13 @@ public class SchoolApiTests(AppHostFixture fixture)
return state;
}
internal static async Task<SchoolResponse> CreateAsync(HttpClient client, string name, DateTime startDate)
internal static async Task<SchoolResponse> CreateAsync(
HttpClient client,
string name,
DateTime startDate,
int? seed = null)
{
using var response = await PostAsync(client, name, startDate);
using var response = await PostAsync(client, name, startDate, seed);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<SchoolResponse>(TestContext.Current.CancellationToken);
@@ -406,10 +410,14 @@ public class SchoolApiTests(AppHostFixture fixture)
links = new[] { new { a = "yard", b = "office" } },
};
private static Task<HttpResponseMessage> PostAsync(HttpClient client, string name, DateTime startDate) =>
private static Task<HttpResponseMessage> PostAsync(
HttpClient client,
string name,
DateTime startDate,
int? seed = null) =>
client.PostAsJsonAsync(
"/api/schools",
new { name, startDate },
seed is null ? (object)new { name, startDate } : new { name, startDate, seed },
TestContext.Current.CancellationToken);
private static async Task<string?> ProblemCodeAsync(HttpResponseMessage response)
@@ -418,7 +426,7 @@ public class SchoolApiTests(AppHostFixture fixture)
return problem?.Code;
}
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex);
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex, int Seed);
internal sealed record SchoolsResponse(
int MaxSchools,
@@ -0,0 +1,90 @@
using System.Net.Http.Json;
namespace HSchool.AppHost.Tests;
/// <summary>
/// The people seed is a school field of its own, not the school id. These tests share the
/// AppHost, so each one starts by clearing the list.
/// </summary>
[Collection(AppHostCollection.Name)]
public class SchoolSeedTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task SameSeed_PopulatesTheSamePeople_DifferentSeedsDoNot()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var first = await SchoolApiTests.CreateAsync(client, "Сид одинаковый А", Start, seed: 42);
var second = await SchoolApiTests.CreateAsync(client, "Сид одинаковый Б", Start, seed: 42);
var other = await SchoolApiTests.CreateAsync(client, "Сид другой", Start, seed: 7);
var firstYearFive = await YearFiveAsync(client, first.Id);
var secondYearFive = await YearFiveAsync(client, second.Id);
var otherYearFive = await YearFiveAsync(client, other.Id);
Assert.Equal(firstYearFive, secondYearFive);
Assert.NotEqual(firstYearFive, otherYearFive);
}
[Fact]
public async Task UnseededSchool_KeepsItsPeopleAfterReload()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var created = await SchoolApiTests.CreateAsync(client, "Сид брошен", Start);
var before = await YearFiveAsync(client, created.Id);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var restored = (await SchoolApiTests.GetSchoolsAsync(client)).Schools.Single(school => school.Id == created.Id);
Assert.Equal(created.Seed, restored.Seed);
Assert.Equal(before, await YearFiveAsync(client, created.Id));
}
[Fact]
public async Task PassedSeed_IsVisibleInTheSchoolList()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var created = await SchoolApiTests.CreateAsync(client, "Сид в списке", Start, seed: 20260819);
Assert.Equal(20260819, created.Seed);
var listed = (await SchoolApiTests.GetSchoolsAsync(client)).Schools.Single(school => school.Id == created.Id);
Assert.Equal(20260819, listed.Seed);
}
[Fact]
public async Task TwoUnseededSchools_ArePopulatedDifferently()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var first = await SchoolApiTests.CreateAsync(client, "Без сида А", Start);
var second = await SchoolApiTests.CreateAsync(client, "Без сида Б", Start);
Assert.NotEqual(first.Seed, second.Seed);
Assert.NotEqual(await YearFiveAsync(client, first.Id), await YearFiveAsync(client, second.Id));
}
private static async Task<(string Id, string FullName)[]> YearFiveAsync(HttpClient client, int schoolId)
{
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{schoolId}/people?role=student&year=5&sort=surname&pageSize=100",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
Assert.NotEmpty(page.People);
return [.. page.People.Select(person => (person.Id, person.FullName))];
}
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id, string FullName);
}