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

+347
View File
@@ -0,0 +1,347 @@
import { Hono } from 'hono';
import { describe, expect, it } from 'vitest';
import { StorageAccountOnlineError } from '../src/webui/storage-management';
import {
ALL_ACCOUNTS_CONFIRMATION,
registerStorageRoutes,
type StorageRouteDependencies,
} from '../src/webui/storage-routes';
import { StorageSettingsTransactionError } from '../src/webui/storage-settings';
const settingsState = {
saved: { logMaxTotalMb: 1024, logRetainDays: 7, logPerUin: false },
effective: { logMaxTotalMb: 1024, logRetainDays: 7, logPerUin: false },
envOverrides: [],
};
const snapshot = {
logs: {
state: 'healthy' as const,
totalBytes: 100,
maxTotalBytes: 1024 * 1024 * 1024,
retainDays: 7,
perUinEnabled: false,
fileCount: 1,
activeFileCount: 1,
droppedLines: 0,
},
temporary: { totalBytes: 20, fileCount: 2, activeItemCount: 0 },
accounts: [],
totals: {
logsBytes: 100,
temporaryBytes: 20,
accountDataBytes: 0,
managedBytes: 120,
},
};
function createDependencies(
overrides: Partial<StorageRouteDependencies> = {},
): StorageRouteDependencies {
return {
storage: {
snapshot: () => snapshot,
clearLogs: async () => ({
deletedFiles: 1,
freedBytes: 90,
failures: [],
status: { ...snapshot.logs, totalBytes: 10 },
}),
clearTemporary: () => ({
deletedFiles: 2,
freedBytes: 20,
skippedActiveItems: 0,
failures: [],
}),
clearAccountData: (category, uin) => ({
category: category as 'messages' | 'media' | 'reactions',
uins: [uin],
deletedFiles: 1,
freedBytes: 10,
failures: [],
}),
clearAllAccountData: (category) => ({
category: category as 'messages' | 'media' | 'reactions',
uins: ['12345', '67890'],
deletedFiles: 2,
freedBytes: 20,
failures: [],
}),
},
settings: {
read: () => settingsState,
update: async () => ({
settings: settingsState,
status: snapshot.logs,
}),
},
audit: () => undefined,
reportError: () => undefined,
now: () => new Date('2026-07-24T04:00:00.000Z'),
...overrides,
};
}
function createApp(deps: StorageRouteDependencies): Hono {
const app = new Hono();
registerStorageRoutes(app, deps);
return app;
}
describe('storage management routes', () => {
it('returns settings and a path-free storage snapshot', async () => {
const response = await createApp(createDependencies()).request('/api/system/storage');
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toEqual({ settings: settingsState, snapshot, lastCleanup: null });
expect(JSON.stringify(body)).not.toContain('/private/');
});
it('updates log settings and returns the refreshed snapshot', async () => {
let received: unknown;
const deps = createDependencies({
settings: {
read: () => settingsState,
update: async (body) => {
received = body;
return { settings: settingsState, status: snapshot.logs };
},
},
});
const response = await createApp(deps).request('/api/system/storage/settings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ logRetainDays: 0 }),
});
expect(response.status).toBe(200);
expect(received).toEqual({ logRetainDays: 0 });
await expect(response.json()).resolves.toMatchObject({
success: true,
settings: settingsState,
snapshot,
});
});
it('reports both the failed settings operation and every failed rollback', async () => {
const operationError = new Error('apply failed');
const rollbackError = new Error('rollback failed');
const reports: Array<{ operation: string; error: unknown }> = [];
const deps = createDependencies({
settings: {
read: () => settingsState,
update: async () => {
throw new StorageSettingsTransactionError(
'transaction failed',
operationError,
[rollbackError],
);
},
},
reportError: (operation, error) => reports.push({ operation, error }),
});
const response = await createApp(deps).request('/api/system/storage/settings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ logRetainDays: 0 }),
});
expect(response.status).toBe(500);
expect(reports).toEqual([
{
operation: 'update log storage settings',
error: expect.any(StorageSettingsTransactionError),
},
{ operation: 'update log storage settings operation', error: operationError },
{ operation: 'rollback log storage settings step=1', error: rollbackError },
]);
});
it('serializes settings changes and cleanup operations', async () => {
let releaseSettings!: () => void;
let notifySettingsStarted!: () => void;
const settingsGate = new Promise<void>((resolve) => {
releaseSettings = resolve;
});
const settingsStarted = new Promise<void>((resolve) => {
notifySettingsStarted = resolve;
});
let cleanupCalls = 0;
const deps = createDependencies({
settings: {
read: () => settingsState,
update: async () => {
notifySettingsStarted();
await settingsGate;
return { settings: settingsState, status: snapshot.logs };
},
},
});
deps.storage.clearTemporary = () => {
cleanupCalls += 1;
return {
deletedFiles: 0,
freedBytes: 0,
skippedActiveItems: 0,
failures: [],
};
};
const app = createApp(deps);
const settingsRequest = app.request('/api/system/storage/settings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ logRetainDays: 0 }),
});
await settingsStarted;
const cleanupRequest = app.request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'temporary' }),
});
await Promise.resolve();
expect(cleanupCalls).toBe(0);
releaseSettings();
await expect(settingsRequest).resolves.toMatchObject({ status: 200 });
await expect(cleanupRequest).resolves.toMatchObject({ status: 200 });
expect(cleanupCalls).toBe(1);
});
it('rejects arbitrary paths, unknown scopes, and missing cleanup fields', async () => {
const app = createApp(createDependencies());
for (const body of [
{ scope: 'logs', path: '/etc' },
{ scope: 'everything' },
{ scope: 'account', category: 'messages', uin: '../12345' },
{ scope: 'account', category: '../config', uin: '12345' },
{ scope: 'account', category: 'messages' },
]) {
const response = await app.request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
expect(response.status, JSON.stringify(body)).toBe(400);
}
});
it('requires the fixed second confirmation for all-account cleanup', async () => {
const app = createApp(createDependencies());
const rejected = await app.request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
scope: 'allAccounts',
category: 'messages',
confirmation: 'yes',
}),
});
expect(rejected.status).toBe(400);
const accepted = await app.request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
scope: 'allAccounts',
category: 'messages',
confirmation: ALL_ACCOUNTS_CONFIRMATION,
}),
});
expect(accepted.status).toBe(200);
await expect(accepted.json()).resolves.toMatchObject({
success: true,
scope: 'allAccounts',
cleanup: { uins: ['12345', '67890'], deletedFiles: 2 },
});
});
it('maps an online-account preflight failure to conflict', async () => {
const deps = createDependencies();
deps.storage.clearAccountData = () => {
throw new StorageAccountOnlineError(['12345']);
};
const response = await createApp(deps).request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'account', category: 'messages', uin: '12345' }),
});
expect(response.status).toBe(409);
await expect(response.json()).resolves.toMatchObject({
success: false,
onlineUins: ['12345'],
});
});
it('returns HTTP 500 with the structured partial result when deletion fails', async () => {
const audits: unknown[] = [];
const reports: Array<{ operation: string; error: unknown }> = [];
const deps = createDependencies({
audit: (event) => audits.push(event),
reportError: (operation, error) => reports.push({ operation, error }),
});
deps.storage.clearTemporary = () => ({
deletedFiles: 1,
freedBytes: 5,
skippedActiveItems: 0,
failures: [{ item: 'locked.bin', message: 'permission denied' }],
});
const app = createApp(deps);
const response = await app.request('/api/system/storage/cleanup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'temporary' }),
});
expect(response.status).toBe(500);
await expect(response.json()).resolves.toMatchObject({
success: false,
cleanup: {
deletedFiles: 1,
failures: [{ item: 'locked.bin', message: 'permission denied' }],
},
lastCleanup: {
failureCount: 1,
failures: [{ item: 'locked.bin', message: 'permission denied' }],
},
snapshot,
});
expect(audits).toEqual([
{
scope: 'temporary',
accountScope: 'global',
deletedFiles: 1,
freedBytes: 5,
failureCount: 1,
},
]);
expect(reports).toEqual([
{
operation: 'clean storage scope=temporary item=locked.bin',
error: expect.objectContaining({ message: 'permission denied' }),
},
]);
const refreshed = await app.request('/api/system/storage');
await expect(refreshed.json()).resolves.toMatchObject({
lastCleanup: {
at: '2026-07-24T04:00:00.000Z',
scope: 'temporary',
accountScope: 'global',
deletedFiles: 1,
freedBytes: 5,
failureCount: 1,
skippedActiveItems: 0,
failures: [{ item: 'locked.bin', message: 'permission denied' }],
},
});
});
});