2026-09-29 19:40:36 +02:00
|
|
|
using System;
|
2026-10-14 23:35:31 +02:00
|
|
|
using System.Collections.Generic;
|
2026-09-29 19:40:36 +02:00
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using GameCult.Caching;
|
|
|
|
|
using GameCult.Caching.MessagePack;
|
|
|
|
|
using MessagePack;
|
|
|
|
|
using NUnit.Framework;
|
|
|
|
|
|
|
|
|
|
#nullable enable
|
|
|
|
|
|
|
|
|
|
namespace GameCult.Mesh.Tests;
|
|
|
|
|
|
|
|
|
|
// C2a: a single-file document written or read through CultMesh is judged as a cache judges it: ids minted, a list that
|
|
|
|
|
// cannot hold ids refused, the header decided by what the file holds.
|
|
|
|
|
public sealed class CultMeshSingleFileElementIdTests
|
|
|
|
|
{
|
|
|
|
|
private const string HexA = "aaaaaaaaaaaa";
|
|
|
|
|
private static readonly CultRecordKey Key = new("publication");
|
2026-10-03 13:08:44 +02:00
|
|
|
// A raw write lands beside the typed record, never over it: neither declares the other's schema id.
|
|
|
|
|
private static readonly CultRecordKey RawKey = new("raw");
|
2026-09-29 19:40:36 +02:00
|
|
|
|
|
|
|
|
private string _root = null!;
|
|
|
|
|
|
|
|
|
|
[SetUp]
|
|
|
|
|
public void SetUp()
|
|
|
|
|
{
|
|
|
|
|
_root = Path.Combine(Path.GetTempPath(), "cultmesh-single-file-ids", Guid.NewGuid().ToString("N"));
|
|
|
|
|
Directory.CreateDirectory(_root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TearDown]
|
|
|
|
|
public void TearDown()
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(_root)) Directory.Delete(_root, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CultMeshBodyPublicationDocument Publication(params CultMeshBodyDescriptor[] representations) => new()
|
|
|
|
|
{
|
|
|
|
|
BodyId = "aetheria:entities",
|
|
|
|
|
ProducerId = "aetheria",
|
|
|
|
|
SchemaId = "eve.entity_soa.v1",
|
|
|
|
|
Representations = representations
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private static string HeaderOf(string path) =>
|
|
|
|
|
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).FormatVersion;
|
|
|
|
|
|
|
|
|
|
private static CultMeshBodyDescriptor Descriptor(string id = "") => new() { Id = id, BodyId = "body" };
|
|
|
|
|
|
|
|
|
|
private string Path_(string name) => Path.Combine(_root, name);
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TypedWriteMintsTheElementIdsAndMarksTheFileThatHoldsThem()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("ids.cc");
|
|
|
|
|
var publication = Publication(Descriptor(), Descriptor());
|
|
|
|
|
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, publication);
|
|
|
|
|
|
|
|
|
|
publication.Representations.Select(entry => entry.Id).Should().OnlyContain(id => id.Length == 12).And.OnlyHaveUniqueItems();
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3");
|
|
|
|
|
CultMesh.ReadSingleFileDocument<CultMeshBodyPublicationDocument>(path, Key).Representations.Select(entry => entry.Id)
|
|
|
|
|
.Should().Equal(publication.Representations.Select(entry => entry.Id));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 21:54:01 +02:00
|
|
|
// A typed write replaces the whole file with a document it can see, so the ids that document holds decide the header.
|
2026-09-29 19:40:36 +02:00
|
|
|
[Test]
|
2026-09-29 21:54:01 +02:00
|
|
|
public void TypedWriteDecidesTheHeaderByTheDocumentItWritesEvenOverAMarkedFile()
|
2026-09-29 19:40:36 +02:00
|
|
|
{
|
|
|
|
|
var path = Path_("none.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication());
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(Descriptor()));
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication());
|
2026-09-29 21:54:01 +02:00
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1", "the whole file is replaced by a document that holds no id");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A raw payload is opaque: the writer cannot see what it holds, so a marked file stays marked.
|
|
|
|
|
[Test]
|
|
|
|
|
public void RawPayloadWriteOverAMarkedFileKeepsItMarkedAndOverAnUnmarkedOneStaysV1()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("raw.cc");
|
|
|
|
|
var schema = new CultMeshSingleFileDocumentSchema("raw:schema", "RawSchema", "1");
|
|
|
|
|
var payload = new byte[] { 0x90 };
|
|
|
|
|
|
2026-10-03 13:08:44 +02:00
|
|
|
CultMesh.WriteSingleFileDocumentPayload(path, RawKey, schema, null, payload);
|
2026-09-29 21:54:01 +02:00
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(Descriptor()));
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3");
|
2026-10-03 13:08:44 +02:00
|
|
|
CultMesh.WriteSingleFileDocumentPayload(path, RawKey, schema, null, payload);
|
2026-09-29 21:54:01 +02:00
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3", "the payload's ids are not the writer's to see");
|
2026-09-29 22:53:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-03 15:09:17 +02:00
|
|
|
// A raw write at the typed record's own key, by an entry that lists the typed record's id compatible and so may replace it:
|
|
|
|
|
// the file then holds only the opaque payload, whose ids the writer cannot see, so the marked file stays marked. (Which files
|
|
|
|
|
// a raw write may replace is CultMeshSingleFileReadabilityTests, over the shared vectors.)
|
2026-09-29 22:53:13 +02:00
|
|
|
[Test]
|
2026-10-03 15:09:17 +02:00
|
|
|
public void RawPayloadReplacingTheOnlyMarkedRecordKeepsTheFileMarked()
|
2026-09-29 22:53:13 +02:00
|
|
|
{
|
2026-10-03 15:09:17 +02:00
|
|
|
var path = Path_("replaced.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(Descriptor()));
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3");
|
|
|
|
|
var typed = CultDocumentRegistry.Shared.GetRequired<CultMeshBodyPublicationDocument>().SchemaId;
|
|
|
|
|
var schema = new CultMeshSingleFileDocumentSchema("raw:schema", "RawSchema", "1") { CompatibleSchemaIds = new[] { typed } };
|
Every runtime rewrites only a file that is exactly one store
A rewrite read the header of the file it replaces but not whether the file was
a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and
took a non-array value or an array whose first slot is a string that is not a
store header for a headerless legacy file (so did the C# Mesh raw write, which
uses it); TypeScript's pushAll took any non-array value or non-header first slot
for v1; Rust ignored bytes after the array.
The rule, one per runtime: a file is a store on rewrite only when it is exactly
one complete MessagePack array. Bytes after it, a value that is not an array,
and a first slot that is a string but not a header this runtime reads are
refused, and the file is left untouched. An empty file and a legacy envelope
array (first slot not a string, empty included) carry no header and are still
rewritten.
- C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header
first string, with a NotSupportedException naming which.
- TypeScript: storeHeader (store-format.ts) decides the rewrite header;
@msgpack/msgpack's decode already refuses trailing bytes.
- Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks
for the whole file, the read paths keep reading as before.
- Python: push_all already reads the whole file (unpackb refuses trailing
bytes, the legacy decoder refuses anything but a list of maps); pinned by a
test.
Tests in each runtime run the same file matrix (truncated, trailing, scalar,
map, string first; empty, empty array, legacy), plus a C# unconditional commit
over a store with trailing bytes and the C# Mesh raw write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:01:58 +02:00
|
|
|
|
2026-10-03 15:09:17 +02:00
|
|
|
CultMesh.WriteSingleFileDocumentPayload(path, Key, schema, null, new byte[] { 0x90 });
|
|
|
|
|
|
|
|
|
|
CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Should().ContainSingle()
|
|
|
|
|
.Which.SchemaId.Should().Be("raw:schema");
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3", "the payload's ids are not the writer's to see");
|
2026-09-29 19:40:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
|
public void TypedWriteRefusesADuplicateIdAndAnElementObjectTwiceAndLeavesTheFileAlone()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("refused.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(Descriptor()));
|
|
|
|
|
var before = File.ReadAllBytes(path);
|
|
|
|
|
|
|
|
|
|
var duplicate = Publication(Descriptor(HexA), Descriptor(HexA));
|
|
|
|
|
var refusal = Assert.Throws<CultElementIdException>(() => CultMesh.WriteSingleFileDocument(path, Key, duplicate))!;
|
|
|
|
|
refusal.ElementId.Should().Be(HexA);
|
|
|
|
|
|
|
|
|
|
var once = Descriptor();
|
|
|
|
|
var twice = Publication(once, once);
|
|
|
|
|
Assert.Throws<CultElementIdException>(() => CultMesh.WriteSingleFileDocument(path, Key, twice))!.Message.Should().Contain("twice");
|
|
|
|
|
once.Id.Should().BeEmpty("a refusal mints nothing");
|
|
|
|
|
|
|
|
|
|
File.ReadAllBytes(path).Should().Equal(before);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 21:20:01 +02:00
|
|
|
[Test]
|
|
|
|
|
public void AFailedTypedWriteGivesTheMintedIdsBack()
|
|
|
|
|
{
|
|
|
|
|
var blocked = Path_("blocked.cc");
|
|
|
|
|
Directory.CreateDirectory(blocked);
|
|
|
|
|
var element = Descriptor();
|
|
|
|
|
|
|
|
|
|
Assert.That(() => CultMesh.WriteSingleFileDocument(blocked, Key, Publication(element)), Throws.Exception);
|
|
|
|
|
|
|
|
|
|
element.Id.Should().BeEmpty("a write that does not land gives its ids back");
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-29 19:40:36 +02:00
|
|
|
[Test]
|
|
|
|
|
public void TypedReadRefusesWhatALoadRefusesAndMintsWhatALoadMints()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("disk.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(Descriptor()));
|
|
|
|
|
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
|
|
|
|
|
|
|
|
|
|
var duplicate = Publication(Descriptor(HexA), Descriptor(HexA));
|
|
|
|
|
snapshot.Records[0].Payload = MessagePackSerializer.Serialize(duplicate);
|
|
|
|
|
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
|
|
|
|
var refusal = Assert.Throws<CultElementIdException>(() => CultMesh.ReadSingleFileDocument<CultMeshBodyPublicationDocument>(path, Key))!;
|
|
|
|
|
refusal.ElementId.Should().Be(HexA);
|
|
|
|
|
|
|
|
|
|
// A payload written before ids: two reads, and a cache load, agree on the ids minted.
|
|
|
|
|
snapshot.Records[0].Payload = MessagePackSerializer.Serialize(Publication(Descriptor(), Descriptor()));
|
|
|
|
|
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
|
|
|
|
var first = CultMesh.ReadSingleFileDocument<CultMeshBodyPublicationDocument>(path, Key).Representations.Select(entry => entry.Id).ToArray();
|
|
|
|
|
first.Should().OnlyContain(id => id.Length == 12);
|
|
|
|
|
CultMesh.ReadSingleFileDocument<CultMeshBodyPublicationDocument>(path, Key).Representations.Select(entry => entry.Id).Should().Equal(first);
|
|
|
|
|
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions
|
|
|
|
|
{
|
|
|
|
|
Registry = CultDocumentRegistry.ForTypes(new[] { typeof(CultMeshBodyPublicationDocument) }),
|
|
|
|
|
ReadOnly = true
|
|
|
|
|
});
|
|
|
|
|
cache.Get<CultMeshBodyPublicationDocument>(Key)!.Representations.Select(entry => entry.Id).Should().Equal(first);
|
|
|
|
|
}
|
2026-10-14 23:35:31 +02:00
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
// A list document defined in this assembly: the record another writer leaves holding ids under an unmarked header.
|
|
|
|
|
[CultDocument("mesh.test.id_list", "mesh.test.id_list.v1")]
|
2026-10-14 23:35:31 +02:00
|
|
|
[MessagePackObject]
|
2026-10-14 23:39:50 +02:00
|
|
|
public sealed class MeshIdList
|
2026-10-14 23:35:31 +02:00
|
|
|
{
|
2026-10-14 23:39:50 +02:00
|
|
|
[Key(0)] [CultName] public string Name { get; set; } = "";
|
|
|
|
|
[Key(1)] public List<MeshIdItem> Items { get; set; } = new();
|
2026-10-14 23:35:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
2026-10-14 23:39:50 +02:00
|
|
|
public sealed class MeshIdItem
|
2026-10-14 23:35:31 +02:00
|
|
|
{
|
2026-10-14 23:39:50 +02:00
|
|
|
[Key(0)] public string N { get; set; } = "";
|
|
|
|
|
[Key(1)] [CultElementId] public string Id { get; set; } = "";
|
2026-10-14 23:35:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
private static readonly CultRecordKey IdsKey = new("ids");
|
|
|
|
|
private const string HexB = "bbbbbbbbbbbb";
|
2026-10-14 23:37:10 +02:00
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
// A v1 file whose record holds 12-hex element ids: the document is written with the type (v3), then the header is put back
|
|
|
|
|
// to the v1 a writer that does not decide would have left.
|
|
|
|
|
private string UnmarkedStoreHoldingIds()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("unmarked.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(
|
|
|
|
|
path,
|
|
|
|
|
IdsKey,
|
|
|
|
|
new MeshIdList { Name = "ids", Items = { new MeshIdItem { Id = HexA }, new MeshIdItem { Id = HexB } } },
|
|
|
|
|
registry: Registry(typeof(MeshIdList)));
|
|
|
|
|
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
|
|
|
|
|
snapshot.FormatVersion = CultPersistedStoreSnapshot.FormatV1;
|
|
|
|
|
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
return path;
|
|
|
|
|
}
|
2026-10-14 23:35:31 +02:00
|
|
|
|
|
|
|
|
private string PythonStore()
|
|
|
|
|
{
|
|
|
|
|
string? vectors = null;
|
|
|
|
|
for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null && vectors == null; dir = dir.Parent)
|
|
|
|
|
{
|
|
|
|
|
var candidate = Path.Combine(dir.FullName, "tests", "vectors", "numeric-leaves", "py-store", "leaf-record.cc");
|
|
|
|
|
if (File.Exists(candidate)) vectors = candidate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vectors.Should().NotBeNull("the shared vectors sit above the test directory");
|
|
|
|
|
var path = Path_("leaf-record.cc");
|
|
|
|
|
File.Copy(vectors!, path);
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1", "cultcache-py leaves the header unmarked");
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static CultDocumentRegistry Registry(params Type[] types) => CultDocumentRegistry.ForTypes(types);
|
|
|
|
|
|
|
|
|
|
// A CultMesh write is a writer with a type: it decides for the copied record from the bytes the file holds, whatever any
|
|
|
|
|
// earlier read saw (a fresh store per call has read nothing).
|
|
|
|
|
[Test]
|
2026-10-14 23:39:50 +02:00
|
|
|
public void ACultMeshWriteThatCopiesARecordHoldingIdsMarksTheStore()
|
2026-10-14 23:35:31 +02:00
|
|
|
{
|
2026-10-14 23:39:50 +02:00
|
|
|
var path = UnmarkedStoreHoldingIds();
|
2026-10-14 23:35:31 +02:00
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(), registry: Registry(typeof(MeshIdList), typeof(CultMeshBodyPublicationDocument)));
|
2026-10-14 23:35:31 +02:00
|
|
|
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v3");
|
2026-10-14 23:39:50 +02:00
|
|
|
CultMesh.ReadSingleFileDocument<MeshIdList>(path, IdsKey, Registry(typeof(MeshIdList))).Items.Select(item => item.Id).Should().Equal(HexA, HexB);
|
2026-10-14 23:35:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
// A raw payload is opaque by design: it attaches no registry and decides nothing, even beside a record it could have read.
|
2026-10-14 23:35:31 +02:00
|
|
|
[Test]
|
2026-10-14 23:39:50 +02:00
|
|
|
public void ARawCultMeshWriteBesideARecordHoldingIdsLeavesTheHeaderAlone()
|
2026-10-14 23:35:31 +02:00
|
|
|
{
|
2026-10-14 23:39:50 +02:00
|
|
|
var path = UnmarkedStoreHoldingIds();
|
2026-10-14 23:35:31 +02:00
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
CultMesh.WriteSingleFileDocumentPayload(path, RawKey, new CultMeshSingleFileDocumentSchema("raw:schema", "RawSchema", "1"), null, new byte[] { 0x90 });
|
2026-10-14 23:35:31 +02:00
|
|
|
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
// Soul s1 P8, committed: the writer's registry has only the type it writes, so the python record is foreign to it and it
|
|
|
|
|
// decides nothing for it (variants:question:non-csharp-writers-mark-ids-2).
|
2026-10-14 23:35:31 +02:00
|
|
|
[Test]
|
2026-10-14 23:39:50 +02:00
|
|
|
public void ACultMeshWriteWhoseRegistryLacksTheRecordsTypeDecidesNothingForIt()
|
2026-10-14 23:35:31 +02:00
|
|
|
{
|
|
|
|
|
var path = PythonStore();
|
|
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(), registry: Registry(typeof(CultMeshBodyPublicationDocument)));
|
2026-10-14 23:35:31 +02:00
|
|
|
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-15 00:23:17 +02:00
|
|
|
// The copy check never refuses a write: a record the registry claims by schema name but cannot resolve (the catalog entry
|
|
|
|
|
// names another type for a slot) is copied as its bytes and decides nothing. Soul s2 P5, incompatible-catalog-slot12.
|
|
|
|
|
[Test]
|
|
|
|
|
public void AnUnresolvableCopiedRecordIsCopiedAndDecidesNothing()
|
|
|
|
|
{
|
|
|
|
|
var path = UnmarkedStoreHoldingIds();
|
|
|
|
|
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path));
|
|
|
|
|
const string changedId = "mesh.test.id_list.v0";
|
|
|
|
|
var entry = snapshot.SchemaCatalog.Single(candidate => candidate.SchemaName == "mesh.test.id_list");
|
|
|
|
|
entry.SchemaId = changedId;
|
2026-10-15 00:28:30 +02:00
|
|
|
entry.ContentHash = "persisted-by-another-runtime";
|
2026-10-15 00:23:17 +02:00
|
|
|
entry.Members.Single(member => member.Slot == 1).TypeName = "System.Collections.Generic.List`1[[Other.Inner]]";
|
|
|
|
|
snapshot.Records.Single(record => record.Key == IdsKey.Value).SchemaId = changedId;
|
|
|
|
|
File.WriteAllBytes(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
|
|
|
|
var before = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Single(record => record.Key == IdsKey.Value);
|
|
|
|
|
|
|
|
|
|
Assert.DoesNotThrow(() => CultMesh.WriteSingleFileDocument(path, Key, Publication(), registry: Registry(typeof(MeshIdList), typeof(CultMeshBodyPublicationDocument))));
|
|
|
|
|
|
|
|
|
|
var after = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(path)).Records.Single(record => record.Key == IdsKey.Value);
|
|
|
|
|
after.SchemaId.Should().Be(changedId);
|
|
|
|
|
after.Payload.Should().Equal(before.Payload);
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 23:39:50 +02:00
|
|
|
// Typed writes beside records that hold no id leave the store unmarked: the check marks only on an id it found.
|
2026-10-14 23:35:31 +02:00
|
|
|
[Test]
|
|
|
|
|
public void ATypedWriteBesideARecordThatHoldsNoIdKeepsTheStoreUnmarked()
|
|
|
|
|
{
|
|
|
|
|
var path = Path_("plain.cc");
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, Key, Publication(), registry: Registry(typeof(CultMeshBodyPublicationDocument)));
|
|
|
|
|
CultMesh.WriteSingleFileDocument(path, new CultRecordKey("second"), Publication(), registry: Registry(typeof(CultMeshBodyPublicationDocument)));
|
|
|
|
|
|
|
|
|
|
HeaderOf(path).Should().Be("cultcache.store.v1");
|
|
|
|
|
}
|
2026-09-29 19:40:36 +02:00
|
|
|
}
|