From 583a200a6aeb4584bdcc5bddb2cf70321d755ec1 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 22:13:43 +0000 Subject: [PATCH 1/3] Key-identity vector: retype refusal, foreign carry and key-in-two-stores rows written and walked by C# Co-Authored-By: Claude Sonnet 5.5 --- docs/write-ownership-map.md | 3 + .../KeyIdentityVectorTests.cs | 354 ++++++++++++++++++ .../UniqueIndexVectorTests.cs | 37 +- tests/GameCult.Caching.Tests/VectorFiles.cs | 50 +++ tests/vectors/key-identity/README.md | 46 +++ tests/vectors/key-identity/catalog-store.cc | Bin 0 -> 638 bytes tests/vectors/key-identity/foreign-store.cc | Bin 0 -> 1254 bytes tests/vectors/key-identity/rows.msgpack | 1 + tests/vectors/key-identity/run-store.cc | Bin 0 -> 634 bytes 9 files changed, 459 insertions(+), 32 deletions(-) create mode 100644 tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs create mode 100644 tests/GameCult.Caching.Tests/VectorFiles.cs create mode 100644 tests/vectors/key-identity/README.md create mode 100644 tests/vectors/key-identity/catalog-store.cc create mode 100644 tests/vectors/key-identity/foreign-store.cc create mode 100644 tests/vectors/key-identity/rows.msgpack create mode 100644 tests/vectors/key-identity/run-store.cc diff --git a/docs/write-ownership-map.md b/docs/write-ownership-map.md index 33229fb1..6e3f0efc 100644 --- a/docs/write-ownership-map.md +++ b/docs/write-ownership-map.md @@ -40,6 +40,9 @@ Probes: scratch tests in this worktree, removed. C# filtered runs on Starfire (o | P5 cache {A,B}: remove, then add in a second commit | C | n/a | n/a | n/a | | P6 N2 over an N1 record (undeclared name drift), cache {N2} / cacheless reg {N2} | C restamped / C restamped | not probed | not probed | C (cache) | +The C#-written probe the leaves are held to is `tests/vectors/key-identity` (11 rows: retype, foreign carry, key in two stores), walked by +`KeyIdentityVectorTests`; each leaf cut corrects its own column above. + What this establishes: - C#: one record per key. The only path that retypes is the cache arm (P1): `CacheBackingStore.RefuseUndeclared` diff --git a/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs b/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs new file mode 100644 index 00000000..a4153b9e --- /dev/null +++ b/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs @@ -0,0 +1,354 @@ +#nullable enable +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using MessagePack; +using NUnit.Framework; + +namespace GameCult.Caching.Tests +{ + // tests/vectors/key-identity: a cache holds one record per key whatever its type, and a store holding a record no registered + // type claims keeps it (variants:ruling:cache-refuses-retype, write-ownership-by-written-type, unheld-removal-refused-at-commit). + // The C# reference is the only writer: under CULTCACHE_WRITE_VECTORS=1 the rows are run and what the reference answers is + // written; every other run walks the committed rows against the reference and compares the files byte for byte. + public class KeyIdentityVectorTests + { + private const string Vector = "key-identity"; + + private static readonly Type Alpha = Kind("KiAlpha", "tests.ki_alpha"); + private static readonly Type Beta = Kind("KiBeta", "tests.ki_beta"); + private static readonly Type Gamma = Kind("KiGamma", "tests.ki_gamma"); + + private static Type Kind(string type, string schema) => + EmittedDocumentTypes.Emit(type, schema, schema + ".v1", new[] { new EmittedDocumentTypes.Field("Text", typeof(string), 0) }); + + private static readonly Dictionary Types = new() { ["alpha"] = Alpha, ["beta"] = Beta, ["gamma"] = Gamma }; + + private sealed record Op(string Kind, string? File = null, string[]? Types = null, string? Type = null, string? Key = null, string? Text = null); + + private sealed record Row(string Label, string[] Registered, Op[] Ops); + + // [refusal key or nil, held [type, key, text], file [key, schema name] or nil, foreign keys, kept foreign keys], all sorted. + private sealed record Expect(string? Refusal, string[][] Held, string[][]? File, string[] Foreign, string[] Kept); + + private static Op Open(string? file) => new("open", file); + private static Op Attach(string file, params string[] home) => new("attach", file, home); + private static Op Reopen() => new("reopen"); + private static Op Put(string type, string key, string text) => new("put", Type: type, Key: key, Text: text); + private static Op Delete(string type, string key) => new("delete", Type: type, Key: key); + + private static readonly string[] AB = { "alpha", "beta" }; + private static readonly string[] A = { "alpha" }; + + // Only the ops are typed here; every verdict is what the reference answers. + private static readonly Row[] Table = + { + new("retype-refused-held-in-file", AB, new[] { Open(null), Put("alpha", "k", "a"), Put("beta", "k", "b") }), + new("retype-by-remove-then-add", AB, new[] { Open(null), Put("alpha", "k", "a"), Delete("alpha", "k"), Put("beta", "k", "b") }), + new("retype-refused-memory-cache", AB, new[] { Open("memory"), Put("alpha", "k", "a"), Put("beta", "k", "b") }), + new("same-type-replaces", A, new[] { Open(null), Put("alpha", "k", "a"), Put("alpha", "k", "b") }), + new("retype-refused-after-reload", AB, new[] { Open(null), Put("alpha", "k", "a"), Reopen(), Put("beta", "k", "b") }), + new("foreign-loads", A, new[] { Open("foreign-store.cc") }), + new("foreign-kept-by-a-write", A, new[] { Open("foreign-store.cc"), Put("alpha", "m", "m") }), + new("foreign-key-write-refused", A, new[] { Open("foreign-store.cc"), Put("alpha", "k", "x") }), + new("foreign-key-delete-refused", A, new[] { Open("foreign-store.cc"), Delete("alpha", "k") }), + new("foreign-claimed-by-a-later-registration", new[] { "alpha", "gamma" }, new[] { Open("foreign-store.cc") }), + new("key-in-two-stores-refused", AB, new[] { Open("memory"), Attach("catalog-store.cc", "alpha"), Attach("run-store.cc", "beta") }), + }; + + // The stores C# writes: [file name, registered types, (type, key, text) records]. Never typed by a runtime. + private static readonly (string File, string[] Registered, (string Type, string Key, string Text)[] Records)[] Stores = + { + ("foreign-store.cc", new[] { "alpha", "gamma" }, new[] { ("alpha", "j", "a"), ("gamma", "k", "g") }), + ("catalog-store.cc", new[] { "alpha" }, new[] { ("alpha", "k", "a") }), + ("run-store.cc", new[] { "beta" }, new[] { ("beta", "k", "b") }), + }; + + private static CultDocumentRegistry RegistryOf(IEnumerable names) => CultDocumentRegistry.ForTypes(names.Select(name => Types[name]).ToArray()); + + private static object Doc(string type, string text) => EmittedDocumentTypes.New(Types[type], ("Text", text)); + + private static byte[] StoreBytes(string[] registered, (string Type, string Key, string Text)[] records) + { + var directory = Path.Combine(Path.GetTempPath(), $"cultlib-kiv-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + try + { + var path = Path.Combine(directory, "store.cc"); + using (var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = RegistryOf(registered), StoreFlushOnDispose = true })) + { + cache.Clock = () => VectorFiles.StoredAt; + foreach (var record in records) + cache.UpsertAsync(Types[record.Type], Doc(record.Type, record.Text), new CultRecordKey(record.Key)).GetAwaiter().GetResult(); + cache.FlushAsync().GetAwaiter().GetResult(); + } + + return File.ReadAllBytes(path); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + private static CultPersistedStoreSnapshot Snapshot(byte[] bytes) => CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes); + + private static string Name(CultPersistedStoreSnapshot snapshot, CultPersistedRecord record) => + snapshot.SchemaCatalog.Single(entry => entry.SchemaId == record.SchemaId).SchemaName; + + private static string Short(string schemaName) => Types.Single(pair => pair.Key == schemaName.Replace("tests.ki_", "")).Key; + + private static string RecordBytes(CultPersistedRecord record) => $"{record.SchemaId}|{record.StoredAt}|{Convert.ToBase64String(record.Payload)}"; + + // Runs the ops and returns what the reference answers. Every op but the last must succeed; the last may refuse. + private static Expect Observe(Row row) + { + var directory = Path.Combine(Path.GetTempPath(), $"cultlib-kiw-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + var registry = RegistryOf(row.Registered); + string? path = null; + byte[]? before = null; + CultCache? cache = null; + try + { + Exception? refusal = null; + string? refusedKey = null; + for (var i = 0; i < row.Ops.Length; i++) + { + var op = row.Ops[i]; + try + { + switch (op.Kind) + { + case "open" or "reopen": + cache?.Dispose(); + if (op.Kind == "open" && op.File == "memory") + { + cache = new CultCache(registry, CultCacheMessagePack.CreateCodec(registry)); + break; + } + + if (op.Kind == "open") + { + path = Path.Combine(directory, "store.cc"); + if (op.File != null) File.WriteAllBytes(path, VectorFiles.Committed(Vector, op.File)); + before = File.Exists(path) ? File.ReadAllBytes(path) : null; + } + + cache = CultCacheMessagePack.Create(path!, new CultCacheOpenOptions { Registry = registry, StoreFlushOnDispose = true }); + break; + case "attach": + { + var attached = Path.Combine(directory, op.File!); + File.WriteAllBytes(attached, VectorFiles.Committed(Vector, op.File!)); + var held = cache!.AllStoredDocuments.Select(stored => stored.Key.Value).ToHashSet(StringComparer.Ordinal); + var shared = Snapshot(File.ReadAllBytes(attached)).Records.Select(record => record.Key).Where(held.Contains).ToArray(); + try + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(attached), op.Types!.Select(name => Types[name]).ToArray()); + cache.PullAllBackingStoresAsync().GetAwaiter().GetResult(); + } + catch (InvalidOperationException) when (shared.Length == 1) + { + refusedKey = shared[0]; + Assert.That(File.ReadAllBytes(attached), Is.EqualTo(VectorFiles.Committed(Vector, op.File!)), "a refused attach leaves the store as it was"); + throw; + } + + break; + } + case "put": + cache!.Commit(batch => batch.Upsert(Types[op.Type!], Doc(op.Type!, op.Text!), new CultRecordKey(op.Key!))); + break; + case "delete": + cache!.Commit(batch => batch.Remove(new CultRecordKey(op.Key!))); + break; + default: + throw new InvalidOperationException(op.Kind); + } + } + catch (Exception failure) when (i == row.Ops.Length - 1 && (failure is CultSchemaConflictException || failure is InvalidOperationException)) + { + refusal = failure; + } + } + + if (refusal is CultSchemaConflictException conflict) + { + refusedKey = conflict.RecordKey; + Assert.That(row.Ops[^1].Key, Is.EqualTo(conflict.RecordKey), $"{row.Label}: the refusal names the key written or removed"); + } + + if (refusal != null) + Assert.That(refusal.Message, Does.Contain(refusedKey ?? "").And.Not.Contain("\"\""), $"{row.Label}: the refusal names the key"); + + var heldNow = cache!.AllStoredDocuments + .Select(stored => new[] { Short(stored.Descriptor.SchemaName), stored.Key.Value, (string)EmittedDocumentTypes.Read(stored.Document, "Text")! }) + .OrderBy(entry => entry[0], StringComparer.Ordinal).ThenBy(entry => entry[1], StringComparer.Ordinal).ToArray(); + var foreign = cache.BackingStores.SelectMany(store => store.ForeignRecords).Select(record => record.Key).OrderBy(key => key, StringComparer.Ordinal).ToArray(); + + string[][]? file = null; + var kept = Array.Empty(); + if (path != null) + { + cache.FlushAsync().GetAwaiter().GetResult(); + var after = Snapshot(File.ReadAllBytes(path)); + file = after.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => new[] { record.Key, Name(after, record) }).ToArray(); + var start = before == null ? null : Snapshot(before); + kept = foreign.Where(key => + { + var original = start?.Records.SingleOrDefault(record => record.Key == key); + return original != null && RecordBytes(original) == RecordBytes(after.Records.Single(record => record.Key == key)); + }).ToArray(); + } + + return new Expect(refusedKey, heldNow, file, foreign, kept); + } + finally + { + cache?.Dispose(); + Directory.Delete(directory, recursive: true); + } + } + + // ---- rows.msgpack: [label, registered, ops, expect] ---- + + private static void WriteStrings(ref MessagePackWriter writer, IEnumerable values) + { + var list = values.ToArray(); + writer.WriteArrayHeader(list.Length); + foreach (var value in list) + if (value == null) writer.WriteNil(); else writer.Write(value); + } + + private static void WriteRows(ref MessagePackWriter writer, Row row, Expect expect) + { + writer.WriteArrayHeader(4); + writer.Write(row.Label); + WriteStrings(ref writer, row.Registered); + writer.WriteArrayHeader(row.Ops.Length); + foreach (var op in row.Ops) + { + var fields = op.Kind switch + { + "open" => new object?[] { op.Kind, op.File }, + "attach" => new object?[] { op.Kind, op.File, op.Types }, + "reopen" => new object?[] { op.Kind }, + "put" => new object?[] { op.Kind, op.Type, op.Key, op.Text }, + _ => new object?[] { op.Kind, op.Type, op.Key }, + }; + writer.WriteArrayHeader(fields.Length); + foreach (var field in fields) + switch (field) + { + case null: writer.WriteNil(); break; + case string[] list: WriteStrings(ref writer, list); break; + default: writer.Write((string)field); break; + } + } + + writer.WriteArrayHeader(5); + if (expect.Refusal == null) writer.WriteNil(); else WriteStrings(ref writer, new[] { expect.Refusal }); + writer.WriteArrayHeader(expect.Held.Length); + foreach (var held in expect.Held) WriteStrings(ref writer, held); + if (expect.File == null) writer.WriteNil(); + else + { + writer.WriteArrayHeader(expect.File.Length); + foreach (var record in expect.File) WriteStrings(ref writer, record); + } + + WriteStrings(ref writer, expect.Foreign); + WriteStrings(ref writer, expect.Kept); + } + + private static byte[] RowsFile() + { + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(Table.Length); + foreach (var row in Table) + WriteRows(ref writer, row, Observe(row)); + writer.Flush(); + return buffer.WrittenSpan.ToArray(); + } + + private static string[] ReadStrings(ref MessagePackReader reader) + { + var values = new string[reader.ReadArrayHeader()]; + for (var i = 0; i < values.Length; i++) values[i] = reader.ReadString()!; + return values; + } + + private static (Row Row, Expect Expect) ReadRow(ref MessagePackReader reader) + { + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(4)); + var label = reader.ReadString()!; + var registered = ReadStrings(ref reader); + var ops = new Op[reader.ReadArrayHeader()]; + for (var i = 0; i < ops.Length; i++) + { + var length = reader.ReadArrayHeader(); + var kind = reader.ReadString()!; + ops[i] = kind switch + { + "open" => new Op(kind, reader.TryReadNil() ? null : reader.ReadString()), + "attach" => new Op(kind, reader.ReadString(), ReadStrings(ref reader)), + "reopen" => new Op(kind), + "put" => new Op(kind, Type: reader.ReadString(), Key: reader.ReadString(), Text: reader.ReadString()), + "delete" => new Op(kind, Type: reader.ReadString(), Key: reader.ReadString()), + _ => throw new InvalidOperationException(kind), + }; + Assert.That(length, Is.EqualTo(kind switch { "reopen" => 1, "open" => 2, "delete" => 3, "attach" => 3, _ => 4 }), $"{label}: op {kind}"); + } + + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(5)); + var refusal = reader.TryReadNil() ? null : ReadStrings(ref reader).Single(); + var held = new string[reader.ReadArrayHeader()][]; + for (var i = 0; i < held.Length; i++) held[i] = ReadStrings(ref reader); + string[][]? file = null; + if (!reader.TryReadNil()) + { + file = new string[reader.ReadArrayHeader()][]; + for (var i = 0; i < file.Length; i++) file[i] = ReadStrings(ref reader); + } + + var foreign = ReadStrings(ref reader); + var kept = ReadStrings(ref reader); + return (new Row(label, registered, ops), new Expect(refusal, held, file, foreign, kept)); + } + + private static string Render(Expect expect) => + $"refusal={expect.Refusal ?? "-"} held={string.Join(";", expect.Held.Select(h => string.Join(",", h)))} " + + $"file={(expect.File == null ? "-" : string.Join(";", expect.File.Select(f => string.Join(",", f))))} " + + $"foreign={string.Join(",", expect.Foreign)} kept={string.Join(",", expect.Kept)}"; + + [Test] + public void KeyIdentityRowsHoldTheirVerdicts() + { + foreach (var store in Stores) + VectorFiles.Check(Vector, store.File, StoreBytes(store.Registered, store.Records)); + VectorFiles.Check(Vector, "rows.msgpack", RowsFile()); + + var reader = new MessagePackReader(VectorFiles.Committed(Vector, "rows.msgpack")); + var count = reader.ReadArrayHeader(); + Assert.That(count, Is.EqualTo(Table.Length)); + var walked = 0; + var failed = new List(); + for (var i = 0; i < count; i++) + { + var (row, expect) = ReadRow(ref reader); + var observed = Observe(row); + if (Render(observed) == Render(expect)) walked++; + else failed.Add($"{row.Label}: committed {Render(expect)} but the reference answers {Render(observed)}"); + } + + Assert.That(failed, Is.Empty); + Assert.That(walked, Is.EqualTo(11), "rows walked"); + } + } +} diff --git a/tests/GameCult.Caching.Tests/UniqueIndexVectorTests.cs b/tests/GameCult.Caching.Tests/UniqueIndexVectorTests.cs index 1fd026ae..40c3fbc7 100644 --- a/tests/GameCult.Caching.Tests/UniqueIndexVectorTests.cs +++ b/tests/GameCult.Caching.Tests/UniqueIndexVectorTests.cs @@ -18,43 +18,16 @@ namespace GameCult.Caching.Tests // compares them byte for byte. Other runtimes are held to the same text and the same verdicts. public class UniqueIndexVectorTests { - private const string WriteVariable = "CULTCACHE_WRITE_VECTORS"; - private static readonly DateTimeOffset StoredAt = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); - private static bool Writing => Environment.GetEnvironmentVariable(WriteVariable) == "1"; + private static readonly DateTimeOffset StoredAt = VectorFiles.StoredAt; + private static bool Writing => VectorFiles.Writing; - private static string VectorDirectory() - { - for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent) - { - var vectors = Path.Combine(dir.FullName, "tests", "vectors"); - if (Directory.Exists(vectors)) return Path.Combine(vectors, "unique-index"); - } - - throw new DirectoryNotFoundException($"tests/vectors not found above {TestContext.CurrentContext.TestDirectory}."); - } + private static string VectorDirectory() => VectorFiles.Directory("unique-index"); private static string VectorPath(string name) => Path.Combine(VectorDirectory(), name); - private static void Check(string name, byte[] reference) - { - var path = VectorPath(name); - if (Writing) - { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllBytes(path, reference); - return; - } + private static void Check(string name, byte[] reference) => VectorFiles.Check("unique-index", name, reference); - Assert.That(File.Exists(path), Is.True, $"vector {name} is committed"); - Assert.That(Convert.ToHexString(reference), Is.EqualTo(Convert.ToHexString(File.ReadAllBytes(path))), $"vector {name}"); - } - - private static byte[] Committed(string name) - { - var path = VectorPath(name); - Assert.That(File.Exists(path), Is.True, $"vector {name} is committed"); - return File.ReadAllBytes(path); - } + private static byte[] Committed(string name) => VectorFiles.Committed("unique-index", name); // ---- values.msgpack: [label, kind, value, text-or-nil] ---- diff --git a/tests/GameCult.Caching.Tests/VectorFiles.cs b/tests/GameCult.Caching.Tests/VectorFiles.cs new file mode 100644 index 00000000..33ac90c4 --- /dev/null +++ b/tests/GameCult.Caching.Tests/VectorFiles.cs @@ -0,0 +1,50 @@ +#nullable enable +using System; +using System.IO; +using NUnit.Framework; + +namespace GameCult.Caching.Tests +{ + // The one place a C#-written vector directory is found, written and compared. The C# reference is the only writer + // (CULTCACHE_WRITE_VECTORS=1, never set in CI); every other run reads the committed bytes and compares them byte for byte. + internal static class VectorFiles + { + private const string WriteVariable = "CULTCACHE_WRITE_VECTORS"; + + internal static readonly DateTimeOffset StoredAt = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + + internal static bool Writing => Environment.GetEnvironmentVariable(WriteVariable) == "1"; + + internal static string Directory(string vector) + { + for (var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); dir != null; dir = dir.Parent) + { + var vectors = System.IO.Path.Combine(dir.FullName, "tests", "vectors"); + if (System.IO.Directory.Exists(vectors)) return System.IO.Path.Combine(vectors, vector); + } + + throw new DirectoryNotFoundException($"tests/vectors not found above {TestContext.CurrentContext.TestDirectory}."); + } + + internal static void Check(string vector, string name, byte[] reference) + { + var path = System.IO.Path.Combine(Directory(vector), name); + if (Writing) + { + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)!); + File.WriteAllBytes(path, reference); + return; + } + + Assert.That(File.Exists(path), Is.True, $"vector {name} is committed"); + Assert.That(Convert.ToHexString(reference), Is.EqualTo(Convert.ToHexString(File.ReadAllBytes(path))), $"vector {name}"); + } + + internal static byte[] Committed(string vector, string name) + { + var path = System.IO.Path.Combine(Directory(vector), name); + Assert.That(File.Exists(path), Is.True, $"vector {name} is committed"); + return File.ReadAllBytes(path); + } + } +} diff --git a/tests/vectors/key-identity/README.md b/tests/vectors/key-identity/README.md new file mode 100644 index 00000000..f10e1fdb --- /dev/null +++ b/tests/vectors/key-identity/README.md @@ -0,0 +1,46 @@ +# key-identity vectors + +Written only by the C# reference, `tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs`, with +`CULTCACHE_WRITE_VECTORS=1` (never set in CI). Every other run reads these files, runs every row against the reference and +compares the files byte for byte; a changed vector is a reviewed commit. Other runtimes are held to the same verdicts +(`variants:ruling:cache-refuses-retype`, `variants:ruling:write-ownership-by-written-type`, +`variants:ruling:unheld-removal-refused-at-commit`). + +RULE: a cache holds one record per key whatever its type; a write of a type that does not own the held record's schema id at +that key is refused as a schema conflict naming the key, with the cache, its store file and every foreign record +byte-identical; retyping is a delete then a put. A store that holds a record no registered type claims loads: the cache does +not serve it, lists its key as foreign, keeps its bytes through every write, and refuses a write or a removal at its key. +Registering a type that declares its schema id claims it at the next load. + +## Files + +- Schemas: `tests.ki_alpha`, `tests.ki_beta`, `tests.ki_gamma` (`alpha`, `beta`, `gamma` below), each one string slot + `[0: Text]`, version the name plus `.v1`. The schema ids C# writes in the store catalogs and on every record: + - `tests.ki_alpha`: `sha256:b20703a98f49c253898404ab50ba2c630720f0b8a5e1a41614d5650be22799fe` + - `tests.ki_beta`: `sha256:dee4b727cf2ee04e61ab635ff0c5f3dca7345a411dc5f1018e583ab16009cf94` + - `tests.ki_gamma`: `sha256:fbc26e351e00c9e7185e8de171c7486aba707c229c949f0aae6b889288af5f6d` +- `foreign-store.cc`: a single-file store, alpha `a` at key `j`, gamma `g` at key `k`, stored-at pinned to + `2026-01-01T00:00:00Z`. `catalog-store.cc`: alpha `a` at `k`. `run-store.cc`: beta `b` at `k` (row 11). +- `rows.msgpack`: an array of `[label, registered, ops, expect]`. + - `registered`: the types the cache registers, a subset of `[alpha, beta, gamma]`. + - `ops`, in order: `[open, file-or-nil-or-memory]` (a cache on a fresh copy of the vector file; nil is a fresh path with no + file; the string `memory` is a cache with no store), `[attach, file, [home types]]` (route one more store, a fresh copy + of the vector file, to those home types, and pull every store), `[reopen]` (dispose, then a new cache on the same path), + `[put, type, key, text]`, `[delete, type, key]` (each one commit). + - `expect` is `[refusal, held, file, foreign, kept]`. Every op but the last succeeds; the last succeeds or refuses. + `refusal` is nil or `[key]`: a typed schema-conflict refusal naming the key for a put or a delete (rows 1, 3, 5, 8, 9), + and for row 11 any refusal whose message names the key, the store file it was refused from left byte-identical. + `held` is the cache's records, sorted by type then key, as `[type, key, text]`. `file` is the store file's records after + the ops, sorted by key, as `[key, schema name]`, nil for a cache with no store. `foreign` is the keys the cache lists as + foreign, sorted. `kept` is the foreign keys whose stored record (schema id, stored-at, payload) after the ops equals the + file's before the first op. + +## Rows + +1 retype-refused-held-in-file, 2 retype-by-remove-then-add, 3 retype-refused-memory-cache, 4 same-type-replaces, 5 +retype-refused-after-reload, 6 foreign-loads, 7 foreign-kept-by-a-write, 8 foreign-key-write-refused, 9 +foreign-key-delete-refused, 10 foreign-claimed-by-a-later-registration (registered `[alpha, gamma]`: row 6's store loads +with nothing foreign), 11 key-in-two-stores-refused. + +Every runtime walks 11 rows and asserts the count; none is exempt (every row is single-record ops). A leaf never registers +gamma in rows 6 and 8; only row 10 does. diff --git a/tests/vectors/key-identity/catalog-store.cc b/tests/vectors/key-identity/catalog-store.cc new file mode 100644 index 0000000000000000000000000000000000000000..01c7dd2d3f0a947688dfbed6876360dac319db7c GIT binary patch literal 638 zcmb_Z%}T>S5EgtAbG3%qO?LBh^W@EgEnY-qvYE7@Nm{a7X^}oeP|$0^tAaj3DZWJB z#_bQq;!&4@WoEwlzHfFPKh>l#3e|vIZALY@_pEttFZ2Mpka5Bx01u-u^CQKD7e=8E zJ|+SZ_Q+9q)xL{UcXhP2V{Fu%iMJiw2`)N1XOfM=k>Bp`%9 zF&2PO%#$KhJhC^Y2%!ogkQ^fGHcP@(2rfB~e906A*tQRjwp)~Vi6uF)b13C>cHL{L zbv~+o0jw8`DXB~>z=>}xTQM?D3>~N16830nEX2&lYx?d?8!Fwdsq?CD%elUyjB2Ws z?PgeLvTRt*_KCr|Pp13BVl^oWJ6NSOJuh;!s=JEbuCQU5-_D!;BmMO|nwO*3^%`#n l?pLejYS}z3hX1D1o^asu4491`K)eYTw&!${eH}Lo{080e%-sM0 literal 0 HcmV?d00001 diff --git a/tests/vectors/key-identity/foreign-store.cc b/tests/vectors/key-identity/foreign-store.cc new file mode 100644 index 0000000000000000000000000000000000000000..2c0deb8bc53a2db127eabe6b67dfb2a2688f648c GIT binary patch literal 1254 zcmd6l&x#X45XJ?c#JReL>Yo0anWfA0q35UKhN&pii&_ zUm|Z~ZL($)gdoI&O+!;%HT8Yfzxnhdp4B;G98s^!lPUE+h|AB*7u^V@HvJ&MLW$lb z<)hT4^G<=npdp})rUWY?L2#%^pb|!?N*hj+l-B!1H<`NJ^-jj`us$8(_uY`SHcB89 zL>DX}*eHYr9Ytw$iqi9m6@rTntdZc^Z3?c^S{f<6G9r@2_E@e~cd#DQ*_KML3j*EG}wE-3nMxLZBRMIVlecHQup&NG+f+38*T!K};F^ago4Zijq5 z?p{%%DYcQa3G0ry8@97eka0SsJlL|>p4Byf*j9A@ydEQ~-rscV5msF;-d@Z%k8~)G z`Ikpu@5cDH<9@NYUEDq}Pa#S}r4|GbJz3#2xr&4p(JE&!pk+k-^3f}w0Fg{^&P(So z>13)0=0j|nE#|9mo4F+hn4p6;=TvA}H%!naVQyz`g{# z*u#7!wU7C4FaJODLvie3etC8M{-6C@%O?_~c?N>_F+hK{d$3+lSJBVM^J~1iKG|p< KI@@1r4u1heMU>wF literal 0 HcmV?d00001 diff --git a/tests/vectors/key-identity/rows.msgpack b/tests/vectors/key-identity/rows.msgpack new file mode 100644 index 00000000..76363e86 --- /dev/null +++ b/tests/vectors/key-identity/rows.msgpack @@ -0,0 +1 @@ +›”»retype-refused-held-in-file’¥alpha¤beta“’¤openÀ”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”¹retype-by-remove-then-add’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a“¦delete¥alpha¡k”£put¤beta¡k¡b•À‘“¤beta¡k¡b‘’¡k­tests.ki_beta��”»retype-refused-memory-cache’¥alpha¤beta“’¤open¦memory”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡aÀ��”²same-type-replaces‘¥alpha“’¤openÀ”£put¥alpha¡k¡a”£put¥alpha¡k¡b•À‘“¥alpha¡k¡b‘’¡k®tests.ki_alpha��”»retype-refused-after-reload’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a‘¦reopen”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”­foreign-loads‘¥alpha‘’¤open°foreign-store.cc•À‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”·foreign-kept-by-a-write‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡m¡m•À’“¥alpha¡j¡a“¥alpha¡m¡m“’¡j®tests.ki_alpha’¡k®tests.ki_gamma’¡m®tests.ki_alpha‘¡k‘¡k”¹foreign-key-write-refused‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡k¡x•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”ºforeign-key-delete-refused‘¥alpha’’¤open°foreign-store.cc“¦delete¥alpha¡k•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”Ù'foreign-claimed-by-a-later-registration’¥alpha¥gamma‘’¤open°foreign-store.cc•À’“¥alpha¡j¡a“¥gamma¡k¡g’’¡j®tests.ki_alpha’¡k®tests.ki_gamma��”¹key-in-two-stores-refused’¥alpha¤beta“’¤open¦memory“¦attach°catalog-store.cc‘¥alpha“¦attach¬run-store.cc‘¤beta•‘¡k‘“¥alpha¡k¡aÀ�� \ No newline at end of file diff --git a/tests/vectors/key-identity/run-store.cc b/tests/vectors/key-identity/run-store.cc new file mode 100644 index 0000000000000000000000000000000000000000..4f07ad6670043f3da1abba0f24323e26886fceac GIT binary patch literal 634 zcmb_Z%Sr<=6czjuXKNkum}D}ubLGy36*nT1$4xtQW-60ZEYgn{6m(l~TM)mX6#qf` zH>MAYb*tfS&OJBhoSQs-=uzpl)P0DZA2tx*6(YM4<$OJbV|g9+uSK`CV#C>Bx}VnhZeP0;e|F>+ytN}kIKqCDDjiaJ~J3K~}o z>)(7x5=JVhRN$lhI%t(cALXbM)f>4>c<^DwkMCCE-Ldmf#a-VNbuY9FcL5d}sC8hi zEFA4UssGa=Xi}i58J8w2J^Vj6aWAK literal 0 HcmV?d00001 -- 2.47.3 From 53a2f8cbad9283107b947beae8de77e38ae7e062 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 23:17:17 +0000 Subject: [PATCH 2/3] Key-identity vector: row-11 key read from the refusal as a delimited token, memory same-type row, only schema conflicts count as refusals Co-Authored-By: Claude Sonnet 5.5 --- docs/write-ownership-map.md | 2 +- .../KeyIdentityVectorTests.cs | 29 ++++++++++++++---- tests/vectors/key-identity/README.md | 12 +++++--- tests/vectors/key-identity/catalog-store.cc | Bin 638 -> 639 bytes tests/vectors/key-identity/rows.msgpack | 2 +- tests/vectors/key-identity/run-store.cc | Bin 634 -> 635 bytes 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/write-ownership-map.md b/docs/write-ownership-map.md index 6e3f0efc..e1f046cf 100644 --- a/docs/write-ownership-map.md +++ b/docs/write-ownership-map.md @@ -40,7 +40,7 @@ Probes: scratch tests in this worktree, removed. C# filtered runs on Starfire (o | P5 cache {A,B}: remove, then add in a second commit | C | n/a | n/a | n/a | | P6 N2 over an N1 record (undeclared name drift), cache {N2} / cacheless reg {N2} | C restamped / C restamped | not probed | not probed | C (cache) | -The C#-written probe the leaves are held to is `tests/vectors/key-identity` (11 rows: retype, foreign carry, key in two stores), walked by +The C#-written probe the leaves are held to is `tests/vectors/key-identity` (12 rows: retype, foreign carry, key in two stores), walked by `KeyIdentityVectorTests`; each leaf cut corrects its own column above. What this establishes: diff --git a/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs b/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs index a4153b9e..da5eeb3b 100644 --- a/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs +++ b/tests/GameCult.Caching.Tests/KeyIdentityVectorTests.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using GameCult.Caching; using GameCult.Caching.MessagePack; using MessagePack; @@ -58,14 +59,15 @@ namespace GameCult.Caching.Tests new("foreign-key-delete-refused", A, new[] { Open("foreign-store.cc"), Delete("alpha", "k") }), new("foreign-claimed-by-a-later-registration", new[] { "alpha", "gamma" }, new[] { Open("foreign-store.cc") }), new("key-in-two-stores-refused", AB, new[] { Open("memory"), Attach("catalog-store.cc", "alpha"), Attach("run-store.cc", "beta") }), + new("same-type-replaces-memory-cache", A, new[] { Open("memory"), Put("alpha", "k", "a"), Put("alpha", "k", "b") }), }; // The stores C# writes: [file name, registered types, (type, key, text) records]. Never typed by a runtime. private static readonly (string File, string[] Registered, (string Type, string Key, string Text)[] Records)[] Stores = { ("foreign-store.cc", new[] { "alpha", "gamma" }, new[] { ("alpha", "j", "a"), ("gamma", "k", "g") }), - ("catalog-store.cc", new[] { "alpha" }, new[] { ("alpha", "k", "a") }), - ("run-store.cc", new[] { "beta" }, new[] { ("beta", "k", "b") }), + ("catalog-store.cc", new[] { "alpha" }, new[] { ("alpha", "qx", "a") }), + ("run-store.cc", new[] { "beta" }, new[] { ("beta", "qx", "b") }), }; private static CultDocumentRegistry RegistryOf(IEnumerable names) => CultDocumentRegistry.ForTypes(names.Select(name => Types[name]).ToArray()); @@ -102,6 +104,9 @@ namespace GameCult.Caching.Tests private static string Short(string schemaName) => Types.Single(pair => pair.Key == schemaName.Replace("tests.ki_", "")).Key; + // The key as a delimited token: not a piece of a longer word, schema name or path. + private static bool Delimited(string message, string key) => Regex.IsMatch(message, $@"(? $"{record.SchemaId}|{record.StoredAt}|{Convert.ToBase64String(record.Payload)}"; // Runs the ops and returns what the reference answers. Every op but the last must succeed; the last may refuse. @@ -154,7 +159,6 @@ namespace GameCult.Caching.Tests } catch (InvalidOperationException) when (shared.Length == 1) { - refusedKey = shared[0]; Assert.That(File.ReadAllBytes(attached), Is.EqualTo(VectorFiles.Committed(Vector, op.File!)), "a refused attach leaves the store as it was"); throw; } @@ -171,7 +175,8 @@ namespace GameCult.Caching.Tests throw new InvalidOperationException(op.Kind); } } - catch (Exception failure) when (i == row.Ops.Length - 1 && (failure is CultSchemaConflictException || failure is InvalidOperationException)) + catch (Exception failure) when (i == row.Ops.Length - 1 && + (failure is CultSchemaConflictException || (op.Kind == "attach" && failure is InvalidOperationException))) { refusal = failure; } @@ -183,8 +188,20 @@ namespace GameCult.Caching.Tests Assert.That(row.Ops[^1].Key, Is.EqualTo(conflict.RecordKey), $"{row.Label}: the refusal names the key written or removed"); } + if (refusal is InvalidOperationException && refusal is not CultSchemaConflictException) + { + // The key is read out of the refusal's own text, not taken from the row: the second-store refusal opens "Record is held by". + var named = Regex.Match(refusal.Message, @"^Record (\S+) is held by"); + Assert.That(named.Success, Is.True, $"{row.Label}: the refusal opens by naming a key: {refusal.Message}"); + refusedKey = named.Groups[1].Value; + var shared = Snapshot(VectorFiles.Committed(Vector, "run-store.cc")).Records.Select(record => record.Key).ToArray(); + Assert.That(shared, Does.Contain(refusedKey), $"{row.Label}: the key the refusal names is the shared one"); + Assert.That(Regex.Matches(refusal.Message, Regex.Escape(refusedKey)).Count, Is.EqualTo(Regex.Matches(refusal.Message, $@"(? new[] { Short(stored.Descriptor.SchemaName), stored.Key.Value, (string)EmittedDocumentTypes.Read(stored.Document, "Text")! }) @@ -348,7 +365,7 @@ namespace GameCult.Caching.Tests } Assert.That(failed, Is.Empty); - Assert.That(walked, Is.EqualTo(11), "rows walked"); + Assert.That(walked, Is.EqualTo(12), "rows walked"); } } } diff --git a/tests/vectors/key-identity/README.md b/tests/vectors/key-identity/README.md index f10e1fdb..a7d3ea53 100644 --- a/tests/vectors/key-identity/README.md +++ b/tests/vectors/key-identity/README.md @@ -20,16 +20,18 @@ Registering a type that declares its schema id claims it at the next load. - `tests.ki_beta`: `sha256:dee4b727cf2ee04e61ab635ff0c5f3dca7345a411dc5f1018e583ab16009cf94` - `tests.ki_gamma`: `sha256:fbc26e351e00c9e7185e8de171c7486aba707c229c949f0aae6b889288af5f6d` - `foreign-store.cc`: a single-file store, alpha `a` at key `j`, gamma `g` at key `k`, stored-at pinned to - `2026-01-01T00:00:00Z`. `catalog-store.cc`: alpha `a` at `k`. `run-store.cc`: beta `b` at `k` (row 11). + `2026-01-01T00:00:00Z`. `catalog-store.cc`: alpha `a` at `qx`. `run-store.cc`: beta `b` at `qx` (row 11). The row-11 key is `qx` because it must + occur in the refusal's message only as the key, never inside a schema name (`k` is inside `ki_alpha`). - `rows.msgpack`: an array of `[label, registered, ops, expect]`. - `registered`: the types the cache registers, a subset of `[alpha, beta, gamma]`. - `ops`, in order: `[open, file-or-nil-or-memory]` (a cache on a fresh copy of the vector file; nil is a fresh path with no file; the string `memory` is a cache with no store), `[attach, file, [home types]]` (route one more store, a fresh copy of the vector file, to those home types, and pull every store), `[reopen]` (dispose, then a new cache on the same path), `[put, type, key, text]`, `[delete, type, key]` (each one commit). - - `expect` is `[refusal, held, file, foreign, kept]`. Every op but the last succeeds; the last succeeds or refuses. + - `expect` is `[refusal, held, file, foreign, kept]`. Every op but the last succeeds; the last succeeds or refuses, and in the generator only a schema conflict (or, for an attach, an invalid-operation refusal) counts as a refusal: any other exception fails the run. `refusal` is nil or `[key]`: a typed schema-conflict refusal naming the key for a put or a delete (rows 1, 3, 5, 8, 9), - and for row 11 any refusal whose message names the key, the store file it was refused from left byte-identical. + and for row 11 any refusal (an invalid-operation error, not a schema conflict) whose message opens `Record is held by` and + contains the key only as a delimited token (not inside a schema name or other word); the key in `expect` is read out of that message, the store file it was refused from left byte-identical. `held` is the cache's records, sorted by type then key, as `[type, key, text]`. `file` is the store file's records after the ops, sorted by key, as `[key, schema name]`, nil for a cache with no store. `foreign` is the keys the cache lists as foreign, sorted. `kept` is the foreign keys whose stored record (schema id, stored-at, payload) after the ops equals the @@ -40,7 +42,7 @@ Registering a type that declares its schema id claims it at the next load. 1 retype-refused-held-in-file, 2 retype-by-remove-then-add, 3 retype-refused-memory-cache, 4 same-type-replaces, 5 retype-refused-after-reload, 6 foreign-loads, 7 foreign-kept-by-a-write, 8 foreign-key-write-refused, 9 foreign-key-delete-refused, 10 foreign-claimed-by-a-later-registration (registered `[alpha, gamma]`: row 6's store loads -with nothing foreign), 11 key-in-two-stores-refused. +with nothing foreign), 11 key-in-two-stores-refused, 12 same-type-replaces-memory-cache (a cache with no store replaces a same-type record: admitted). -Every runtime walks 11 rows and asserts the count; none is exempt (every row is single-record ops). A leaf never registers +Every runtime walks 12 rows and asserts the count; none is exempt (every row is single-record ops). A leaf never registers gamma in rows 6 and 8; only row 10 does. diff --git a/tests/vectors/key-identity/catalog-store.cc b/tests/vectors/key-identity/catalog-store.cc index 01c7dd2d3f0a947688dfbed6876360dac319db7c..4821af2580ae4409e3ac86cdd0466f93aafaa780 100644 GIT binary patch delta 15 Wcmeyz@}Fe`Hxu)s!ivefOoae2$OU8o delta 14 Vcmey*@{eT$Hxtvs?8$sgg#apX1myq# diff --git a/tests/vectors/key-identity/rows.msgpack b/tests/vectors/key-identity/rows.msgpack index 76363e86..056a9bac 100644 --- a/tests/vectors/key-identity/rows.msgpack +++ b/tests/vectors/key-identity/rows.msgpack @@ -1 +1 @@ -›”»retype-refused-held-in-file’¥alpha¤beta“’¤openÀ”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”¹retype-by-remove-then-add’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a“¦delete¥alpha¡k”£put¤beta¡k¡b•À‘“¤beta¡k¡b‘’¡k­tests.ki_beta��”»retype-refused-memory-cache’¥alpha¤beta“’¤open¦memory”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡aÀ��”²same-type-replaces‘¥alpha“’¤openÀ”£put¥alpha¡k¡a”£put¥alpha¡k¡b•À‘“¥alpha¡k¡b‘’¡k®tests.ki_alpha��”»retype-refused-after-reload’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a‘¦reopen”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”­foreign-loads‘¥alpha‘’¤open°foreign-store.cc•À‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”·foreign-kept-by-a-write‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡m¡m•À’“¥alpha¡j¡a“¥alpha¡m¡m“’¡j®tests.ki_alpha’¡k®tests.ki_gamma’¡m®tests.ki_alpha‘¡k‘¡k”¹foreign-key-write-refused‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡k¡x•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”ºforeign-key-delete-refused‘¥alpha’’¤open°foreign-store.cc“¦delete¥alpha¡k•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”Ù'foreign-claimed-by-a-later-registration’¥alpha¥gamma‘’¤open°foreign-store.cc•À’“¥alpha¡j¡a“¥gamma¡k¡g’’¡j®tests.ki_alpha’¡k®tests.ki_gamma��”¹key-in-two-stores-refused’¥alpha¤beta“’¤open¦memory“¦attach°catalog-store.cc‘¥alpha“¦attach¬run-store.cc‘¤beta•‘¡k‘“¥alpha¡k¡aÀ�� \ No newline at end of file +œ”»retype-refused-held-in-file’¥alpha¤beta“’¤openÀ”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”¹retype-by-remove-then-add’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a“¦delete¥alpha¡k”£put¤beta¡k¡b•À‘“¤beta¡k¡b‘’¡k­tests.ki_beta��”»retype-refused-memory-cache’¥alpha¤beta“’¤open¦memory”£put¥alpha¡k¡a”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡aÀ��”²same-type-replaces‘¥alpha“’¤openÀ”£put¥alpha¡k¡a”£put¥alpha¡k¡b•À‘“¥alpha¡k¡b‘’¡k®tests.ki_alpha��”»retype-refused-after-reload’¥alpha¤beta”’¤openÀ”£put¥alpha¡k¡a‘¦reopen”£put¤beta¡k¡b•‘¡k‘“¥alpha¡k¡a‘’¡k®tests.ki_alpha��”­foreign-loads‘¥alpha‘’¤open°foreign-store.cc•À‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”·foreign-kept-by-a-write‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡m¡m•À’“¥alpha¡j¡a“¥alpha¡m¡m“’¡j®tests.ki_alpha’¡k®tests.ki_gamma’¡m®tests.ki_alpha‘¡k‘¡k”¹foreign-key-write-refused‘¥alpha’’¤open°foreign-store.cc”£put¥alpha¡k¡x•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”ºforeign-key-delete-refused‘¥alpha’’¤open°foreign-store.cc“¦delete¥alpha¡k•‘¡k‘“¥alpha¡j¡a’’¡j®tests.ki_alpha’¡k®tests.ki_gamma‘¡k‘¡k”Ù'foreign-claimed-by-a-later-registration’¥alpha¥gamma‘’¤open°foreign-store.cc•À’“¥alpha¡j¡a“¥gamma¡k¡g’’¡j®tests.ki_alpha’¡k®tests.ki_gamma��”¹key-in-two-stores-refused’¥alpha¤beta“’¤open¦memory“¦attach°catalog-store.cc‘¥alpha“¦attach¬run-store.cc‘¤beta•‘¢qx‘“¥alpha¢qx¡aÀ��”¿same-type-replaces-memory-cache‘¥alpha“’¤open¦memory”£put¥alpha¡k¡a”£put¥alpha¡k¡b•À‘“¥alpha¡k¡bÀ�� \ No newline at end of file diff --git a/tests/vectors/key-identity/run-store.cc b/tests/vectors/key-identity/run-store.cc index 4f07ad6670043f3da1abba0f24323e26886fceac..9166ed095f3323f5c07993271aa166da070ef6f6 100644 GIT binary patch delta 19 bcmeyx@|$G?I}`Jw!ivcY89gTlF=+q*OgskC delta 18 acmey(@{45yI}_8w?8yrmJtqe -- 2.47.3 From cea49093a761d30f22c554d60f379d62f15cf9e5 Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 11 Nov 2026 23:21:15 +0000 Subject: [PATCH 3/3] Key-identity vector README: row 11's cross-runtime contract is the key as a delimited token; the message prefix is the C# walker's own Co-Authored-By: Claude Sonnet 5.5 --- tests/vectors/key-identity/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/vectors/key-identity/README.md b/tests/vectors/key-identity/README.md index a7d3ea53..f6ad6516 100644 --- a/tests/vectors/key-identity/README.md +++ b/tests/vectors/key-identity/README.md @@ -30,8 +30,7 @@ Registering a type that declares its schema id claims it at the next load. `[put, type, key, text]`, `[delete, type, key]` (each one commit). - `expect` is `[refusal, held, file, foreign, kept]`. Every op but the last succeeds; the last succeeds or refuses, and in the generator only a schema conflict (or, for an attach, an invalid-operation refusal) counts as a refusal: any other exception fails the run. `refusal` is nil or `[key]`: a typed schema-conflict refusal naming the key for a put or a delete (rows 1, 3, 5, 8, 9), - and for row 11 any refusal (an invalid-operation error, not a schema conflict) whose message opens `Record is held by` and - contains the key only as a delimited token (not inside a schema name or other word); the key in `expect` is read out of that message, the store file it was refused from left byte-identical. + and for row 11 any refusal (an invalid-operation error, not a schema conflict) whose message contains the key as a delimited token (not inside a schema name or other word; the C# walker also pins its own `Record is held by` opening, which a leaf need not copy); the key in `expect` is the one the stores share, the store file it was refused from left byte-identical. `held` is the cache's records, sorted by type then key, as `[type, key, text]`. `file` is the store file's records after the ops, sorted by key, as `[key, schema name]`, nil for a cache with no store. `foreign` is the keys the cache lists as foreign, sorted. `kept` is the foreign keys whose stored record (schema id, stored-at, payload) after the ops equals the -- 2.47.3