Merge main into review/slice-7bc-climate-wear; journal 32-37.
Integrate stage A review from main; resolve reviewed.md; fix dressRulesPanel unused import, managementPanel test, PortraitApiTests URL.
This commit is contained in:
@@ -54,6 +54,38 @@ public class DressRulesApiTests(AppHostFixture fixture)
|
||||
Assert.Equal(before.Worn.Select(row => row.DefName), after.Worn.Select(row => row.DefName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownForm_Returns400()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Плохая форма", Start, seed: 35);
|
||||
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/dress-rules",
|
||||
new { students = new { form = "tuxedo", color = "noBright" } },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("unknown-form", await SchoolApiTests.ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnknownColor_Returns400()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Плохой цвет", Start, seed: 35);
|
||||
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/dress-rules",
|
||||
new { students = new { form = "regular", color = "neon" } },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("unknown-color", await SchoolApiTests.ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
private sealed record DressRulesDto(DressRulePairDto Students, DressRulePairDto Staff);
|
||||
|
||||
private sealed record DressRulePairDto(string Form, string Color);
|
||||
|
||||
@@ -86,6 +86,20 @@ public class PortraitApiTests(AppHostFixture fixture)
|
||||
Assert.Equal("avatar", payload!.Kind);
|
||||
Assert.Contains("age", payload.Positive, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.Negative));
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.PresetId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSwarmUiSettings_ReturnsPresets()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
var settings = await client.GetFromJsonAsync<SwarmUiSettingsPayload>(
|
||||
"/api/settings/swarmui",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(settings);
|
||||
Assert.NotEmpty(settings!.Presets);
|
||||
Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -134,7 +148,17 @@ public class PortraitApiTests(AppHostFixture fixture)
|
||||
bool HasFullBody,
|
||||
string? CustomPortraitPrompt);
|
||||
|
||||
private sealed record PortraitPromptPayload(string Kind, string Positive, string Negative, string? PromptExtra);
|
||||
private sealed record PortraitPromptPayload(
|
||||
string Kind,
|
||||
string Positive,
|
||||
string Negative,
|
||||
string? PromptExtra,
|
||||
string PresetId,
|
||||
string PresetLabel);
|
||||
|
||||
private sealed record SwarmUiSettingsPayload(string ActivePresetId, IReadOnlyList<SwarmUiPresetPayload> Presets);
|
||||
|
||||
private sealed record SwarmUiPresetPayload(string Id, string Label);
|
||||
|
||||
private sealed record SavesDirectoryResponse(string Path);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,17 @@ public class AppropriatenessTests
|
||||
Assert.True(Appropriateness.SkirtLengthFits(context, catalog.Things["ShortSkirt"]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhiteTopBlackBottom_RejectsWrongTopOrBottom()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var rules = new DressRulePair(FormPolicies.Regular, ColorPolicies.WhiteTopBlackBottom);
|
||||
Assert.False(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["Shirt"], "Gray"));
|
||||
Assert.True(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["Shirt"], "White"));
|
||||
Assert.False(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["Jeans"], "Blue"));
|
||||
Assert.True(Appropriateness.ColorAllowed(catalog, rules, catalog.Things["Jeans"], "Black"));
|
||||
}
|
||||
|
||||
private static DefCatalog LoadCatalog()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
|
||||
@@ -8,14 +8,10 @@ public class PortraitPromptBuilderTests
|
||||
[Fact]
|
||||
public void Build_IncludesAgeHairAndWornClothing()
|
||||
{
|
||||
var settings = new SwarmUiSettings
|
||||
{
|
||||
Positive = "School photo.",
|
||||
Avatar = new SwarmUiSettings.SwarmUiPreset { Positive = "Head and shoulders." },
|
||||
};
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
|
||||
|
||||
var card = SampleCard();
|
||||
var (positive, _) = PortraitPromptBuilder.Build(card, settings, PortraitKind.Avatar);
|
||||
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar);
|
||||
|
||||
Assert.Contains("age 12", positive, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("black", positive, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -27,15 +23,9 @@ public class PortraitPromptBuilderTests
|
||||
[Fact]
|
||||
public void Build_FullBody_AddsFullBodyPreset()
|
||||
{
|
||||
var settings = new SwarmUiSettings
|
||||
{
|
||||
Positive = "School photo.",
|
||||
Avatar = new SwarmUiSettings.SwarmUiPreset { Positive = "Head and shoulders." },
|
||||
FullBody = new SwarmUiSettings.SwarmUiPreset { Positive = "Standing full body." },
|
||||
};
|
||||
|
||||
var (avatarPositive, _) = PortraitPromptBuilder.Build(SampleCard(), settings, PortraitKind.Avatar);
|
||||
var (fullPositive, _) = PortraitPromptBuilder.Build(SampleCard(), settings, PortraitKind.Full);
|
||||
var preset = SamplePreset();
|
||||
var (avatarPositive, _) = PortraitPromptBuilder.Build(SampleCard(), preset.ToProfile(PortraitKind.Avatar), PortraitKind.Avatar);
|
||||
var (fullPositive, _) = PortraitPromptBuilder.Build(SampleCard(), preset.ToProfile(PortraitKind.Full), PortraitKind.Full);
|
||||
|
||||
Assert.Contains("Head and shoulders.", avatarPositive, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Standing full body.", avatarPositive, StringComparison.Ordinal);
|
||||
@@ -45,17 +35,11 @@ public class PortraitPromptBuilderTests
|
||||
[Fact]
|
||||
public void Build_Custom_UsesPromptExtraInsteadOfPreset()
|
||||
{
|
||||
var settings = new SwarmUiSettings
|
||||
{
|
||||
Positive = "School photo.",
|
||||
Avatar = new SwarmUiSettings.SwarmUiPreset { Positive = "Head and shoulders." },
|
||||
FullBody = new SwarmUiSettings.SwarmUiPreset { Positive = "Standing full body." },
|
||||
};
|
||||
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Custom);
|
||||
const string extra = "standing in a school hallway, soft window light";
|
||||
var (customPositive, _) = PortraitPromptBuilder.Build(
|
||||
SampleCard(),
|
||||
settings,
|
||||
profile,
|
||||
PortraitKind.Custom,
|
||||
extra);
|
||||
|
||||
@@ -65,6 +49,35 @@ public class PortraitPromptBuilderTests
|
||||
Assert.DoesNotContain("Standing full body.", customPositive, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ChildAge_UsesChildSubjectDescriptor()
|
||||
{
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
|
||||
var child = SampleCard() with { Age = 8 };
|
||||
var (positive, _) = PortraitPromptBuilder.Build(child, profile, PortraitKind.Avatar);
|
||||
|
||||
Assert.Contains("young girl", positive, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static SwarmUiPresetDefinition SamplePreset()
|
||||
{
|
||||
var preset = SwarmUiPresetDefinition.CreateDefault();
|
||||
preset.Positive = "School photo.";
|
||||
preset.Avatar = new SwarmUiKindPreset
|
||||
{
|
||||
Width = 512,
|
||||
Height = 512,
|
||||
Positive = "Head and shoulders.",
|
||||
};
|
||||
preset.FullBody = new SwarmUiKindPreset
|
||||
{
|
||||
Width = 512,
|
||||
Height = 768,
|
||||
Positive = "Standing full body.",
|
||||
};
|
||||
return preset;
|
||||
}
|
||||
|
||||
private static PersonCardResponse SampleCard() =>
|
||||
new(
|
||||
"f0.c0",
|
||||
|
||||
@@ -22,15 +22,10 @@ public class SwarmUiClientTests
|
||||
Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }),
|
||||
NullLogger<SwarmUiClient>.Instance);
|
||||
|
||||
var settings = new SwarmUiSettings
|
||||
{
|
||||
Model = "model.safetensors",
|
||||
Steps = 8,
|
||||
CfgScale = 1,
|
||||
Avatar = new SwarmUiSettings.SwarmUiPreset { Width = 512, Height = 512 },
|
||||
};
|
||||
var preset = SwarmUiPresetDefinition.CreateDefault();
|
||||
var profile = preset.ToProfile(PortraitKind.Avatar);
|
||||
|
||||
var bytes = await client.GenerateAsync("a student", "bad", settings, PortraitKind.Avatar, CancellationToken.None);
|
||||
var bytes = await client.GenerateAsync("a student", "bad", profile, CancellationToken.None);
|
||||
|
||||
Assert.Equal([0x89, 0x50, 0x4E, 0x47], bytes.Take(4));
|
||||
Assert.Contains("/API/GetNewSession", handler.Requests[0]);
|
||||
@@ -85,18 +80,15 @@ public class SwarmUiClientTests
|
||||
Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }),
|
||||
NullLogger<SwarmUiClient>.Instance);
|
||||
|
||||
var settings = new SwarmUiSettings
|
||||
{
|
||||
Model = "model.safetensors",
|
||||
Steps = 4,
|
||||
CfgScale = 2,
|
||||
ClipSkip = 2,
|
||||
Sampler = "dpmpp_sde",
|
||||
Scheduler = "karras",
|
||||
Avatar = new SwarmUiSettings.SwarmUiPreset { Width = 512, Height = 512 },
|
||||
};
|
||||
var preset = SwarmUiPresetDefinition.CreateDefault();
|
||||
preset.Steps = 4;
|
||||
preset.CfgScale = 2;
|
||||
preset.ClipSkip = 2;
|
||||
preset.Sampler = "dpmpp_sde";
|
||||
preset.Scheduler = "karras";
|
||||
var profile = preset.ToProfile(PortraitKind.Avatar);
|
||||
|
||||
await client.GenerateAsync("a student", "bad", settings, PortraitKind.Avatar, CancellationToken.None);
|
||||
await client.GenerateAsync("a student", "bad", profile, CancellationToken.None);
|
||||
|
||||
using var document = JsonDocument.Parse(handler.GenerateBody!);
|
||||
Assert.Equal(-2, document.RootElement.GetProperty("clipstopatlayer").GetInt32());
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using HSchool.Server.Game;
|
||||
|
||||
namespace HSchool.Server.Tests;
|
||||
|
||||
public class SwarmUiSettingsStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Resolve_UsesAgeRulePreset()
|
||||
{
|
||||
var config = SwarmUiConfigFile.CreateDefault();
|
||||
var profile = config.Resolve(8, PortraitKind.Avatar);
|
||||
|
||||
Assert.Equal("child", profile.PresetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_FallsBackToActivePresetForAdults()
|
||||
{
|
||||
var config = SwarmUiConfigFile.CreateDefault();
|
||||
var profile = config.Resolve(34, PortraitKind.Full);
|
||||
|
||||
Assert.Equal("default", profile.PresetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeAfterLoad_MigratesLegacyFlatFile()
|
||||
{
|
||||
var config = new SwarmUiConfigFile
|
||||
{
|
||||
Model = "legacy.safetensors",
|
||||
Steps = 12,
|
||||
Positive = "legacy prompt",
|
||||
Avatar = new SwarmUiKindPreset { Width = 640, Height = 640 },
|
||||
};
|
||||
|
||||
config.NormalizeAfterLoad();
|
||||
|
||||
Assert.Single(config.Presets);
|
||||
Assert.Equal("default", config.Presets[0].Id);
|
||||
Assert.Equal("legacy.safetensors", config.Presets[0].Model);
|
||||
Assert.Equal(12, config.Presets[0].Steps);
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,63 @@ public class AppropriatenessSimulationTests
|
||||
Assert.Equal(0, BrightWornCount(school));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingNoBright_AfterMorning_RemovesBrightWornColours()
|
||||
{
|
||||
using var school = Open(TuesdayMorning, seed: 31);
|
||||
MorningDress.Apply(school);
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
PendingStudents = new DressRulePair(FormPolicies.Regular, ColorPolicies.Free),
|
||||
};
|
||||
AdvanceMorning(school);
|
||||
Assert.True(BrightWornCount(school) > 0, "Free colours should allow at least one bright item.");
|
||||
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
PendingStudents = new DressRulePair(FormPolicies.Regular, ColorPolicies.NoBright),
|
||||
};
|
||||
AdvanceMorning(school);
|
||||
Assert.Equal(0, BrightWornCount(school));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhiteTopBlackBottom_AfterMorning_WhiteShirtsOnStudents()
|
||||
{
|
||||
using var school = Open(TuesdayMorning, seed: 31);
|
||||
MorningDress.Apply(school);
|
||||
school.DressRules = school.DressRules with
|
||||
{
|
||||
PendingStudents = new DressRulePair(FormPolicies.Regular, ColorPolicies.WhiteTopBlackBottom),
|
||||
};
|
||||
AdvanceMorning(school);
|
||||
|
||||
var checkedAny = false;
|
||||
foreach (var pupil in school.Roster!.People.Where(person => person.IsStudent))
|
||||
{
|
||||
foreach (var item in pupil.Items.Where(row => row.Location == ItemLocations.Worn))
|
||||
{
|
||||
if (!school.Catalog!.Things[item.Def].Layers.Any(layer =>
|
||||
layer.Equals(ApparelLayers.Top, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
checkedAny = true;
|
||||
Assert.Equal("White", item.Color);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(checkedAny, "Expected at least one pupil with a top-layer shirt.");
|
||||
}
|
||||
|
||||
private static void AdvanceMorning(School school)
|
||||
{
|
||||
var before = school.Clock.Time;
|
||||
school.Clock.JumpTo(before.Date.AddDays(1).AddHours(6));
|
||||
ApparelWear.Apply(school, gameMinutes: 0, before);
|
||||
}
|
||||
|
||||
private static int BrightWornCount(School school)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
Reference in New Issue
Block a user