chore: address Copilot review round 16
`parseClaudeVersion` dropped SemVer suffixes, so an installed `2.1.250-beta.1`
parsed as `[2, 1, 250]` and compared EQUAL to the stable pin — a different
validator schema running locally while the log claimed an exact match, which is
the cross-machine disagreement the pin exists to prevent.
It now returns `{ parts, prerelease }`, because the two callers want different
things: `isPinnedVersion` rejects any prerelease (build metadata still matches,
per SemVer precedence), while the eval's availability probe only needs some
usable CLI. `formatClaudeVersion` keeps the suffix in the messages, so a
prerelease is reported as what it is rather than as the pin.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
3905ceef06
commit
c26995bf82
2 changed files with 102 additions and 54 deletions
|
|
@ -253,21 +253,48 @@ export function validateEvalCases(skillName, cases) {
|
|||
export const PINNED_CLI_VERSION = "2.1.250";
|
||||
|
||||
/**
|
||||
* Read a semver triple out of `claude --version` output ("2.1.250 (Claude Code)").
|
||||
* Read the version out of `claude --version` output ("2.1.250 (Claude Code)").
|
||||
*
|
||||
* Returns the release triple **and** whether a prerelease/build suffix was
|
||||
* attached, because the two callers want different things: the pin comparison
|
||||
* must reject `2.1.250-beta.1` (a different validator schema that would compare
|
||||
* equal to the stable pin while the log claimed an exact match — Copilot),
|
||||
* while the eval's availability probe only needs *some* usable CLI.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {number[] | null}
|
||||
* @returns {{ parts: number[], prerelease: string | null } | null}
|
||||
*/
|
||||
export function parseClaudeVersion(text) {
|
||||
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
||||
return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||
const m =
|
||||
/(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?/.exec(text);
|
||||
if (m === null) return null;
|
||||
return {
|
||||
parts: [Number(m[1]), Number(m[2]), Number(m[3])],
|
||||
prerelease: m[4] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** A parsed version, formatted the way the CLI reports it. */
|
||||
export function formatClaudeVersion(version) {
|
||||
if (version === null) return "(unreadable version)";
|
||||
return (
|
||||
version.parts.join(".") +
|
||||
(version.prerelease ? `-${version.prerelease}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} a
|
||||
* @param {number[]} b
|
||||
* @returns {number} negative if a < b, 0 if equal, positive if a > b.
|
||||
* Whether a parsed version IS the pin — same triple and no prerelease.
|
||||
*
|
||||
* @param {{ parts: number[], prerelease: string | null } | null} version
|
||||
* @param {string} pin e.g. `PINNED_CLI_VERSION`.
|
||||
*/
|
||||
export function isPinnedVersion(version, pin) {
|
||||
if (version === null || version.prerelease !== null) return false;
|
||||
const pinned = parseClaudeVersion(pin);
|
||||
return pinned !== null && compareVersions(version.parts, pinned.parts) === 0;
|
||||
}
|
||||
|
||||
export function compareVersions(a, b) {
|
||||
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
||||
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import {
|
|||
parseClaudeVersion,
|
||||
compareVersions,
|
||||
PINNED_CLI_VERSION,
|
||||
isPinnedVersion,
|
||||
formatClaudeVersion,
|
||||
} from "./skill-manifest.mjs";
|
||||
|
||||
const fm = (body) => `---\n${body}\n---\n\n# Body\n`;
|
||||
|
|
@ -203,23 +205,47 @@ test("validateEvalCases rejects malformed cases", () => {
|
|||
});
|
||||
|
||||
test("parseClaudeVersion reads the CLI's version banner", () => {
|
||||
assert.deepEqual(parseClaudeVersion("2.1.250 (Claude Code)"), [2, 1, 250]);
|
||||
assert.deepEqual(parseClaudeVersion("claude 10.0.4\n"), [10, 0, 4]);
|
||||
assert.deepEqual(parseClaudeVersion("2.1.250 (Claude Code)"), {
|
||||
parts: [2, 1, 250],
|
||||
prerelease: null,
|
||||
});
|
||||
assert.deepEqual(parseClaudeVersion("claude 10.0.4\n"), {
|
||||
parts: [10, 0, 4],
|
||||
prerelease: null,
|
||||
});
|
||||
assert.equal(parseClaudeVersion("no version here"), null);
|
||||
});
|
||||
|
||||
test("compareVersions can decide exact equality with the pin", () => {
|
||||
// Both validators require the EXACT pinned version, so what matters is that
|
||||
// "equal" is distinguishable from newer as well as older — a floor would let a
|
||||
// newer local CLI validate against a different schema than CI's.
|
||||
const pin = parseClaudeVersion(PINNED_CLI_VERSION);
|
||||
assert.equal(compareVersions(pin, pin), 0);
|
||||
assert.ok(compareVersions([pin[0], pin[1], pin[2] - 1], pin) < 0);
|
||||
assert.ok(compareVersions([pin[0], pin[1], pin[2] + 1], pin) > 0);
|
||||
assert.ok(compareVersions([pin[0] + 1, 0, 0], pin) > 0);
|
||||
// Shorter and longer triples still order sanely.
|
||||
assert.ok(compareVersions([pin[0]], pin) < 0);
|
||||
assert.equal(compareVersions([...pin, 0], pin), 0);
|
||||
test("parseClaudeVersion keeps a prerelease suffix rather than dropping it", () => {
|
||||
// Dropping it made `2.1.250-beta.1` compare EQUAL to the stable pin — a
|
||||
// different validator schema running while the log claimed an exact match.
|
||||
assert.deepEqual(parseClaudeVersion("2.1.250-beta.1 (Claude Code)"), {
|
||||
parts: [2, 1, 250],
|
||||
prerelease: "beta.1",
|
||||
});
|
||||
assert.equal(
|
||||
formatClaudeVersion(parseClaudeVersion("2.1.250-beta.1")),
|
||||
"2.1.250-beta.1",
|
||||
);
|
||||
assert.equal(formatClaudeVersion(null), "(unreadable version)");
|
||||
});
|
||||
|
||||
test("isPinnedVersion accepts only the exact stable pin", () => {
|
||||
const pin = PINNED_CLI_VERSION;
|
||||
assert.equal(isPinnedVersion(parseClaudeVersion(pin), pin), true);
|
||||
// Build metadata is ignored for precedence by SemVer, so it still matches.
|
||||
assert.equal(
|
||||
isPinnedVersion(parseClaudeVersion(`${pin}+build.7`), pin),
|
||||
true,
|
||||
);
|
||||
// A prerelease of the same triple is NOT the pin.
|
||||
assert.equal(
|
||||
isPinnedVersion(parseClaudeVersion(`${pin}-beta.1`), pin),
|
||||
false,
|
||||
);
|
||||
assert.equal(isPinnedVersion(parseClaudeVersion("2.1.249"), pin), false);
|
||||
assert.equal(isPinnedVersion(parseClaudeVersion("99.0.0"), pin), false);
|
||||
assert.equal(isPinnedVersion(null, pin), false);
|
||||
});
|
||||
|
||||
test("the listing-entry cap covers description AND when_to_use together", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue