Retry a failed portrait from the notice with the same scene builder.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class NoticeImageApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task OwnerGenerate_MatchesCardOutcome_GuestIs403()
|
||||
{
|
||||
using var owner = await SchoolApiTests.CreateIsolatedClientAsync(fixture.App, "NoticeImageOwner");
|
||||
await SchoolApiTests.WipeAllSavesAsync(owner);
|
||||
var school = await SchoolApiTests.CreateAsync(owner, "Кнопка хозяина", Start);
|
||||
var personId = await FirstPersonIdAsync(owner, school.Id);
|
||||
var posted = await PostNoticeAsync(owner, school.Id, "GenerationFailed", personId, "full");
|
||||
|
||||
using var card = await owner.PostAsync(
|
||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait?kind=full",
|
||||
content: null,
|
||||
TestContext.Current.CancellationToken);
|
||||
using var fromNotice = await owner.PostAsync(
|
||||
$"/api/schools/{school.Id}/notices/{posted.Id}/generate",
|
||||
content: null,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(card.StatusCode, fromNotice.StatusCode);
|
||||
Assert.Equal(await SchoolApiTests.ProblemCodeAsync(card), await SchoolApiTests.ProblemCodeAsync(fromNotice));
|
||||
|
||||
var paused = (await SchoolApiTests.GetSchoolsAsync(owner)).Schools.Single(item => item.Id == school.Id);
|
||||
Assert.False(paused.Running);
|
||||
|
||||
using var guest = await SchoolApiTests.CreateIsolatedClientAsync(fixture.App, "NoticeImageGuest");
|
||||
using var forbidden = await guest.PostAsync(
|
||||
$"/api/schools/{school.Id}/notices/{posted.Id}/generate",
|
||||
content: null,
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode);
|
||||
Assert.Equal("not-owner", await SchoolApiTests.ProblemCodeAsync(forbidden));
|
||||
|
||||
var stillPaused = (await SchoolApiTests.GetSchoolsAsync(owner)).Schools.Single(item => item.Id == school.Id);
|
||||
Assert.False(stillPaused.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateWithoutPerson_DoesNotCallSwarm()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Без человека", Start);
|
||||
var posted = await PostNoticeAsync(client, school.Id, "GenerationFailed");
|
||||
|
||||
using var response = await client.PostAsync(
|
||||
$"/api/schools/{school.Id}/notices/{posted.Id}/generate",
|
||||
content: null,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var code = await SchoolApiTests.ProblemCodeAsync(response);
|
||||
Assert.Equal("notice-cannot-generate", code);
|
||||
Assert.NotEqual("swarmui-not-configured", code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoticePersonPrompt_ContainsThePersonNotAnEmptyCorridor()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Сцена с человеком", Start);
|
||||
var personId = await FirstPersonIdAsync(client, school.Id);
|
||||
await PostNoticeAsync(client, school.Id, "GenerationFailed", personId, "full");
|
||||
|
||||
using var response = await client.GetAsync(
|
||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait/prompt?kind=full",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<PortraitPromptPayload>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.Contains("years old", payload!.Positive, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("wearing", payload.Positive, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotMatch(@"\bage \d+", payload.Positive);
|
||||
Assert.DoesNotContain("empty corridor", payload.Positive, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.Negative));
|
||||
}
|
||||
|
||||
private static async Task<string> FirstPersonIdAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var page = await client.GetFromJsonAsync<PeopleListResponse>(
|
||||
$"/api/schools/{schoolId}/people?pageSize=1",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(page);
|
||||
Assert.NotEmpty(page!.People);
|
||||
return page.People[0].Id;
|
||||
}
|
||||
|
||||
private static async Task<DevNoticeResponse> PostNoticeAsync(
|
||||
HttpClient client,
|
||||
int schoolId,
|
||||
string defName,
|
||||
string? person = null,
|
||||
string? kind = null)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/dev/schools/{schoolId}/notices",
|
||||
new { defName, person, kind },
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var body = await response.Content.ReadFromJsonAsync<DevNoticeResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
private sealed record PeopleListResponse(int Total, IReadOnlyList<PersonListItem> People);
|
||||
|
||||
private sealed record PersonListItem(string Id);
|
||||
|
||||
private sealed record DevNoticeResponse(uint Id, string DefName, bool Pause);
|
||||
|
||||
private sealed record PortraitPromptPayload(string Kind, string Positive, string Negative);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class EventDefTests
|
||||
Assert.True(catalog.Events["GenerationFailed"].Pause);
|
||||
Assert.Equal(0, catalog.Events["GenerationFailed"].TtlMs);
|
||||
Assert.Equal(EventTriggers.GenerationFailed, catalog.Events["GenerationFailed"].Trigger);
|
||||
Assert.Equal(EventActions.None, catalog.Events["GenerationFailed"].Action);
|
||||
Assert.Equal(EventActions.GenerateImage, catalog.Events["GenerationFailed"].Action);
|
||||
Assert.Equal("Начало дня", catalog.Label("ru", catalog.Events["DayStarted"]));
|
||||
Assert.Equal("The day has started", catalog.Label("en", catalog.Events["DayStarted"]));
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class ProtocolCodecTests
|
||||
[Fact]
|
||||
public void Notice_RoundTripsAndMatchesByteLayout()
|
||||
{
|
||||
var message = new ServerNoticeMessage(0x0A0B0C0D, "DayStarted", NoticeSeverity.Info, Pause: false, TtlMs: 8000, PersonId: 0);
|
||||
var message = new ServerNoticeMessage(0x0A0B0C0D, "DayStarted", NoticeSeverity.Info, Pause: false, TtlMs: 8000, PersonId: 0, Action: NoticeAction.None);
|
||||
var size = ProtocolCodec.NoticeSize(message);
|
||||
Span<byte> buffer = stackalloc byte[size];
|
||||
|
||||
@@ -128,6 +128,8 @@ public class ProtocolCodecTests
|
||||
Assert.Equal(0, buffer[18]);
|
||||
Assert.Equal(8000u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buffer[19..23]));
|
||||
Assert.Equal(0u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buffer[23..27]));
|
||||
Assert.Equal((ushort)4, System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(buffer[27..29]));
|
||||
Assert.Equal("none"u8.ToArray(), buffer[29..33].ToArray());
|
||||
Assert.Equal(message, ProtocolCodec.ReadNotice(buffer[..length]));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using HSchool.Server.Game;
|
||||
|
||||
namespace HSchool.Server.Tests;
|
||||
|
||||
public class GenerationFailedPosterTests
|
||||
{
|
||||
[Fact]
|
||||
public void SwarmFailure_ShouldPost_SuccessAndNotConfiguredShouldNot()
|
||||
{
|
||||
Assert.True(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.Unavailable));
|
||||
Assert.True(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.TimedOut));
|
||||
Assert.False(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.Succeeded));
|
||||
Assert.False(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.NotConfigured));
|
||||
Assert.False(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.InvalidPrompt));
|
||||
Assert.False(GenerationFailedPoster.ShouldPost(PortraitGenerationOutcome.UnknownPerson));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryEnqueue_WithPerson_PostsGenerationFailed()
|
||||
{
|
||||
var queue = new GameCommandQueue();
|
||||
|
||||
GenerationFailedPoster.TryEnqueue(queue, schoolId: 4, personId: "a0.p0", PortraitKind.Avatar);
|
||||
|
||||
var command = await queue.Reader.ReadAsync(TestContext.Current.CancellationToken);
|
||||
var post = Assert.IsType<GameCommand.PostSchoolNotice>(command);
|
||||
Assert.Equal(4, post.SchoolId);
|
||||
Assert.Equal(GenerationFailedPoster.DefName, post.DefName);
|
||||
Assert.Equal("a0.p0", post.PersonKey);
|
||||
Assert.Equal("avatar", post.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryEnqueue_WithoutPerson_DoesNotPost()
|
||||
{
|
||||
var queue = new GameCommandQueue();
|
||||
|
||||
GenerationFailedPoster.TryEnqueue(queue, schoolId: 4, personId: "", PortraitKind.Full);
|
||||
GenerationFailedPoster.TryEnqueue(queue, schoolId: 4, personId: " ", PortraitKind.Full);
|
||||
|
||||
Assert.False(queue.Reader.TryRead(out _));
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,12 @@ public class NoticeBoardTests
|
||||
{
|
||||
var board = new NoticeBoard(maxSticky: 8);
|
||||
|
||||
Assert.True(board.TryPost(Info(), personId: 0, out var info));
|
||||
Assert.True(board.TryPost(Info(), personKey: "", kind: "", out var info));
|
||||
Assert.False(info.Pause);
|
||||
Assert.False(board.HasPausing);
|
||||
Assert.Empty(board.Sticky);
|
||||
|
||||
Assert.True(board.TryPost(Pausing(), personId: 0, out var warning));
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "", kind: "", out var warning));
|
||||
Assert.True(warning.Pause);
|
||||
Assert.True(board.HasPausing);
|
||||
Assert.Equal("GenerationFailed", Assert.Single(board.Sticky).DefName);
|
||||
@@ -29,10 +29,10 @@ public class NoticeBoardTests
|
||||
var board = new NoticeBoard(maxSticky: 8);
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
Assert.True(board.TryPost(Pausing(), personId: 0, out _));
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "", kind: "", out _));
|
||||
}
|
||||
|
||||
Assert.False(board.TryPost(Pausing(), personId: 0, out var ninth));
|
||||
Assert.False(board.TryPost(Pausing(), personKey: "", kind: "", out var ninth));
|
||||
Assert.Equal(default, ninth);
|
||||
Assert.Equal(8, board.Sticky.Count);
|
||||
}
|
||||
@@ -41,8 +41,8 @@ public class NoticeBoardTests
|
||||
public void Dismiss_RemovesSticky_DoesNotClearTheBoardUntilTheLast()
|
||||
{
|
||||
var board = new NoticeBoard(maxSticky: 8);
|
||||
Assert.True(board.TryPost(Pausing(), personId: 0, out var first));
|
||||
Assert.True(board.TryPost(Pausing(), personId: 0, out var second));
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "", kind: "", out var first));
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "", kind: "", out var second));
|
||||
|
||||
Assert.True(board.TryDismiss(first.Id));
|
||||
Assert.True(board.HasPausing);
|
||||
@@ -71,6 +71,30 @@ public class NoticeBoardTests
|
||||
Assert.Equal(NoticeSeverity.Error, sticky.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateImage_WithoutPerson_StillPosts_WirePersonIdIsZero()
|
||||
{
|
||||
var board = new NoticeBoard(maxSticky: 8);
|
||||
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "", kind: "full", out var message));
|
||||
Assert.Equal(0u, message.PersonId);
|
||||
Assert.Equal(EventActions.GenerateImage, message.Action);
|
||||
Assert.Equal("", Assert.Single(board.Sticky).PersonKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateImage_WithPerson_StoresKeyAndKind()
|
||||
{
|
||||
var board = new NoticeBoard(maxSticky: 8);
|
||||
|
||||
Assert.True(board.TryPost(Pausing(), personKey: "a0.p0", kind: "avatar", out var message));
|
||||
Assert.Equal(1u, message.PersonId);
|
||||
Assert.Equal(EventActions.GenerateImage, message.Action);
|
||||
var sticky = Assert.Single(board.Sticky);
|
||||
Assert.Equal("a0.p0", sticky.PersonKey);
|
||||
Assert.Equal("avatar", sticky.Kind);
|
||||
}
|
||||
|
||||
private static EventDef Info() => new()
|
||||
{
|
||||
DefName = "DayStarted",
|
||||
@@ -88,6 +112,6 @@ public class NoticeBoardTests
|
||||
Pause = true,
|
||||
TtlMs = 0,
|
||||
Trigger = EventTriggers.GenerationFailed,
|
||||
Action = EventActions.None,
|
||||
Action = EventActions.GenerateImage,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user