From 80c4e85073fdad233e928401badc8aa9bb706b50 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 10 Apr 2026 00:53:36 +0900 Subject: [PATCH] fix: remove shell mode from uloop update --- .../src/Cli~/src/__tests__/cli-update.test.ts | 129 ++++++++++++++++++ Packages/src/Cli~/src/cli.ts | 13 +- Packages/src/Cli~/src/project-root.ts | 2 +- 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 Packages/src/Cli~/src/__tests__/cli-update.test.ts diff --git a/Packages/src/Cli~/src/__tests__/cli-update.test.ts b/Packages/src/Cli~/src/__tests__/cli-update.test.ts new file mode 100644 index 000000000..f0ae81663 --- /dev/null +++ b/Packages/src/Cli~/src/__tests__/cli-update.test.ts @@ -0,0 +1,129 @@ +type SpawnArgs = [string, string[], Record?]; + +const mockSpawn = jest.fn(); + +jest.mock('child_process', () => ({ + spawn: (...args: SpawnArgs): unknown => mockSpawn(...args), +})); + +jest.mock( + 'launch-unity', + () => ({ + orchestrateLaunch: jest.fn(), + }), + { virtual: true }, +); + +import { getInstalledVersion, updateCli } from '../cli.js'; + +type CloseHandler = (code: number | null) => void; +type ErrorHandler = (error: Error) => void; +type DataHandler = (chunk: Buffer) => void; + +interface MockChildProcess { + stdout: { + on: jest.Mock; + }; + on: jest.Mock; + emitStdout: (chunk: string) => void; + emitClose: (code: number | null) => void; + emitError: (error: Error) => void; +} + +function createMockChildProcess(): MockChildProcess { + let closeHandler: CloseHandler | undefined; + let errorHandler: ErrorHandler | undefined; + let dataHandler: DataHandler | undefined; + + return { + stdout: { + on: jest.fn((event: string, handler: DataHandler) => { + if (event === 'data') { + dataHandler = handler; + } + }), + }, + on: jest.fn((event: string, handler: CloseHandler | ErrorHandler) => { + if (event === 'close') { + closeHandler = handler as CloseHandler; + } + + if (event === 'error') { + errorHandler = handler as ErrorHandler; + } + }), + emitStdout: (chunk: string): void => { + dataHandler?.(Buffer.from(chunk)); + }, + emitClose: (code: number | null): void => { + closeHandler?.(code); + }, + emitError: (error: Error): void => { + errorHandler?.(error); + }, + }; +} + +describe('CLI update npm invocation', () => { + const expectedNpmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + + beforeEach(() => { + mockSpawn.mockReset(); + jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('gets installed version without enabling shell mode', () => { + const child = createMockChildProcess(); + const callback = jest.fn(); + mockSpawn.mockReturnValue(child); + + getInstalledVersion(callback); + + expect(mockSpawn).toHaveBeenCalledWith(expectedNpmCommand, [ + 'list', + '-g', + 'uloop-cli', + '--json', + ]); + expect(mockSpawn.mock.calls[0]).toHaveLength(2); + + child.emitStdout(JSON.stringify({ dependencies: { 'uloop-cli': { version: '1.8.0' } } })); + child.emitClose(0); + + expect(callback).toHaveBeenCalledWith('1.8.0'); + }); + + it('updates the CLI without enabling shell mode', () => { + const updateChild = createMockChildProcess(); + const listChild = createMockChildProcess(); + mockSpawn.mockReturnValueOnce(updateChild).mockReturnValueOnce(listChild); + + updateCli(); + + expect(mockSpawn).toHaveBeenNthCalledWith( + 1, + expectedNpmCommand, + ['install', '-g', 'uloop-cli@latest'], + { stdio: 'inherit' }, + ); + const installOptions = mockSpawn.mock.calls[0]?.[2]; + expect(installOptions?.['shell']).toBeUndefined(); + + updateChild.emitClose(0); + + expect(mockSpawn).toHaveBeenNthCalledWith(2, expectedNpmCommand, [ + 'list', + '-g', + 'uloop-cli', + '--json', + ]); + expect(mockSpawn.mock.calls[1]).toHaveLength(2); + + listChild.emitStdout(JSON.stringify({ dependencies: { 'uloop-cli': { version: '1.7.1' } } })); + listChild.emitClose(0); + }); +}); diff --git a/Packages/src/Cli~/src/cli.ts b/Packages/src/Cli~/src/cli.ts index 71698bf8f..bf3acb678 100644 --- a/Packages/src/Cli~/src/cli.ts +++ b/Packages/src/Cli~/src/cli.ts @@ -597,11 +597,9 @@ compdef _uloop uloop`; /** * Get the currently installed version of uloop-cli from npm. */ -function getInstalledVersion(callback: (version: string | null) => void): void { +export function getInstalledVersion(callback: (version: string | null) => void): void { const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; - const child = spawn(npmCommand, ['list', '-g', 'uloop-cli', '--json'], { - shell: true, - }); + const child = spawn(npmCommand, ['list', '-g', 'uloop-cli', '--json']); let stdout = ''; child.stdout.on('data', (data: Buffer) => { @@ -656,14 +654,13 @@ function getInstalledVersion(callback: (version: string | null) => void): void { /** * Update uloop CLI to the latest version using npm. */ -function updateCli(): void { +export function updateCli(): void { const previousVersion = VERSION; console.log('Updating uloop-cli to the latest version...'); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const child = spawn(npmCommand, ['install', '-g', 'uloop-cli@latest'], { stdio: 'inherit', - shell: true, }); child.on('close', (code) => { @@ -1032,4 +1029,6 @@ async function main(): Promise { program.parse(); } -void main(); +if (process.env.JEST_WORKER_ID === undefined) { + void main(); +} diff --git a/Packages/src/Cli~/src/project-root.ts b/Packages/src/Cli~/src/project-root.ts index 756c320cc..b87fefb52 100644 --- a/Packages/src/Cli~/src/project-root.ts +++ b/Packages/src/Cli~/src/project-root.ts @@ -34,7 +34,7 @@ export function getUnitySettingsCandidatePaths(dirPath: string): string[] { } export function hasUloopInstalled(dirPath: string): boolean { - return getUnitySettingsCandidatePaths(dirPath).some(path => existsSync(path)); + return getUnitySettingsCandidatePaths(dirPath).some((path) => existsSync(path)); } function isUnityProjectWithUloop(dirPath: string): boolean {