Skip to content

Commit a15910d

Browse files
authored
Merge pull request #823 from Chris0Jeky/test/resilience-degraded-mode-behavior
test: resilience and degraded-mode behavior tests (#720)
2 parents 1306428 + 9ae405f commit a15910d

3 files changed

Lines changed: 901 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using FluentAssertions;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Taskdeck.Api.Tests.Support;
6+
using Taskdeck.Application.DTOs;
7+
using Taskdeck.Application.Interfaces;
8+
using Taskdeck.Domain.Entities;
9+
using Taskdeck.Domain.Enums;
10+
using Taskdeck.Infrastructure.Persistence;
11+
using Xunit;
12+
13+
namespace Taskdeck.Api.Tests.Resilience;
14+
15+
/// <summary>
16+
/// Tests that queue items accumulate correctly when workers are not processing
17+
/// (simulated by having processing disabled or worker stopped), and that items
18+
/// remain consistent (no corruption) and are processable on restart.
19+
/// Covers issue #720 (TST-67): "All workers stopped → queue items accumulate
20+
/// but don't corrupt; restart processes them."
21+
/// </summary>
22+
public class QueueAccumulationResilienceTests : IClassFixture<TestWebApplicationFactory>
23+
{
24+
private readonly TestWebApplicationFactory _factory;
25+
26+
public QueueAccumulationResilienceTests(TestWebApplicationFactory factory)
27+
{
28+
_factory = factory;
29+
}
30+
31+
// ── Queue Items Accumulate Without Corruption ─────────────────────
32+
33+
[Fact]
34+
public async Task QueueItems_AccumulateWithoutCorruption_WhenWorkersNotProcessing()
35+
{
36+
// Arrange: create a user, then enqueue multiple capture items.
37+
// The background worker may process them, but the important assertion is
38+
// that items are created with correct status and no data corruption occurs
39+
// regardless of worker state.
40+
using var client = _factory.CreateClient();
41+
await ApiTestHarness.AuthenticateAsync(client, "queue-accum-resilience");
42+
43+
// Create multiple capture items in quick succession.
44+
var itemIds = new List<Guid>();
45+
for (var i = 0; i < 5; i++)
46+
{
47+
var response = await client.PostAsJsonAsync(
48+
"/api/capture/items",
49+
new CreateCaptureItemDto(null, $"Queue accumulation test item {i}"));
50+
response.StatusCode.Should().Be(HttpStatusCode.Created,
51+
$"capture item {i} should be accepted");
52+
53+
var item = await response.Content.ReadFromJsonAsync<CaptureItemDto>();
54+
item.Should().NotBeNull();
55+
itemIds.Add(item!.Id);
56+
}
57+
58+
// Assert: all items should exist and have consistent state.
59+
var listResponse = await client.GetAsync("/api/capture/items?limit=100");
60+
listResponse.StatusCode.Should().Be(HttpStatusCode.OK);
61+
62+
var listPayload = await listResponse.Content.ReadFromJsonAsync<CaptureItemSummaryDto[]>();
63+
listPayload.Should().NotBeNull();
64+
65+
// All 5 items should be present (they may have been processed already by the worker,
66+
// but none should be missing or corrupted).
67+
foreach (var id in itemIds)
68+
{
69+
listPayload!.Should().Contain(
70+
i => i.Id == id,
71+
$"item {id} should exist in the queue regardless of worker state");
72+
}
73+
74+
// No item should be in an invalid/corrupted status.
75+
foreach (var item in listPayload!.Where(i => itemIds.Contains(i.Id)))
76+
{
77+
item.Status.Should().BeDefined(
78+
"item status should always be set to a valid enum value");
79+
}
80+
}
81+
82+
// ── Queue Items Are Processable After Accumulation ───────────────
83+
84+
[Fact]
85+
public async Task QueuedItems_RemainProcessable_AfterAccumulation()
86+
{
87+
// Verify that items created during worker downtime have valid status
88+
// and are in a state that allows future processing.
89+
using var scope = _factory.Services.CreateScope();
90+
var dbContext = scope.ServiceProvider.GetRequiredService<TaskdeckDbContext>();
91+
92+
var user = new User("queue-processable-user", "queue-processable@example.com", "hash");
93+
dbContext.Users.Add(user);
94+
await dbContext.SaveChangesAsync();
95+
96+
// Create LLM queue items directly (bypassing API to simulate accumulated items).
97+
var items = new List<LlmRequest>();
98+
for (var i = 0; i < 3; i++)
99+
{
100+
var item = new LlmRequest(user.Id, "instruction", $"Create card {i}", null);
101+
items.Add(item);
102+
dbContext.Add(item);
103+
}
104+
await dbContext.SaveChangesAsync();
105+
106+
// Assert: all items should start as Pending and be individually processable.
107+
foreach (var item in items)
108+
{
109+
await dbContext.Entry(item).ReloadAsync();
110+
item.Status.Should().Be(RequestStatus.Pending,
111+
"accumulated items should be in Pending status, ready for processing");
112+
item.RetryCount.Should().Be(0,
113+
"fresh items should have zero retry count");
114+
}
115+
116+
// Simulate a worker picking up the first item (MarkAsProcessing).
117+
items[0].MarkAsProcessing();
118+
await dbContext.SaveChangesAsync();
119+
await dbContext.Entry(items[0]).ReloadAsync();
120+
121+
items[0].Status.Should().Be(RequestStatus.Processing,
122+
"first item should transition to Processing when claimed by a worker");
123+
124+
// Other items should remain Pending (not affected by the first item's transition).
125+
await dbContext.Entry(items[1]).ReloadAsync();
126+
await dbContext.Entry(items[2]).ReloadAsync();
127+
items[1].Status.Should().Be(RequestStatus.Pending,
128+
"other items should remain Pending when one is claimed");
129+
items[2].Status.Should().Be(RequestStatus.Pending);
130+
}
131+
132+
// ── Capture Items Do Not Corrupt Under Rapid Submission ──────────
133+
134+
[Fact]
135+
public async Task RapidCaptureSubmission_DoesNotCorruptQueue()
136+
{
137+
using var client = _factory.CreateClient();
138+
await ApiTestHarness.AuthenticateAsync(client, "queue-rapid-submit");
139+
140+
// Submit captures as fast as possible (no await between sends).
141+
var tasks = Enumerable.Range(0, 10).Select(i =>
142+
client.PostAsJsonAsync(
143+
"/api/capture/items",
144+
new CreateCaptureItemDto(null, $"Rapid item {i}")));
145+
146+
var responses = await Task.WhenAll(tasks);
147+
148+
// All submissions should succeed (201 Created).
149+
foreach (var response in responses)
150+
{
151+
response.StatusCode.Should().Be(HttpStatusCode.Created,
152+
"every rapid submission should succeed without corruption");
153+
}
154+
155+
// Verify items are retrievable.
156+
var listResponse = await client.GetAsync("/api/capture/items?limit=100");
157+
listResponse.StatusCode.Should().Be(HttpStatusCode.OK);
158+
159+
var payload = await listResponse.Content.ReadFromJsonAsync<CaptureItemSummaryDto[]>();
160+
payload.Should().NotBeNull();
161+
payload!.Should().HaveCountGreaterThanOrEqualTo(10,
162+
"all rapidly submitted items should be present");
163+
}
164+
}

0 commit comments

Comments
 (0)