Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
+107
@@ -0,0 +1,107 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using ItWorx.KnowledgeHub.Contracts.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.Domain.Knowledge;
|
||||
using ItWorx.KnowledgeHub.Domain.Projects;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SortKey = ItWorx.KnowledgeHub.Domain.Knowledge.SortKey;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.UnifiedSearch;
|
||||
|
||||
[Collection(PostgresTestGroup.Name)]
|
||||
[Trait("Category", "ReleaseScale")]
|
||||
public sealed class UnifiedSearchScaleTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Unified_search_counts_and_cursor_pages_stay_bounded_with_10000_pages_and_2000_projects()
|
||||
{
|
||||
using var client = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var workspaceId = await db.Workspaces.Select(item => item.Id).SingleAsync();
|
||||
var actorId = await db.Users.Select(item => item.Id).SingleAsync();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var sortKey = SortKey.First();
|
||||
|
||||
for (var offset = 0; offset < 10_000; offset += 500)
|
||||
{
|
||||
for (var index = offset; index < offset + 500; index++)
|
||||
{
|
||||
var title = index == 0 ? "Measure uniqueneedle page" : $"Measure page {index:D5}";
|
||||
var page = Page.Create(workspaceId, null, sortKey, PageTitle.Create(title), DocumentType.WikiPage,
|
||||
"Measure synthetic documentation with a bounded result snippet.", actorId, now);
|
||||
page.ReplaceTags(["platform-scale"]);
|
||||
db.Pages.Add(page);
|
||||
sortKey = SortKey.Next(sortKey);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
|
||||
for (var offset = 0; offset < 2_000; offset += 500)
|
||||
{
|
||||
for (var index = offset; index < offset + 500; index++)
|
||||
{
|
||||
var title = index == 0 ? "Measure uniqueneedle project" : $"Measure project {index:D5}";
|
||||
db.Projects.Add(Project.Create(workspaceId, title, "Measure synthetic project summary.",
|
||||
ProjectPriority.Medium, "Measure project state.", actorId, actorId, now));
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
await db.Database.ExecuteSqlRawAsync("ANALYZE pages; ANALYZE projects;");
|
||||
|
||||
var repository = new UnifiedSearchRepository(db);
|
||||
var first = await repository.SearchAsync(workspaceId, "measure", null, null, null, 50, CancellationToken.None);
|
||||
Assert.Equal(10_000, first.Counts.Knowledge);
|
||||
Assert.Equal(2_000, first.Counts.Projects);
|
||||
Assert.Equal(50, first.Items.Count);
|
||||
Assert.NotNull(first.NextCursor);
|
||||
var next = await repository.SearchAsync(workspaceId, "measure", null, null, first.NextCursor, 50, CancellationToken.None);
|
||||
Assert.Equal(first.Counts, next.Counts);
|
||||
Assert.Equal(50, next.Items.Count);
|
||||
Assert.Empty(first.Items.Select(item => item.Id).Intersect(next.Items.Select(item => item.Id)));
|
||||
|
||||
var report = new StringBuilder();
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"Unified search scale — {DateTimeOffset.UtcNow:O}; 10000 pages, 2000 projects; 30 warm samples per query.");
|
||||
foreach (var query in new[] { "measure", "uniqueneedle" })
|
||||
{
|
||||
var samples = new List<double>();
|
||||
for (var sample = 0; sample < 30; sample++)
|
||||
{
|
||||
var watch = Stopwatch.StartNew();
|
||||
var result = await repository.SearchAsync(workspaceId, query, null, null, null, 50, CancellationToken.None);
|
||||
watch.Stop();
|
||||
Assert.Equal(query == "measure" ? 10_000 : 1, result.Counts.Knowledge);
|
||||
Assert.Equal(query == "measure" ? 2_000 : 1, result.Counts.Projects);
|
||||
Assert.InRange(result.Items.Count, 1, 50);
|
||||
samples.Add(watch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
samples.Sort();
|
||||
var p95 = samples[28];
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"{query}: p50={samples[14]:F2}ms p95={p95:F2}ms max={samples[^1]:F2}ms");
|
||||
// Same broad correctness/latency budget as existing scale gates; this
|
||||
// exercises the new grouped-count query rather than the legacy search.
|
||||
Assert.True(p95 < 3_000, $"Unified search p95 for '{query}' was {p95:F2}ms (budget 3000ms).");
|
||||
}
|
||||
|
||||
using var response = await client.GetAsync("/api/v1/unified-search?q=measure&limit=50");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
var body = await response.Content.ReadFromJsonAsync<UnifiedSearchResponse>();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(50, body.Items.Count);
|
||||
Assert.Equal(12_000, body.Counts.Total);
|
||||
Assert.True(bytes.Length < 100_000, $"One 50-item search page contained {bytes.Length} bytes.");
|
||||
report.AppendLine(CultureInfo.InvariantCulture, $"HTTP 50-item response: {bytes.Length} bytes; total=12000; no duplicate IDs across cursor pages.");
|
||||
Directory.CreateDirectory("artifacts/performance");
|
||||
await File.WriteAllTextAsync("artifacts/performance/platform-unified-search-scale.txt", report.ToString());
|
||||
}
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using ItWorx.KnowledgeHub.Application.Identity;
|
||||
using ItWorx.KnowledgeHub.Contracts.Admin;
|
||||
using ItWorx.KnowledgeHub.Contracts.Automation;
|
||||
using ItWorx.KnowledgeHub.Contracts.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.Domain.Automation;
|
||||
using ItWorx.KnowledgeHub.Domain.Decisions;
|
||||
using ItWorx.KnowledgeHub.Domain.Identity;
|
||||
using ItWorx.KnowledgeHub.Domain.Infrastructure;
|
||||
using ItWorx.KnowledgeHub.Domain.Knowledge;
|
||||
using ItWorx.KnowledgeHub.Domain.Projects;
|
||||
using ItWorx.KnowledgeHub.Domain.Runbooks;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.UnifiedSearch;
|
||||
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.UnifiedSearch;
|
||||
|
||||
[Collection(PostgresTestGroup.Name)]
|
||||
public sealed class UnifiedSearchTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Search_returns_workspace_scoped_visible_items_with_full_counts_snippets_and_global_pagination()
|
||||
{
|
||||
using var owner = await LoginOwnerAsync();
|
||||
var expected = await SeedSearchCorpusAsync();
|
||||
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var workspaces = scope.ServiceProvider.GetRequiredService<IWorkspaceRepository>();
|
||||
var workspaceId = await workspaces.GetDefaultWorkspaceIdAsync(CancellationToken.None);
|
||||
Assert.NotNull(workspaceId);
|
||||
|
||||
var repository = new UnifiedSearchRepository(db);
|
||||
var firstPage = await repository.SearchAsync(workspaceId.Value, "needle", null, null, null, 2, CancellationToken.None);
|
||||
|
||||
Assert.Equal(new UnifiedSearchCountsResultExpected(1, 1, 1, 1, 1), new UnifiedSearchCountsResultExpected(
|
||||
firstPage.Counts.Knowledge, firstPage.Counts.Projects, firstPage.Counts.Services,
|
||||
firstPage.Counts.Runbooks, firstPage.Counts.Decisions));
|
||||
Assert.Equal(2, firstPage.Items.Count);
|
||||
Assert.NotNull(firstPage.NextCursor);
|
||||
|
||||
var allHits = firstPage.Items.ToList();
|
||||
var cursor = firstPage.NextCursor;
|
||||
while (cursor is not null)
|
||||
{
|
||||
var next = await repository.SearchAsync(workspaceId.Value, "needle", null, null, cursor, 2, CancellationToken.None);
|
||||
Assert.Equal(firstPage.Counts, next.Counts);
|
||||
allHits.AddRange(next.Items);
|
||||
cursor = next.NextCursor;
|
||||
}
|
||||
|
||||
Assert.Equal(5, allHits.Count);
|
||||
Assert.Equal(5, allHits.Select(item => (item.Kind, item.Id)).Distinct().Count());
|
||||
Assert.Equal(new[] { "decision", "knowledge", "project", "runbook", "service" },
|
||||
allHits.Select(item => item.Kind).Order(StringComparer.Ordinal).ToArray());
|
||||
Assert.All(allHits, item => Assert.Contains("**needle**", item.Snippet, StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(allHits, item => item.Id == expected.KnowledgePageId && item.Href == $"/knowledge/{expected.KnowledgePageId}/needle-knowledge-guide");
|
||||
Assert.Contains(allHits, item => item.Id == expected.ProjectId && item.Href == $"/projects/{expected.ProjectId}");
|
||||
Assert.Contains(allHits, item => item.Id == expected.ServiceId && item.Href == $"/infrastructure/services/{expected.ServiceId}");
|
||||
Assert.Contains(allHits, item => item.Id == expected.RunbookId && item.Href == $"/runbooks/{expected.RunbookId}");
|
||||
Assert.Contains(allHits, item => item.Id == expected.DecisionId && item.Href == $"/projects/{expected.ProjectId}/decisions/{expected.DecisionId}");
|
||||
|
||||
using var reader = await CreateReaderClientAsync(owner);
|
||||
var firstHttpPage = await reader.GetFromJsonAsync<UnifiedSearchResponse>("/api/v1/unified-search?q=needle&limit=2");
|
||||
Assert.NotNull(firstHttpPage);
|
||||
Assert.Equal(2, firstHttpPage.Items.Count);
|
||||
Assert.Equal(1, firstHttpPage.Counts.Knowledge);
|
||||
Assert.Equal(1, firstHttpPage.Counts.Projects);
|
||||
Assert.Equal(1, firstHttpPage.Counts.Services);
|
||||
Assert.Equal(1, firstHttpPage.Counts.Runbooks);
|
||||
Assert.Equal(1, firstHttpPage.Counts.Decisions);
|
||||
Assert.NotNull(firstHttpPage.NextCursor);
|
||||
|
||||
var serviceHttpPage = await reader.GetFromJsonAsync<UnifiedSearchResponse>("/api/v1/unified-search?q=needle&kind=services&limit=2");
|
||||
Assert.NotNull(serviceHttpPage);
|
||||
Assert.Single(serviceHttpPage.Items);
|
||||
Assert.Equal("services", serviceHttpPage.Items[0].Kind);
|
||||
Assert.Equal(5, serviceHttpPage.Counts.Total);
|
||||
using var invalidKind = await reader.GetAsync("/api/v1/unified-search?q=needle&kind=service");
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, invalidKind.StatusCode);
|
||||
|
||||
Assert.DoesNotContain(allHits, item => item.Title.Contains("hidden", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(allHits, item => item.Title.Contains("archived", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(allHits, item => item.Title.Contains("proposed", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(allHits, item => item.Title.Contains("trashed", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(allHits, item => item.Title.Contains("foreign", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(allHits, item => item.Snippet.Contains("hidden secret", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tag_only_search_filters_visible_page_kinds_and_binds_cursors_to_the_normalized_tag()
|
||||
{
|
||||
using var owner = await LoginOwnerAsync();
|
||||
var expected = await SeedSearchCorpusAsync();
|
||||
var decoyPageId = await AddTagFilterDecoyAsync(expected.WorkspaceId);
|
||||
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var repository = new UnifiedSearchRepository(db);
|
||||
var firstPage = await repository.SearchAsync(
|
||||
expected.WorkspaceId, string.Empty, " TOPIC ", null, null, 1, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, firstPage.Counts.Knowledge);
|
||||
Assert.Equal(0, firstPage.Counts.Projects);
|
||||
Assert.Equal(0, firstPage.Counts.Services);
|
||||
Assert.Equal(1, firstPage.Counts.Runbooks);
|
||||
Assert.Equal(1, firstPage.Counts.Decisions);
|
||||
Assert.Single(firstPage.Items);
|
||||
Assert.Equal(expected.DecisionId, firstPage.Items[0].Id);
|
||||
Assert.NotNull(firstPage.NextCursor);
|
||||
|
||||
// Casing and surrounding whitespace normalize to the same tag, so this cursor
|
||||
// continues the original page instead of repeating its first row.
|
||||
var nextPage = await repository.SearchAsync(
|
||||
expected.WorkspaceId, string.Empty, "topic", null, firstPage.NextCursor, 2, CancellationToken.None);
|
||||
Assert.Equal(2, nextPage.Items.Count);
|
||||
Assert.Null(nextPage.NextCursor);
|
||||
Assert.DoesNotContain(nextPage.Items, item => item.Id == firstPage.Items[0].Id);
|
||||
Assert.Contains(nextPage.Items, item => item.Id == expected.KnowledgePageId);
|
||||
Assert.Contains(nextPage.Items, item => item.Id == expected.RunbookId);
|
||||
Assert.Equal(firstPage.Counts, nextPage.Counts);
|
||||
|
||||
// A cursor from the broader topic result must not skip the only item in a
|
||||
// different tag result, even when that item is the same decision page.
|
||||
var differentTag = await repository.SearchAsync(
|
||||
expected.WorkspaceId, string.Empty, "DECISION-ONLY", null, firstPage.NextCursor, 2, CancellationToken.None);
|
||||
Assert.Single(differentTag.Items);
|
||||
Assert.Equal(expected.DecisionId, differentTag.Items[0].Id);
|
||||
Assert.Equal(0, differentTag.Counts.Knowledge);
|
||||
Assert.Equal(0, differentTag.Counts.Projects);
|
||||
Assert.Equal(0, differentTag.Counts.Services);
|
||||
Assert.Equal(0, differentTag.Counts.Runbooks);
|
||||
Assert.Equal(1, differentTag.Counts.Decisions);
|
||||
|
||||
var exactTagWithText = await repository.SearchAsync(
|
||||
expected.WorkspaceId, "needle", "TOPIC", null, null, 10, CancellationToken.None);
|
||||
Assert.Equal(1, exactTagWithText.Counts.Knowledge);
|
||||
Assert.Equal(0, exactTagWithText.Counts.Projects);
|
||||
Assert.Equal(0, exactTagWithText.Counts.Services);
|
||||
Assert.Equal(1, exactTagWithText.Counts.Runbooks);
|
||||
Assert.Equal(1, exactTagWithText.Counts.Decisions);
|
||||
Assert.DoesNotContain(exactTagWithText.Items, item => item.Id == decoyPageId);
|
||||
|
||||
using var reader = await CreateReaderClientAsync(owner);
|
||||
var tagOnlyHttp = await reader.GetFromJsonAsync<UnifiedSearchResponse>("/api/v1/unified-search?tag=TOPIC&limit=10");
|
||||
Assert.NotNull(tagOnlyHttp);
|
||||
Assert.Equal(3, tagOnlyHttp.Items.Count);
|
||||
Assert.Equal(1, tagOnlyHttp.Counts.Knowledge);
|
||||
Assert.Equal(0, tagOnlyHttp.Counts.Projects);
|
||||
Assert.Equal(0, tagOnlyHttp.Counts.Services);
|
||||
Assert.Equal(1, tagOnlyHttp.Counts.Runbooks);
|
||||
Assert.Equal(1, tagOnlyHttp.Counts.Decisions);
|
||||
|
||||
var textAndTagHttp = await reader.GetFromJsonAsync<UnifiedSearchResponse>("/api/v1/unified-search?q=needle&tag=TOPIC&limit=10");
|
||||
Assert.NotNull(textAndTagHttp);
|
||||
Assert.Equal(3, textAndTagHttp.Items.Count);
|
||||
Assert.DoesNotContain(textAndTagHttp.Items, item => item.Id == decoyPageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unified_search_requires_a_cookie_reader_and_does_not_grant_unified_access_to_knowledge_search_tokens()
|
||||
{
|
||||
using var owner = await LoginOwnerAsync();
|
||||
using var reader = await CreateReaderClientAsync(owner);
|
||||
|
||||
using var readerResponse = await reader.GetAsync("/api/v1/unified-search?q=scope-probe");
|
||||
Assert.Equal(HttpStatusCode.OK, readerResponse.StatusCode);
|
||||
|
||||
var tokenResponse = await SetupHelper.PostWithCsrfAsync(
|
||||
owner,
|
||||
"/api/v1/service-accounts",
|
||||
new CreateServiceAccountRequest($"unified-search-{Guid.NewGuid():N}", "Search authorization test.", [Scopes.KnowledgeSearch]));
|
||||
Assert.Equal(HttpStatusCode.OK, tokenResponse.StatusCode);
|
||||
var token = await tokenResponse.Content.ReadFromJsonAsync<CreatedServiceAccountToken>();
|
||||
Assert.NotNull(token);
|
||||
|
||||
using var bearer = Factory.CreateClient();
|
||||
bearer.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.RawToken);
|
||||
using var existingKnowledgeSearch = await bearer.GetAsync("/api/v1/search?q=scope-probe");
|
||||
Assert.Equal(HttpStatusCode.OK, existingKnowledgeSearch.StatusCode);
|
||||
|
||||
using var unifiedSearch = await bearer.GetAsync("/api/v1/unified-search?q=scope-probe");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, unifiedSearch.StatusCode);
|
||||
|
||||
using var anonymous = Factory.CreateClient();
|
||||
using var anonymousResponse = await anonymous.GetAsync("/api/v1/unified-search?q=scope-probe");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, anonymousResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unified_search_rate_limits_a_reader_after_sixty_requests_per_minute()
|
||||
{
|
||||
using var owner = await LoginOwnerAsync();
|
||||
using var reader = await CreateReaderClientAsync(owner);
|
||||
|
||||
for (var requestNumber = 1; requestNumber <= 61; requestNumber++)
|
||||
{
|
||||
using var response = await reader.GetAsync("/api/v1/unified-search?q=");
|
||||
var expected = requestNumber <= 60 ? HttpStatusCode.OK : HttpStatusCode.TooManyRequests;
|
||||
Assert.Equal(expected, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpClient> LoginOwnerAsync()
|
||||
{
|
||||
var owner = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(owner, OwnerEmail, OwnerPassword);
|
||||
return owner;
|
||||
}
|
||||
|
||||
private async Task<HttpClient> CreateReaderClientAsync(HttpClient owner)
|
||||
{
|
||||
var email = $"reader-{Guid.NewGuid():N}@itworx.test";
|
||||
var created = await SetupHelper.PostWithCsrfAsync(
|
||||
owner,
|
||||
"/api/v1/admin/users",
|
||||
new CreateUserRequest("Unified search Reader", email, "reader-strong-password", "Reader"));
|
||||
Assert.Equal(HttpStatusCode.OK, created.StatusCode);
|
||||
|
||||
var reader = Factory.CreateClient();
|
||||
await SetupHelper.LoginAsync(reader, email, "reader-strong-password");
|
||||
return reader;
|
||||
}
|
||||
|
||||
private async Task<SearchCorpusIds> SeedSearchCorpusAsync()
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var workspaceId = await scope.ServiceProvider.GetRequiredService<IWorkspaceRepository>()
|
||||
.GetDefaultWorkspaceIdAsync(CancellationToken.None);
|
||||
Assert.NotNull(workspaceId);
|
||||
var actorId = await db.Users.AsNoTracking()
|
||||
.Where(user => user.Email == OwnerEmail)
|
||||
.Select(user => user.Id)
|
||||
.SingleAsync();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var project = Project.Create(workspaceId.Value, "Needle project", "Project summary for needle search.",
|
||||
ProjectPriority.Medium, "Project state needle.", actorId, actorId, now);
|
||||
|
||||
var knowledgePage = CreatePage(workspaceId.Value, actorId, now, "Needle knowledge guide", DocumentType.WikiPage,
|
||||
"Knowledge body with needle details.");
|
||||
var trashedPage = CreatePage(workspaceId.Value, actorId, now, "Needle trashed entry", DocumentType.WikiPage,
|
||||
"This trashed record must not appear.");
|
||||
trashedPage.Trash(actorId, now, TimeSpan.FromDays(30));
|
||||
|
||||
var runbookPage = CreatePage(workspaceId.Value, actorId, now, "Needle runbook procedure", DocumentType.Runbook,
|
||||
"Runbook body with needle procedure details.");
|
||||
var runbook = RunbookProfile.Create(runbookPage.Id, actorId, RunbookRiskLevel.Low, false, 20, null, now);
|
||||
|
||||
var decisionPage = CreatePage(workspaceId.Value, actorId, now, "Needle decision record", DocumentType.ArchitectureDecision,
|
||||
"Decision body with needle context.");
|
||||
knowledgePage.ReplaceTags(["Topic"]);
|
||||
trashedPage.ReplaceTags(["TOPIC"]);
|
||||
runbookPage.ReplaceTags(["topic"]);
|
||||
decisionPage.ReplaceTags(["tOpIc", "Decision-Only"]);
|
||||
var decision = DecisionRecord.Create(workspaceId.Value, project.Id, decisionPage.Id, 1,
|
||||
DateOnly.FromDateTime(now.UtcDateTime), "Decision summary for needle search.", null, actorId, now);
|
||||
|
||||
var foreignWorkspace = Workspace.Create(WorkspaceName.Create("Second search workspace"), now);
|
||||
var foreignPage = CreatePage(foreignWorkspace.Id, actorId, now, "Needle foreign page", DocumentType.WikiPage,
|
||||
"Foreign workspace content with needle.");
|
||||
foreignPage.ReplaceTags(["Topic"]);
|
||||
|
||||
var snapshot = InventorySnapshot.Create(workspaceId.Value, $"unified-search-{Guid.NewGuid():N}",
|
||||
Guid.NewGuid().ToString("N"), now, now, 0, 4);
|
||||
var visibleService = CreateService(workspaceId.Value, snapshot.Id, "needle-service-visible", "Needle visible service", actorId, now, reviewed: true);
|
||||
var hiddenService = CreateService(workspaceId.Value, snapshot.Id, "needle-service-hidden", "Needle hidden service", actorId, now, reviewed: true);
|
||||
var archivedService = CreateService(workspaceId.Value, snapshot.Id, "needle-service-archived", "Needle archived service", actorId, now, reviewed: true);
|
||||
var proposedService = CreateService(workspaceId.Value, snapshot.Id, "needle-service-proposed", "Needle proposed service", actorId, now, reviewed: false);
|
||||
var visibleProfile = CreateServiceProfile(workspaceId.Value, visibleService.Id, actorId, now, isHidden: false, isArchived: false);
|
||||
var hiddenProfile = CreateServiceProfile(workspaceId.Value, hiddenService.Id, actorId, now, isHidden: true, isArchived: false);
|
||||
var archivedProfile = CreateServiceProfile(workspaceId.Value, archivedService.Id, actorId, now, isHidden: false, isArchived: true);
|
||||
|
||||
db.Workspaces.Add(foreignWorkspace);
|
||||
db.Projects.Add(project);
|
||||
db.Pages.AddRange(knowledgePage, trashedPage, runbookPage, decisionPage, foreignPage);
|
||||
db.RunbookProfiles.Add(runbook);
|
||||
db.DecisionRecords.Add(decision);
|
||||
db.InventorySnapshots.Add(snapshot);
|
||||
db.InfrastructureServices.AddRange(visibleService, hiddenService, archivedService, proposedService);
|
||||
db.EditorialProfiles.AddRange(visibleProfile, hiddenProfile, archivedProfile);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return new SearchCorpusIds(workspaceId.Value, knowledgePage.Id, project.Id, visibleService.Id, runbook.Id, decision.Id);
|
||||
}
|
||||
|
||||
private async Task<Guid> AddTagFilterDecoyAsync(Guid workspaceId)
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
var actorId = await db.Users.AsNoTracking()
|
||||
.Where(user => user.Email == OwnerEmail)
|
||||
.Select(user => user.Id)
|
||||
.SingleAsync();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var decoy = CreatePage(workspaceId, actorId, now, "Needle tag decoy", DocumentType.WikiPage,
|
||||
"A needle page tagged with a longer, non-matching label.");
|
||||
decoy.ReplaceTags(["topic-extra"]);
|
||||
db.Pages.Add(decoy);
|
||||
await db.SaveChangesAsync();
|
||||
return decoy.Id;
|
||||
}
|
||||
|
||||
private static Page CreatePage(Guid workspaceId, Guid actorId, DateTimeOffset now, string title, DocumentType type, string markdown) =>
|
||||
Page.Create(workspaceId, null, SortKey.First(), PageTitle.Create(title), type, DocumentStatus.Active, markdown, actorId, now);
|
||||
|
||||
private static InfrastructureService CreateService(
|
||||
Guid workspaceId, Guid snapshotId, string sourceKey, string name, Guid actorId, DateTimeOffset now, bool reviewed)
|
||||
{
|
||||
var service = InfrastructureService.Create(workspaceId, sourceKey, name, now, snapshotId);
|
||||
service.Refresh(null, name, "container", "running", HealthState.Healthy, "example/needle:latest", null, string.Empty,
|
||||
false, false, false, FactConfidence.High, snapshotId, now);
|
||||
if (reviewed)
|
||||
{
|
||||
service.Confirm();
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
private static EditorialProfile CreateServiceProfile(
|
||||
Guid workspaceId, Guid serviceId, Guid actorId, DateTimeOffset now, bool isHidden, bool isArchived)
|
||||
{
|
||||
var profile = EditorialProfile.Create(workspaceId, InfrastructureObjectKind.Service, serviceId, actorId, now);
|
||||
profile.Update("Needle service profile", "Editorial needle summary.", "Infrastructure", "Purpose with needle context.",
|
||||
null, null, null, null, null, null, null, null, null, null, ["needle"], false, isHidden, isArchived, actorId, now);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private sealed record SearchCorpusIds(Guid WorkspaceId, Guid KnowledgePageId, Guid ProjectId, Guid ServiceId, Guid RunbookId, Guid DecisionId);
|
||||
|
||||
private sealed record UnifiedSearchCountsResultExpected(int Knowledge, int Projects, int Services, int Runbooks, int Decisions);
|
||||
}
|
||||
Reference in new issue
Block a user