Caching tests: content-version vector, written by the C# reference

store.cc and versions.msgpack pin the content version of a stored record
(SHA-256 of its persisted encoding) for other runtimes to match.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Libby 2026-11-13 16:00:09 +00:00
commit 1f698ff225
4 changed files with 133 additions and 0 deletions

View file

@ -0,0 +1,115 @@
#nullable enable
using System;
using System.Buffers;
using System.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using MessagePack;
using NUnit.Framework;
namespace GameCult.Caching.Tests
{
// tests/vectors/content-version: the content version of a stored record, the lowercase hex SHA-256 of its persisted encoding
// (variants:ruling:write-set-content-version-declared-ownership), as the reference reads it from a single-file store. The C#
// reference is the only writer: under CULTCACHE_WRITE_VECTORS=1 the store is written and the versions read from it are written
// beside it; every other run opens the committed store.cc and compares what the reference reads to versions.msgpack. Another
// runtime computes the version from the same file and must equal these rows.
public class ContentVersionVectorTests
{
private const string Vector = "content-version";
private static readonly Type Item =
EmittedDocumentTypes.Emit("CvItem", "tests.cv_item", "tests.cv_item.v1", new[] { new EmittedDocumentTypes.Field("Text", typeof(string), 0) });
private static readonly CultDocumentRegistry Registry = CultDocumentRegistry.ForTypes(new[] { Item });
private static readonly (string Key, string Text)[] Records = { ("alpha", "first"), ("beta", "second") };
private static byte[] StoreBytes()
{
var directory = Path.Combine(Path.GetTempPath(), $"cultlib-cvv-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
try
{
var path = Path.Combine(directory, "store.cc");
using (var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, StoreFlushOnDispose = true }))
{
cache.Clock = () => VectorFiles.StoredAt;
foreach (var (key, text) in Records)
cache.UpsertAsync(Item, EmittedDocumentTypes.New(Item, ("Text", text)), new CultRecordKey(key)).GetAwaiter().GetResult();
cache.FlushAsync().GetAwaiter().GetResult();
}
return File.ReadAllBytes(path);
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
// What the reference reads from a store file: [key, version] for each record, sorted by key.
private static (string Key, string Version)[] ReadVersions(byte[] store)
{
var directory = Path.Combine(Path.GetTempPath(), $"cultlib-cvr-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
try
{
var path = Path.Combine(directory, "store.cc");
File.WriteAllBytes(path, store);
using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry });
return cache.AllStoredDocuments
.Select(stored => (Key: stored.Key.Value, Version: stored.StoredVersion))
.OrderBy(row => row.Key, StringComparer.Ordinal)
.ToArray();
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
private static byte[] RowsFile((string Key, string Version)[] rows)
{
var buffer = new ArrayBufferWriter<byte>();
var writer = new MessagePackWriter(buffer);
writer.WriteArrayHeader(rows.Length);
foreach (var (key, version) in rows)
{
writer.WriteArrayHeader(2);
writer.Write(key);
writer.Write(version);
}
writer.Flush();
return buffer.WrittenSpan.ToArray();
}
[Test]
public void TheReferenceReadsTheCommittedVersions()
{
if (VectorFiles.Writing)
{
var store = StoreBytes();
VectorFiles.Check(Vector, "store.cc", store);
VectorFiles.Check(Vector, "versions.msgpack", RowsFile(ReadVersions(store)));
return;
}
var committed = VectorFiles.Committed(Vector, "store.cc");
var rows = ReadVersions(committed);
Assert.That(rows.Select(row => row.Key), Is.EqualTo(Records.Select(record => record.Key)), "the store holds the two records");
Assert.That(rows.Select(row => row.Version).Distinct().Count(), Is.EqualTo(rows.Length), "two different records, two versions");
Assert.That(rows.All(row => row.Version.Length == 64 && row.Version == row.Version.ToLowerInvariant()), Is.True, "lowercase hex SHA-256");
Assert.That(Convert.ToHexString(RowsFile(rows)), Is.EqualTo(Convert.ToHexString(VectorFiles.Committed(Vector, "versions.msgpack"))), "versions.msgpack");
}
[Test]
public void TheCommittedStoreIsWhatTheReferenceWrites()
{
if (VectorFiles.Writing) return;
Assert.That(Convert.ToHexString(StoreBytes()), Is.EqualTo(Convert.ToHexString(VectorFiles.Committed(Vector, "store.cc"))), "store.cc");
}
}
}

View file

@ -0,0 +1,17 @@
# content-version vectors
Written only by the C# reference, `tests/GameCult.Caching.Tests/ContentVersionVectorTests.cs`, with
`CULTCACHE_WRITE_VECTORS=1` (never set in CI). Every other run opens the committed `store.cc`, compares what the reference reads
to `versions.msgpack` and compares the store bytes the reference writes to the file; a changed vector is a reviewed commit.
Other runtimes compute the version of each record of `store.cc` and must equal these rows
(`variants:ruling:write-set-content-version-declared-ownership`).
RULE: the content version of a stored record is the lowercase hex SHA-256 of its persisted encoding, the MessagePack array
`[key, schemaId, storedAt, payload bytes]` exactly as the store writes the record.
## Files
- `store.cc`: a single-file store of one type, `tests.cv_item` (one string slot, `[0: Text]`), holding `alpha` = "first" and
`beta` = "second", stored-at pinned to `2026-01-01T00:00:00Z`.
- `versions.msgpack`: an array of `[key, version]`, sorted by key, the version the reference reads for the record
(`GetStored(key).StoredVersion`).

Binary file not shown.

View file

@ -0,0 +1 @@
’’¥alphaÙ@d330976df4f3946d0ad590d2091666aed4d5ebeac5d473792b4ef401b25c92fd’¤betaÙ@878026249c0df9a969ac1cac67bcdaab61f1b085ce4916c3c63f0fbef8f6da9f