Interop: a C# in-place edit is written, and Rust and Python read the new value

The C# peer gains edit-in-place (payload and override through the public snapshot
API; collection and nested object through the held document), and the four-peer
test has Rust and Python read each edit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 10:47:31 +02:00
commit 49f8abd1b6
2 changed files with 115 additions and 0 deletions

View file

@ -1365,6 +1365,50 @@ test("CultCache element ids cross C#, TypeScript, Python and Rust as ordinary me
assert.deepEqual(back.ids, tsIds);
});
// A C# record edited in place is written as edited, however the consumer reached it: a decoded snapshot record's payload bytes, a
// variant override's value bytes, a collection element or a nested object of the document the cache holds. Rust and Python read the
// edit through their own readers; the variant and the nested object are read by decoding the file with Python's msgpack.
test("C# writes an edit made in place, and Rust and Python read the new value", async () => {
await buildInteropPeers();
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-edit-in-place-"));
const csharp = (name: string, args: string[]) => runJsonCommand(name, dotnetCommand, [csharpInteropDll, ...args], cultLibRoot);
const edit = (file: string, kind: string) => csharp(`csharp-edit-${kind}`, ["edit-in-place", "--file", file, "--kind", kind]);
const readers = (file: string) => [
runJsonCommand("rust-read", rustInteropBinary, ["read", "--file", file], cultcacheRsRoot),
runJsonCommand("python-read", pythonCommand, ["-m", "cultcache_py.interop", "read", "--file", file], cultcachePyRoot, { PYTHONPATH: cultcachePySrc }),
];
const dump = (file: string) => runJsonCommand("python-dump", pythonCommand, [
"-c",
"import msgpack,json,sys\n"
+ "s=msgpack.unpackb(open(sys.argv[1],'rb').read(),raw=False)\n"
+ "print(json.dumps([{'key':r[0],'overrides':[o[3] for o in r[4][1]]} if len(r)>4 else {'key':r[0],'payload':msgpack.unpackb(r[3],raw=False)} for r in s[2]]))",
file,
], cultcachePyRoot);
const payload = join(tempDir, "payload.cc");
await csharp("csharp-write", ["write", "--file", payload, "--runtime-id", "csharp-writer"]);
await edit(payload, "payload");
for (const read of await Promise.all(readers(payload))) assert.equal(read.title, "csharp-writer WROTE a CultCache note");
const collection = join(tempDir, "collection.cc");
await csharp("csharp-write", ["write", "--file", collection, "--runtime-id", "csharp-writer"]);
await edit(collection, "collection");
for (const read of await Promise.all(readers(collection))) assert.deepEqual(read.tags, ["EDITED", "csharp", "interop"]);
const override = join(tempDir, "override.cc");
await csharp("csharp-write", ["write", "--file", override, "--runtime-id", "csharp-writer"]);
await edit(override, "override");
const variant = (await dump(override)).find((record: any) => record.key === "note:variant");
assert.deepEqual(variant.overrides, ["VARIANT title"]);
const object = join(tempDir, "object.cc");
await csharp("csharp-write-deck", ["write-deck", "--file", object]);
await edit(object, "object");
const deck = (await dump(object)).find((record: any) => record.key === "deck:csharp");
assert.equal(deck.payload[1][0][0], "EDITED");
assert.equal(deck.payload[1][1][0], "twin", "the neighbouring mark is untouched");
});
test("CultCache interop reader accepts missing compatible trailing slots and rejects mismatched slots", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "cultcache-interop-"));
const compatible = join(tempDir, "compatible.msgpack");

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@ -47,6 +48,9 @@ static async Task<int> ProgramMainAsync(string[] args)
case "read-deck":
await ReadDeckAsync(file);
return 0;
case "edit-in-place":
await EditInPlaceAsync(file, RequireArg(options, "kind"));
return 0;
default:
throw new InvalidOperationException($"Unknown mode {mode}.");
}
@ -122,6 +126,73 @@ static async Task ReadDeckAsync(string file)
static void WriteDeckJsonLine(CultCacheInteropDeck deck) =>
Console.Out.WriteLine(JsonSerializer.Serialize(new { documentId = deck.DocumentId, ids = deck.Marks.Select(mark => mark.Id).ToArray() }));
// Edits a stored record in place, as a consumer holding the object would, and writes it: the file must hold the edit, in whatever
// runtime reads it. payload and override go through the public snapshot API (a decoded record's bytes edited, then
// SerializeSnapshot); collection and object edit the document the cache holds and put the same instance back.
static async Task EditInPlaceAsync(string file, string kind)
{
switch (kind)
{
case "payload":
{
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file));
OverwriteInPlace(snapshot.Records.Single().Payload, "wrote", "WROTE");
File.WriteAllBytes(file, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
break;
}
case "override":
{
var cache = BuildCache(file);
await cache.PullAllBackingStoresAsync();
var note = cache.AllEntries.OfType<CultCacheInteropNote>().Single();
await cache.UpsertVariantAsync(
new CultRecordKey("note:variant"),
new CultRecordKey(note.DocumentId),
new[] { cache.Override<CultCacheInteropNote>(nameof(CultCacheInteropNote.Title), "variant title") });
cache.FlushAllBackingStores();
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(file));
var variant = snapshot.Records.Single(record => record.Variant != null).Variant!;
OverwriteInPlace(variant.Overrides.Single().Value, "variant", "VARIANT");
File.WriteAllBytes(file, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
break;
}
case "collection":
{
var cache = BuildCache(file);
await cache.PullAllBackingStoresAsync();
var note = cache.AllEntries.OfType<CultCacheInteropNote>().Single();
note.Tags[0] = "EDITED";
await cache.UpsertAsync(note, new CultRecordHandle<CultCacheInteropNote>(new CultRecordKey(note.DocumentId)));
cache.FlushAllBackingStores();
break;
}
case "object":
{
var cache = BuildCache(file);
await cache.PullAllBackingStoresAsync();
var deck = cache.AllEntries.OfType<CultCacheInteropDeck>().Single();
deck.Marks[0].Label = "EDITED";
await cache.UpsertAsync(deck, new CultRecordHandle<CultCacheInteropDeck>(new CultRecordKey(deck.DocumentId)));
cache.FlushAllBackingStores();
break;
}
default:
throw new InvalidOperationException($"Unknown edit kind {kind}.");
}
Console.Out.WriteLine(JsonSerializer.Serialize(new { kind }));
}
static void OverwriteInPlace(byte[] bytes, string from, string to)
{
var was = System.Text.Encoding.UTF8.GetBytes(from);
var now = System.Text.Encoding.UTF8.GetBytes(to);
var at = bytes.AsSpan().IndexOf(was);
if (at < 0 || now.Length != was.Length)
throw new InvalidOperationException($"Cannot overwrite '{from}' with '{to}' in place.");
now.CopyTo(bytes, at);
}
// Two routed single-file stores: each file is a complete single-store snapshot holding only its own type.
static void WriteRouted(string catalogFile, string runFile)
{