Files
Ludarium release export df869819ce
Public source validation / source (push) Successful in 2m16s
Publish Ludarium source
2026-09-03 02:08:58 +02:00

72 lines
3.5 KiB
C#

using Ludarium.Application;
using Ludarium.Domain;
using Ludarium.Infrastructure;
using Npgsql;
using Testcontainers.PostgreSql;
namespace Ludarium.IntegrationTests;
/// <summary>
/// What a scan concludes about the files that were not there any more.
/// </summary>
/// <remarks>
/// Reconciliation compared the stored state against the name "Present" while every other query in
/// the store reads the enum's ordinal. The comparison matched no row, so a ROM deleted from the
/// archive stayed Present: it kept its Play control, counted towards the archive's size and never
/// appeared as missing. Only the real database can prove the ordinal contract, so it is proven here.
/// </remarks>
public sealed class ArtifactReconciliationTests
{
[Fact]
[Trait("Category", "Container")]
public async Task AFileThatLeftTheArchiveIsMarkedMissing()
{
if (!string.Equals(Environment.GetEnvironmentVariable("LUDARIUM_RUN_CONTAINER_TESTS"), "1", StringComparison.Ordinal))
return;
await using var postgres = new PostgreSqlBuilder("postgres:16.15-bookworm")
.WithDatabase("ludarium_reconciliation_test")
.WithUsername("ludarium")
.WithPassword("synthetic-test-password")
.Build();
await postgres.StartAsync();
await using var dataSource = NpgsqlDataSource.Create(postgres.GetConnectionString());
var store = new PostgresStore(dataSource);
await store.InitializeAsync(CancellationToken.None);
var library = LibraryRoot.Create("Reconciliation fixture",
Path.GetFullPath(Path.Combine(Path.GetTempPath(), "ludarium-reconciliation-library")), LibraryKind.Rom) with
{ IsReadOnly = true, IsAvailable = true };
await store.UpsertLibraryAsync(library, CancellationToken.None);
var observed = "roms/nes/Kept.nes";
var removed = "roms/nes/Deleted.nes";
foreach (var path in new[] { observed, removed })
await store.UpsertArtifactAsync(
new(Guid.NewGuid(), library.Id, path, 24_592, DateTimeOffset.UtcNow.AddDays(-2),
ArtifactState.Present, MediaType.Rom, Confidence.Deterministic, null,
DateTimeOffset.UtcNow.AddDays(-2), DateTimeOffset.UtcNow.AddDays(-2), null, "nes"),
null, [], CancellationToken.None);
var at = DateTimeOffset.UtcNow;
var marked = await store.MarkMissingExceptAsync(library.Id, new HashSet<string>([observed]), at, CancellationToken.None);
Assert.Equal(1, marked);
var gone = await store.FindByPathAsync(library.Id, removed, CancellationToken.None);
// Reading it back through the store proves the written state is the shape the record expects.
Assert.Equal(ArtifactState.Missing, gone!.State);
Assert.Equal(2, gone.Version);
var kept = await store.FindByPathAsync(library.Id, observed, CancellationToken.None);
Assert.Equal(ArtifactState.Present, kept!.State);
Assert.Equal(1, kept.Version);
// The same ordinal contract is read by the storage summary, so the archive reports the loss.
var summary = await store.GetStorageSummaryAsync(CancellationToken.None);
var entry = Assert.Single(summary.Libraries, item => item.LibraryId == library.Id);
Assert.Equal(1, entry.Missing);
// A second scan that still cannot see the file leaves it alone rather than counting it twice.
Assert.Equal(0, await store.MarkMissingExceptAsync(library.Id, new HashSet<string>([observed]), at, CancellationToken.None));
}
}