feat(#1463): add capability outdated (per-source update check); drop phantom slash-command docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-20 09:38:34 -04:00
parent 67a3842776
commit 7c93d9e222
11 changed files with 1244 additions and 20 deletions

View File

@@ -30,12 +30,26 @@ const {
parseSpec,
_setCapabilitySourceHttpGet,
_setHttpsGetImpl,
peekLatestVersion,
pickHighestSemverTag,
splitNpmSpec,
pickHighestNpmVersion,
MAX_RESPONSE_BYTES,
MANIFEST_MAX_BYTES,
MAX_STAGED_BUNDLE_BYTES,
MAX_STAGED_BUNDLE_ENTRIES,
} = capSource;
const { EventEmitter } = require('node:events');
const fc = require('fast-check');
/** Build a `git ls-remote --tags` style stdout line for a tag. */
function lsRemoteLine(tag) {
return `0000000000000000000000000000000000000000\trefs/tags/${tag}`;
}
/** A SpawnResult-shaped success. */
function spawnOk(stdout) {
return { exitCode: 0, stdout, stderr: '', signal: null, error: null };
}
// ---------------------------------------------------------------------------
// Helpers
@@ -1410,3 +1424,405 @@ describe('#1461 finding 2 — tar header-size parse removed; NAME/TYPE guards re
assert.ok(fs.existsSync(result.stagedDir), 'staged dir exists after extraction');
});
});
// ---------------------------------------------------------------------------
// #1463: pickHighestSemverTag — pure highest-stable-semver-tag parser
// ---------------------------------------------------------------------------
describe('#1463 pickHighestSemverTag (git ls-remote --tags parser)', () => {
test('BOUNDARY: picks the NUMERIC max across v1.1.0/v1.2.0/v1.10.0 + junk (1.10.0, not 1.2.0)', () => {
// revert-fails: a lexical (string) compare would pick "1.2.0" > "1.10.0"; the numeric compare
// (compareSemverCore) must pick 1.10.0. Junk + a non-semver tag must be ignored.
const out = [
lsRemoteLine('v1.1.0'),
lsRemoteLine('v1.2.0'),
lsRemoteLine('v1.10.0'),
lsRemoteLine('not-a-version'),
lsRemoteLine('release-candidate'),
].join('\n');
assert.strictEqual(pickHighestSemverTag(out), '1.10.0');
});
test('ignores ^{} peeled-annotation entries (same tag, not a distinct version)', () => {
const out = [
lsRemoteLine('v2.0.0'),
lsRemoteLine('v2.0.0^{}'),
lsRemoteLine('v1.5.0'),
].join('\n');
assert.strictEqual(pickHighestSemverTag(out), '2.0.0');
});
test('ignores prerelease/non-triplet tags; bare (no-v) triplets accepted', () => {
const out = [
lsRemoteLine('v1.0.0-rc.1'),
lsRemoteLine('1.4.2'),
lsRemoteLine('v2'),
lsRemoteLine('v1.0'),
].join('\n');
assert.strictEqual(pickHighestSemverTag(out), '1.4.2');
});
test('no parseable semver tags → null', () => {
assert.strictEqual(pickHighestSemverTag('0000\trefs/heads/main\n0000\trefs/tags/latest'), null);
assert.strictEqual(pickHighestSemverTag(''), null);
});
test('PROPERTY (fc): result is the numeric max of the injected stable triplets, ignoring junk', () => {
fc.assert(
fc.property(
fc.array(fc.tuple(fc.nat(50), fc.nat(50), fc.nat(50)), { minLength: 1, maxLength: 12 }),
(triplets) => {
const tags = triplets.map(([a, b, c]) => `v${a}.${b}.${c}`);
// Interleave non-semver junk that must be ignored.
const lines = [];
for (const t of tags) {
lines.push(lsRemoteLine(t));
lines.push(lsRemoteLine('junk-' + t)); // non-triplet → ignored
lines.push(lsRemoteLine(`${t}-rc.1`)); // prerelease → ignored
}
const got = pickHighestSemverTag(lines.join('\n'));
// Expected max computed numerically (not lexically).
const expected = triplets
.slice()
.sort((x, y) => (x[0] - y[0]) || (x[1] - y[1]) || (x[2] - y[2]))
.pop();
return got === `${expected[0]}.${expected[1]}.${expected[2]}`;
},
),
{ numRuns: 200 },
);
});
});
// ---------------------------------------------------------------------------
// #1463: peekLatestVersion — per-source latest-version peek (ADR-1244 D6)
// ---------------------------------------------------------------------------
describe('#1463 peekLatestVersion (D6 per-source matrix)', () => {
test('git: highest remote tag returned as latest (status ok)', () => {
const fakeGit = (args) => {
assert.ok(args.includes('ls-remote'), 'must use ls-remote (metadata only — no clone)');
assert.ok(!args.includes('clone'), 'must NOT clone for a peek');
return spawnOk([lsRemoteLine('v1.0.0'), lsRemoteLine('v1.3.0'), lsRemoteLine('v1.10.0')].join('\n'));
};
const r = peekLatestVersion('https://github.com/org/repo.git', { execOverrides: { git: fakeGit } });
assert.deepStrictEqual(r, { status: 'ok', version: '1.10.0' });
});
test('git: ls-remote non-zero exit → status unknown (DEGRADE, no throw)', () => {
const fakeGit = () => ({ exitCode: 128, stdout: '', stderr: 'fatal', signal: null, error: null });
const r = peekLatestVersion('https://github.com/org/repo.git', { execOverrides: { git: fakeGit } });
assert.strictEqual(r.status, 'unknown');
assert.strictEqual(r.version, null);
});
test('git: ls-remote timeout (signal set) → status unknown', () => {
// revert-fails: without the timeout→unknown branch, a killed peek (signal) would not degrade.
const fakeGit = () => ({ exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: null });
const r = peekLatestVersion('https://github.com/org/repo.git', { execOverrides: { git: fakeGit } });
assert.strictEqual(r.status, 'unknown');
});
test('git: bounded timeout is passed to execGit (≤30s)', () => {
let seenTimeout;
const fakeGit = (_args, o) => { seenTimeout = o && o.timeout; return spawnOk(lsRemoteLine('v1.0.0')); };
peekLatestVersion('https://github.com/org/repo.git', { execOverrides: { git: fakeGit } });
assert.ok(typeof seenTimeout === 'number' && seenTimeout <= 30_000, `git peek must be bounded ≤30s (got ${seenTimeout})`);
});
test('git: source pinned to a commit SHA (#sha:…) → status pinned, NEVER outdated (update stays on the ref)', () => {
// A `#sha:<commit>` pin is immutable: re-resolving the recorded source checks out the SAME commit,
// so a newer remote tag is irrelevant. revert-fails: without the parsed.ref pinned check the peek
// returns the highest tag with status 'ok', which outdatedCapabilities renders 'outdated' — this
// assert (status 'pinned') then fails.
const fakeGit = () => spawnOk([lsRemoteLine('v1.0.0'), lsRemoteLine('v9.9.9')].join('\n'));
const r = peekLatestVersion('https://github.com/org/repo.git#sha:abcdef1234567890abcdef1234567890abcdef12', { execOverrides: { git: fakeGit } });
assert.strictEqual(r.status, 'pinned');
});
test('git: source pinned to an explicit tag (#tag:…) → status pinned', () => {
const fakeGit = () => spawnOk([lsRemoteLine('v1.0.0'), lsRemoteLine('v2.0.0')].join('\n'));
const r = peekLatestVersion('https://github.com/org/repo.git#tag:v1.0.0', { execOverrides: { git: fakeGit } });
assert.strictEqual(r.status, 'pinned');
});
test('git: bare ref (#<ref>) resolving to a TAG (refs/tags/…) → status pinned (immutable tag)', () => {
// #1463 Fix 2 (R Medium): a bare `#<ref>` is ambiguous (tag OR branch). We classify it with a
// bounded `git ls-remote <url> <ref>`. When the remote resolves it under refs/tags/ it is an
// immutable tag → pinned. The ls-remote query is the SAME safe seam (argv + `--`).
const fakeGit = (args) => {
assert.ok(args.includes('ls-remote'), 'classification must use ls-remote (metadata only)');
assert.ok(args.includes('--'), 'argv must terminate options with `--`');
assert.ok(args.includes('release-1'), 'the bare ref is passed to ls-remote for classification');
// ls-remote <url> <ref> prints the matching ref line(s).
return spawnOk('1111111111111111111111111111111111111111\trefs/tags/release-1');
};
const r = peekLatestVersion('https://github.com/org/repo.git#release-1', { execOverrides: { git: fakeGit } });
assert.strictEqual(r.status, 'pinned');
});
test('git: bare ref (#main) resolving to a BRANCH (refs/heads/…) → NEVER pinned (mutable; no installed sha ⇒ unknown)', () => {
// #1463 Fix 2 (R Medium) — THE bug: `repo.git#main` is a MUTABLE branch (`update` re-clones and
// checks out the ref, so it can move). The OLD isGitRefPinned reported ANY non-empty parsed.ref as
// 'pinned', so this asserted 'pinned' and was WRONG. The ledger records NO installed commit sha for
// git sources (integrity is null), so a moved branch HEAD cannot be compared → DEGRADE to 'unknown'.
// revert-fails: with the old blanket isGitRefPinned this returns 'pinned' and the !== 'pinned'
// assert below fails (and the === 'unknown' assert fails too).
const fakeGit = (args) => {
assert.ok(args.includes('ls-remote'), 'classification must use ls-remote');
return spawnOk('2222222222222222222222222222222222222222\trefs/heads/main');
};
const r = peekLatestVersion('https://github.com/org/repo.git#main', { execOverrides: { git: fakeGit } });
assert.notStrictEqual(r.status, 'pinned', 'a mutable branch ref must NEVER be reported pinned');
assert.strictEqual(r.status, 'unknown', 'no installed sha recorded ⇒ cannot compare branch HEAD ⇒ unknown');
});
test('git: bare ref classification — ls-remote error/timeout → status unknown (DEGRADE, never pinned/crash)', () => {
const errGit = () => ({ exitCode: 128, stdout: '', stderr: 'fatal', signal: null, error: null });
const r1 = peekLatestVersion('https://github.com/org/repo.git#main', { execOverrides: { git: errGit } });
assert.notStrictEqual(r1.status, 'pinned');
assert.strictEqual(r1.status, 'unknown');
const killGit = () => ({ exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: null });
const r2 = peekLatestVersion('https://github.com/org/repo.git#main', { execOverrides: { git: killGit } });
assert.strictEqual(r2.status, 'unknown');
});
test('git: bare ref classification — unresolvable/empty ls-remote output → status unknown (never pinned)', () => {
const fakeGit = () => spawnOk('');
const r = peekLatestVersion('https://github.com/org/repo.git#mystery-ref', { execOverrides: { git: fakeGit } });
assert.notStrictEqual(r.status, 'pinned');
assert.strictEqual(r.status, 'unknown');
});
test('git: bare ref classification — ls-remote returns BOTH refs/tags/<r> AND refs/heads/<r> (true ambiguity) → status unknown (NOT pinned)', () => {
// #1463 accuracy fix: when ls-remote resolves a bare ref under BOTH refs/tags/ AND refs/heads/
// the ref is genuinely ambiguous (a tag and a branch share the same name). The classifier must
// NOT prefer the tag and report 'pinned' — the mutable branch reading means the ref could move.
// The safe fallback is 'unknown'.
// revert-fails: a classifier that scans lines and picks the FIRST refs/tags/ hit (or any tag-wins
// strategy) would return 'pinned' here, making the notStrictEqual('pinned') assert below fail.
const ambiguousRef = 'release-1';
const fakeGit = (args) => {
assert.ok(args.includes('ls-remote'), 'must use ls-remote for bare-ref classification');
assert.ok(args.includes(ambiguousRef), 'the bare ref must be passed to ls-remote');
// ls-remote output: same name exists as BOTH a tag and a branch head.
return spawnOk(
`1111111111111111111111111111111111111111\trefs/tags/${ambiguousRef}\n` +
`2222222222222222222222222222222222222222\trefs/heads/${ambiguousRef}\n`,
);
};
const r = peekLatestVersion(`https://github.com/org/repo.git#${ambiguousRef}`, { execOverrides: { git: fakeGit } });
assert.notStrictEqual(r.status, 'pinned', 'ambiguous tag+branch ref must NEVER be reported pinned');
assert.strictEqual(r.status, 'unknown', 'ambiguous ref degrades to unknown (safe fallback)');
});
test('git: bare ref classification — bounded timeout (≤30s) passed to the ls-remote classify call', () => {
let seenTimeout;
const fakeGit = (_args, o) => { seenTimeout = o && o.timeout; return spawnOk('33\trefs/heads/main'); };
peekLatestVersion('https://github.com/org/repo.git#main', { execOverrides: { git: fakeGit } });
assert.ok(typeof seenTimeout === 'number' && seenTimeout <= 30_000, `classify peek must be bounded ≤30s (got ${seenTimeout})`);
});
test('git: UNPINNED source (no #ref, tracks default branch) → highest tag with status ok (NOT pinned)', () => {
// revert-fails-guard for over-pinning: an unpinned source must STILL peek and resolve a version.
const fakeGit = () => spawnOk([lsRemoteLine('v1.0.0'), lsRemoteLine('v1.4.0')].join('\n'));
const r = peekLatestVersion('https://github.com/org/repo.git', { execOverrides: { git: fakeGit } });
assert.deepStrictEqual(r, { status: 'ok', version: '1.4.0' });
});
test('npm: RANGE spec — npm view prints EVERY matching version (real multi-line output) → highest matching is chosen', () => {
// npm's REAL behaviour for `npm view <pkg>@<range> version`: when the range matches multiple
// versions it prints one annotated line PER matching version, e.g.
// @org/gsd-cap-foo@1.0.0 '1.0.0'
// @org/gsd-cap-foo@1.3.0 '1.3.0'
// @org/gsd-cap-foo@1.10.0 '1.10.0'
// (NOT a single bare token). The peek must parse ALL tokens and pick the HIGHEST numerically.
// revert-fails: the old single-token NPM_VERSION_RE.test(stdout.trim()) parse sees multi-line
// output as non-semver and DEGRADES to status 'unknown' — this assert then fails.
const multiLine = [
"@org/gsd-cap-foo@1.0.0 '1.0.0'",
"@org/gsd-cap-foo@1.3.0 '1.3.0'",
"@org/gsd-cap-foo@1.10.0 '1.10.0'",
].join('\n') + '\n';
const fakeNpm = (args, o) => {
assert.ok(args.includes('view'), 'must use npm view');
assert.ok(typeof o.timeout === 'number' && o.timeout <= 60_000, 'npm peek must be bounded ≤60s');
// Invocation shape is unchanged: ['view','--',<target>,'version'] with the range still on target.
assert.deepStrictEqual(args, ['view', '--', '@org/gsd-cap-foo@^1', 'version']);
return spawnOk(multiLine);
};
const r = peekLatestVersion('npm:@org/gsd-cap-foo@^1', { execOverrides: { npm: fakeNpm } });
// 1.10.0 must beat 1.3.0 numerically (not lexically), and it satisfies ^1.
assert.deepStrictEqual(r, { status: 'ok', version: '1.10.0' });
});
test('npm: RANGE spec — highest MATCHING version is bounded by the range (out-of-range versions ignored)', () => {
// ^1 must NOT pick a 2.x even if npm happened to print one; the chosen version must satisfy the range.
const multiLine = [
"@org/cap@1.4.0 '1.4.0'",
"@org/cap@1.9.0 '1.9.0'",
"@org/cap@2.0.0 '2.0.0'",
].join('\n') + '\n';
const r = peekLatestVersion('npm:@org/cap@^1', { execOverrides: { npm: () => spawnOk(multiLine) } });
assert.deepStrictEqual(r, { status: 'ok', version: '1.9.0' });
});
test('npm: RANGE spec — installed < highest matching ⇒ caller sees newer; installed == highest ⇒ same', () => {
// Two ledgers: the peek itself only resolves the highest-matching version; the outdated/current
// decision lives in outdatedCapabilities. Here we lock the peek's resolution (the input to that).
const multiLine = [
"@org/cap@1.2.0 '1.2.0'",
"@org/cap@1.5.0 '1.5.0'",
].join('\n') + '\n';
const r = peekLatestVersion('npm:@org/cap@^1', { execOverrides: { npm: () => spawnOk(multiLine) } });
assert.strictEqual(r.version, '1.5.0', 'highest matching is the version update would install');
});
test('npm: NO-version spec (tracks latest) — single bare latest line → status ok', () => {
const fakeNpm = (args) => {
// No version on the spec ⇒ target is just the bare name; npm view prints a single latest token.
assert.deepStrictEqual(args, ['view', '--', '@org/gsd-cap-foo', 'version']);
return spawnOk('2.4.1\n');
};
const r = peekLatestVersion('npm:@org/gsd-cap-foo', { execOverrides: { npm: fakeNpm } });
assert.deepStrictEqual(r, { status: 'ok', version: '2.4.1' });
});
test('npm: EXACT-pinned spec (@1.2.3) → status pinned (update re-resolves to the SAME version, never outdated)', () => {
// revert-fails: without the exact-pin → 'pinned' branch, the npm peek would run npm view and
// compare, so a pinned source could be reported outdated; this assert requires status 'pinned'.
let called = false;
const fakeNpm = () => { called = true; return spawnOk('9.9.9\n'); };
const r = peekLatestVersion('npm:@org/gsd-cap-foo@1.2.3', { execOverrides: { npm: fakeNpm } });
assert.strictEqual(r.status, 'pinned');
assert.strictEqual(r.version, '1.2.3', 'pinned reports the pinned exact version');
assert.strictEqual(called, false, 'an exact-pinned npm source needs no remote peek (update will not move it)');
});
test('npm: npm view error/timeout → status unknown (no crash)', () => {
const r1 = peekLatestVersion('npm:@org/cap@^1', { execOverrides: { npm: () => ({ exitCode: 1, stdout: '', stderr: 'E404', signal: null, error: null }) } });
assert.strictEqual(r1.status, 'unknown');
const r2 = peekLatestVersion('npm:@org/cap@^1', { execOverrides: { npm: () => ({ exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: null }) } });
assert.strictEqual(r2.status, 'unknown');
});
test('npm: non-semver output → status unknown (untrusted output)', () => {
const r = peekLatestVersion('npm:@org/cap@^1', { execOverrides: { npm: () => spawnOk('not a version\n') } });
assert.strictEqual(r.status, 'unknown');
});
test('local: re-reads capability.json version (status ok)', () => {
const dir = makeLocalCap(featureCap('local-peek', { version: '3.1.0' }));
try {
const r = peekLatestVersion(dir);
assert.deepStrictEqual(r, { status: 'ok', version: '3.1.0' });
} finally {
cleanup(dir);
}
});
test('local: missing path → status unknown (no throw)', () => {
const r = peekLatestVersion('/no/such/path/that/exists');
assert.strictEqual(r.status, 'unknown');
});
test('tarball: not auto-detectable → status manual (no version)', () => {
const r = peekLatestVersion('https://host/path/cap-1.0.0.tgz');
assert.strictEqual(r.status, 'manual');
assert.strictEqual(r.version, null);
});
test('registry: unimplemented → status unsupported', () => {
const r = peekLatestVersion('my-cap@gsd-registry');
assert.strictEqual(r.status, 'unsupported');
assert.strictEqual(r.version, null);
});
});
// ---------------------------------------------------------------------------
// #1463: pure npm-spec / npm-view parsers (splitNpmSpec, pickHighestNpmVersion)
// ---------------------------------------------------------------------------
describe('#1463 splitNpmSpec (name vs version-selector)', () => {
test('scoped package with exact version → split at the LAST @ (not the scope @)', () => {
assert.deepStrictEqual(splitNpmSpec('@org/pkg@1.2.3'), { name: '@org/pkg', selector: '1.2.3' });
});
test('scoped package with range → selector is the range', () => {
assert.deepStrictEqual(splitNpmSpec('@org/pkg@^1'), { name: '@org/pkg', selector: '^1' });
});
test('scoped package, no version → empty selector (tracks latest)', () => {
assert.deepStrictEqual(splitNpmSpec('@org/pkg'), { name: '@org/pkg', selector: '' });
});
test('unscoped package with version → split at the single @', () => {
assert.deepStrictEqual(splitNpmSpec('pkg@2.0.0'), { name: 'pkg', selector: '2.0.0' });
});
test('unscoped package, no version → empty selector', () => {
assert.deepStrictEqual(splitNpmSpec('pkg'), { name: 'pkg', selector: '' });
});
});
describe('#1463 pickHighestNpmVersion (robust multi-line range parse)', () => {
test('multi-line annotated range output → highest matching (numeric, not lexical)', () => {
const out = ["@org/pkg@1.2.0 '1.2.0'", "@org/pkg@1.10.0 '1.10.0'", "@org/pkg@1.3.0 '1.3.0'"].join('\n');
assert.strictEqual(pickHighestNpmVersion(out, '^1'), '1.10.0');
});
test('range bound is honored — out-of-range versions ignored', () => {
const out = ["@org/pkg@1.9.0 '1.9.0'", "@org/pkg@2.0.0 '2.0.0'"].join('\n');
assert.strictEqual(pickHighestNpmVersion(out, '^1'), '1.9.0');
});
test('empty selector = no constraint → overall max', () => {
const out = ["@org/pkg@1.9.0 '1.9.0'", "@org/pkg@2.4.0 '2.4.0'"].join('\n');
assert.strictEqual(pickHighestNpmVersion(out, ''), '2.4.0');
});
test('single bare token (latest dist-tag) parses', () => {
assert.strictEqual(pickHighestNpmVersion('2.4.1\n', ''), '2.4.1');
});
test('garbage / no version tokens → null (DEGRADE)', () => {
assert.strictEqual(pickHighestNpmVersion('not a version\n', ''), null);
assert.strictEqual(pickHighestNpmVersion('', '^1'), null);
});
test('no token satisfies the range → null', () => {
const out = ["@org/pkg@2.0.0 '2.0.0'", "@org/pkg@3.0.0 '3.0.0'"].join('\n');
assert.strictEqual(pickHighestNpmVersion(out, '^1'), null);
});
test('package NAME contains a version-like substring → resolves the RESOLVED version, not the name token', () => {
// #1463 Fix 1 (R Medium): npm view (range) prints `<name>@<version> '<version>'`. When the package
// NAME itself contains an `x.y.z`-shaped substring (`@scope/cap-1.2.3`), the version must come from
// its CANONICAL position (the quoted token / the token after the LAST `@`), NOT any token on the line.
// revert-fails: the old any-token regex matches `1.2.3` from the NAME first and returns it (the
// highest token that satisfies ^1 is `1.5.0`, but `1.2.3` < `1.5.0`, so a name-poisoned parse could
// also wrongly surface `1.2.3` as a candidate). With both lines present the CORRECT answer is 1.5.0.
const out = [
"@scope/cap-1.2.3@1.0.0 '1.0.0'",
"@scope/cap-1.2.3@1.5.0 '1.5.0'",
].join('\n');
assert.strictEqual(pickHighestNpmVersion(out, '^1'), '1.5.0');
});
test('single name-poisoned line → resolves the resolved version (not the name substring)', () => {
// revert-fails: with one line `@scope/cap-1.2.3@1.0.0 '1.0.0'` and range `^1.0.0`, the any-token
// regex picks `1.2.3` (the FIRST/HIGHEST satisfying token, from the NAME); the canonical parse must
// return `1.0.0` (the resolved version). 1.2.3 !== 1.0.0 so the assert flips on revert.
const out = "@scope/cap-1.2.3@1.0.0 '1.0.0'";
assert.strictEqual(pickHighestNpmVersion(out, '^1.0.0'), '1.0.0');
});
test('property: with no range constraint, picks the numeric max of the printed versions', () => {
fc.assert(
fc.property(
fc.array(fc.tuple(fc.nat(40), fc.nat(40), fc.nat(40)), { minLength: 1, maxLength: 12 }),
(triplets) => {
const lines = triplets.map(([a, b, c]) => `@org/pkg@${a}.${b}.${c} '${a}.${b}.${c}'`);
const got = pickHighestNpmVersion(lines.join('\n'), '');
const expected = triplets
.slice()
.sort((x, y) => (x[0] - y[0]) || (x[1] - y[1]) || (x[2] - y[2]))
.pop();
return got === `${expected[0]}.${expected[1]}.${expected[2]}`;
},
),
{ numRuns: 200 },
);
});
});