File size: 7,928 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { vi, describe, it, expect, beforeEach } from 'vitest';
import { getVersion } from '../get-release-version.js';
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';

vi.mock('node:child_process');
vi.mock('node:fs');

describe('getVersion', () => {
  beforeEach(() => {
    vi.resetAllMocks();
    vi.setSystemTime(new Date('2025-09-17T00:00:00.000Z'));
    // Mock package.json being read by getNightlyVersion
    vi.mocked(readFileSync).mockReturnValue(
      JSON.stringify({ version: '0.8.0' }),
    );
  });

  // This is the base mock for a clean state with no conflicts or rollbacks
  const mockExecSync = (command) => {
    // NPM dist-tags
    if (command.includes('npm view') && command.includes('--tag=latest'))
      return '0.6.1';
    if (command.includes('npm view') && command.includes('--tag=preview'))
      return '0.7.0-preview.1';
    if (command.includes('npm view') && command.includes('--tag=nightly'))
      return '0.8.0-nightly.20250916.abcdef';

    // NPM versions list
    if (command.includes('npm view') && command.includes('versions --json'))
      return JSON.stringify([
        '0.6.0',
        '0.6.1',
        '0.7.0-preview.0',
        '0.7.0-preview.1',
        '0.8.0-nightly.20250916.abcdef',
      ]);

    // Deprecation checks (default to not deprecated)
    if (command.includes('deprecated')) return '';

    // Git Tag Mocks
    if (command.includes("git tag -l 'v[0-9].[0-9].[0-9]'")) return 'v0.6.1';
    if (command.includes("git tag -l 'v*-preview*'")) return 'v0.7.0-preview.1';
    if (command.includes("git tag -l 'v*-nightly*'"))
      return 'v0.8.0-nightly.20250916.abcdef';

    // Git Hash Mock
    if (command.includes('git rev-parse --short HEAD')) return 'd3bf8a3d';

    // For doesVersionExist checks - default to not found
    if (
      command.includes('npm view') &&
      command.includes('@google/gemini-cli@')
    ) {
      throw new Error('NPM version not found');
    }
    if (command.includes('git tag -l')) return '';
    if (command.includes('gh release view')) {
      throw new Error('GH release not found');
    }

    return '';
  };

  describe('Happy Path - Version Calculation', () => {
    it('should calculate the next stable version from the latest preview', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'stable' });
      expect(result.releaseVersion).toBe('0.7.0');
      expect(result.npmTag).toBe('latest');
      expect(result.previousReleaseTag).toBe('v0.6.1');
    });

    it('should calculate the next preview version from the latest nightly', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      expect(result.releaseVersion).toBe('0.8.0-preview.0');
      expect(result.npmTag).toBe('preview');
      expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
    });

    it('should calculate the next nightly version from package.json', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'nightly' });
      // Note: The base version now comes from package.json, not the previous nightly tag.
      expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.gd3bf8a3d');
      expect(result.npmTag).toBe('nightly');
      expect(result.previousReleaseTag).toBe('v0.8.0-nightly.20250916.abcdef');
    });

    it('should calculate the next patch version for a stable release', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'patch', 'patch-from': 'stable' });
      expect(result.releaseVersion).toBe('0.6.2');
      expect(result.npmTag).toBe('latest');
      expect(result.previousReleaseTag).toBe('v0.6.1');
    });

    it('should calculate the next patch version for a preview release', () => {
      vi.mocked(execSync).mockImplementation(mockExecSync);
      const result = getVersion({ type: 'patch', 'patch-from': 'preview' });
      expect(result.releaseVersion).toBe('0.7.0-preview.2');
      expect(result.npmTag).toBe('preview');
      expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
    });
  });

  describe('Advanced Scenarios', () => {
    it('should ignore a deprecated version and use the next highest', () => {
      const mockWithDeprecated = (command) => {
        // The highest nightly is 0.9.0, but it's deprecated
        if (command.includes('npm view') && command.includes('versions --json'))
          return JSON.stringify([
            '0.8.0-nightly.20250916.abcdef',
            '0.9.0-nightly.20250917.deprecated', // This one is deprecated
          ]);
        // Mock the deprecation check
        if (
          command.includes(
            'npm view @google/gemini-cli@0.9.0-nightly.20250917.deprecated deprecated',
          )
        )
          return 'This version is deprecated';
        // The dist-tag still points to the older, valid version
        if (command.includes('npm view') && command.includes('--tag=nightly'))
          return '0.8.0-nightly.20250916.abcdef';

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithDeprecated);

      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      // It should base the preview off 0.8.0, not the deprecated 0.9.0
      expect(result.releaseVersion).toBe('0.8.0-preview.0');
    });

    it('should auto-increment patch version if the calculated one already exists', () => {
      const mockWithConflict = (command) => {
        // The calculated version 0.7.0 already exists as a git tag
        if (command.includes("git tag -l 'v0.7.0'")) return 'v0.7.0';
        // The next version, 0.7.1, is available
        if (command.includes("git tag -l 'v0.7.1'")) return '';

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithConflict);

      const result = getVersion({ type: 'stable' });
      // Should have skipped 0.7.0 and landed on 0.7.1
      expect(result.releaseVersion).toBe('0.7.1');
    });

    it('should auto-increment preview number if the calculated one already exists', () => {
      const mockWithConflict = (command) => {
        // The calculated preview 0.8.0-preview.0 already exists on NPM
        if (
          command.includes(
            'npm view @google/gemini-cli@0.8.0-preview.0 version',
          )
        )
          return '0.8.0-preview.0';
        // The next one is available
        if (
          command.includes(
            'npm view @google/gemini-cli@0.8.0-preview.1 version',
          )
        )
          throw new Error('Not found');

        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithConflict);

      const result = getVersion({
        type: 'preview',
        'stable-base-version': '0.7.0',
      });
      // Should have skipped preview.0 and landed on preview.1
      expect(result.releaseVersion).toBe('0.8.0-preview.1');
    });

    it('should preserve a git hash with a leading zero via the g prefix', () => {
      const mockWithLeadingZeroHash = (command) => {
        // Return an all-numeric hash with a leading zero
        if (command.includes('git rev-parse --short HEAD')) return '017972622';
        return mockExecSync(command);
      };
      vi.mocked(execSync).mockImplementation(mockWithLeadingZeroHash);

      const result = getVersion({ type: 'nightly' });
      // The 'g' prefix forces semver to treat this as an alphanumeric
      // identifier, preventing it from stripping the leading zero.
      expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.g017972622');
    });
  });
});