From bb25bf04cb7037e40a688df8f286848077e1455a Mon Sep 17 00:00:00 2001 From: Libby Date: Thu, 5 Nov 2026 09:43:55 +0000 Subject: [PATCH] docs/start: verbatim drift check for the C# and TypeScript first-call blocks Both blocks now come from a snippet region in their test, and a second test fails when docs/start/README.md stops showing it, as the Rust and Python blocks already do. The TS block differs only in the package import specifier. Co-Authored-By: Claude Sonnet 5.5 --- docs/start/README.md | 61 ++++++++------ packages/cultcache-ts/test/first-call.test.ts | 59 +++++++++----- .../GameCult.Caching.Tests/FirstCallTests.cs | 80 ++++++++++++------- 3 files changed, 127 insertions(+), 73 deletions(-) diff --git a/docs/start/README.md b/docs/start/README.md index c178c7f0..99b66354 100644 --- a/docs/start/README.md +++ b/docs/start/README.md @@ -34,6 +34,7 @@ which is what `dotnet test tests/GameCult.Caching.Tests --filter FirstCallTests` cannot drift without the test failing. ```csharp +using System.Threading.Tasks; using GameCult.Caching; using GameCult.Caching.MessagePack; using MessagePack; @@ -47,20 +48,27 @@ public sealed class Item [Key(2)] public int Value; } -var store = new SingleFileMessagePackBackingStore("first-call.cc"); -var cache = new CultCache(); -cache.AddBackingStore(store); -var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); -store.PushAll(); // write the .cc file +public static class FirstCall +{ + public static async Task Run(string path) + { + var store = new SingleFileMessagePackBackingStore(path); + var cache = new CultCache(); + cache.AddBackingStore(store); + var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); + store.PushAll(); // write the .cc file -var reopened = new CultCache(); -reopened.AddBackingStore(new SingleFileMessagePackBackingStore("first-call.cc")); -await reopened.PullAllBackingStoresAsync(); // read it back -var potion = reopened.Get(handle.Key); // by key -var byName = reopened.GetByName("Potion"); // by [CultName] + var reopened = new CultCache(); + reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path)); + await reopened.PullAllBackingStoresAsync(); // read it back + var potion = reopened.Get(handle.Key); // by key + var byName = reopened.GetByName("Potion"); // by [CultName] + return byName ?? potion; + } +} ``` -The test's `Item` is nested and internal, so it carries `[MessagePackObject(AllowPrivate = true)]`; the snippet above is the public-class form and is not itself compiled. More: `src/GameCult.Caching/README.md`. +`FirstCall.Run` is the call; a second test in that file fails if the block above stops matching it. More: `src/GameCult.Caching/README.md`. ## TypeScript first call @@ -90,23 +98,26 @@ const itemDocument = defineDocumentType({ }); const registry = defineDocumentRegistry(itemDocument); -const cache = CultCache.builder() - .withRegistry(registry) - .withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc")) - .build(); -await cache.pullAllBackingStores(); -await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); +async function firstCall(path: string) { + const cache = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await cache.pullAllBackingStores(); + await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); -const reopened = CultCache.builder() - .withRegistry(registry) - .withGenericStore(new SingleFileMessagePackBackingStore("first-call.cc")) - .build(); -await reopened.pullAllBackingStores(); -const potion = reopened.get(itemDocument, "item:potion"); -const byName = reopened.getByName(itemDocument, "Potion"); + const reopened = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await reopened.pullAllBackingStores(); + const potion = reopened.get(itemDocument, "item:potion"); + const byName = reopened.getByName(itemDocument, "Potion"); + return { potion, byName }; +} ``` -The test imports from `../src/index`; an application imports `@gamecult/cultcache-ts`. More: +The test imports from `../src/index` where the block reads `@gamecult/cultcache-ts`; a second test in that file fails if the block stops matching the rest. More: `packages/cultcache-ts/README.md`. ## Rust first call diff --git a/packages/cultcache-ts/test/first-call.test.ts b/packages/cultcache-ts/test/first-call.test.ts index aec9faa1..3c0e22e5 100644 --- a/packages/cultcache-ts/test/first-call.test.ts +++ b/packages/cultcache-ts/test/first-call.test.ts @@ -1,11 +1,15 @@ import assert from "node:assert/strict"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; +// The TypeScript first-call snippet that docs/start/README.md shows: define a typed document, store it +// in a .cc file, read it back through a second cache. Keep it small; it is the front door. The docs show +// the region between the `snippet` markers verbatim, except that the package import reads +// "@gamecult/cultcache-ts" where this file reads "../src/index"; the docs test below fails when they differ. +// snippet:begin import { z } from "zod"; - import { CultCache, SingleFileMessagePackBackingStore, @@ -13,8 +17,6 @@ import { defineDocumentType, } from "../src/index"; -// The TypeScript first-call snippet that docs/start/README.md links: define a typed document, -// store it in a .cc file, read it back through a second cache. Keep it small; it is the front door. const itemDocument = defineDocumentType({ type: "item", schemaId: "gamecult.first_call_item.v1", @@ -26,26 +28,43 @@ const itemDocument = defineDocumentType({ }); const registry = defineDocumentRegistry(itemDocument); +async function firstCall(path: string) { + const cache = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await cache.pullAllBackingStores(); + await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); + + const reopened = CultCache.builder() + .withRegistry(registry) + .withGenericStore(new SingleFileMessagePackBackingStore(path)) + .build(); + await reopened.pullAllBackingStores(); + const potion = reopened.get(itemDocument, "item:potion"); + const byName = reopened.getByName(itemDocument, "Potion"); + return { potion, byName }; +} +// snippet:end + test("define, store and read back a typed document", async () => { const dir = await mkdtemp(join(tmpdir(), "cultcache-first-call-")); - const path = join(dir, "first-call.cc"); try { - const cache = CultCache.builder() - .withRegistry(registry) - .withGenericStore(new SingleFileMessagePackBackingStore(path)) - .build(); - await cache.pullAllBackingStores(); - await cache.put(itemDocument, "item:potion", { name: "Potion", category: "Consumable", value: 50 }); - - const reopened = CultCache.builder() - .withRegistry(registry) - .withGenericStore(new SingleFileMessagePackBackingStore(path)) - .build(); - await reopened.pullAllBackingStores(); - - assert.equal(reopened.get(itemDocument, "item:potion")?.value, 50); - assert.equal(reopened.getByName(itemDocument, "Potion")?.category, "Consumable"); + const { potion, byName } = await firstCall(join(dir, "first-call.cc")); + assert.equal(potion?.value, 50); + assert.equal(byName?.category, "Consumable"); } finally { await rm(dir, { recursive: true, force: true }); } }); + +test("docs show the tested snippet", async () => { + // Compiled to dist-test/test/first-call.test.js, so the package root is two levels up. + const source = await readFile(join(__dirname, "..", "..", "test", "first-call.test.ts"), "utf8"); + const snippet = source + .split("// snippet:begin\n")[1] + .split("// snippet:end\n")[0] + .replace('"../src/index"', '"@gamecult/cultcache-ts"'); + const docs = (await readFile(join(__dirname, "..", "..", "..", "..", "docs", "start", "README.md"), "utf8")).replace(/\r\n/g, "\n"); + assert.ok(docs.includes("```ts\n" + snippet + "```"), "docs/start/README.md no longer shows test/first-call.test.ts"); +}); diff --git a/tests/GameCult.Caching.Tests/FirstCallTests.cs b/tests/GameCult.Caching.Tests/FirstCallTests.cs index d14ad72c..8540d9d0 100644 --- a/tests/GameCult.Caching.Tests/FirstCallTests.cs +++ b/tests/GameCult.Caching.Tests/FirstCallTests.cs @@ -1,54 +1,78 @@ #nullable enable using System; using System.IO; +using NUnit.Framework; + +// snippet:begin using System.Threading.Tasks; using GameCult.Caching; using GameCult.Caching.MessagePack; using MessagePack; -using NUnit.Framework; + +[CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")] +[MessagePackObject] +public sealed class Item +{ + [Key(0)] [CultName] public string Name = string.Empty; + [Key(1)] [CultIndex] public string Category = string.Empty; + [Key(2)] public int Value; +} + +public static class FirstCall +{ + public static async Task Run(string path) + { + var store = new SingleFileMessagePackBackingStore(path); + var cache = new CultCache(); + cache.AddBackingStore(store); + var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); + store.PushAll(); // write the .cc file + + var reopened = new CultCache(); + reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path)); + await reopened.PullAllBackingStoresAsync(); // read it back + var potion = reopened.Get(handle.Key); // by key + var byName = reopened.GetByName("Potion"); // by [CultName] + return byName ?? potion; + } +} +// snippet:end namespace GameCult.Caching.Tests { - // The C# first-call snippet that docs/start/README.md links: define a typed document, store it in a - // .cc file, read it back through a second cache. Keep it small; it is the front door. + // The C# first-call snippet that docs/start/README.md shows verbatim: define a typed document, store it + // in a .cc file, read it back through a second cache. DocsShowTheTestedSnippet fails when they differ. public class FirstCallTests { - [CultDocument("gamecult.first_call_item", "gamecult.first_call_item.v1")] - [MessagePackObject(AllowPrivate = true)] - internal sealed class Item - { - [Key(0)] [CultName] public string Name = string.Empty; - [Key(1)] [CultIndex] public string Category = string.Empty; - [Key(2)] public int Value; - } - [Test] public async Task DefineStoreAndReadBackATypedDocument() { var path = Path.Combine(Path.GetTempPath(), $"first-call-{Guid.NewGuid():N}.cc"); try { - // Write: a cache with one backing store, one typed document. - var store = new SingleFileMessagePackBackingStore(path); - var cache = new CultCache(); - cache.AddBackingStore(store); - var handle = await cache.AddAsync(new Item { Name = "Potion", Category = "Consumable", Value = 50 }); - store.PushAll(); - - // Read: a fresh cache over the same file. - var reopened = new CultCache(); - reopened.AddBackingStore(new SingleFileMessagePackBackingStore(path)); - await reopened.PullAllBackingStoresAsync(); - - var byKey = reopened.Get(handle.Key); - Assert.That(byKey, Is.Not.Null); - Assert.That(byKey!.Value, Is.EqualTo(50)); - Assert.That(reopened.GetByName("Potion")!.Category, Is.EqualTo("Consumable")); + var potion = await FirstCall.Run(path); + Assert.That(potion, Is.Not.Null); + Assert.That(potion!.Value, Is.EqualTo(50)); + Assert.That(potion.Category, Is.EqualTo("Consumable")); } finally { File.Delete(path); } } + + [Test] + public void DocsShowTheTestedSnippet() + { + var source = File.ReadAllText(SourcePath()); + const string begin = "// snippet:begin\n"; + var start = source.IndexOf(begin, StringComparison.Ordinal) + begin.Length; + var snippet = source[start..source.IndexOf("// snippet:end\n", StringComparison.Ordinal)]; + var docs = File.ReadAllText(Path.Combine(Path.GetDirectoryName(SourcePath())!, "..", "..", "docs", "start", "README.md")); + Assert.That(docs.Replace("\r\n", "\n"), Does.Contain("```csharp\n" + snippet + "```"), + "docs/start/README.md no longer shows tests/GameCult.Caching.Tests/FirstCallTests.cs verbatim"); + } + + static string SourcePath([System.Runtime.CompilerServices.CallerFilePath] string path = "") => path; } } -- 2.47.3