using System.Diagnostics; using System.Net.Http.Json; using ItWorx.KnowledgeHub.Contracts.Knowledge; using ItWorx.KnowledgeHub.Domain.Knowledge; using ItWorx.KnowledgeHub.Infrastructure.Persistence; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; using Microsoft.Extensions.DependencyInjection; namespace ItWorx.KnowledgeHub.IntegrationTests.Knowledge; /// A 10,000-node synthetic hierarchy remains navigable without loading the /// entire tree into the browser (docs/27-acceptance-matrix.md, M2-A07): the API must /// serve one bounded page of children quickly, regardless of how many siblings exist /// in total. [Collection(PostgresTestGroup.Name)] public sealed class PageTreeScaleTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres) { private const int ChildCount = 10_000; [Fact] public async Task Fetching_one_page_of_children_under_a_10000_child_node_stays_fast_and_bounded() { using var client = Factory.CreateClient(); await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword); var createResponse = await SetupHelper.PostWithCsrfAsync( client, "/api/v1/pages", new CreatePageRequest(null, "Grote categorie", "WikiPage", "Root", null)); var parent = await createResponse.Content.ReadFromJsonAsync(); Assert.NotNull(parent); await SeedChildrenDirectlyAsync(parent.Id); var stopwatch = Stopwatch.StartNew(); var firstPage = await client.GetFromJsonAsync( $"/api/v1/pages/{parent.Id}/children?limit=50"); stopwatch.Stop(); Assert.NotNull(firstPage); Assert.Equal(50, firstPage.Items.Count); Assert.False(string.IsNullOrEmpty(firstPage.NextCursor)); // Generous bound for a CI/dev machine: the point is "bounded and fast", not a // precise performance benchmark (that belongs to the M8 release scale suite). Assert.True( stopwatch.Elapsed < TimeSpan.FromSeconds(5), $"Fetching one bounded page of children took {stopwatch.Elapsed}, expected it to stay well under 5s regardless of the {ChildCount} total children."); var secondPage = await client.GetFromJsonAsync( $"/api/v1/pages/{parent.Id}/children?limit=50&cursor={Uri.EscapeDataString(firstPage.NextCursor!)}"); Assert.NotNull(secondPage); Assert.Equal(50, secondPage.Items.Count); Assert.Empty(firstPage.Items.Select(item => item.Id).Intersect(secondPage.Items.Select(item => item.Id))); } private async Task SeedChildrenDirectlyAsync(Guid parentId) { // Seeded directly through the DbContext rather than 10,000 HTTP calls — this // test is about proving the *read* path stays bounded at scale, not about // exercising page creation (already covered by PageCrudTests). using var scope = Factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var parent = await db.Pages.FindAsync(parentId); Assert.NotNull(parent); var workspaceId = parent.WorkspaceId; var actorId = parent.CreatedBy ?? Guid.NewGuid(); var now = DateTimeOffset.UtcNow; var sortKey = SortKey.First(); var batch = new List(1000); for (var i = 0; i < ChildCount; i++) { var page = Page.Create( workspaceId, parentId, sortKey, PageTitle.Create($"Kindpagina {i:D5}"), DocumentType.WikiPage, "Inhoud", actorId, now); batch.Add(page); sortKey = SortKey.Next(sortKey); if (batch.Count == 1000) { db.Pages.AddRange(batch); await db.SaveChangesAsync(); db.ChangeTracker.Clear(); batch.Clear(); } } if (batch.Count > 0) { db.Pages.AddRange(batch); await db.SaveChangesAsync(); } } }