mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-07 17:22:37 +00:00
987 lines
25 KiB
JavaScript
987 lines
25 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const fetchMock = vi.fn();
|
|
const execSyncMock = vi.fn();
|
|
const readFileSyncMock = vi.fn();
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execSync: execSyncMock,
|
|
}));
|
|
|
|
vi.mock('node:fs', () => ({
|
|
readFileSync: readFileSyncMock,
|
|
}));
|
|
|
|
global.fetch = fetchMock;
|
|
|
|
const modulePath = '../../packages/sdk-python/scripts/get-release-version.js';
|
|
|
|
async function loadGetVersion() {
|
|
const mod = await import(`${modulePath}?t=${Date.now()}-${Math.random()}`);
|
|
return mod.getVersion;
|
|
}
|
|
|
|
function makeResponse({ status = 200, json = {}, statusText = 'OK' } = {}) {
|
|
return {
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
statusText,
|
|
json: async () => json,
|
|
};
|
|
}
|
|
|
|
function makeExecError(message, { stderr = '', stdout = '', status } = {}) {
|
|
const error = new Error(message);
|
|
if (stderr) {
|
|
error.stderr = Buffer.from(stderr);
|
|
}
|
|
if (stdout) {
|
|
error.stdout = Buffer.from(stdout);
|
|
}
|
|
if (status !== undefined) {
|
|
error.status = status;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
function makeTimeoutError(command) {
|
|
const error = new Error(`Command failed: ${command}\nSIGTERM`);
|
|
// Real Node.js execSync timeout shape (verified on Node 20+):
|
|
// killed=undefined, signal='SIGTERM', code='ETIMEDOUT'
|
|
error.code = 'ETIMEDOUT';
|
|
error.signal = 'SIGTERM';
|
|
error.status = null;
|
|
return error;
|
|
}
|
|
|
|
function makeExecSyncMock({
|
|
tags = {},
|
|
tagError = null,
|
|
releases = {},
|
|
gitHash = 'abc1234',
|
|
} = {}) {
|
|
return (command) => {
|
|
if (command === 'git rev-parse --short HEAD') {
|
|
return Buffer.from(gitHash);
|
|
}
|
|
|
|
const tagMatch = command.match(/^git tag -l '(.+)'$/);
|
|
if (tagMatch) {
|
|
if (tagError) {
|
|
throw tagError;
|
|
}
|
|
return Buffer.from(tags[tagMatch[1]] ?? '');
|
|
}
|
|
|
|
const releaseMatch = command.match(
|
|
/^gh release view "(.+)" --json tagName --jq \.tagName$/,
|
|
);
|
|
if (releaseMatch) {
|
|
const releaseName = releaseMatch[1];
|
|
const outcome = releases[releaseName];
|
|
if (outcome instanceof Error) {
|
|
throw outcome;
|
|
}
|
|
if (typeof outcome === 'string') {
|
|
return Buffer.from(outcome);
|
|
}
|
|
throw makeExecError('release not found', { status: 1 });
|
|
}
|
|
|
|
throw new Error(`Unexpected execSync command: ${command}`);
|
|
};
|
|
}
|
|
|
|
describe('python sdk get-release-version', () => {
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-04-30T03:15:16.000Z'));
|
|
readFileSyncMock.mockReturnValue('version = "0.1.0"\n');
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: { releases: {} },
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(makeExecSyncMock());
|
|
});
|
|
|
|
it('returns empty previousReleaseTag for preview and nightly releases', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
'0.1.1.dev20260429010101': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.1-preview.1',
|
|
previousReleaseTag: '',
|
|
});
|
|
|
|
await expect(getVersion({ type: 'nightly' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.1-nightly.20260430031516.abc1234',
|
|
releaseVersion: '0.1.1-nightly.20260430031516.abc1234',
|
|
packageVersion: '0.1.1.dev20260430031516',
|
|
publishChannel: 'nightly',
|
|
previousReleaseTag: '',
|
|
});
|
|
});
|
|
|
|
it('fails when an explicit override conflicts with existing PyPI version', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.1rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'preview',
|
|
preview_version_override: 'v0.1.1-preview.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested preview release 0.1.1-preview.0 already exists on PyPI, GitHub releases.',
|
|
);
|
|
});
|
|
|
|
it('fails when an explicit preview override only conflicts with PyPI state', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.1rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'preview',
|
|
preview_version_override: 'v0.1.1-preview.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested preview release 0.1.1-preview.0 already exists on PyPI.',
|
|
);
|
|
});
|
|
|
|
it('fails when an explicit override conflicts with an existing git tag', async () => {
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tags: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'preview',
|
|
preview_version_override: 'v0.1.1-preview.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested preview release 0.1.1-preview.0 already exists on git tags.',
|
|
);
|
|
});
|
|
|
|
it('fails when an explicit override conflicts with an existing GitHub release', async () => {
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'preview',
|
|
preview_version_override: 'v0.1.1-preview.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested preview release 0.1.1-preview.0 already exists on GitHub releases.',
|
|
);
|
|
});
|
|
|
|
it('fails closed when git tag conflict checks error', async () => {
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tagError: new Error('git tag failed'),
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'Failed to check git tags for conflicts: git tag failed',
|
|
);
|
|
});
|
|
|
|
it('fails if GitHub release lookup errors for reasons other than not found', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const authError = new Error('HTTP 403 rate limited');
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': authError,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'Failed to check GitHub releases for conflicts: HTTP 403 rate limited',
|
|
);
|
|
});
|
|
|
|
it('fails closed when unrelated lowercase not-found errors occur', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': makeExecError('host not found'),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'Failed to check GitHub releases for conflicts: host not found',
|
|
);
|
|
});
|
|
|
|
it('reuses a PyPI version when GitHub release finalization needs to resume', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.1-preview.0',
|
|
packageVersion: '0.1.1rc0',
|
|
resumeExistingRelease: true,
|
|
});
|
|
});
|
|
|
|
it('reuses a stable release when only post-release recovery steps remain', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0',
|
|
packageVersion: '0.1.0',
|
|
resumeExistingRelease: true,
|
|
});
|
|
});
|
|
|
|
it('returns a manual stable override on the happy path', async () => {
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'stable',
|
|
stable_version_override: 'v0.2.0',
|
|
}),
|
|
).resolves.toMatchObject({
|
|
releaseVersion: '0.2.0',
|
|
packageVersion: '0.2.0',
|
|
publishChannel: 'latest',
|
|
});
|
|
});
|
|
|
|
it('fails when an explicit stable override conflicts with a completed release', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'stable',
|
|
stable_version_override: 'v0.1.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested stable release 0.1.0 already exists on PyPI, GitHub releases.',
|
|
);
|
|
});
|
|
|
|
it('fails when the latest preview base is not newer than the latest stable', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.2.0': [{}],
|
|
'0.1.1rc1': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
|
|
'Latest preview base 0.1.1 is not newer than latest stable 0.2.0.',
|
|
);
|
|
});
|
|
|
|
it('uses the latest nightly base for stable releases when no preview exists', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0.dev20260429010101': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.2.0',
|
|
releaseVersion: '0.2.0',
|
|
packageVersion: '0.2.0',
|
|
previousReleaseTag: 'v0.1.0',
|
|
publishChannel: 'latest',
|
|
});
|
|
});
|
|
|
|
it('prefers nightly base over preview when nightly is higher for stable releases', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
'0.2.0.dev20260429010101': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.2.0',
|
|
releaseVersion: '0.2.0',
|
|
packageVersion: '0.2.0',
|
|
previousReleaseTag: 'v0.1.0',
|
|
publishChannel: 'latest',
|
|
});
|
|
});
|
|
|
|
it('fails instead of patch-bumping a stable release derived from preview when its tag already exists', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tags: {
|
|
'sdk-python-v0.1.1': 'sdk-python-v0.1.1',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
|
|
'Stable release 0.1.1 derived from the latest preview already exists.',
|
|
);
|
|
});
|
|
|
|
it('fails instead of patch-bumping a stable release derived from nightly when its tag already exists', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0.dev20260429010101': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tags: {
|
|
'sdk-python-v0.2.0': 'sdk-python-v0.2.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
|
|
'Stable release 0.2.0 derived from the latest nightly already exists.',
|
|
);
|
|
});
|
|
|
|
it('fails instead of patch-bumping the current stable version when its tag already exists', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.0.9': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tags: {
|
|
'sdk-python-v0.1.0': 'sdk-python-v0.1.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).rejects.toThrow(
|
|
'Stable release 0.1.0 already exists. Provide stable_version_override to release a different stable version.',
|
|
);
|
|
});
|
|
|
|
it('returns the previous stable tag for stable releases', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.1',
|
|
previousReleaseTag: 'v0.1.0',
|
|
});
|
|
});
|
|
|
|
it('maps preview versions to PEP 440 package versions', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'preview',
|
|
preview_version_override: 'v0.1.1-preview.2',
|
|
}),
|
|
).resolves.toMatchObject({
|
|
releaseVersion: '0.1.1-preview.2',
|
|
packageVersion: '0.1.1rc2',
|
|
});
|
|
});
|
|
|
|
it('continues the highest prerelease base for preview releases', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseVersion: '0.2.0-preview.1',
|
|
packageVersion: '0.2.0rc1',
|
|
});
|
|
});
|
|
|
|
it('continues the highest nightly-only prerelease base for preview releases', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0.dev20260429010101': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseVersion: '0.2.0-preview.1',
|
|
packageVersion: '0.2.0rc1',
|
|
});
|
|
});
|
|
|
|
it('keeps bumping preview slots until it finds an unused iteration', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.1.1rc0': [{}],
|
|
'0.1.1rc1': [{}],
|
|
'0.1.1rc2': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
'sdk-python-v0.1.1-preview.1': 'sdk-python-v0.1.1-preview.1',
|
|
'sdk-python-v0.1.1-preview.2': 'sdk-python-v0.1.1-preview.2',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseVersion: '0.1.1-preview.3',
|
|
packageVersion: '0.1.1rc3',
|
|
resumeExistingRelease: false,
|
|
});
|
|
});
|
|
|
|
it('throws on nightly conflicts instead of silently changing the timestamped version', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.1.dev20260430031516': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.1-nightly.20260430031516.abc1234':
|
|
'sdk-python-v0.1.1-nightly.20260430031516.abc1234',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'nightly' })).rejects.toThrow(
|
|
'Nightly version conflict for 0.1.1.dev20260430031516',
|
|
);
|
|
});
|
|
|
|
it('throws when PyPI metadata fetch is not ok', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'Failed to fetch PyPI metadata: 503 Service Unavailable',
|
|
);
|
|
});
|
|
|
|
it('treats a PyPI 404 as a first-release scenario', async () => {
|
|
fetchMock.mockResolvedValue(makeResponse({ status: 404 }));
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0',
|
|
releaseVersion: '0.1.0',
|
|
packageVersion: '0.1.0',
|
|
previousReleaseTag: '',
|
|
publishChannel: 'latest',
|
|
});
|
|
});
|
|
|
|
it('ignores fully yanked PyPI versions', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{ yanked: true }],
|
|
'0.0.9': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0',
|
|
releaseVersion: '0.1.0',
|
|
packageVersion: '0.1.0',
|
|
previousReleaseTag: 'v0.0.9',
|
|
});
|
|
});
|
|
|
|
it('detects yanked versions as conflicts on PyPI', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{ yanked: true }],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
// 0.1.0 is yanked so base-version computation ignores it and derives 0.1.0
|
|
// from pyproject.toml, but conflict detection sees it on PyPI. Since it has
|
|
// no GitHub release, the script resumes the existing release.
|
|
await expect(getVersion({ type: 'stable' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0',
|
|
packageVersion: '0.1.0',
|
|
resumeExistingRelease: true,
|
|
});
|
|
});
|
|
|
|
it('rejects stable_version_override that is older than latest stable', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.5.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(
|
|
getVersion({
|
|
type: 'stable',
|
|
stable_version_override: 'v0.1.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'stable_version_override 0.1.0 is older than latest stable 0.5.0',
|
|
);
|
|
});
|
|
|
|
it('allows stable_version_override equal to latest stable', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.5.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.5.0': 'sdk-python-v0.5.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
// Equal version already exists, so the override conflict check fires
|
|
await expect(
|
|
getVersion({
|
|
type: 'stable',
|
|
stable_version_override: 'v0.5.0',
|
|
}),
|
|
).rejects.toThrow(
|
|
'Requested stable release 0.5.0 already exists on PyPI, GitHub releases.',
|
|
);
|
|
});
|
|
|
|
it('uses pyproject.toml version for first preview release when PyPI has no versions', async () => {
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0-preview.0',
|
|
releaseVersion: '0.1.0-preview.0',
|
|
packageVersion: '0.1.0rc0',
|
|
});
|
|
});
|
|
|
|
it('uses pyproject.toml version for first nightly release when PyPI has no versions', async () => {
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'nightly' })).resolves.toMatchObject({
|
|
releaseTag: 'v0.1.0-nightly.20260430031516.abc1234',
|
|
releaseVersion: '0.1.0-nightly.20260430031516.abc1234',
|
|
packageVersion: '0.1.0.dev20260430031516',
|
|
});
|
|
});
|
|
|
|
it('continues the prerelease base when it equals the stable baseline', async () => {
|
|
readFileSyncMock.mockReturnValue('version = "0.2.0"\n');
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.2.0-preview.0': 'sdk-python-v0.2.0-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseVersion: '0.2.0-preview.1',
|
|
packageVersion: '0.2.0rc1',
|
|
});
|
|
});
|
|
|
|
it('emits a warning when skipping a preview slot due to an orphan git tag', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tags: {
|
|
'sdk-python-v0.1.1-preview.0': 'sdk-python-v0.1.1-preview.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const consoleSpy = vi.spyOn(console, 'error');
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).resolves.toMatchObject({
|
|
releaseVersion: '0.1.1-preview.1',
|
|
packageVersion: '0.1.1rc1',
|
|
});
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('::warning::Orphan git tag'),
|
|
);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('excludes current release from previousReleaseTag on resume', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
makeResponse({
|
|
json: {
|
|
releases: {
|
|
'0.1.0': [{}],
|
|
'0.2.0': [{}],
|
|
'0.2.0rc0': [{}],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.2.0': 'sdk-python-v0.2.0',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
const result = await getVersion({ type: 'stable' });
|
|
expect(result).toMatchObject({
|
|
releaseTag: 'v0.2.0',
|
|
previousReleaseTag: 'v0.1.0',
|
|
resumeExistingRelease: true,
|
|
});
|
|
});
|
|
|
|
it('throws a timeout error when gh release view times out', async () => {
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
releases: {
|
|
'sdk-python-v0.1.0-preview.0': makeTimeoutError(
|
|
'gh release view "sdk-python-v0.1.0-preview.0"',
|
|
),
|
|
},
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'gh release view timed out after 30s checking "sdk-python-v0.1.0-preview.0" — GitHub API may be unavailable',
|
|
);
|
|
});
|
|
|
|
it('throws a timeout error when git tag -l times out', async () => {
|
|
execSyncMock.mockImplementation(
|
|
makeExecSyncMock({
|
|
tagError: makeTimeoutError('git tag -l'),
|
|
}),
|
|
);
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'preview' })).rejects.toThrow(
|
|
'git tag -l timed out after 10s — local git may be unresponsive',
|
|
);
|
|
});
|
|
|
|
it('throws a timeout error when git rev-parse times out', async () => {
|
|
execSyncMock.mockImplementation((command) => {
|
|
if (command === 'git rev-parse --short HEAD') {
|
|
throw makeTimeoutError('git rev-parse --short HEAD');
|
|
}
|
|
return makeExecSyncMock()(command);
|
|
});
|
|
|
|
const getVersion = await loadGetVersion();
|
|
|
|
await expect(getVersion({ type: 'nightly' })).rejects.toThrow(
|
|
'git rev-parse timed out after 10s — local git may be unresponsive',
|
|
);
|
|
});
|
|
});
|