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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
import * as fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { applyPendingUpdate, exists, readManifest, updateDirectory } from '../src/update/files';
import { releaseFixture } from './helpers/update-fixture';
vi.mock('node:fs/promises', async (original) => {
const actual = await original<typeof import('node:fs/promises')>();
return { ...actual, rename: vi.fn(actual.rename) };
});
let root: string;
let staging: string;
beforeEach(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'snowluma-update-test-'));
await releaseFixture(root, '1.0.0', 'win32-x64', 'old.mjs');
staging = await updateDirectory(root);
await releaseFixture(path.join(staging, 'pending/payload'), '2.0.0', 'win32-x64', 'new.mjs');
});
afterEach(async () => {
vi.mocked(fs.rename).mockReset();
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
vi.mocked(fs.rename).mockImplementation(actual.rename);
await fs.rm(root, { recursive: true, force: true });
await fs.rm(staging, { recursive: true, force: true });
});
describe('staged updates', () => {
it.each(['02.0.0', '2.0.0-rc.01', '2.0.0-rc_1', '2.0.0+build..1'])(
'rejects invalid manifest versions before replacing files: %s',
async (version) => {
const payload = path.join(staging, 'pending/payload');
const manifest = await readManifest(payload);
await fs.writeFile(
path.join(payload, 'release.json'),
JSON.stringify({ ...manifest, version }),
);
await expect(applyPendingUpdate(root)).rejects.toThrow('清单无效');
expect((await readManifest(root)).version).toBe('1.0.0');
},
);
it('accepts prerelease identifiers and build metadata in manifests', async () => {
const payload = path.join(staging, 'pending/payload');
const manifest = await readManifest(payload);
const version = '2.0.0-rc.10+build.01';
await fs.writeFile(
path.join(payload, 'release.json'),
JSON.stringify({ ...manifest, version }),
);
expect((await readManifest(payload)).version).toBe(version);
});
it('stages outside the installation and only renames replacements on the destination filesystem', async () => {
expect(path.dirname(staging)).toBe(os.tmpdir());
expect(staging.startsWith(root + path.sep)).toBe(false);
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
vi.mocked(fs.rename).mockImplementation(async (source, destination) => {
if (
path.dirname(String(destination)) === root &&
path.dirname(String(source)) !== root
) {
throw Object.assign(new Error('cross-device rename'), { code: 'EXDEV' });
}
return actual.rename(source, destination);
});
expect(await applyPendingUpdate(root)).toBe('2.0.0');
expect(await exists(path.join(root, '.update-install'))).toBe(false);
expect(await exists(staging)).toBe(false);
});
it('rejects an entry point replaced by a directory before touching the installed entry point', async () => {
const entry = path.join(staging, 'pending/payload/index.mjs');
await fs.rm(entry);
await fs.mkdir(entry);
await expect(applyPendingUpdate(root)).rejects.toThrow('文件类型无效');
expect(await fs.readFile(path.join(root, 'index.mjs'), 'utf8')).toBe('1.0.0');
});
it('cleans a download interrupted before staging', async () => {
await fs.mkdir(path.join(staging, 'downloading'));
await fs.writeFile(path.join(staging, 'downloading/archive'), 'partial');
await fs.rm(path.join(staging, 'pending'), { recursive: true });
expect(await applyPendingUpdate(root)).toBeNull();
expect(await exists(staging)).toBe(false);
});
it('replaces program files, removes obsolete chunks and preserves user data and Node', async () => {
for (const file of [
'config/runtime.json',
'data/messages.db',
'logs/current.log',
'tmp/unrelated',
'node.exe',
'custom.mjs',
]) {
await fs.mkdir(path.dirname(path.join(root, file)), { recursive: true });
await fs.writeFile(path.join(root, file), 'keep');
}
expect(await applyPendingUpdate(root)).toBe('2.0.0');
expect((await readManifest(root)).version).toBe('2.0.0');
expect(await fs.readFile(path.join(root, 'native/binding.node'), 'utf8')).toBe('2.0.0');
expect(await exists(path.join(root, 'old.mjs'))).toBe(false);
expect(await exists(path.join(root, 'new.mjs'))).toBe(true);
for (const file of [
'config/runtime.json',
'data/messages.db',
'logs/current.log',
'tmp/unrelated',
'node.exe',
'custom.mjs',
]) {
expect(await fs.readFile(path.join(root, file), 'utf8')).toBe('keep');
}
expect(await fs.readdir(path.join(root, 'tmp'))).toEqual(['unrelated']);
expect(await applyPendingUpdate(root)).toBeNull();
});
it('only installs files declared by the release manifest', async () => {
await fs.writeFile(path.join(staging, 'pending/payload/unlisted.mjs'), 'ignored');
expect(await applyPendingUpdate(root)).toBe('2.0.0');
expect(await exists(path.join(root, 'unlisted.mjs'))).toBe(false);
});
it('restores the entire old release after a replacement fails, then retries on next startup', async () => {
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
let failed = false;
vi.mocked(fs.rename).mockImplementation(async (source, destination) => {
if (!failed && destination === path.join(root, 'client')) {
failed = true;
throw new Error('file locked');
}
return actual.rename(source, destination);
});
await expect(applyPendingUpdate(root)).rejects.toThrow('file locked');
expect((await readManifest(root)).version).toBe('1.0.0');
expect(await fs.readFile(path.join(root, 'app.mjs'), 'utf8')).toBe('1.0.0');
expect(await fs.readFile(path.join(root, 'client/index.html'), 'utf8')).toBe('1.0.0');
expect(await exists(path.join(root, 'new.mjs'))).toBe(false);
expect(await exists(path.join(staging, 'pending/payload'))).toBe(true);
await expect(applyPendingUpdate(root)).resolves.toBe('2.0.0');
});
it('recovers an interrupted transaction before retrying', async () => {
const current = await readManifest(root);
const next = await readManifest(path.join(staging, 'pending/payload'));
const backup = path.join(staging, 'pending/backup');
await fs.mkdir(backup);
for (const entry of current.entries) {
await fs.cp(path.join(root, entry), path.join(backup, entry), { recursive: true });
}
await fs.writeFile(
path.join(staging, 'pending/transaction.json'),
JSON.stringify({
entries: [...new Set([...current.entries, ...next.entries])],
original: current.entries,
}),
);
await fs.writeFile(path.join(root, 'app.mjs'), 'interrupted');
await fs.rm(path.join(root, 'native'), { recursive: true });
await fs.writeFile(path.join(root, 'new.mjs'), 'partial');
// Fail before installation so we can inspect the recovered release.
const entry = path.join(staging, 'pending/payload/index.mjs');
await fs.rm(entry);
await fs.mkdir(entry);
await expect(applyPendingUpdate(root)).rejects.toThrow('文件类型无效');
expect(await fs.readFile(path.join(root, 'index.mjs'), 'utf8')).toBe('1.0.0');
expect(await fs.readFile(path.join(root, 'app.mjs'), 'utf8')).toBe('1.0.0');
expect(await fs.readFile(path.join(root, 'native/binding.node'), 'utf8')).toBe('1.0.0');
expect(await exists(path.join(root, 'new.mjs'))).toBe(false);
await fs.rm(entry, { recursive: true });
await fs.writeFile(entry, '2.0.0');
expect(await applyPendingUpdate(root)).toBe('2.0.0');
});
it('cleans an already committed transaction without restoring its backup', async () => {
await fs.rename(path.join(staging, 'pending'), path.join(staging, 'completed'));
expect(await applyPendingUpdate(root)).toBeNull();
expect(await exists(staging)).toBe(false);
});
it.each(['config', '../outside', 'node.exe', 'data', 'C:/outside'])(
'rejects a manifest owning %s',
async (entry) => {
const payload = path.join(staging, 'pending/payload');
const manifest = await readManifest(payload);
manifest.entries.push(entry);
await fs.writeFile(path.join(payload, 'release.json'), JSON.stringify(manifest));
await expect(applyPendingUpdate(root)).rejects.toThrow('清单无效');
expect((await readManifest(root)).version).toBe('1.0.0');
},
);
it('rejects a release for a different platform before replacing anything', async () => {
const payload = path.join(staging, 'pending/payload');
await fs.rm(payload, { recursive: true });
await releaseFixture(payload, '2.0.0', 'linux-x64');
await expect(applyPendingUpdate(root)).rejects.toThrow('平台不匹配');
expect((await readManifest(root)).version).toBe('1.0.0');
});
});