/** * Consumer-pins planner tests — covers the pure inner that decides * what dep updates a single package.json would receive. The disk- * walking wrapper (`planConsumerPins`) is a thin orchestrator on top * of this; testing the pure layer exercises all the policy. */ import { describe, expect, test } from 'bun:test'; import { bumpUpdatesFor } from './consumer-pins'; import type { PackageJson } from './types'; function latest(entries: Record): Map { return new Map(Object.entries(entries)); } describe('bumpUpdatesFor', () => { test('empty package returns no updates', () => { expect(bumpUpdatesFor({}, latest({ '@celilo/cli': '0.4.0' }))).toEqual([]); }); test('skips deps not in the published map (only manages our own)', () => { const pkg: PackageJson = { dependencies: { lodash: '^4.17.21', '@celilo/cli': '^0.3.0' }, }; const updates = bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0' })); expect(updates).toHaveLength(1); expect(updates[0].depName).toBe('@celilo/cli'); expect(updates[0].newSpec).toBe('^0.4.0'); }); test('preserves caret pin', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': '^0.3.0' } }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0' }))).toEqual([ { bucket: 'dependencies', depName: '@celilo/cli', oldSpec: '^0.3.0', newSpec: '^0.4.0', }, ]); }); test('preserves tilde pin', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': '~0.3.0' } }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0' }))).toEqual([ { bucket: 'dependencies', depName: '@celilo/cli', oldSpec: '~0.3.0', newSpec: '~0.4.0', }, ]); }); test('preserves exact pin (no operator widening)', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': '0.3.0' } }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0' }))).toEqual([ { bucket: 'dependencies', depName: '@celilo/cli', oldSpec: '0.3.0', newSpec: '0.4.0', }, ]); }); test('skips workspace:* protocol specs', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': 'workspace:^', '@celilo/e2e': 'workspace:*', }, }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0', '@celilo/e2e': '0.8.0' }))).toEqual( [], ); }); test('skips file: / git: protocol specs (deliberate local pins)', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': 'file:../celilo', '@celilo/e2e': 'git://github.com/celilo/e2e.git', }, }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0', '@celilo/e2e': '0.8.0' }))).toEqual( [], ); }); test('bumps npm: protocol specs (those are real npm refs)', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': 'npm:@celilo/cli@^0.3.0' }, }; // Edge: the bareVersion strip won't recover "0.3.0" cleanly from // the full npm: spec, so the equality check doesn't fire and we // attempt a withOperator rewrite — but withOperator only strips // operators it knows, leaving the npm:-prefixed spec different // from newSpec, so an update IS emitted. This documents current // behavior; bumping npm:-aliased deps isn't a tested use case. const updates = bumpUpdatesFor(pkg, latest({ '@celilo/cli': '0.4.0' })); expect(updates).toHaveLength(1); }); test('skips deps already at the target bare version', () => { const pkg: PackageJson = { dependencies: { a: '0.4.0', b: '^0.4.0', c: '~0.4.0', }, }; expect(bumpUpdatesFor(pkg, latest({ a: '0.4.0', b: '0.4.0', c: '0.4.0' }))).toEqual([]); }); test('walks every dependency bucket', () => { const pkg: PackageJson = { dependencies: { a: '^1.0.0' }, devDependencies: { b: '^1.0.0' }, peerDependencies: { c: '^1.0.0' }, optionalDependencies: { d: '^1.0.0' }, }; const updates = bumpUpdatesFor(pkg, latest({ a: '1.1.0', b: '1.1.0', c: '1.1.0', d: '1.1.0' })); expect(updates.map((u) => u.bucket)).toEqual([ 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', ]); }); test('emits multiple updates for multiple changed deps', () => { const pkg: PackageJson = { dependencies: { '@celilo/cli': '^0.3.0', '@celilo/e2e': '^0.7.0', '@celilo/event-bus': '^0.1.0', }, }; const updates = bumpUpdatesFor( pkg, latest({ '@celilo/cli': '0.4.0', '@celilo/e2e': '0.8.0', '@celilo/event-bus': '0.1.5', }), ); expect(updates).toHaveLength(3); expect(updates.map((u) => u.depName).sort()).toEqual([ '@celilo/cli', '@celilo/e2e', '@celilo/event-bus', ]); }); // #399: pins are now written during the version phase from WORKSPACE versions, // which run ahead of npm until the publish lands. The publish-phase pass still // reads npm-latest, so without a guard it would walk those pins BACKWARDS and // ship modules bundling the previous capability code — the exact bundle/repo // drift ISS-0104 exists to prevent. describe('never downgrades a pin', () => { test('an older npm-latest leaves a newer workspace pin alone', () => { const pkg: PackageJson = { dependencies: { '@celilo/capabilities': '^0.7.1' } }; expect(bumpUpdatesFor(pkg, latest({ '@celilo/capabilities': '0.7.0' }))).toEqual([]); }); test('across a minor and a major', () => { expect( bumpUpdatesFor( { dependencies: { '@celilo/cli': '^0.13.0' } }, latest({ '@celilo/cli': '0.12.1' }), ), ).toEqual([]); expect( bumpUpdatesFor( { dependencies: { '@celilo/cli': '^1.0.0' } }, latest({ '@celilo/cli': '0.99.9' }), ), ).toEqual([]); }); test('still bumps forward', () => { const updates = bumpUpdatesFor( { dependencies: { '@celilo/capabilities': '^0.7.0' } }, latest({ '@celilo/capabilities': '0.7.1' }), ); expect(updates).toHaveLength(1); expect(updates[0].newSpec).toBe('^0.7.1'); }); test('a prerelease suffix does not confuse the comparison', () => { expect( bumpUpdatesFor( { dependencies: { '@celilo/cli': '^0.13.0' } }, latest({ '@celilo/cli': '0.12.1-alpha.3' }), ), ).toEqual([]); }); }); });