Bust stale critical source image caches

This commit is contained in:
2026-04-12 21:49:37 +02:00
parent 0968864af6
commit 222abc155b
3 changed files with 152 additions and 748 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -49,10 +49,18 @@ api.MapGet("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, i
var result = await lookupService.GetCriticalCellEditorAsync(slug, resultId, cancellationToken);
return result is null ? Results.NotFound() : Results.Ok(result);
});
api.MapGet("/tables/critical/{slug}/cells/{resultId:int}/source-image", async (string slug, int resultId, LookupService lookupService, CancellationToken cancellationToken) =>
api.MapGet("/tables/critical/{slug}/cells/{resultId:int}/source-image", async (string slug, int resultId, LookupService lookupService, HttpContext httpContext, CancellationToken cancellationToken) =>
{
var filePath = await lookupService.GetCriticalSourceImagePathAsync(slug, resultId, cancellationToken);
return filePath is null ? Results.NotFound() : Results.File(filePath, "image/png");
if (filePath is null)
{
return Results.NotFound();
}
httpContext.Response.Headers.CacheControl = "no-store, no-cache, max-age=0";
httpContext.Response.Headers.Pragma = "no-cache";
httpContext.Response.Headers.Expires = "0";
return Results.File(filePath, "image/png");
});
api.MapPost("/tables/critical/{slug}/cells/{resultId:int}/reparse", async (string slug, int resultId, CriticalCellReparseRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
{

View File

@@ -1,5 +1,4 @@
using Microsoft.EntityFrameworkCore;
using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
using RolemasterDb.App.Features;
@@ -25,22 +24,9 @@ public sealed class LookupServiceCurationIntegrationTests
Assert.NotNull(initialResponse);
Assert.False(initialResponse!.IsCurated);
Assert.Equal(2, initialResponse.SourcePageNumber);
Assert.Equal($"/api/tables/critical/slash/cells/{resultId}/source-image", initialResponse.SourceImageUrl);
Assert.StartsWith($"/api/tables/critical/slash/cells/{resultId}/source-image?v=", initialResponse.SourceImageUrl, StringComparison.Ordinal);
var updateRequest = new CriticalCellUpdateRequest(
initialResponse.RawCellText,
initialResponse.QuickParseInput,
initialResponse.DescriptionText,
initialResponse.RawAffixText,
initialResponse.ParseStatus,
initialResponse.ParsedJson,
true,
initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden,
initialResponse.AreBranchesOverridden,
initialResponse.Effects,
initialResponse.Branches);
var updateRequest = new CriticalCellUpdateRequest(initialResponse.RawCellText, initialResponse.QuickParseInput, initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, true, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches);
var updatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, updateRequest);
Assert.NotNull(updatedResponse);
@@ -56,6 +42,32 @@ public sealed class LookupServiceCurationIntegrationTests
Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
}
[Fact]
public async Task Lookup_service_changes_source_image_url_when_source_provenance_changes()
{
var databasePath = CreateEmptyDatabasePath();
var repositoryRoot = CreateTemporaryRepositoryRoot();
var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2);
WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png");
WriteSourceImage(repositoryRoot, "slash/cells/source-cell-v2.png");
var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
var resultId = await GetResultIdAsync(databasePath);
var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
Assert.NotNull(initialResponse);
Assert.StartsWith($"/api/tables/critical/slash/cells/{resultId}/source-image?v=", initialResponse!.SourceImageUrl, StringComparison.Ordinal);
await UpdateSourceMetadataAsync(databasePath, resultId, sourcePageNumber: 4, sourceImagePath: "slash/cells/source-cell-v2.png", sourceImageCropJson: "{\"pageNumber\":4,\"boundsLeft\":12}");
var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
Assert.NotNull(reopenedResponse);
Assert.StartsWith($"/api/tables/critical/slash/cells/{resultId}/source-image?v=", reopenedResponse!.SourceImageUrl, StringComparison.Ordinal);
Assert.NotEqual(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
}
[Fact]
public async Task Lookup_service_clears_curated_state_when_any_content_is_edited()
{
@@ -72,29 +84,13 @@ public sealed class LookupServiceCurationIntegrationTests
var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
Assert.NotNull(initialResponse);
var markCuratedRequest = new CriticalCellUpdateRequest(
initialResponse!.RawCellText,
initialResponse.QuickParseInput,
initialResponse.DescriptionText,
initialResponse.RawAffixText,
initialResponse.ParseStatus,
initialResponse.ParsedJson,
true,
initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden,
initialResponse.AreBranchesOverridden,
initialResponse.Effects,
initialResponse.Branches);
var markCuratedRequest = new CriticalCellUpdateRequest(initialResponse!.RawCellText, initialResponse.QuickParseInput, initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, true, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches);
var curatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, markCuratedRequest);
Assert.NotNull(curatedResponse);
Assert.True(curatedResponse!.IsCurated);
var editedRequest = markCuratedRequest with
{
DescriptionText = "Edited description after curation."
};
var editedRequest = markCuratedRequest with { DescriptionText = "Edited description after curation." };
var editedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, editedRequest);
Assert.NotNull(editedResponse);
@@ -123,44 +119,12 @@ public sealed class LookupServiceCurationIntegrationTests
Assert.NotNull(initialResponse);
Assert.False(initialResponse!.IsCurated);
var reparsedResponse = await lookupService.ReparseCriticalCellAsync(
"slash",
resultId,
new CriticalCellUpdateRequest(
initialResponse.RawCellText,
"Edited quick parse input.",
initialResponse.DescriptionText,
initialResponse.RawAffixText,
initialResponse.ParseStatus,
initialResponse.ParsedJson,
initialResponse.IsCurated,
initialResponse.IsDescriptionOverridden,
initialResponse.IsRawAffixTextOverridden,
initialResponse.AreEffectsOverridden,
initialResponse.AreBranchesOverridden,
initialResponse.Effects,
initialResponse.Branches));
var reparsedResponse = await lookupService.ReparseCriticalCellAsync("slash", resultId, new CriticalCellUpdateRequest(initialResponse.RawCellText, "Edited quick parse input.", initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, initialResponse.IsCurated, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches));
Assert.NotNull(reparsedResponse);
Assert.False(reparsedResponse!.IsCurated);
var curatedResponse = await lookupService.UpdateCriticalCellAsync(
"slash",
resultId,
new CriticalCellUpdateRequest(
reparsedResponse.RawCellText,
reparsedResponse.QuickParseInput,
reparsedResponse.DescriptionText,
reparsedResponse.RawAffixText,
reparsedResponse.ParseStatus,
reparsedResponse.ParsedJson,
true,
reparsedResponse.IsDescriptionOverridden,
reparsedResponse.IsRawAffixTextOverridden,
reparsedResponse.AreEffectsOverridden,
reparsedResponse.AreBranchesOverridden,
reparsedResponse.Effects,
reparsedResponse.Branches));
var curatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, new CriticalCellUpdateRequest(reparsedResponse.RawCellText, reparsedResponse.QuickParseInput, reparsedResponse.DescriptionText, reparsedResponse.RawAffixText, reparsedResponse.ParseStatus, reparsedResponse.ParsedJson, true, reparsedResponse.IsDescriptionOverridden, reparsedResponse.IsRawAffixTextOverridden, reparsedResponse.AreEffectsOverridden, reparsedResponse.AreBranchesOverridden, reparsedResponse.Effects, reparsedResponse.Branches));
Assert.NotNull(curatedResponse);
Assert.True(curatedResponse!.IsCurated);
@@ -263,26 +227,29 @@ public sealed class LookupServiceCurationIntegrationTests
private static async Task<int> GetResultIdAsync(string databasePath)
{
await using var dbContext = CreateDbContext(databasePath);
return await dbContext.CriticalResults
.Where(item => item.CriticalTable.Slug == "slash")
.Select(item => item.Id)
.SingleAsync();
return await dbContext.CriticalResults.Where(item => item.CriticalTable.Slug == "slash").Select(item => item.Id).SingleAsync();
}
private static async Task UpdateSourceMetadataAsync(string databasePath, int resultId, int sourcePageNumber, string sourceImagePath, string sourceImageCropJson)
{
await using var dbContext = CreateDbContext(databasePath);
var result = await dbContext.CriticalResults.SingleAsync(item => item.Id == resultId);
result.SourcePageNumber = sourcePageNumber;
result.SourceImagePath = sourceImagePath;
result.SourceImageCropJson = sourceImageCropJson;
await dbContext.SaveChangesAsync();
}
private static RolemasterDbContext CreateDbContext(string databasePath)
{
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
.UseSqlite($"Data Source={databasePath}")
.Options;
var options = new DbContextOptionsBuilder<RolemasterDbContext>().UseSqlite($"Data Source={databasePath}").Options;
return new RolemasterDbContext(options);
}
private static IDbContextFactory<RolemasterDbContext> CreateDbContextFactory(string databasePath)
{
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
.UseSqlite($"Data Source={databasePath}")
.Options;
var options = new DbContextOptionsBuilder<RolemasterDbContext>().UseSqlite($"Data Source={databasePath}").Options;
return new TestRolemasterDbContextFactory(options);
}