test(saves): lock unresolved-type handling in v4 and legacy v3 loading

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-07-16 12:08:49 -07:00
parent 98b054ad8d
commit 50d02cffec
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace Server.Tests;
@ -47,4 +49,104 @@ public class GenericEntityPersistenceTypeTableTests
persistence.Unregister();
}
}
[Fact]
public void UnresolvedTableEntrySkipsItsRecordsAfterConfirmation()
{
var previousAssemblies = AssemblyHandler.Assemblies;
AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly];
var dir = Path.Combine(Path.GetTempPath(), $"muo-typetable-{Guid.NewGuid():N}");
Directory.CreateDirectory(Path.Combine(dir, "TypeTable"));
TypeTablePersistence loaded = null;
var previousIn = Console.In;
try
{
// Hand-write a v4 idx: two table entries (one bogus), one record per type.
using (var idx = new FileBufferWriter(Path.Combine(dir, "TypeTable", "TypeTable.idx")))
{
idx.Write(4); // version
idx.Write(2); // type table count
idx.WriteRaw(typeof(RoundTripEntity).FullName); // index 0: resolvable
idx.WriteRaw("Server.Tests.DoesNotExistAnymore"); // index 1: bogus
idx.Write(2); // record count
idx.Write((ushort)0); // record 1: real type
idx.Write(1u); // serial
idx.Write(DateTime.UtcNow.Ticks);
idx.Write(0L); // position
idx.Write(4); // length
idx.Write((ushort)1); // record 2: bogus type
idx.Write(2u);
idx.Write(DateTime.UtcNow.Ticks);
idx.Write(4L);
idx.Write(4);
}
// GetConstructorFor prompts on the console; answer "y" (delete those types).
Console.SetIn(new StringReader("y\n"));
loaded = new TypeTablePersistence();
loaded.DeserializeIndexes(dir, null);
Assert.Single(loaded.EntitiesBySerial);
Assert.True(loaded.EntitiesBySerial.ContainsKey((Serial)1u));
Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
}
finally
{
Console.SetIn(previousIn);
loaded?.Unregister();
AssemblyHandler.Assemblies = previousAssemblies;
Directory.Delete(dir, true);
}
}
[Fact]
public void LegacyV3IndexesStillLoadAndHydrateTheTypeTable()
{
var previousAssemblies = AssemblyHandler.Assemblies;
AssemblyHandler.Assemblies = [.. previousAssemblies ?? [], typeof(RoundTripEntity).Assembly];
var dir = Path.Combine(Path.GetTempPath(), $"muo-legacyidx-{Guid.NewGuid():N}");
Directory.CreateDirectory(Path.Combine(dir, "TypeTable"));
TypeTablePersistence loaded = null;
try
{
var hash = AssemblyHandler.GetTypeHash(typeof(RoundTripEntity));
// Hand-write a v3 idx: records carry flag + 8-byte hash, resolved via typesDb.
using (var idx = new FileBufferWriter(Path.Combine(dir, "TypeTable", "TypeTable.idx")))
{
idx.Write(3); // version
idx.Write(1); // record count
idx.Write((byte)2); // xxHash3 flag
idx.Write(hash);
idx.Write(1u); // serial
idx.Write(DateTime.UtcNow.Ticks);
idx.Write(0L); // position
idx.Write(4); // length
}
var typesDb = new Dictionary<ulong, string> { [hash] = typeof(RoundTripEntity).FullName };
loaded = new TypeTablePersistence();
loaded.DeserializeIndexes(dir, typesDb);
Assert.Single(loaded.EntitiesBySerial);
Assert.True(loaded.EntitiesBySerial.ContainsKey((Serial)1u));
// Legacy loads must hydrate the table so the NEXT save can write v4 indexes.
Assert.True(loaded.TryGetTypeIndex(typeof(RoundTripEntity), out _));
}
finally
{
loaded?.Unregister();
AssemblyHandler.Assemblies = previousAssemblies;
Directory.Delete(dir, true);
}
}
}