From f9df4153921034f276d3059f08af9849b3918798 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Fri, 31 Oct 2025 14:18:39 -0700 Subject: [PATCH] feat(core): Introduce release channel detection (#12257) Co-authored-by: shishu314 Co-authored-by: gemini-cli-robot Co-authored-by: Shardul Natu <43422294+kiranani@users.noreply.github.com> Co-authored-by: Shnatu Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/core/src/utils/channel.test.ts | 159 ++++++++++++++++++++++++ packages/core/src/utils/channel.ts | 55 ++++++++ 2 files changed, 214 insertions(+) create mode 100644 packages/core/src/utils/channel.test.ts create mode 100644 packages/core/src/utils/channel.ts diff --git a/packages/core/src/utils/channel.test.ts b/packages/core/src/utils/channel.test.ts new file mode 100644 index 0000000000..1e9ee6f13d --- /dev/null +++ b/packages/core/src/utils/channel.test.ts @@ -0,0 +1,159 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { isNightly, isPreview, isStable, _clearCache } from './channel.js'; +import * as packageJson from './package.js'; + +vi.mock('./package.js', () => ({ + getPackageJson: vi.fn(), +})); + +describe('channel', () => { + beforeEach(() => { + vi.resetAllMocks(); + _clearCache(); + }); + + describe('isStable', () => { + it('should return true for a stable version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0', + }); + await expect(isStable('/test/dir')).resolves.toBe(true); + }); + + it('should return false for a nightly version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-nightly.1', + }); + await expect(isStable('/test/dir')).resolves.toBe(false); + }); + + it('should return false for a preview version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-preview.1', + }); + await expect(isStable('/test/dir')).resolves.toBe(false); + }); + + it('should return false if package.json is not found', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue(undefined); + await expect(isStable('/test/dir')).resolves.toBe(false); + }); + + it('should return false if version is not defined', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + }); + await expect(isStable('/test/dir')).resolves.toBe(false); + }); + }); + + describe('isNightly', () => { + it('should return false for a stable version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0', + }); + await expect(isNightly('/test/dir')).resolves.toBe(false); + }); + + it('should return true for a nightly version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-nightly.1', + }); + await expect(isNightly('/test/dir')).resolves.toBe(true); + }); + + it('should return false for a preview version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-preview.1', + }); + await expect(isNightly('/test/dir')).resolves.toBe(false); + }); + + it('should return true if package.json is not found', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue(undefined); + await expect(isNightly('/test/dir')).resolves.toBe(true); + }); + + it('should return true if version is not defined', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + }); + await expect(isNightly('/test/dir')).resolves.toBe(true); + }); + }); + + describe('isPreview', () => { + it('should return false for a stable version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0', + }); + await expect(isPreview('/test/dir')).resolves.toBe(false); + }); + + it('should return false for a nightly version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-nightly.1', + }); + await expect(isPreview('/test/dir')).resolves.toBe(false); + }); + + it('should return true for a preview version', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + version: '1.0.0-preview.1', + }); + await expect(isPreview('/test/dir')).resolves.toBe(true); + }); + + it('should return false if package.json is not found', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue(undefined); + await expect(isPreview('/test/dir')).resolves.toBe(false); + }); + + it('should return false if version is not defined', async () => { + vi.spyOn(packageJson, 'getPackageJson').mockResolvedValue({ + name: 'test', + }); + await expect(isPreview('/test/dir')).resolves.toBe(false); + }); + }); + + describe('memoization', () => { + it('should only call getPackageJson once for the same cwd', async () => { + const spy = vi + .spyOn(packageJson, 'getPackageJson') + .mockResolvedValue({ name: 'test', version: '1.0.0' }); + + await expect(isStable('/test/dir')).resolves.toBe(true); + await expect(isNightly('/test/dir')).resolves.toBe(false); + await expect(isPreview('/test/dir')).resolves.toBe(false); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call getPackageJson again for a different cwd', async () => { + const spy = vi + .spyOn(packageJson, 'getPackageJson') + .mockResolvedValue({ name: 'test', version: '1.0.0' }); + + await expect(isStable('/test/dir1')).resolves.toBe(true); + await expect(isStable('/test/dir2')).resolves.toBe(true); + + expect(spy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/core/src/utils/channel.ts b/packages/core/src/utils/channel.ts new file mode 100644 index 0000000000..e67e108b2e --- /dev/null +++ b/packages/core/src/utils/channel.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getPackageJson } from './package.js'; + +export enum ReleaseChannel { + NIGHTLY = 'nightly', + PREVIEW = 'preview', + STABLE = 'stable', +} + +const cache = new Map(); + +/** + * Clears the cache for testing purposes. + * @private + */ +export function _clearCache() { + cache.clear(); +} + +async function _getReleaseChannel(cwd: string): Promise { + if (cache.has(cwd)) { + return cache.get(cwd)!; + } + + const packageJson = await getPackageJson(cwd); + const version = packageJson?.version ?? ''; + + let channel: ReleaseChannel; + if (version.includes('nightly') || version === '') { + channel = ReleaseChannel.NIGHTLY; + } else if (version.includes('preview')) { + channel = ReleaseChannel.PREVIEW; + } else { + channel = ReleaseChannel.STABLE; + } + cache.set(cwd, channel); + return channel; +} + +export async function isNightly(cwd: string): Promise { + return (await _getReleaseChannel(cwd)) === ReleaseChannel.NIGHTLY; +} + +export async function isPreview(cwd: string): Promise { + return (await _getReleaseChannel(cwd)) === ReleaseChannel.PREVIEW; +} + +export async function isStable(cwd: string): Promise { + return (await _getReleaseChannel(cwd)) === ReleaseChannel.STABLE; +}