- Support Simplified Chinese, Traditional Chinese, English, and Japanese - Follow the UI language and show a notice when notes are unavailable - Avoid language and content fallbacks - Generate Release bodies and update manifests from per-version Markdown - Add collapsible notes, publication dates, and links to full releases - Test publishing, legacy manifest compatibility, and localized rendering
166 lines
6.0 KiB
TypeScript
166 lines
6.0 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, test } from 'bun:test';
|
|
import {
|
|
generatePortableUpdateManifest,
|
|
portableUpdateManifestName,
|
|
portableUpdateManifestNames,
|
|
} from '../scripts/manifest.mjs';
|
|
|
|
test('macOS uses a new primary channel and keeps the legacy manifest alias', () => {
|
|
expect(portableUpdateManifestName('windows')).toBe('portable-update-windows.json');
|
|
expect(portableUpdateManifestName('linux')).toBe('portable-update-linux.json');
|
|
expect(portableUpdateManifestName('darwin')).toBe('portable-update-darwin-v2.json');
|
|
expect(portableUpdateManifestNames('darwin')).toEqual([
|
|
'portable-update-darwin-v2.json',
|
|
'portable-update-darwin.json',
|
|
]);
|
|
expect(portableUpdateManifestNames('linux')).toEqual(['portable-update-linux.json']);
|
|
});
|
|
|
|
test('macOS manifest CLI publishes both update channels', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'easycli-darwin-manifest-cli-'));
|
|
try {
|
|
for (const arch of ['amd64', 'aarch64']) {
|
|
await writeFile(
|
|
join(root, `EasyCLIProxyAPI-v1.2.3-Darwin-${arch}.dmg`),
|
|
`darwin ${arch} release`,
|
|
);
|
|
}
|
|
const script = fileURLToPath(new URL('../scripts/manifest.mjs', import.meta.url));
|
|
const notes = { 'zh-CN': '## 新增\n\n- 软件更新说明。\n', en: '## Added\n\n- Release notes.\n' };
|
|
const notesPath = join(root, 'release-notes.json');
|
|
await writeFile(notesPath, JSON.stringify(notes), 'utf8');
|
|
const result = spawnSync('node', [
|
|
script,
|
|
'--directory',
|
|
root,
|
|
'--platform',
|
|
'darwin',
|
|
'--repository',
|
|
'router-for-me/EasyCLIProxyAPI',
|
|
'--tag',
|
|
'v1.2.3',
|
|
'--release-notes',
|
|
notesPath,
|
|
], { encoding: 'utf8' });
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe('');
|
|
const primary = JSON.parse(
|
|
await readFile(join(root, 'portable-update-darwin-v2.json'), 'utf8'),
|
|
);
|
|
const legacy = JSON.parse(
|
|
await readFile(join(root, 'portable-update-darwin.json'), 'utf8'),
|
|
);
|
|
expect(legacy).toEqual(primary);
|
|
expect(primary.releaseNotes).toEqual(notes);
|
|
expect(primary.releaseNotes.ja).toBeUndefined();
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('Windows 便携更新清单', () => {
|
|
test('URL、大小和哈希与实际上传资产一致', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'easycli-manifest-test-'));
|
|
try {
|
|
const payloads = {
|
|
amd64: Buffer.from('amd64 full portable package'),
|
|
aarch64: Buffer.from('aarch64 full portable package'),
|
|
};
|
|
for (const [arch, contents] of Object.entries(payloads)) {
|
|
await writeFile(
|
|
join(root, `EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip`),
|
|
contents,
|
|
);
|
|
}
|
|
|
|
const output = join(root, 'portable-update-windows.json');
|
|
const manifest = await generatePortableUpdateManifest({
|
|
directory: root,
|
|
output,
|
|
repository: 'router-for-me/EasyCLIProxyAPI',
|
|
gitcodeRepository: 'mirror-owner/EasyCLIProxyAPI',
|
|
tag: 'v1.2.3',
|
|
publishedAt: '2026-07-24T00:00:00.000Z',
|
|
});
|
|
const saved = JSON.parse(await readFile(output, 'utf8'));
|
|
|
|
expect(saved).toEqual(manifest);
|
|
for (const arch of ['amd64', 'aarch64'] as const) {
|
|
const asset = manifest.assets[`windows-${arch}`];
|
|
expect(asset.url).toBe(
|
|
`https://github.com/router-for-me/EasyCLIProxyAPI/releases/download/v1.2.3/EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip`,
|
|
);
|
|
expect(asset.fallbackUrls).toEqual([
|
|
`https://api.gitcode.com/api/v5/repos/mirror-owner/EasyCLIProxyAPI/releases/v1.2.3/attach_files/EasyCLIProxyAPI-v1.2.3-Windows-${arch}.zip/download`,
|
|
]);
|
|
expect(asset.sizeBytes).toBe(payloads[arch].byteLength);
|
|
expect(asset.sha256).toBe(createHash('sha256').update(payloads[arch]).digest('hex'));
|
|
}
|
|
expect(manifest.fullAssets).toBeUndefined();
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('缺少任一架构资产时拒绝生成清单', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'easycli-manifest-missing-'));
|
|
try {
|
|
await writeFile(
|
|
join(root, 'EasyCLIProxyAPI-v1.2.3-Windows-amd64.zip'),
|
|
'amd64',
|
|
);
|
|
await expect(generatePortableUpdateManifest({
|
|
directory: root,
|
|
output: join(root, 'portable-update-windows.json'),
|
|
repository: 'router-for-me/EasyCLIProxyAPI',
|
|
tag: 'v1.2.3',
|
|
})).rejects.toThrow();
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
describe('跨平台便携更新清单', () => {
|
|
test.each([
|
|
['linux', 'Linux', 'tar.gz'],
|
|
['darwin', 'Darwin', 'dmg'],
|
|
] as const)('%s 清单直接引用完整发布包', async (platform, display, suffix) => {
|
|
const root = await mkdtemp(join(tmpdir(), `easycli-${platform}-manifest-test-`));
|
|
try {
|
|
for (const arch of ['amd64', 'aarch64']) {
|
|
await writeFile(
|
|
join(root, `EasyCLIProxyAPI-v1.2.3-${display}-${arch}.${suffix}`),
|
|
`${platform} ${arch} full package`,
|
|
);
|
|
}
|
|
const output = join(root, portableUpdateManifestName(platform));
|
|
const manifest = await generatePortableUpdateManifest({
|
|
directory: root,
|
|
platform,
|
|
repository: 'router-for-me/EasyCLIProxyAPI',
|
|
tag: 'v1.2.3',
|
|
publishedAt: '2026-08-10T00:00:00.000Z',
|
|
});
|
|
|
|
expect(manifest.fullAssets).toBeUndefined();
|
|
for (const arch of ['amd64', 'aarch64']) {
|
|
expect(manifest.assets[`${platform}-${arch}`].url).toEndWith(
|
|
`/EasyCLIProxyAPI-v1.2.3-${display}-${arch}.${suffix}`,
|
|
);
|
|
}
|
|
expect(JSON.parse(await readFile(output, 'utf8'))).toEqual(manifest);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|