diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a052e94..72320f4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "signs-of-ai", "description": "Measure a text for the signs of AI writing and read back the evidence — offline, EN/ES, with the tool's own false-positive rate attached.", - "version": "0.4.0", + "version": "0.7.1", "author": { "name": "Pedro Hernández — PeopleWorks", "url": "https://github.com/peopleworks" diff --git a/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs b/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs new file mode 100644 index 0000000..e132f3c --- /dev/null +++ b/tests/SignsOfAI.Core.Tests/ReleaseVersionTests.cs @@ -0,0 +1,75 @@ +using System.Text.RegularExpressions; +using Xunit; + +namespace SignsOfAI.Core.Tests; + +/// +/// The version is typed into seven places across five files, and a release bumps them by hand. +/// .claude-plugin/plugin.json was three releases behind — it said 0.4.0 while every channel +/// shipped 0.7.1 — and nothing failed, because the release checklist never listed it. +/// +/// That file is what a plugin directory reads, which makes it the version a stranger sees first. +/// So this holds every one of them to the engine's own, and fails by file name when a release +/// forgets one. The eighth time something this project says about itself went stale. +/// +public class ReleaseVersionTests +{ + private static readonly string Root = FindRoot(); + + /// The authority: what SignsOfAI.Core ships as. + private static readonly string Engine = Read( + Path.Combine("src", "SignsOfAI.Core", "SignsOfAI.Core.csproj"), + @"(?[^<]+)"); + + private static string FindRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "src"))) + dir = dir.Parent; + + Assert.NotNull(dir); + return dir!.FullName; + } + + private static string Read(string relativePath, string pattern) + { + var text = File.ReadAllText(Path.Combine(Root, relativePath)); + var match = Regex.Match(text, pattern); + Assert.True(match.Success, $"{relativePath} no longer carries a version matching {pattern}."); + return match.Groups["v"].Value.Trim(); + } + + public static TheoryData Declarations() => new() + { + // Published to NuGet. + { Path.Combine("src", "SignsOfAI.Cli", "SignsOfAI.Cli.csproj"), @"(?[^<]+)" }, + { Path.Combine("src", "SignsOfAI.Mcp", "SignsOfAI.Mcp.csproj"), @"(?[^<]+)" }, + // What the download page offers, which the release workflow also enforces. + { Path.Combine("src", "SignsOfAI.UI", "Services", "DesktopRelease.cs"), + @"public const string Version = ""(?[^""]+)"";" }, + // What a plugin directory shows a stranger. This is the one that drifted. + { Path.Combine(".claude-plugin", "plugin.json"), @"""version"":\s*""(?[^""]+)""" }, + }; + + [Theory] + [MemberData(nameof(Declarations))] + public void Every_shipped_manifest_declares_the_engine_version(string relativePath, string pattern) + { + Assert.True(Engine == Read(relativePath, pattern), + $"{relativePath} says {Read(relativePath, pattern)} and the engine ships {Engine}. " + + "A release bumps all of them or none of them."); + } + + [Fact] + public void The_mcp_registry_manifest_declares_it_in_both_places() + { + // server.json carries the version twice, and publishing with them out of step is rejected by + // the registry after NuGet has already gone out — the worst moment to find out. + var path = Path.Combine("src", "SignsOfAI.Mcp", ".mcp", "server.json"); + var found = Regex.Matches(File.ReadAllText(Path.Combine(Root, path)), + @"""version"":\s*""(?[^""]+)""").Select(m => m.Groups["v"].Value).ToList(); + + Assert.Equal(2, found.Count); + Assert.All(found, v => Assert.Equal(Engine, v)); + } +}