SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,138 @@
|
||||
import { updateDirectory } from '../src/update/files';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createUpdateService } from '../src/update/service';
|
||||
import { updateRoutes } from '../src/webui/update-routes';
|
||||
import { releaseFixture } from './helpers/update-fixture';
|
||||
|
||||
let root: string;
|
||||
let staging: string;
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(os.tmpdir(), 'snowluma-update-service-'));
|
||||
await releaseFixture(root, '1.0.0');
|
||||
staging = await updateDirectory(root);
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await rm(staging, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('update service', () => {
|
||||
it('stages a single download, survives a service reload and leaves running files alone', async () => {
|
||||
let finish!: () => void;
|
||||
const wait = new Promise<void>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
const download = vi.fn(async (directory: string, version: string, target: string) => {
|
||||
await wait;
|
||||
await releaseFixture(path.join(directory, 'payload'), version, target);
|
||||
await writeFile(path.join(directory, 'archive'), 'archive');
|
||||
});
|
||||
const service = await createUpdateService(root, '1.0.0', { download, isContainer: false });
|
||||
service.start('2.0.0');
|
||||
expect(service.status()).toEqual({
|
||||
current: '1.0.0',
|
||||
phase: 'downloading',
|
||||
version: '2.0.0',
|
||||
});
|
||||
expect(() => service.start('2.0.0')).toThrow('正在进行');
|
||||
finish();
|
||||
await vi.waitFor(() => expect(service.status().phase).toBe('ready'));
|
||||
expect(download).toHaveBeenCalledOnce();
|
||||
expect(await readdir(path.join(staging, 'pending'))).toEqual(['payload']);
|
||||
const reloaded = await createUpdateService(root, '1.0.0', { download, isContainer: false });
|
||||
expect(reloaded.status()).toEqual({ current: '1.0.0', phase: 'ready', version: '2.0.0' });
|
||||
reloaded.restart();
|
||||
expect(reloaded.status().phase).toBe('restarting');
|
||||
expect(() => reloaded.restart()).toThrow('尚未就绪');
|
||||
});
|
||||
|
||||
it('cleans a failed download and allows retry', async () => {
|
||||
const download = vi.fn(async (directory: string) => {
|
||||
await writeFile(path.join(directory, 'archive'), 'partial');
|
||||
throw new Error('connection reset');
|
||||
});
|
||||
const service = await createUpdateService(root, '1.0.0', { download, isContainer: false });
|
||||
service.start('2.0.0');
|
||||
await vi.waitFor(() =>
|
||||
expect(service.status()).toEqual({
|
||||
current: '1.0.0',
|
||||
phase: 'error',
|
||||
message: 'connection reset',
|
||||
}),
|
||||
);
|
||||
expect(await readdir(staging)).toEqual([]);
|
||||
expect(() => service.start('2.0.0')).not.toThrow();
|
||||
await vi.waitFor(() => expect(download).toHaveBeenCalledTimes(2));
|
||||
await vi.waitFor(() => expect(service.status().phase).toBe('error'));
|
||||
});
|
||||
|
||||
it.each([
|
||||
'1.0.0',
|
||||
'0.9.0',
|
||||
'2.0.0-rc.1',
|
||||
'02.0.0',
|
||||
'2.0.0\n',
|
||||
'1.0.0+build.2',
|
||||
'../../outside',
|
||||
null,
|
||||
])('rejects invalid or older version %s', async (version) => {
|
||||
const download = vi.fn();
|
||||
const service = await createUpdateService(root, '1.0.0', {
|
||||
download,
|
||||
isContainer: false,
|
||||
});
|
||||
expect(() => service.start(version)).toThrow('正式版本');
|
||||
expect(download).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('downloads stable versions with build metadata without dropping the tag suffix', async () => {
|
||||
const download = vi.fn(async (directory: string, version: string, target: string) => {
|
||||
await releaseFixture(path.join(directory, 'payload'), version, target);
|
||||
await writeFile(path.join(directory, 'archive'), 'archive');
|
||||
});
|
||||
const service = await createUpdateService(root, '1.0.0', { download, isContainer: false });
|
||||
service.start('2.0.0+build.01');
|
||||
await vi.waitFor(() => expect(service.status().phase).toBe('ready'));
|
||||
expect(download).toHaveBeenCalledWith(expect.any(String), '2.0.0+build.01', 'win32-x64');
|
||||
expect(service.status()).toMatchObject({ version: '2.0.0+build.01' });
|
||||
});
|
||||
|
||||
it('does not enable updates for a source checkout', async () => {
|
||||
const service = await createUpdateService(undefined, '1.0.0');
|
||||
expect(service.status().phase).toBe('unavailable');
|
||||
expect(() => service.start('2.0.0')).toThrow('发行包');
|
||||
expect(() => service.restart()).toThrow('尚未就绪');
|
||||
});
|
||||
|
||||
it('does not update container image layers', async () => {
|
||||
const service = await createUpdateService(root, '1.0.0', { isContainer: true });
|
||||
expect(service.status()).toMatchObject({ phase: 'unavailable', message: '请更新容器镜像' });
|
||||
});
|
||||
|
||||
it('restarts only after the HTTP response has finished', async () => {
|
||||
await releaseFixture(path.join(staging, 'pending/payload'), '2.0.0');
|
||||
const service = await createUpdateService(root, '1.0.0', { isContainer: false });
|
||||
const restart = vi.fn();
|
||||
const outgoing = new EventEmitter();
|
||||
const app = updateRoutes(service, restart);
|
||||
const response = await app.request('/restart', { method: 'POST' }, { outgoing });
|
||||
expect(response.status).toBe(202);
|
||||
expect(await response.json()).toMatchObject({ phase: 'restarting' });
|
||||
expect(restart).not.toHaveBeenCalled();
|
||||
outgoing.emit('finish');
|
||||
expect(restart).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each(['{}', 'not json'])('rejects an invalid download request: %s', async (body) => {
|
||||
const service = await createUpdateService(root, '1.0.0', { isContainer: false });
|
||||
const app = updateRoutes(service, vi.fn());
|
||||
const response = await app.request('/download', { method: 'POST', body });
|
||||
expect(response.status).toBe(400);
|
||||
expect(service.status().phase).toBe('idle');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user