diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index dcef8d0aac845..6e25f94340632 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -66,7 +66,8 @@ const reifyOutput = (npm, arb, extras = {}) => { if (showDiff) { output.standard(`${chalk.green('add')} ${d.ideal.name} ${d.ideal.package.version}`) } - if (actualTree.inventory.has(d.ideal)) { + // Linked store packages live under .store, absent from the logical actualTree, so identity lookup misses them; count each store package node (non-link). + if (actualTree.inventory.has(d.ideal) || (d.ideal.isInStore && !d.ideal.isLink)) { summary.added++ summary.add.push({ name: d.ideal.name, diff --git a/tap-snapshots/test/lib/utils/reify-output.js.test.cjs b/tap-snapshots/test/lib/utils/reify-output.js.test.cjs index 40d4cd221ca02..828a99679eab6 100644 --- a/tap-snapshots/test/lib/utils/reify-output.js.test.cjs +++ b/tap-snapshots/test/lib/utils/reify-output.js.test.cjs @@ -10,6 +10,11 @@ exports[`test/lib/utils/reify-output.js TAP added packages should be looked up w added 1 package in {TIME} ` +exports[`test/lib/utils/reify-output.js TAP added packages should be looked up within returned tree linked store package counted though absent from actualTree > must match snapshot 1`] = ` + +added 1 package in {TIME} +` + exports[`test/lib/utils/reify-output.js TAP added packages should be looked up within returned tree missing added pkg in inventory > must match snapshot 1`] = ` up to date in {TIME} diff --git a/test/lib/commands/ls.js b/test/lib/commands/ls.js index ab98773bc68e5..17733bc03ef3f 100644 --- a/test/lib/commands/ls.js +++ b/test/lib/commands/ls.js @@ -5332,6 +5332,25 @@ t.test('ls --install-strategy=linked', async t => { node_modules: { 'workspace-a': t.fixture('symlink', '../packages/workspace-a'), // workspace-b intentionally NOT linked (undeclared in dependencies) + // The hidden lockfile a real linked install writes records only the + // declared workspace as linked into root node_modules, so loadActual + // resolves workspace-b's root edge as missing (the undeclared-workspace case). + '.package-lock.json': JSON.stringify({ + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/workspace-a': { + resolved: 'packages/workspace-a', + link: true, + }, + 'packages/workspace-a': { + version: '1.0.0', + }, + 'packages/workspace-b': { + version: '1.0.0', + }, + }, + }), }, }, }) @@ -5397,7 +5416,20 @@ t.test('ls --install-strategy=linked', async t => { }, }, node_modules: { - // workspace-a is declared but its symlink is missing + // workspace-a is declared but its symlink is missing. + // The hidden lockfile records the workspace target without a root + // node_modules link, so loadActual resolves the declared workspace's + // root edge as missing (the declared-but-missing case), which must + // still be reported as UNMET DEPENDENCY. + '.package-lock.json': JSON.stringify({ + lockfileVersion: 3, + requires: true, + packages: { + 'packages/workspace-a': { + version: '1.0.0', + }, + }, + }), }, }, }) diff --git a/test/lib/utils/reify-output.js b/test/lib/utils/reify-output.js index 596272f21fa97..0f2be24b0246e 100644 --- a/test/lib/utils/reify-output.js +++ b/test/lib/utils/reify-output.js @@ -391,6 +391,25 @@ t.test('added packages should be looked up within returned tree', async t => { t.matchSnapshot(out) }) + + t.test('linked store package counted though absent from actualTree', async t => { + const out = await mockReify(t, { + actualTree: { + name: 'foo', + inventory: { + has: () => false, + }, + }, + diff: { + children: [ + { action: 'ADD', ideal: { path: 'test/baz', name: 'baz', isInStore: true, isLink: false, package: { version: '1.0.0' } } }, + { action: 'ADD', ideal: { path: 'test/baz-link', name: 'baz', isInStore: false, isLink: true, package: { version: '1.0.0' } } }, + ], + }, + }) + + t.matchSnapshot(out) + }) }) t.test('prints dedupe difference on dry-run', async t => {