Skip to content

Commit 056f718

Browse files
committed
test: add end-to-end regression coverage for monorepo package manager detection
Execute the upgrade/remove task callbacks and the undo-summary path with the real findPackageManagerByLockfile against a monorepo shape (ancestor pnpm-lock.yaml, app nested without its own lockfile), proving the upgrade command feeds ancestor-lockfile detection into exec and the undo instructions rather than relying on the suite-wide stub.
1 parent 4fbc067 commit 056f718

2 files changed

Lines changed: 143 additions & 3 deletions

File tree

packages/cli/src/commands/hydrogen/upgrade.test.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {createRequire} from 'node:module';
22
import {tmpdir} from 'node:os';
3-
import {mkdtemp, readFile, rm} from 'node:fs/promises';
4-
import {describe, it, expect, vi, beforeEach} from 'vitest';
3+
import {mkdir, mkdtemp, readFile, rm} from 'node:fs/promises';
4+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
55
import {writeFile, fileExists} from '@shopify/cli-kit/node/fs';
66
import {joinPath} from '@shopify/cli-kit/node/path';
77
import {
@@ -13,9 +13,11 @@ import {
1313
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output';
1414
import {type PackageJson} from '@shopify/cli-kit/node/node-package-manager';
1515
import {exec} from '@shopify/cli-kit/node/system';
16+
import * as system from '@shopify/cli-kit/node/system';
1617
import {
1718
buildUpgradeCommandArgs,
1819
displayConfirmation,
20+
displayUpgradeSummary,
1921
generateUpgradeInstructionsFile,
2022
getAbsoluteVersion,
2123
getAvailableUpgrades,
@@ -31,6 +33,7 @@ import {
3133
getChangelog,
3234
displayDevUpgradeNotice,
3335
} from './upgrade.js';
36+
import * as packageManagers from '../../lib/package-managers.js';
3437
import {getSkeletonSourceDir} from '../../lib/build.js';
3538

3639
// Test version numbers used to avoid conflicts with duplicate changelog versions
@@ -48,6 +51,19 @@ vi.mock('../../lib/package-managers.js', async () => {
4851
};
4952
});
5053

54+
vi.mock('@shopify/cli-kit/node/system', async () => {
55+
const original = await vi.importActual<
56+
typeof import('@shopify/cli-kit/node/system')
57+
>('@shopify/cli-kit/node/system');
58+
59+
return {
60+
...original,
61+
// Delegates to the real exec by default so helpers that drive git keep
62+
// working; individual tests override it to capture command invocations.
63+
exec: vi.fn(original.exec),
64+
};
65+
});
66+
5167
vi.mock('../../lib/shell.js', () => ({getCliCommand: vi.fn(() => 'h2')}));
5268

5369
vi.mock('@shopify/cli-kit/node/ui', async () => {
@@ -2010,6 +2026,130 @@ describe('upgrade', async () => {
20102026
expect(args).toEqual(result);
20112027
});
20122028
});
2029+
2030+
// Regression coverage for monorepo workspaces, where the lockfile lives at
2031+
// the workspace root rather than in the app directory. These tests use the
2032+
// real findPackageManagerByLockfile (not the suite-wide 'pnpm' stub) and
2033+
// execute the command's task/summary paths end-to-end, so they fail if the
2034+
// upgrade command stops feeding ancestor-lockfile detection into the
2035+
// install/remove tasks or the undo instructions.
2036+
describe('monorepo package manager detection', () => {
2037+
afterEach(async () => {
2038+
// Restore the suite-wide stubs for the rest of the file, since the
2039+
// module-level mock implementations persist across tests.
2040+
vi.mocked(
2041+
packageManagers.findPackageManagerByLockfile,
2042+
).mockImplementation(() => Promise.resolve('pnpm'));
2043+
const actualSystem = await vi.importActual<
2044+
typeof import('@shopify/cli-kit/node/system')
2045+
>('@shopify/cli-kit/node/system');
2046+
vi.mocked(system.exec).mockImplementation(actualSystem.exec);
2047+
});
2048+
2049+
async function useRealLockfileDetection() {
2050+
const {findPackageManagerByLockfile} = await vi.importActual<
2051+
typeof import('../../lib/package-managers.js')
2052+
>('../../lib/package-managers.js');
2053+
vi.mocked(
2054+
packageManagers.findPackageManagerByLockfile,
2055+
).mockImplementation(findPackageManagerByLockfile);
2056+
}
2057+
2058+
it('runs the install and remove tasks with the workspace package manager from an ancestor lockfile', async () => {
2059+
await useRealLockfileDetection();
2060+
2061+
const {releases} = await getChangelog();
2062+
const selectedRelease = releases.find(
2063+
(release) => release.version === '2023.10.0',
2064+
) as (typeof releases)[0];
2065+
2066+
const currentDependencies = {
2067+
...OUTDATED_HYDROGEN_PACKAGE_JSON.dependencies,
2068+
...OUTDATED_HYDROGEN_PACKAGE_JSON.devDependencies,
2069+
};
2070+
2071+
await inTemporaryDirectory(async (workspaceRoot) => {
2072+
// Monorepo shape: the only lockfile is at the workspace root and the
2073+
// Hydrogen app lives in a nested directory with no lockfile of its own.
2074+
await writeFile(joinPath(workspaceRoot, 'pnpm-lock.yaml'), '');
2075+
const appPath = joinPath(workspaceRoot, 'apps', 'storefront');
2076+
await mkdir(appPath, {recursive: true});
2077+
2078+
// Capture the package-manager invocation without running a real install.
2079+
const execSpy = vi
2080+
.mocked(system.exec)
2081+
.mockResolvedValue(undefined as never);
2082+
2083+
await upgradeNodeModules({
2084+
appPath,
2085+
selectedRelease,
2086+
currentDependencies,
2087+
// present in currentDependencies, so the removal task is created
2088+
cumulativeRemoveDependencies: ['@remix-run/react'],
2089+
cumulativeRemoveDevDependencies: [],
2090+
});
2091+
2092+
// renderTasks is stubbed to a no-op, so run the task callbacks directly.
2093+
const tasks = vi.mocked(renderTasks).mock.calls.at(-1)?.[0] as Array<{
2094+
title: string;
2095+
task: () => Promise<void>;
2096+
}>;
2097+
2098+
const removalTask = tasks.find(
2099+
(task) => task.title === 'Removing deprecated dependencies',
2100+
);
2101+
const upgradeTask = tasks.find(
2102+
(task) => task.title === 'Upgrading dependencies',
2103+
);
2104+
2105+
await removalTask?.task();
2106+
await upgradeTask?.task();
2107+
2108+
expect(execSpy).toHaveBeenCalledWith(
2109+
'pnpm',
2110+
expect.arrayContaining(['remove', '@remix-run/react']),
2111+
expect.objectContaining({cwd: appPath}),
2112+
);
2113+
expect(execSpy).toHaveBeenCalledWith(
2114+
'pnpm',
2115+
expect.arrayContaining(['add']),
2116+
expect.objectContaining({cwd: appPath}),
2117+
);
2118+
// Never falls back to npm + --legacy-peer-deps for a pnpm workspace.
2119+
expect(execSpy).not.toHaveBeenCalledWith(
2120+
'npm',
2121+
expect.anything(),
2122+
expect.anything(),
2123+
);
2124+
});
2125+
});
2126+
2127+
it('renders the workspace package manager in the undo instructions for an ancestor lockfile', async () => {
2128+
await useRealLockfileDetection();
2129+
2130+
const {releases} = await getChangelog();
2131+
const selectedRelease = releases.find(
2132+
(release) => release.version === '2023.10.0',
2133+
) as (typeof releases)[0];
2134+
2135+
await inTemporaryDirectory(async (workspaceRoot) => {
2136+
await writeFile(joinPath(workspaceRoot, 'pnpm-lock.yaml'), '');
2137+
const appPath = joinPath(workspaceRoot, 'apps', 'storefront');
2138+
await mkdir(appPath, {recursive: true});
2139+
2140+
await displayUpgradeSummary({
2141+
appPath,
2142+
currentVersion: '2023.1.6',
2143+
selectedRelease,
2144+
});
2145+
2146+
const output = outputMock.info();
2147+
expect(output).toContain('Undo these upgrades?');
2148+
expect(output).toContain('&& pnpm i`');
2149+
expect(output).not.toContain('&& npm i`');
2150+
});
2151+
});
2152+
});
20132153
});
20142154

20152155
// cumulative result when upgrading from 2023.1.6 (outdated) to 2023.4.1

packages/cli/src/commands/hydrogen/upgrade.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,7 @@ async function promptUpgradeOptions(
12561256
/**
12571257
* Displays a summary of the upgrade and next steps
12581258
*/
1259-
async function displayUpgradeSummary({
1259+
export async function displayUpgradeSummary({
12601260
appPath,
12611261
currentVersion,
12621262
selectedRelease,

0 commit comments

Comments
 (0)