From 71af277b79b482bed2ad6cdaf275beb13f33d33e Mon Sep 17 00:00:00 2001 From: Corwin Marsh Date: Sat, 18 Jul 2026 10:43:56 -0700 Subject: [PATCH] fix: decouple command contract from release version --- contracts/cli-commands.json | 3 +-- src/contracts/cli-commands.ts | 6 ++---- tests/unit/cli-contract.test.ts | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/contracts/cli-commands.json b/contracts/cli-commands.json index 4f9d839..9a24115 100644 --- a/contracts/cli-commands.json +++ b/contracts/cli-commands.json @@ -1,6 +1,5 @@ { - "schemaVersion": 1, - "cliVersion": "1.20.0", + "schemaVersion": 2, "commands": [ { "path": "add", diff --git a/src/contracts/cli-commands.ts b/src/contracts/cli-commands.ts index 6c06d84..ee93a6f 100644 --- a/src/contracts/cli-commands.ts +++ b/src/contracts/cli-commands.ts @@ -208,8 +208,7 @@ export function validateCommandSemantics(paths: string[], metadata: CommandSeman } export interface CliCommandContract { - schemaVersion: 1; - cliVersion: string; + schemaVersion: 2; commands: ContractCommand[]; } interface ContractCommand { @@ -266,8 +265,7 @@ export function generateCommandContract( }; visit(program, ""); return { - schemaVersion: 1, - cliVersion: program.version() ?? "", + schemaVersion: 2, commands: commands.toSorted((a, b) => a.path.localeCompare(b.path)), }; } diff --git a/tests/unit/cli-contract.test.ts b/tests/unit/cli-contract.test.ts index d86736d..e33c815 100644 --- a/tests/unit/cli-contract.test.ts +++ b/tests/unit/cli-contract.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { Command } from "commander"; +import pkg from "../../package.json"; import { buildProgram, discoverCommandPaths } from "../../src/cli-program.ts"; import { commandSemantics, @@ -40,6 +41,7 @@ describe("CLI command contract", () => { expect(first).toBeInstanceOf(Command); expect(second).not.toBe(first); expect(first.name()).toBe("arashi"); + expect(first.version()).toBe(pkg.version); }); test("discovers every registered command path exactly", () => { @@ -108,11 +110,23 @@ describe("CLI command contract", () => { ); expect(first).toBe(second); const contract = JSON.parse(first); - expect(contract.schemaVersion).toBe(1); + expect(contract.schemaVersion).toBe(2); + expect(contract).not.toHaveProperty("cliVersion"); expect(contract.commands.map((command: { path: string }) => command.path)).toEqual( expectedPaths, ); expect(contract.commands[0]).toHaveProperty("options"); expect(first.endsWith("\n")).toBe(true); }); + + test("does not change when only the runtime release version changes", () => { + const current = buildProgram({ includeHelpBanner: false }); + const alternateRelease = new Command().name("arashi").version("999.0.0"); + for (const command of buildProgram({ includeHelpBanner: false }).commands) + alternateRelease.addCommand(command); + + expect(serializeCommandContract(generateCommandContract(current, commandSemantics))).toBe( + serializeCommandContract(generateCommandContract(alternateRelease, commandSemantics)), + ); + }); });