* feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2) Promote the registry from a frozen data file to loadRegistry({includeInstalled}), composing the first-party registry with a validated installed overlay (ADR-1244 D2): - Extract the conformance validator to a shared runtime-callable module (gsd-core/bin/lib/capability-validator.cjs); the generator re-exports it verbatim, guarded by a generative-parity test (no build-time/runtime drift). - capability-loader.cts: loadRegistry({includeInstalled}) composes first-party ∪ validated overlay from $GSD_HOME/.gsd/capabilities (global) and <root>/.gsd/capabilities (project) via the canonical buildRegistry. First-party always wins (id/skill/agent/config/command-family + reserved gsd-/anthropic- prefixes); full merged-set cross-capability validation; engines.gsd load-time re-gate (skip-with-warning); gate-kind capabilities FAIL CLOSED; fragment-path escapes rejected. - semverSatisfies (hand-written, no dep) for the engines.gsd gate, fail-closed. - Wire surface/state + loop to the overlay; loop injects a blocking gate for each skipped gate-kind overlay (fail-closed). - cwd-aware overlay config-key federation: config-loader _federatedConfigSchema(cwd) + config-schema isValidConfigKey(key, cwd) compose the overlay per loadConfig/ config-set call (never eager at module load, never wrong-cwd); first-party path unchanged with no cwd. - run-tests.cjs sandboxes GSD_HOME (idempotent — nested spawns reuse it) for test hermeticity; capability-loader.cjs git+eslint-ignored (tsc artifact); capability-validator.cjs stays linted (#551 migration coverage). Closes #1431 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1431): add changeset for runtime capability registry overlay Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1431): kill config-schema cwd-aware federation mutants (Stryker ≥52) The cwd-aware overlay config-key federation added to config-schema.cts (_capabilityConfigSchema(cwd) + isCapabilityConfigKey/isValidConfigKey cwd threading) introduced mutable surface uncovered by config-schema's mutation test set, dropping its score to 39.58% (below the 52 break threshold). Add a real-overlay-fixture describe block exercising every branch (cwd guard, overlay loadRegistry, found-branch, first-party fallback, cwd threading); local Stryker score 39.58% -> 77.08%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
180 lines
5.6 KiB
JavaScript
180 lines
5.6 KiB
JavaScript
/**
|
|
* Shared semver compare utility tests.
|
|
*
|
|
* These assertions lock the normalization policy used by update-check,
|
|
* statusline dev-install detection, and changeset extraction range compare.
|
|
*/
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
compareSemverCore,
|
|
isSemverNewer,
|
|
toNumericTuple,
|
|
semverSatisfies,
|
|
} = require('../gsd-core/bin/lib/semver-compare.cjs');
|
|
|
|
describe('isSemverNewer (shared semver comparison)', () => {
|
|
test('newer major version', () => {
|
|
assert.strictEqual(isSemverNewer('2.0.0', '1.0.0'), true);
|
|
});
|
|
|
|
test('newer minor version', () => {
|
|
assert.strictEqual(isSemverNewer('1.1.0', '1.0.0'), true);
|
|
});
|
|
|
|
test('newer patch version', () => {
|
|
assert.strictEqual(isSemverNewer('1.0.1', '1.0.0'), true);
|
|
});
|
|
|
|
test('equal versions', () => {
|
|
assert.strictEqual(isSemverNewer('1.0.0', '1.0.0'), false);
|
|
});
|
|
|
|
test('older version returns false', () => {
|
|
assert.strictEqual(isSemverNewer('1.0.0', '2.0.0'), false);
|
|
});
|
|
|
|
test('installed ahead of npm (git install scenario)', () => {
|
|
assert.strictEqual(isSemverNewer('1.30.0', '1.31.0'), false);
|
|
});
|
|
|
|
test('npm ahead of installed (real update available)', () => {
|
|
assert.strictEqual(isSemverNewer('1.31.0', '1.30.0'), true);
|
|
});
|
|
|
|
test('pre-release suffix stripped', () => {
|
|
assert.strictEqual(isSemverNewer('1.0.1-beta.1', '1.0.0'), true);
|
|
});
|
|
|
|
test('pre-release on both sides', () => {
|
|
assert.strictEqual(isSemverNewer('2.0.0-rc.1', '1.9.0-beta.2'), true);
|
|
});
|
|
|
|
test('null/undefined handled', () => {
|
|
assert.strictEqual(isSemverNewer(null, '1.0.0'), false);
|
|
assert.strictEqual(isSemverNewer('1.0.0', null), true);
|
|
assert.strictEqual(isSemverNewer(null, null), false);
|
|
});
|
|
|
|
test('empty string handled', () => {
|
|
assert.strictEqual(isSemverNewer('', '1.0.0'), false);
|
|
assert.strictEqual(isSemverNewer('1.0.0', ''), true);
|
|
});
|
|
|
|
test('two-segment version (missing patch)', () => {
|
|
assert.strictEqual(isSemverNewer('1.1', '1.0'), true);
|
|
assert.strictEqual(isSemverNewer('1.0', '1.1'), false);
|
|
});
|
|
|
|
test('v-prefixed versions normalize consistently', () => {
|
|
assert.strictEqual(isSemverNewer('v1.2.1', '1.2.0'), true);
|
|
assert.strictEqual(isSemverNewer('1.2.0', 'v1.2.0'), false);
|
|
assert.deepStrictEqual(toNumericTuple('v1.2.3-rc.1'), [1, 2, 3]);
|
|
});
|
|
|
|
test('core comparator uses three-way ordering', () => {
|
|
assert.strictEqual(compareSemverCore('1.2.0', '1.2.0'), 0);
|
|
assert.strictEqual(compareSemverCore('1.2.1', '1.2.0'), 1);
|
|
assert.strictEqual(compareSemverCore('1.2.0', '1.2.1'), -1);
|
|
});
|
|
});
|
|
|
|
describe('semverSatisfies (ADR-1244 engines.gsd range gate)', () => {
|
|
const sat = (v, r, expected) =>
|
|
assert.strictEqual(semverSatisfies(v, r), expected, `expected satisfies(${JSON.stringify(v)}, ${JSON.stringify(r)}) === ${expected}`);
|
|
|
|
test('>= comparator', () => {
|
|
sat('1.6.0', '>=1.6.0', true);
|
|
sat('1.6.1', '>=1.6.0', true);
|
|
sat('2.0.0', '>=1.6.0', true);
|
|
sat('1.5.9', '>=1.6.0', false);
|
|
});
|
|
|
|
test('> < <= = comparators', () => {
|
|
sat('1.6.1', '>1.6.0', true);
|
|
sat('1.6.0', '>1.6.0', false);
|
|
sat('1.5.0', '<1.6.0', true);
|
|
sat('1.6.0', '<1.6.0', false);
|
|
sat('1.6.0', '<=1.6.0', true);
|
|
sat('1.6.1', '<=1.6.0', false);
|
|
sat('1.6.0', '=1.6.0', true);
|
|
sat('1.6.1', '=1.6.0', false);
|
|
});
|
|
|
|
test('bare exact full version', () => {
|
|
sat('1.6.0', '1.6.0', true);
|
|
sat('1.6.1', '1.6.0', false);
|
|
});
|
|
|
|
test('AND-composed range (whitespace)', () => {
|
|
sat('1.6.0', '>=1.6.0 <3.0.0', true);
|
|
sat('2.9.9', '>=1.6.0 <3.0.0', true);
|
|
sat('3.0.0', '>=1.6.0 <3.0.0', false);
|
|
sat('1.5.0', '>=1.6.0 <3.0.0', false);
|
|
});
|
|
|
|
test('OR-composed range (||)', () => {
|
|
sat('1.6.0', '>=1.6.0 || >=2.0.0', true);
|
|
sat('2.0.0', '<1.0.0 || >=2.0.0', true);
|
|
sat('1.5.0', '<1.0.0 || >=2.0.0', false);
|
|
});
|
|
|
|
test('caret ranges', () => {
|
|
sat('1.2.3', '^1.2.3', true);
|
|
sat('1.9.0', '^1.2.3', true);
|
|
sat('2.0.0', '^1.2.3', false);
|
|
sat('1.2.2', '^1.2.3', false);
|
|
sat('0.2.3', '^0.2.3', true);
|
|
sat('0.3.0', '^0.2.3', false);
|
|
sat('0.0.3', '^0.0.3', true);
|
|
sat('0.0.4', '^0.0.3', false);
|
|
});
|
|
|
|
test('tilde ranges', () => {
|
|
sat('1.2.3', '~1.2.3', true);
|
|
sat('1.2.9', '~1.2.3', true);
|
|
sat('1.3.0', '~1.2.3', false);
|
|
sat('1.2.0', '~1.2', true);
|
|
sat('1.3.0', '~1.2', false);
|
|
sat('1.9.0', '~1', true);
|
|
sat('2.0.0', '~1', false);
|
|
});
|
|
|
|
test('wildcards and partials', () => {
|
|
sat('99.0.0', '*', true);
|
|
sat('0.0.1', '*', true);
|
|
sat('1.0.0', '1.x', true);
|
|
sat('1.9.9', '1.x', true);
|
|
sat('2.0.0', '1.x', false);
|
|
sat('0.9.9', '1.x', false);
|
|
sat('1.2.0', '1.2.x', true);
|
|
sat('1.3.0', '1.2.x', false);
|
|
sat('1.5.0', '1', true);
|
|
sat('2.0.0', '1', false);
|
|
});
|
|
|
|
test('prerelease and v-prefix normalize to numeric core', () => {
|
|
sat('1.6.0-rc.1', '>=1.6.0', true);
|
|
sat('1.5.1-dev.0', '>=1.6.0', false);
|
|
sat('v1.6.0', '>=1.6.0', true);
|
|
});
|
|
|
|
test('FAIL CLOSED on empty/unparseable ranges', () => {
|
|
for (const bad of ['', ' ', 'abc', 'not a range', '>=', '>=x', 'foo.bar.baz', '1.2.3.4', '>=1.2.3 garbage']) {
|
|
sat('1.6.0', bad, false);
|
|
}
|
|
});
|
|
|
|
test('FAIL CLOSED on malformed wildcard tokens (concrete segment after a wildcard)', () => {
|
|
for (const bad of ['1.x.2', '>=1.x.2', '1.*.2', '1.X.0']) {
|
|
sat('1.5.0', bad, false);
|
|
}
|
|
});
|
|
|
|
test('null/undefined inputs do not throw and fail closed', () => {
|
|
sat('1.6.0', null, false);
|
|
sat('1.6.0', undefined, false);
|
|
sat(null, '>=1.6.0', false); // null version -> [0,0,0] -> not >= 1.6.0
|
|
});
|
|
});
|