Files
Firelink/src/store/useDownloadStore.test.ts
T
2026-08-01 22:27:32 +03:30

2853 lines
108 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { dispatchItem, getProxyArgs, getSiteLogin, hasStaleTemporaryMediaEstimate, normalizeCustomProxy, normalizePersistedDownloadProgress, normalizePersistedQueueState, normalizePersistedQueues, useDownloadStore } from './useDownloadStore';
import { useDownloadProgressStore } from './downloadProgressStore';
import { useSettingsStore } from './useSettingsStore';
import * as ipc from '../ipc';
import { MAX_DOWNLOAD_FILENAME_BYTES } from '../utils/downloads';
vi.mock('../ipc', () => ({
invokeCommand: vi.fn(),
}));
// Mock window.__TAURI_INTERNALS__ and log to prevent errors
vi.mock('../utils/logger', () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}));
vi.mock('@tauri-apps/api/path', () => ({
homeDir: vi.fn().mockResolvedValue('/Users/test'),
join: vi.fn(async (...parts: string[]) =>
parts
.map((part, index) => index === 0 ? part.replace(/[\\/]+$/, '') : part.replace(/^[\\/]+|[\\/]+$/g, ''))
.join('/')
),
}));
vi.mock('./useSettingsStore', () => ({
useSettingsStore: {
getState: vi.fn(() => ({
proxyMode: 'none',
siteLogins: [],
globalSpeedLimit: '',
speedLimitPresetValues: [1, 5, 10],
logsEnabled: false,
perServerConnections: 16,
customUserAgent: '',
maxAutomaticRetries: 3,
mediaCookieSource: 'none',
baseDownloadFolder: '~/Downloads',
categorySubfoldersEnabled: true,
categorySubfolders: {
Musics: 'Musics',
Movies: 'Movies',
Compressed: 'Compressed',
Documents: 'Documents',
Pictures: 'Pictures',
Applications: 'Applications',
Other: 'Other',
},
categoryDirectoryOverrides: {},
keychainAccessReady: false,
keychainPromptDismissed: false,
setShowKeychainModal: vi.fn(),
})),
}
}));
describe('useDownloadStore', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useSettingsStore.getState).mockReturnValue({
proxyMode: 'none',
siteLogins: [],
globalSpeedLimit: '',
speedLimitPresetValues: [1, 5, 10],
logsEnabled: false,
perServerConnections: 16,
customUserAgent: '',
maxAutomaticRetries: 3,
mediaCookieSource: 'none',
baseDownloadFolder: '~/Downloads',
categorySubfoldersEnabled: true,
categorySubfolders: {
Musics: 'Musics',
Movies: 'Movies',
Compressed: 'Compressed',
Documents: 'Documents',
Pictures: 'Pictures',
Applications: 'Applications',
Other: 'Other',
},
categoryDirectoryOverrides: {},
keychainAccessReady: false,
keychainPromptDismissed: false,
setShowKeychainModal: vi.fn(),
} as unknown as ReturnType<typeof useSettingsStore.getState>);
useDownloadStore.setState({
downloads: [],
backendRegisteredIds: new Set(),
pendingOrder: [],
isAddModalOpen: false,
pendingAddUrls: '',
pendingAddReferer: '',
pendingAddFilename: '',
pendingAddHeaders: '',
pendingAddCookies: '',
pendingAddMediaUrls: [],
pendingAddBatch: false,
pendingAddBatchName: '',
pendingAddRequestContexts: {},
pendingAddRequestVersion: 0,
});
useDownloadProgressStore.setState({ progressMap: {} });
});
it('invalidates in-flight Add-modal handoffs when the modal is toggled', () => {
const initialVersion = useDownloadStore.getState().pendingAddRequestVersion;
useDownloadStore.getState().toggleAddModal(true);
expect(useDownloadStore.getState().pendingAddRequestVersion).toBe(initialVersion + 1);
useDownloadStore.getState().toggleAddModal(false);
expect(useDownloadStore.getState().pendingAddRequestVersion).toBe(initialVersion + 2);
});
it('normalizes an overlong filename edited in Properties before persisting it', async () => {
useDownloadStore.setState({
downloads: [{
id: 'properties-long-name',
url: 'https://example.com/video',
fileName: 'video.mp4',
status: 'ready',
category: 'Movies',
dateAdded: ''
}] as any[]
});
await useDownloadStore.getState().applyProperties('properties-long-name', {
fileName: `${'title '.repeat(100)}.mp4`
});
const fileName = useDownloadStore.getState().downloads[0].fileName;
expect(new TextEncoder().encode(fileName).length).toBeLessThanOrEqual(MAX_DOWNLOAD_FILENAME_BYTES);
expect(fileName.endsWith('.mp4')).toBe(true);
});
it('replaces stale media intent when an appended handoff reuses a URL', () => {
useDownloadStore.getState().openAddModalWithUrls(
'https://example.com/file.bin', '', '', '', '', true
);
useDownloadStore.getState().openAddModalWithUrls(
'https://example.com/file.bin', '', '', '', '', false
);
const state = useDownloadStore.getState();
expect(state.pendingAddMediaUrls).toEqual([]);
expect(state.pendingAddRequestContexts['https://example.com/file.bin']?.media).toBe(false);
});
it('replaces a paused download URL in place and preserves its progress', async () => {
useDownloadStore.setState({
downloads: [{
id: 'replace-in-place',
url: 'https://expired.example/file.bin',
fileName: 'file.bin',
status: 'paused',
category: 'Other',
dateAdded: '2026-07-15T00:00:00.000Z',
downloadedBytes: 1024,
totalBytes: 4096,
fraction: 0.25
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
const replaced = await useDownloadStore.getState().replaceDownload(
'replace-in-place',
{ url: 'https://fresh.example/file.bin', lastError: undefined },
{ type: 'add-to-queue', queueId: 'main' }
);
expect(replaced).toBe(true);
expect(useDownloadStore.getState().downloads).toEqual([expect.objectContaining({
id: 'replace-in-place',
url: 'https://fresh.example/file.bin',
status: 'paused',
downloadedBytes: 1024,
totalBytes: 4096,
fraction: 0.25
})]);
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('remove_download', expect.anything());
});
it('resumes a replaced paused download without creating a second row', async () => {
useDownloadStore.setState({
downloads: [{
id: 'replace-and-resume',
url: 'https://expired.example/file.bin',
fileName: 'file.bin',
status: 'paused',
category: 'Other',
dateAdded: '',
destination: '/tmp',
downloadedBytes: 2048,
totalBytes: 4096
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'resume_download') return false;
if (command === 'enqueue_download') {
return { id: 'replace-and-resume', filename: 'file.bin' };
}
if (command === 'get_pending_order') return [];
return undefined;
});
const replaced = await useDownloadStore.getState().replaceDownload(
'replace-and-resume',
{ url: 'https://fresh.example/file.bin' },
{ type: 'start-now' }
);
expect(replaced).toBe(true);
expect(useDownloadStore.getState().downloads).toHaveLength(1);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
id: 'replace-and-resume',
url: 'https://fresh.example/file.bin',
downloadedBytes: 2048,
totalBytes: 4096,
hasBeenDispatched: true
});
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('remove_download', expect.anything());
});
it('serializes a replacement and a concurrent pause as one lifecycle operation', async () => {
useDownloadStore.setState({
downloads: [{
id: 'replace-pause-race',
url: 'https://expired.example/file.bin',
fileName: 'file.bin',
status: 'paused',
category: 'Other',
dateAdded: ''
}] as any[]
});
let releaseResume!: () => void;
let signalResumeStarted!: () => void;
const resumeStarted = new Promise<void>(resolve => {
signalResumeStarted = resolve;
});
const resumeGate = new Promise<void>(resolve => {
releaseResume = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'resume_download') {
signalResumeStarted();
await resumeGate;
return true;
}
return undefined;
});
const replacing = useDownloadStore.getState().replaceDownload(
'replace-pause-race',
{ url: 'https://fresh.example/file.bin' },
{ type: 'start-now' }
);
await resumeStarted;
const pausing = useDownloadStore.getState().pauseDownload('replace-pause-race');
await Promise.resolve();
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('pause_download', { id: 'replace-pause-race' });
releaseResume();
await replacing;
await pausing;
expect(ipc.invokeCommand).toHaveBeenCalledWith('pause_download', { id: 'replace-pause-race' });
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
url: 'https://fresh.example/file.bin',
status: 'paused'
});
});
it('rejects empty and duplicate queue names', () => {
useDownloadStore.setState({
queues: [
{ id: 'main', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Downloads', isMain: false }
]
});
expect(useDownloadStore.getState().addQueue('')).toBe(false);
expect(useDownloadStore.getState().addQueue(' downloads ')).toBe(false);
expect(useDownloadStore.getState().addQueue('Archive')).toBe(true);
expect(useDownloadStore.getState().renameQueue('queue-a', ' archive ')).toBe(false);
expect(useDownloadStore.getState().renameQueue('queue-a', '')).toBe(false);
});
it('persists a queue concurrency override only after backend synchronization', async () => {
useDownloadStore.setState({
queues: [
{ id: 'main', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Downloads', isMain: false }
]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().setQueueConcurrency('queue-a', 2);
expect(useDownloadStore.getState().queues).toEqual([
{ id: 'main', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Downloads', isMain: false, maxConcurrent: 2 }
]);
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith(
'set_queue_concurrency_limits',
{
limits: [
{ id: 'main', maxConcurrent: null },
{ id: 'queue-a', maxConcurrent: 2 }
]
}
);
});
it('retains the previous queue concurrency after backend synchronization fails', async () => {
useDownloadStore.setState({
queues: [
{ id: 'main', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Downloads', isMain: false, maxConcurrent: 2 }
]
});
vi.mocked(ipc.invokeCommand).mockRejectedValue(new Error('backend unavailable'));
await expect(useDownloadStore.getState().setQueueConcurrency('queue-a', 3))
.rejects.toThrow('backend unavailable');
expect(useDownloadStore.getState().queues[1].maxConcurrent).toBe(2);
});
it('rebases queue concurrency updates when queue state changes during IPC', async () => {
useDownloadStore.setState({
queues: [
{ id: 'main', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Downloads', isMain: false }
]
});
let releaseFirstSync!: () => void;
const firstSyncReleased = new Promise<void>(resolve => { releaseFirstSync = resolve; });
let syncCalls = 0;
vi.mocked(ipc.invokeCommand).mockImplementation(async command => {
if (command === 'set_queue_concurrency_limits') {
syncCalls += 1;
if (syncCalls === 1) await firstSyncReleased;
}
return undefined;
});
const update = useDownloadStore.getState().setQueueConcurrency('queue-a', 3);
await vi.waitFor(() => expect(syncCalls).toBe(1));
useDownloadStore.setState(state => ({
queues: state.queues.filter(queue => queue.id !== 'queue-a')
}));
releaseFirstSync();
await expect(update).rejects.toThrow('Queue no longer exists.');
expect(useDownloadStore.getState().queues).toEqual([
{ id: 'main', name: 'Main Queue', isMain: true }
]);
expect(syncCalls).toBe(2);
const configCalls = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'set_queue_concurrency_limits');
expect(configCalls[1]).toEqual([
'set_queue_concurrency_limits',
{ limits: [{ id: 'main', maxConcurrent: null }] }
]);
});
it('updates an active normal download after applying a live speed limit', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-speed',
status: 'downloading',
isMedia: false,
speedLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().setDownloadSpeedLimit('live-speed', '2M');
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_download_speed_limit', {
id: 'live-speed',
limit: '2M'
});
expect(useDownloadStore.getState().downloads[0].speedLimit).toBe('2M');
await useDownloadStore.getState().setDownloadSpeedLimit('live-speed', null);
const speedLimitCalls = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'set_download_speed_limit');
expect(speedLimitCalls[speedLimitCalls.length - 1]).toEqual(['set_download_speed_limit', {
id: 'live-speed',
limit: null
}]);
expect(useDownloadStore.getState().downloads[0].speedLimit).toBeUndefined();
});
it('updates an active Torrent upload limit while seeding and clears it', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-upload',
status: 'seeding',
isMedia: false,
isTorrent: true,
torrentUploadLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload', '2M');
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_torrent_upload_limit', {
id: 'live-torrent-upload',
limit: '2M'
});
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBe('2M');
await useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload', null);
const uploadLimitCalls = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'set_torrent_upload_limit');
expect(uploadLimitCalls[uploadLimitCalls.length - 1]).toEqual(['set_torrent_upload_limit', {
id: 'live-torrent-upload',
limit: null
}]);
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBeUndefined();
});
it('rejects live Torrent upload control for ordinary or inactive downloads', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'ordinary-upload', status: 'downloading', isMedia: false, isTorrent: false },
{ id: 'paused-upload', status: 'paused', isMedia: false, isTorrent: true }
] as any[]
});
await expect(useDownloadStore.getState().setTorrentUploadLimit('ordinary-upload', '2M'))
.rejects.toThrow('only for Torrent');
await expect(useDownloadStore.getState().setTorrentUploadLimit('paused-upload', '2M'))
.rejects.toThrow('active Torrent');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('set_torrent_upload_limit', expect.anything());
});
it('keeps the prior Torrent upload limit when the backend rejects the update', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-upload-failure',
status: 'downloading',
isMedia: false,
isTorrent: true,
torrentUploadLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async command => {
if (command === 'set_torrent_upload_limit') throw new Error('aria2 unavailable');
return undefined;
});
await expect(useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload-failure', '2M'))
.rejects.toThrow('aria2 unavailable');
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBe('512K');
});
it('updates active Torrent peer options and clears them to Aria2 defaults', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-peers',
status: 'seeding',
isMedia: false,
isTorrent: true,
torrentMaxPeers: 120,
torrentPeerSpeedLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().setTorrentPeerOptions('live-torrent-peers', '240', '2M');
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_torrent_peer_options', {
id: 'live-torrent-peers',
max_peers: 240,
peer_speed_limit: '2M'
});
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
torrentMaxPeers: 240,
torrentPeerSpeedLimit: '2M'
});
await useDownloadStore.getState().setTorrentPeerOptions('live-torrent-peers', null, null);
const peerOptionCalls = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'set_torrent_peer_options');
expect(peerOptionCalls[peerOptionCalls.length - 1]).toEqual(['set_torrent_peer_options', {
id: 'live-torrent-peers',
max_peers: null,
peer_speed_limit: null
}]);
expect(useDownloadStore.getState().downloads[0].torrentMaxPeers).toBeUndefined();
expect(useDownloadStore.getState().downloads[0].torrentPeerSpeedLimit).toBeUndefined();
});
it('rejects invalid or inactive live Torrent peer options', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'ordinary-peers', status: 'downloading', isMedia: false, isTorrent: false },
{ id: 'paused-peers', status: 'paused', isMedia: false, isTorrent: true }
] as any[]
});
await expect(useDownloadStore.getState().setTorrentPeerOptions('ordinary-peers', '100', '2M'))
.rejects.toThrow('only for Torrent');
await expect(useDownloadStore.getState().setTorrentPeerOptions('paused-peers', '100', '2M'))
.rejects.toThrow('active Torrent');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('set_torrent_peer_options', expect.anything());
useDownloadStore.setState({
downloads: [{ id: 'invalid-peers', status: 'downloading', isMedia: false, isTorrent: true }] as any[]
});
await expect(useDownloadStore.getState().setTorrentPeerOptions('invalid-peers', '1001', '2M'))
.rejects.toThrow('between 0 and 1000');
await expect(useDownloadStore.getState().setTorrentPeerOptions('invalid-peers', '100', 'not-a-rate'))
.rejects.toThrow('valid Torrent peer speed');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('set_torrent_peer_options', expect.anything());
});
it('keeps prior Torrent peer options when the backend rejects the update', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-peers-failure',
status: 'downloading',
isMedia: false,
isTorrent: true,
torrentMaxPeers: 120,
torrentPeerSpeedLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async command => {
if (command === 'set_torrent_peer_options') throw new Error('aria2 unavailable');
return undefined;
});
await expect(useDownloadStore.getState().setTorrentPeerOptions('live-torrent-peers-failure', '240', '2M'))
.rejects.toThrow('aria2 unavailable');
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
torrentMaxPeers: 120,
torrentPeerSpeedLimit: '512K'
});
});
it('rejects live speed changes for media and inactive downloads', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'media-speed', status: 'downloading', isMedia: true, speedLimit: '1M' },
{ id: 'paused-speed', status: 'paused', isMedia: false, speedLimit: '1M' }
] as any[]
});
await expect(useDownloadStore.getState().setDownloadSpeedLimit('media-speed', '2M'))
.rejects.toThrow('media downloads');
await expect(useDownloadStore.getState().setDownloadSpeedLimit('paused-speed', '2M'))
.rejects.toThrow('active download');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('set_download_speed_limit', expect.anything());
});
it('keeps the prior live speed limit when the backend rejects the update', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-speed-failure',
status: 'downloading',
isMedia: false,
speedLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async command => {
if (command === 'set_download_speed_limit') throw new Error('aria2 unavailable');
return undefined;
});
await expect(useDownloadStore.getState().setDownloadSpeedLimit('live-speed-failure', '2M'))
.rejects.toThrow('aria2 unavailable');
expect(useDownloadStore.getState().downloads[0].speedLimit).toBe('512K');
});
it('coalesces duplicate live speed updates for one download', async () => {
useDownloadStore.setState({
downloads: [{ id: 'live-speed-duplicate', status: 'downloading', isMedia: false }] as any[]
});
let releaseBackend!: () => void;
const backendFinished = new Promise<void>(resolve => { releaseBackend = resolve; });
vi.mocked(ipc.invokeCommand).mockImplementation(async () => backendFinished);
const first = useDownloadStore.getState().setDownloadSpeedLimit('live-speed-duplicate', '2M');
const second = useDownloadStore.getState().setDownloadSpeedLimit('live-speed-duplicate', '3M');
expect(second).toBe(first);
releaseBackend();
await first;
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'set_download_speed_limit'))
.toHaveLength(1);
expect(useDownloadStore.getState().downloads[0].speedLimit).toBe('2M');
});
it('normalizes malformed persisted queues around one canonical main queue', () => {
expect(normalizePersistedQueues([
{ id: 'custom-a', name: ' Downloads ', isMain: false },
{ id: 'custom-b', name: 'downloads', isMain: false },
{ id: 'custom-a', name: 'Duplicate ID', isMain: false },
{ id: 'legacy-main', name: 'Primary', isMain: true },
{ id: 'empty-name', name: ' ', isMain: false },
{ id: 'main-id', name: 'Ignored Main', isMain: true }
])).toEqual([
{ id: '00000000-0000-0000-0000-000000000001', name: 'Primary', isMain: true },
{ id: 'custom-a', name: 'Downloads', isMain: false },
{ id: 'custom-b', name: 'downloads (2)', isMain: false },
{ id: 'empty-name', name: 'Queue empty-na', isMain: false }
]);
expect(normalizePersistedQueueState([
{ id: 'legacy-main', name: 'Primary', isMain: true }
]).queueIdRemap.get('legacy-main')).toBe('00000000-0000-0000-0000-000000000001');
});
it('keeps only valid persisted queue concurrency overrides', () => {
expect(normalizePersistedQueues([
{ id: 'main', name: 'Main', isMain: true, maxConcurrent: 4 },
{ id: 'valid', name: 'Valid', isMain: false, maxConcurrent: 12 },
{ id: 'zero', name: 'Zero', isMain: false, maxConcurrent: 0 },
{ id: 'large', name: 'Large', isMain: false, maxConcurrent: 13 },
{ id: 'null', name: 'Null', isMain: false, maxConcurrent: null }
])).toEqual([
{ id: '00000000-0000-0000-0000-000000000001', name: 'Main', isMain: true, maxConcurrent: 4 },
{ id: 'valid', name: 'Valid', isMain: false, maxConcurrent: 12 },
{ id: 'zero', name: 'Zero', isMain: false },
{ id: 'large', name: 'Large', isMain: false },
{ id: 'null', name: 'Null', isMain: false }
]);
});
it('synchronizes normalized queue limits before startup resume can run', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') {
return [
JSON.stringify({ id: 'main', name: 'Main', isMain: true, maxConcurrent: 4 }),
JSON.stringify({ id: 'queue-a', name: 'Queue A', isMain: false, maxConcurrent: 0 })
];
}
if (cmd === 'db_get_all_downloads') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith(
'set_queue_concurrency_limits',
{
limits: [
{ id: '00000000-0000-0000-0000-000000000001', maxConcurrent: 4 },
{ id: 'queue-a', maxConcurrent: null }
]
}
);
});
it('remaps persisted downloads when queue records are malformed or missing', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') {
return [JSON.stringify({ id: 'legacy-main', name: 'Primary', isMain: true })];
}
if (cmd === 'db_get_all_downloads') {
return [
JSON.stringify({
id: 'legacy-download',
url: 'https://example.com/legacy.bin',
fileName: 'legacy.bin',
status: 'ready',
category: 'Other',
dateAdded: '',
queueId: 'legacy-main'
}),
JSON.stringify({
id: 'orphan-download',
url: 'https://example.com/orphan.bin',
fileName: 'orphan.bin',
status: 'ready',
category: 'Other',
dateAdded: '',
queueId: 'missing-queue'
})
];
}
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads.map(download => download.queueId))
.toEqual(['00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001']);
});
it('skips malformed persisted download records without blocking startup', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [
'{not-json',
JSON.stringify(null),
JSON.stringify([]),
JSON.stringify({
id: 'valid-after-corruption',
url: 'https://example.com/valid.bin',
fileName: 'valid.bin',
status: 'ready',
category: 'Other',
dateAdded: ''
})
];
}
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads.map(download => download.id))
.toEqual(['valid-after-corruption']);
});
it('moves persisted paused rows behind runnable rows and assigns contiguous positions', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') {
return [JSON.stringify({ id: 'queue-a', name: 'Queue A', isMain: false })];
}
if (cmd === 'db_get_all_downloads') {
return [
JSON.stringify({ id: 'active', status: 'downloading', queueId: 'queue-a', queuePosition: 0 }),
JSON.stringify({ id: 'queued-one', status: 'queued', queueId: 'queue-a', queuePosition: 1 }),
JSON.stringify({ id: 'paused-one', status: 'paused', queueId: 'queue-a', queuePosition: 2 }),
JSON.stringify({ id: 'queued-two', status: 'queued', queueId: 'queue-a', queuePosition: 3 }),
JSON.stringify({ id: 'legacy-invalid', status: 'queued', queueId: 'queue-a', queuePosition: 'not-a-number' })
];
}
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads
.filter(download => download.queueId === 'queue-a')
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0))
.map(download => download.id)
).toEqual(['active', 'queued-one', 'queued-two', 'legacy-invalid', 'paused-one']);
expect(useDownloadStore.getState().downloads
.filter(download => download.queueId === 'queue-a')
.map(download => download.queuePosition)
.sort((left, right) => (left ?? 0) - (right ?? 0))
).toEqual([0, 1, 2, 3, 4]);
});
it('removes persisted temporary media estimates that are smaller than downloaded bytes', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'stale-media-estimate',
url: 'https://youtube.com/watch?v=stale',
fileName: 'video.mkv',
status: 'queued',
category: 'Movies',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
isMedia: true,
size: '~1.00 KB',
downloadedBytes: 11_989,
totalBytes: 1_024,
totalIsEstimate: true
})];
}
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
size: undefined,
downloadedBytes: 11_989,
totalBytes: undefined,
totalIsEstimate: undefined
});
});
it('does not discard a legitimate large media estimate when downloaded bytes exceed it', () => {
const media = {
isMedia: true,
downloadedBytes: 90_000_000,
totalBytes: 89_817_907,
totalIsEstimate: true,
size: '~85.7 MB'
} as const;
expect(hasStaleTemporaryMediaEstimate(media)).toBe(false);
expect(normalizePersistedDownloadProgress({
id: 'large-estimate',
url: 'https://youtube.com/watch?v=large',
fileName: 'video.mkv',
status: 'queued',
category: 'Movies',
dateAdded: '',
...media
})).toMatchObject({
size: '~85.7 MB',
downloadedBytes: 90_000_000,
totalBytes: 89_817_907,
totalIsEstimate: true
});
});
it('does not discard a legitimate small media estimate without contradictory progress', () => {
const media = {
isMedia: true,
size: '~500 B',
downloadedBytes: 500,
totalBytes: undefined,
totalIsEstimate: true
} as const;
expect(hasStaleTemporaryMediaEstimate(media)).toBe(false);
expect(normalizePersistedDownloadProgress({
id: 'small-media',
url: 'https://youtube.com/watch?v=small',
fileName: 'short.mkv',
status: 'queued',
category: 'Movies',
dateAdded: '',
...media
})).toMatchObject(media);
});
it('recognizes IEC-formatted temporary media estimates', () => {
expect(hasStaleTemporaryMediaEstimate({
isMedia: true,
size: '~1.00 KiB',
downloadedBytes: 2_048,
totalBytes: undefined,
totalIsEstimate: true
})).toBe(true);
});
it('clears malformed persisted Torrent peer options', () => {
const normalized = normalizePersistedDownloadProgress({
id: 'malformed-torrent-options',
url: 'magnet:?xt=urn:btih:bad',
fileName: 'payload',
status: 'queued',
category: 'Other',
dateAdded: '',
isTorrent: true,
torrentMaxPeers: 'not-a-number' as unknown as number,
torrentPeerSpeedLimit: 0 as unknown as string
});
expect(normalized.torrentMaxPeers).toBeUndefined();
expect(normalized.torrentPeerSpeedLimit).toBeUndefined();
});
it('normalizes proxy settings for download dispatch', async () => {
expect(normalizeCustomProxy('127.0.0.1', 8080)).toBe('http://127.0.0.1:8080');
expect(normalizeCustomProxy('http://proxy.local:9000', 8080)).toBe('http://proxy.local:9000');
expect(normalizeCustomProxy(' socks5://127.0.0.1 ', 1080)).toBeNull();
expect(normalizeCustomProxy('https://proxy.local', 8443)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1', NaN)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1:9000', 8080)).toBeNull();
expect(normalizeCustomProxy('127.0.0.1/path', 8080)).toBeNull();
expect(normalizeCustomProxy('[::1]', 8080)).toBe('http://[::1]:8080');
expect(await getProxyArgs({
proxyMode: 'none',
proxyHost: '',
proxyPort: 8080
} as ReturnType<typeof useSettingsStore.getState>)).toBe('none');
vi.mocked(ipc.invokeCommand).mockResolvedValueOnce(null);
expect(await getProxyArgs({
proxyMode: 'system',
proxyHost: '',
proxyPort: 8080
} as ReturnType<typeof useSettingsStore.getState>)).toBe('none');
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('system settings unavailable'));
await expect(getProxyArgs({
proxyMode: 'system',
proxyHost: '',
proxyPort: 8080
} as ReturnType<typeof useSettingsStore.getState>)).rejects.toThrow(
'System proxy configuration could not be read: system settings unavailable'
);
expect(await getProxyArgs({
proxyMode: 'custom',
proxyHost: 'http://127.0.0.1',
proxyPort: 1080
} as ReturnType<typeof useSettingsStore.getState>)).toBe('http://127.0.0.1:1080');
});
it('keeps an item queued when system proxy resolution fails closed', async () => {
vi.mocked(useSettingsStore.getState).mockReturnValue({
...useSettingsStore.getState(),
proxyMode: 'system'
} as unknown as ReturnType<typeof useSettingsStore.getState>);
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'get_system_proxy') {
throw new Error('system settings unavailable');
}
return undefined;
});
useDownloadStore.setState({
downloads: [{
id: 'system-proxy-blocked',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
destination: '/tmp',
status: 'queued',
category: 'Other',
dateAdded: ''
}] as any[],
backendRegisteredIds: new Set()
});
await expect(dispatchItem('system-proxy-blocked')).resolves.toBe(false);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'queued',
lastError: 'System proxy configuration could not be read: system settings unavailable. Choose No Proxy or try again.'
});
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
});
it('matches site logins by host, wildcard host, path, and full URL patterns', () => {
const settings = {
siteLogins: [
{ id: 'host', urlPattern: 'example.com', username: 'host' },
{ id: 'wildcard', urlPattern: '*.cdn.example.com', username: 'wildcard' },
{ id: 'broad', urlPattern: '*.example.com', username: 'broad' },
{ id: 'path', urlPattern: 'secure.example.com/private/*', username: 'path' },
{ id: 'url', urlPattern: 'https://downloads.example.net/releases/*', username: 'url' }
]
} as ReturnType<typeof useSettingsStore.getState>;
expect(getSiteLogin('https://example.com/file.zip', settings)?.id).toBe('host');
expect(getSiteLogin('https://assets.cdn.example.com/file.zip', settings)?.id).toBe('wildcard');
expect(getSiteLogin('https://secure.example.com/private/file.zip', settings)?.id).toBe('path');
expect(getSiteLogin('https://downloads.example.net/releases/app.zip', settings)?.id).toBe('url');
expect(getSiteLogin('https://secure.example.com/public/file.zip', settings)?.id).toBe('broad');
expect(getSiteLogin('https://unrelated.example.org/public/file.zip', settings)).toBeNull();
});
it('Start Queue dispatches exactly once for mixed dispatched/undispatched items', async () => {
useDownloadStore.setState({
downloads: [
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
{ id: '2', url: 'http://test2', fileName: 'f2', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
backendRegisteredIds: new Set(['1']), // 1 is already registered, so it skips dispatch
pendingOrder: ['1'],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['1', '2'];
return undefined;
});
const dispatched = await useDownloadStore.getState().startQueue('MAIN');
expect(dispatched).toEqual(['1', '2']);
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
const enqueues = calls.filter(c => c[0] === 'enqueue_download');
expect(enqueues.length).toBe(1);
expect((enqueues[0] as any)[1].item.id).toBe('2');
});
it('repairs stale queued backend registrations before accepting a queue start', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'stale', url: 'http://test', fileName: 'f', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
] as any[],
backendRegisteredIds: new Set(['stale']),
pendingOrder: [],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'resume_download') return false;
if (cmd === 'get_pending_order') return ['stale'];
return undefined;
});
expect(await useDownloadStore.getState().startQueue('MAIN')).toEqual(['stale']);
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
expect(calls.some(call => call[0] === 'resume_download')).toBe(true);
expect(calls.some(call => call[0] === 'enqueue_download')).toBe(true);
});
it('does not overwrite a downloading event received while starting a queue', async () => {
useDownloadStore.setState({
downloads: [
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'enqueue_download') {
useDownloadStore.getState().updateDownload('1', {
status: 'downloading',
speed: '1 MB/s',
eta: '10s'
});
}
if (cmd === 'get_pending_order') return ['1'];
return undefined;
});
expect(await useDownloadStore.getState().startQueue('MAIN')).toEqual(['1']);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'downloading',
speed: '1 MB/s',
eta: '10s',
hasBeenDispatched: true
});
});
it('does not resurrect a row removed while its backend enqueue is in flight', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'late', url: 'http://test', fileName: 'late.bin', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
});
let resolveEnqueue!: (value: { id: string; filename: string }) => void;
const enqueue = new Promise<{ id: string; filename: string }>(resolve => {
resolveEnqueue = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'enqueue_download') return enqueue as never;
if (command === 'get_pending_order') return Promise.resolve(['late']) as never;
return Promise.resolve(undefined) as never;
});
const start = useDownloadStore.getState().startQueue('MAIN');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'late' }) })
);
});
const remove = useDownloadStore.getState().removeDownload('late');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'cancel_enqueue_generation',
expect.objectContaining({ id: 'late' })
);
});
resolveEnqueue({ id: 'late', filename: 'late.bin' });
await remove;
await expect(start).resolves.toEqual([]);
expect(useDownloadStore.getState().downloads).toEqual([]);
expect(useDownloadStore.getState().backendRegisteredIds.has('late')).toBe(false);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'remove_download')
).toHaveLength(2);
});
it('re-enqueues the edited values only after an obsolete queued dispatch is removed', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'edited', url: 'http://test', fileName: 'old.bin', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
});
let resolveFirstEnqueue!: (value: { id: string; filename: string }) => void;
const firstEnqueue = new Promise<{ id: string; filename: string }>(resolve => {
resolveFirstEnqueue = resolve;
});
let enqueueCount = 0;
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'enqueue_download') {
enqueueCount += 1;
return (enqueueCount === 1
? firstEnqueue
: Promise.resolve({ id: 'edited', filename: 'new.bin' })) as never;
}
if (command === 'get_pending_order') return Promise.resolve(['edited']) as never;
return Promise.resolve(undefined) as never;
});
const start = useDownloadStore.getState().startQueue('MAIN');
await vi.waitFor(() => expect(enqueueCount).toBe(1));
const update = useDownloadStore.getState().applyProperties('edited', { fileName: 'new.bin' });
resolveFirstEnqueue({ id: 'edited', filename: 'old.bin' });
await expect(update).resolves.toBeUndefined();
await expect(start).resolves.toEqual([]);
expect(enqueueCount).toBe(2);
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith(
'get_pending_order',
{ queueId: 'MAIN' }
);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
fileName: 'new.bin',
hasBeenDispatched: true,
});
});
it('settles an in-flight dispatch before pausing so it cannot start after pause succeeds', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'paused', url: 'http://test', fileName: 'paused.bin', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
});
let resolveEnqueue!: (value: { id: string; filename: string }) => void;
const enqueue = new Promise<{ id: string; filename: string }>(resolve => {
resolveEnqueue = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'enqueue_download') return enqueue as never;
return Promise.resolve(undefined) as never;
});
const start = useDownloadStore.getState().startQueue('MAIN');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'paused' }) })
);
});
const pause = useDownloadStore.getState().pauseDownload('paused');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'cancel_enqueue_generation',
expect.objectContaining({ id: 'paused' })
);
});
expect(
vi.mocked(ipc.invokeCommand).mock.calls.some(([command]) => command === 'pause_download')
).toBe(false);
resolveEnqueue({ id: 'paused', filename: 'paused.bin' });
await expect(pause).resolves.toBeUndefined();
await expect(start).resolves.toEqual([]);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({ status: 'paused' });
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'remove_download')
).toHaveLength(1);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'pause_download')
).toHaveLength(1);
});
it('resumeDownload unregisters ID and re-dispatches if un-resumable', async () => {
let enqueueGeneration: string | undefined;
useDownloadStore.setState({
downloads: [
{ id: 'resume-generation', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
] as any[],
backendRegisteredIds: new Set(['resume-generation']),
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string, args?: unknown) => {
if (cmd === 'resume_download') return false; // Not resumable
if (cmd === 'enqueue_download') {
enqueueGeneration = (args as { item: { lifecycle_generation: string } }).item.lifecycle_generation;
return { id: 'resume-generation', filename: 'f1' };
}
if (cmd === 'get_pending_order') return ['resume-generation'];
return undefined;
});
await useDownloadStore.getState().resumeDownload('resume-generation');
// It should have called resume_download, then unregistered, then enqueue_download
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
expect(calls.some(c => c[0] === 'resume_download')).toBe(true);
expect(calls.some(c => c[0] === 'enqueue_download')).toBe(true);
expect(calls.find(c => c[0] === 'resume_download')?.[1]).toEqual({
id: 'resume-generation',
queueId: 'MAIN'
});
expect(enqueueGeneration).toBe('1');
expect(useDownloadStore.getState().downloads[0].lastTry).toEqual(expect.any(String));
expect(useDownloadStore.getState().backendRegisteredIds.has('resume-generation')).toBe(true); // Re-registered by dispatchItem
});
it('coalesces duplicate resume actions while the backend transition is in flight', async () => {
useDownloadStore.setState({
downloads: [{
id: 'resume-double-submit',
url: 'http://test1',
fileName: 'f1',
destination: '/tmp',
status: 'paused',
category: 'Other',
dateAdded: '',
hasBeenDispatched: true
}] as any[],
backendRegisteredIds: new Set(['resume-double-submit'])
});
let releaseResume!: () => void;
const resumeReleased = new Promise<void>(resolve => {
releaseResume = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'resume_download') {
await resumeReleased;
return true;
}
return undefined;
});
const first = useDownloadStore.getState().resumeDownload('resume-double-submit');
await vi.waitFor(() => {
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'resume_download'))
.toHaveLength(1);
});
const second = useDownloadStore.getState().resumeDownload('resume-double-submit');
expect(second).toBe(first);
releaseResume();
await Promise.all([first, second]);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'resume_download')
).toHaveLength(1);
});
it('does not re-enqueue when the existing resume RPC fails', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'resume-rpc-error', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
] as any[],
backendRegisteredIds: new Set(['resume-rpc-error']),
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'resume_download') throw new Error('aria2 RPC unavailable');
return undefined;
});
await expect(useDownloadStore.getState().resumeDownload('resume-rpc-error')).resolves.toBe(false);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.some(([command]) => command === 'enqueue_download')
).toBe(false);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'paused',
lastTry: expect.any(String),
});
});
it('starts a selected queue block in selection order and moves pending rows to the front', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'other', url: 'http://other', fileName: 'other', destination: '/tmp', status: 'staged', category: 'Other', dateAdded: '', queueId: 'selection-queue', queuePosition: 0 },
{ id: 'selected-a', url: 'http://a', fileName: 'a', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'selection-queue', queuePosition: 1, hasBeenDispatched: true },
{ id: 'selected-b', url: 'http://b', fileName: 'b', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'selection-queue', queuePosition: 2, hasBeenDispatched: true },
] as any[],
backendRegisteredIds: new Set(['selected-a', 'selected-b'])
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string, args?: unknown) => {
if (command === 'enqueue_download') {
const id = (args as { item: { id: string } }).item.id;
return { id, filename: id };
}
if (command === 'get_pending_order') return ['selected-b', 'selected-a', 'other'];
if (command === 'move_many_in_queue') return ['selected-b', 'selected-a', 'other'];
return undefined;
});
await expect(useDownloadStore.getState().startSelected(['selected-b', 'selected-a'])).resolves.toBe(2);
const enqueueIds = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'enqueue_download')
.map(([, args]) => (args as { item: { id: string } }).item.id);
expect(enqueueIds).toEqual(['selected-b', 'selected-a']);
expect(ipc.invokeCommand).toHaveBeenCalledWith('move_many_in_queue', {
ids: ['selected-b', 'selected-a'],
queueId: 'selection-queue',
direction: 'up',
targetIndex: 0,
});
expect(useDownloadStore.getState().downloads.map(item => item.id)).toEqual([
'other',
'selected-a',
'selected-b',
]);
expect(useDownloadStore.getState().downloads.find(item => item.id === 'selected-b')?.queuePosition).toBe(0);
expect(useDownloadStore.getState().downloads.find(item => item.id === 'selected-a')?.queuePosition).toBe(1);
});
it('resumes a selected block whose paused rows were never dispatched', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'selected-undispatched-a', url: 'http://a', fileName: 'a', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'selection-undispatched', queuePosition: 0, hasBeenDispatched: false },
{ id: 'selected-undispatched-b', url: 'http://b', fileName: 'b', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'selection-undispatched', queuePosition: 1, hasBeenDispatched: false },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string, args?: unknown) => {
if (command === 'enqueue_download') {
const id = (args as { item: { id: string } }).item.id;
return { id, filename: id };
}
if (command === 'get_pending_order') return ['selected-undispatched-a', 'selected-undispatched-b'];
if (command === 'move_many_in_queue') return ['selected-undispatched-a', 'selected-undispatched-b'];
return undefined;
});
await expect(useDownloadStore.getState().startSelected([
'selected-undispatched-a',
'selected-undispatched-b'
])).resolves.toBe(2);
const enqueueIds = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'enqueue_download')
.map(([, args]) => (args as { item: { id: string } }).item.id);
expect(enqueueIds).toEqual(['selected-undispatched-a', 'selected-undispatched-b']);
});
it('pauses queued items through the global pause action', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'queued-one', url: 'http://one', fileName: 'one', status: 'queued', category: 'Other', dateAdded: '', queueId: 'pause-queue' },
{ id: 'queued-two', url: 'http://two', fileName: 'two', status: 'queued', category: 'Other', dateAdded: '', queueId: 'pause-queue' },
{ id: 'staged-one', url: 'http://staged', fileName: 'staged', status: 'staged', category: 'Other', dateAdded: '', queueId: 'pause-queue' },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await expect(useDownloadStore.getState().pauseAll()).resolves.toBe(3);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'pause_download')
).toHaveLength(3);
expect(useDownloadStore.getState().downloads.every(item => item.status === 'paused')).toBe(true);
});
it('moves a paused row behind the remaining runnable queue rows', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'active', status: 'downloading', queueId: 'ordered-pause', queuePosition: 0 },
{ id: 'queued-one', status: 'queued', queueId: 'ordered-pause', queuePosition: 1 },
{ id: 'pause-target', status: 'queued', queueId: 'ordered-pause', queuePosition: 2 },
{ id: 'queued-two', status: 'queued', queueId: 'ordered-pause', queuePosition: 3 }
] as any[],
pendingOrder: ['queued-one', 'pause-target', 'queued-two']
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().pauseDownload('pause-target');
const ordered = useDownloadStore.getState().downloads
.filter(download => download.queueId === 'ordered-pause')
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0));
expect(ordered.map(download => download.id)).toEqual([
'active', 'queued-one', 'queued-two', 'pause-target'
]);
expect(ordered.map(download => download.queuePosition)).toEqual([0, 1, 2, 3]);
expect(useDownloadStore.getState().pendingOrder).toEqual(['queued-one', 'queued-two']);
expect(useDownloadStore.getState().downloads.find(download => download.id === 'pause-target')?.status)
.toBe('paused');
});
it('does not let a queue pause lose a selected start in flight', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'race-first', url: 'http://first', fileName: 'first', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'race-selected', queuePosition: 0 },
{ id: 'race-second', url: 'http://second', fileName: 'second', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'race-selected', queuePosition: 1 },
] as any[],
});
let releaseEnqueue!: (value: { id: string; filename: string }) => void;
const enqueue = new Promise<{ id: string; filename: string }>(resolve => {
releaseEnqueue = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'enqueue_download') return enqueue;
if (command === 'get_pending_order') return [];
return undefined;
});
const start = useDownloadStore.getState().startSelected(['race-first', 'race-second']);
await vi.waitFor(() => {
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'race-first' }) })
);
});
const pause = useDownloadStore.getState().pauseQueue('race-selected');
await vi.waitFor(() => {
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'cancel_enqueue_generation'))
.toHaveLength(2);
});
releaseEnqueue({ id: 'race-first', filename: 'first' });
await expect(start).resolves.toBe(0);
await expect(pause).resolves.toBe(1);
expect(useDownloadStore.getState().downloads.map(item => item.status)).toEqual(['paused', 'paused']);
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'pause_download'))
.toHaveLength(1);
});
it('cleans an accepted backend enqueue when queue reconciliation fails', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'enqueue-reconcile-error', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'enqueue_download') return { id: 'enqueue-reconcile-error', filename: 'f1' };
if (cmd === 'get_pending_order') throw new Error('queue state unavailable');
return undefined;
});
await expect(useDownloadStore.getState().startQueue('MAIN')).resolves.toEqual([]);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.some(([command]) => command === 'remove_download')
).toBe(true);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'failed',
lastError: 'queue state unavailable',
});
});
it('adds to the selected queue without dispatching', async () => {
await useDownloadStore.getState().addDownload({
id: 'queue-1',
url: 'https://example.com/queue.bin',
fileName: 'queue.bin',
category: 'Other',
dateAdded: ''
}, { type: 'add-to-queue', queueId: 'queue-b' });
const item = useDownloadStore.getState().downloads[0];
expect(item.status).toBe('staged');
expect(item.queueId).toBe('queue-b');
expect(item.queuePosition).toBe(0);
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
});
it('inserts a newly staged queue item before paused rows', async () => {
useDownloadStore.setState({
downloads: [{
id: 'already-paused',
url: 'https://example.com/paused.bin',
fileName: 'paused.bin',
category: 'Other',
dateAdded: '',
status: 'paused',
queueId: 'queue-b',
queuePosition: 0
}] as any[]
});
await useDownloadStore.getState().addDownload({
id: 'new-staged',
url: 'https://example.com/new.bin',
fileName: 'new.bin',
category: 'Other',
dateAdded: ''
}, { type: 'add-to-queue', queueId: 'queue-b' });
const ordered = useDownloadStore.getState().downloads
.filter(item => item.queueId === 'queue-b')
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0));
expect(ordered.map(item => item.id)).toEqual(['new-staged', 'already-paused']);
expect(ordered.map(item => item.queuePosition)).toEqual([0, 1]);
});
it('carries a media format estimate into numeric progress state', async () => {
await useDownloadStore.getState().addDownload({
id: 'media-estimate',
url: 'https://youtube.com/watch?v=estimate',
fileName: 'video.mkv',
category: 'Movies',
dateAdded: '',
isMedia: true,
size: '~85.7 MB',
sizeBytes: 89_817_907
}, { type: 'add-to-queue', queueId: 'queue-b' });
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
totalBytes: 89_817_907,
totalIsEstimate: true
});
expect(useDownloadStore.getState().downloads[0]).not.toHaveProperty('sizeBytes');
});
it('starts immediately in the main queue', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['start-1'];
return undefined;
});
await useDownloadStore.getState().addDownload({
id: 'start-1',
url: 'https://example.com/start.bin',
fileName: 'start.bin',
category: 'Other',
dateAdded: ''
}, { type: 'start-now' });
const item = useDownloadStore.getState().downloads[0];
expect(item.queueId).toBe('00000000-0000-0000-0000-000000000001');
expect(item.hasBeenDispatched).toBe(true);
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({ id: 'start-1' })
})
);
});
it('does not replace an explicit no-limit item speed with the global speed limit', async () => {
const defaultSettings = useSettingsStore.getState();
vi.mocked(useSettingsStore.getState).mockReturnValue({
...defaultSettings,
globalSpeedLimit: '2M'
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['uncapped'];
return undefined;
});
await useDownloadStore.getState().addDownload({
id: 'uncapped',
url: 'https://example.com/uncapped.bin',
fileName: 'uncapped.bin',
category: 'Other',
dateAdded: '',
speedLimit: '0'
}, { type: 'start-now' });
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({
id: 'uncapped',
speed_limit: '0'
})
})
);
});
it('does not copy the global speed limit into a normal download task', async () => {
const defaultSettings = useSettingsStore.getState();
vi.mocked(useSettingsStore.getState).mockReturnValue({
...defaultSettings,
globalSpeedLimit: '2M'
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['inherits-global'];
return undefined;
});
await useDownloadStore.getState().addDownload({
id: 'inherits-global',
url: 'https://example.com/inherits-global.bin',
fileName: 'inherits-global.bin',
category: 'Other',
dateAdded: ''
}, { type: 'start-now' });
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({
id: 'inherits-global',
speed_limit: null
})
})
);
});
it('passes the global speed limit to a new media process when it has no item override', async () => {
const defaultSettings = useSettingsStore.getState();
vi.mocked(useSettingsStore.getState).mockReturnValue({
...defaultSettings,
globalSpeedLimit: '2M'
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['inherits-global-media'];
return undefined;
});
await useDownloadStore.getState().addDownload({
id: 'inherits-global-media',
url: 'https://www.youtube.com/watch?v=example',
fileName: 'media.mp4',
category: 'Movies',
dateAdded: '',
isMedia: true
}, { type: 'start-now' });
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({
id: 'inherits-global-media',
speed_limit: '2M'
})
})
);
});
it('treats the legacy media zero sentinel as inheriting the global limit', async () => {
const defaultSettings = useSettingsStore.getState();
vi.mocked(useSettingsStore.getState).mockReturnValue({
...defaultSettings,
globalSpeedLimit: '2M'
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['legacy-media-limit'];
return undefined;
});
await useDownloadStore.getState().addDownload({
id: 'legacy-media-limit',
url: 'https://www.youtube.com/watch?v=legacy',
fileName: 'media.mp4',
category: 'Movies',
dateAdded: '',
isMedia: true,
speedLimit: '0'
}, { type: 'start-now' });
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({
id: 'legacy-media-limit',
speed_limit: '2M'
})
})
);
});
it('reports a rejected immediate start instead of claiming success', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'enqueue_download') {
throw new Error('backend unavailable');
}
return undefined;
});
const added = await useDownloadStore.getState().addDownload({
id: 'rejected-start',
url: 'https://example.com/rejected.bin',
fileName: 'rejected.bin',
category: 'Other',
dateAdded: ''
}, { type: 'start-now' });
expect(added).toBe(false);
expect(useDownloadStore.getState().downloads[0].status).toBe('failed');
expect(useDownloadStore.getState().downloads[0].lastError).toBe('backend unavailable');
});
it('blocks credential-backed dispatch until the custom access decision is made', async () => {
const setShowKeychainModal = vi.fn();
vi.mocked(useSettingsStore.getState).mockReturnValue({
...useSettingsStore.getState(),
siteLogins: [{ id: 'dispatch-login', urlPattern: 'secure.example.com', username: 'user' }],
keychainAccessReady: false,
keychainPromptDismissed: false,
setShowKeychainModal
} as unknown as ReturnType<typeof useSettingsStore.getState>);
useDownloadStore.setState({
downloads: [{
id: 'credential-gated-dispatch',
url: 'https://secure.example.com/file.bin',
fileName: 'file.bin',
destination: '/tmp',
status: 'ready',
category: 'Other',
dateAdded: ''
}] as any[],
backendRegisteredIds: new Set()
});
await expect(dispatchItem('credential-gated-dispatch')).resolves.toBe(false);
expect(setShowKeychainModal).toHaveBeenCalledWith(true);
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('get_keychain_password', expect.anything());
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
});
it('preserves backend rejection reasons while auto-resuming saved queued items', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-failed',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'enqueue_many') {
return [{
id: 'startup-failed',
success: false,
error: 'aria2 addUri failed: connection refused'
}];
}
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
await useDownloadStore.getState().resumePendingDownloads();
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'failed',
lastError: 'aria2 addUri failed: connection refused'
});
});
it('keeps all startup items retryable when system proxy resolution fails', async () => {
vi.mocked(useSettingsStore.getState).mockReturnValue({
...useSettingsStore.getState(),
proxyMode: 'system'
} as unknown as ReturnType<typeof useSettingsStore.getState>);
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'db_get_all_queues') return [];
if (command === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-proxy-blocked',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (command === 'get_system_proxy') {
throw new Error('system settings unavailable');
}
return undefined;
});
await useDownloadStore.getState().initDB();
await useDownloadStore.getState().resumePendingDownloads();
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'queued',
lastError: 'System proxy configuration could not be read: system settings unavailable. Choose No Proxy or try again.'
});
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_many', expect.anything());
});
it('keeps accepted startup registrations when pending-order refresh fails', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-accepted',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'enqueue_many') {
return [{ id: 'startup-accepted', success: true, filename: 'normalized.bin' }];
}
if (cmd === 'get_pending_order') throw new Error('queue state unavailable');
if (cmd === 'resume_download') return true;
return undefined;
});
await useDownloadStore.getState().initDB();
await useDownloadStore.getState().resumePendingDownloads();
expect(useDownloadStore.getState().backendRegisteredIds.has('startup-accepted')).toBe(true);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
fileName: 'normalized.bin',
status: 'queued',
hasBeenDispatched: true
});
await expect(useDownloadStore.getState().startQueue('00000000-0000-0000-0000-000000000001'))
.resolves.toEqual(['startup-accepted']);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(call => call[0] === 'enqueue_download')
).toHaveLength(0);
});
it('does not restore a registration after a fast startup terminal event', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-completed',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'enqueue_many') {
useDownloadStore.setState(state => ({
backendRegisteredIds: new Set(),
downloads: state.downloads.map(download => download.id === 'startup-completed'
? { ...download, status: 'completed' as const }
: download)
}));
return [{ id: 'startup-completed', success: true, filename: 'file.bin' }];
}
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
await useDownloadStore.getState().resumePendingDownloads();
expect(useDownloadStore.getState().downloads[0].status).toBe('completed');
expect(useDownloadStore.getState().backendRegisteredIds.has('startup-completed')).toBe(false);
});
it('does not read saved credentials during database initialization', async () => {
vi.mocked(useSettingsStore.getState).mockReturnValue({
...useSettingsStore.getState(),
siteLogins: [{ id: 'secure-login', urlPattern: 'secure.example.com', username: 'user' }],
keychainAccessReady: false
} as unknown as ReturnType<typeof useSettingsStore.getState>);
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-credential-gated',
url: 'https://secure.example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'get_keychain_password') return 'secret';
if (cmd === 'enqueue_many') return [{ id: 'startup-credential-gated', success: true }];
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
expect(ipc.invokeCommand).not.toHaveBeenCalledWith(
'get_keychain_password',
{ id: 'secure-login' }
);
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_many', expect.anything());
vi.mocked(useSettingsStore.getState).mockReturnValue({
...useSettingsStore.getState(),
siteLogins: [{ id: 'secure-login', urlPattern: 'secure.example.com', username: 'user' }],
keychainAccessReady: true
} as unknown as ReturnType<typeof useSettingsStore.getState>);
await useDownloadStore.getState().resumePendingDownloads();
expect(ipc.invokeCommand).toHaveBeenCalledWith('get_keychain_password', { id: 'secure-login' });
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_many',
expect.objectContaining({
items: [expect.objectContaining({ password: 'secret' })]
})
);
});
it('shares one startup-resume operation when callers race', async () => {
let releaseEnqueue!: () => void;
const enqueueReleased = new Promise<void>(resolve => {
releaseEnqueue = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-single-flight',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'enqueue_many') {
await enqueueReleased;
return [{ id: 'startup-single-flight', success: true }];
}
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
const first = useDownloadStore.getState().resumePendingDownloads();
const second = useDownloadStore.getState().resumePendingDownloads();
expect(second).toBe(first);
await vi.waitFor(() => {
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(call => call[0] === 'enqueue_many')).toHaveLength(1);
});
releaseEnqueue();
await Promise.all([first, second]);
});
it('redownloads fallback media without requiring a format selector', async () => {
useDownloadStore.setState({
downloads: [{
id: 'media-fallback',
url: 'https://youtube.com/watch?v=test',
fileName: 'watch',
destination: '/tmp',
status: 'completed',
category: 'Other',
dateAdded: '',
isMedia: true
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().redownload('media-fallback');
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({
item: expect.objectContaining({
is_media: true,
format_selector: null
})
})
);
});
it('does not claim a redownload when removing the old backend lifecycle fails', async () => {
useDownloadStore.setState({
downloads: [{
id: 'redownload-remove-failed',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
destination: '/tmp',
status: 'paused',
category: 'Other',
dateAdded: '2026-07-15T00:00:00.000Z',
hasBeenDispatched: true
}] as any[],
backendRegisteredIds: new Set(['redownload-remove-failed'])
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'remove_download') throw new Error('aria2 did not stop');
return undefined;
});
await expect(useDownloadStore.getState().redownload('redownload-remove-failed'))
.rejects.toThrow('aria2 did not stop');
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
status: 'paused',
dateAdded: '2026-07-15T00:00:00.000Z',
hasBeenDispatched: true
});
expect(useDownloadStore.getState().backendRegisteredIds.has('redownload-remove-failed')).toBe(true);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.some(call => call[0] === 'enqueue_download')
).toBe(false);
});
it('coalesces duplicate redownloads and serializes a destructive removal behind them', async () => {
useDownloadStore.setState({
downloads: [{
id: 'redownload-double-submit',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
destination: '/tmp',
status: 'paused',
category: 'Other',
dateAdded: ''
}] as any[]
});
let releaseRedownloadRemoval!: () => void;
const redownloadRemovalReleased = new Promise<void>(resolve => {
releaseRedownloadRemoval = resolve;
});
let removeCallCount = 0;
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'remove_download') {
removeCallCount += 1;
if (removeCallCount === 1) await redownloadRemovalReleased;
return undefined;
}
if (cmd === 'get_pending_order') return [];
return undefined;
});
const first = useDownloadStore.getState().redownload('redownload-double-submit');
await vi.waitFor(() => expect(removeCallCount).toBe(1));
const second = useDownloadStore.getState().redownload('redownload-double-submit');
const remove = useDownloadStore.getState().removeDownload('redownload-double-submit');
expect(second).toBe(first);
expect(removeCallCount).toBe(1);
releaseRedownloadRemoval();
await first;
await remove;
expect(removeCallCount).toBe(2);
expect(useDownloadStore.getState().downloads).toEqual([]);
});
it('starts and pauses all items regardless of legacy missing queue ids', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'ready', url: 'http://ready', fileName: 'ready', status: 'ready', category: 'Other', dateAdded: '' },
{ id: 'active', url: 'http://active', fileName: 'active', status: 'processing', category: 'Other', dateAdded: '' },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['ready'];
return undefined;
});
expect(await useDownloadStore.getState().startAll()).toBe(1);
expect(await useDownloadStore.getState().pauseAll()).toBe(2);
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
expect(calls.some(call => call[0] === 'enqueue_download')).toBe(true);
expect(calls.some(call => call[0] === 'pause_download' && (call[1] as any).id === 'active')).toBe(true);
});
it('direct queue controls treat missing queue ids as the main queue', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'legacy-ready', url: 'http://ready', fileName: 'ready', status: 'ready', category: 'Other', dateAdded: '' },
{ id: 'legacy-active', url: 'http://active', fileName: 'active', status: 'processing', category: 'Other', dateAdded: '' },
] as any[],
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'get_pending_order') return ['legacy-ready'];
return undefined;
});
await expect(useDownloadStore.getState().startQueue('00000000-0000-0000-0000-000000000001'))
.resolves.toEqual(['legacy-ready']);
await expect(useDownloadStore.getState().pauseQueue('00000000-0000-0000-0000-000000000001'))
.resolves.toBe(2);
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'pause_download'))
.toHaveLength(2);
});
it('resumes paused items that were never dispatched through the master start action', async () => {
useDownloadStore.setState({
downloads: [
{
id: 'paused-before-dispatch',
url: 'http://paused-before-dispatch',
fileName: 'paused-before-dispatch',
destination: '/tmp',
status: 'paused',
category: 'Other',
dateAdded: '',
queueId: 'MAIN',
hasBeenDispatched: false
}
] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string, args?: unknown) => {
if (cmd === 'resume_download') return false;
if (cmd === 'enqueue_download') {
const id = (args as { item: { id: string } }).item.id;
return { id, filename: id };
}
if (cmd === 'get_pending_order') return ['paused-before-dispatch'];
return undefined;
});
await expect(useDownloadStore.getState().startAll()).resolves.toBe(1);
expect(ipc.invokeCommand).toHaveBeenCalledWith('resume_download', {
id: 'paused-before-dispatch',
queueId: 'MAIN'
});
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'paused-before-dispatch' }) })
);
expect(useDownloadStore.getState().downloads[0].hasBeenDispatched).toBe(true);
});
it('resumes an individually paused item that has no backend registration', async () => {
useDownloadStore.setState({
downloads: [{
id: 'individual-paused-before-dispatch',
url: 'http://individual-paused-before-dispatch',
fileName: 'individual-paused-before-dispatch',
destination: '/tmp',
status: 'paused',
category: 'Other',
dateAdded: '',
queueId: 'MAIN',
hasBeenDispatched: false
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string, args?: unknown) => {
if (cmd === 'resume_download') return false;
if (cmd === 'enqueue_download') {
const id = (args as { item: { id: string } }).item.id;
return { id, filename: id };
}
if (cmd === 'get_pending_order') return ['individual-paused-before-dispatch'];
return undefined;
});
await expect(useDownloadStore.getState().resumeDownload('individual-paused-before-dispatch'))
.resolves.toBe(true);
expect(ipc.invokeCommand).toHaveBeenCalledWith('resume_download', {
id: 'individual-paused-before-dispatch',
queueId: 'MAIN'
});
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'individual-paused-before-dispatch' }) })
);
expect(useDownloadStore.getState().downloads[0].hasBeenDispatched).toBe(true);
});
it('migrates legacy downloads without queue ids into the main queue', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'legacy',
url: 'https://example.com/legacy.bin',
fileName: 'legacy.bin',
status: 'ready',
category: 'Other',
dateAdded: ''
})];
}
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads[0].queueId)
.toBe('00000000-0000-0000-0000-000000000001');
});
it('pauses queued, downloading, processing, and retrying queue items', async () => {
useDownloadStore.setState({
downloads: ['queued', 'downloading', 'processing', 'retrying'].map((status, index) => ({
id: `${index}`,
url: `https://example.com/${index}`,
fileName: `${index}.bin`,
status,
category: 'Other',
dateAdded: '',
queueId: 'queue-a'
})) as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
expect(await useDownloadStore.getState().pauseQueue('queue-a')).toBe(4);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(call => call[0] === 'pause_download')
).toHaveLength(4);
});
it('assigns selected unfinished downloads to a queue without moving completed items', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'ready', status: 'ready', queueId: 'old' },
{ id: 'done', status: 'completed', queueId: 'old' },
{ id: 'paused', status: 'paused', queueId: 'new', queuePosition: 0 }
] as any[]
});
await useDownloadStore.getState().assignToQueue(['ready', 'done'], 'new');
expect(useDownloadStore.getState().downloads.find(item => item.id === 'ready')?.queueId).toBe('new');
expect(useDownloadStore.getState().downloads.find(item => item.id === 'done')?.queueId).toBe('old');
expect(useDownloadStore.getState().downloads
.filter(item => item.queueId === 'new')
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0))
.map(item => item.id)
).toEqual(['ready', 'paused']);
});
it('does not reassign an item that completes while queue assignment is awaiting cancellation', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'race-ready', status: 'ready', queueId: 'old' },
{ id: 'race-done', status: 'completed', queueId: 'old' }
] as any[]
});
let resolveCancellation!: () => void;
const cancellation = new Promise<void>(resolve => {
resolveCancellation = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'cancel_enqueue_generation') return cancellation as never;
return Promise.resolve(undefined) as never;
});
const assignment = useDownloadStore.getState().assignToQueue(['race-ready', 'race-done'], 'new');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'cancel_enqueue_generation',
expect.objectContaining({ id: 'race-ready' })
);
});
useDownloadStore.getState().updateDownload('race-ready', { status: 'completed' });
resolveCancellation();
await assignment;
expect(useDownloadStore.getState().downloads.find(item => item.id === 'race-ready')).toMatchObject({
status: 'completed',
queueId: 'old'
});
});
it('cancels the rest of a queue start when pause is requested during dispatch', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'queue-first', url: 'http://test/first', fileName: 'first', destination: '/tmp', status: 'ready', category: 'Other', dateAdded: '', queueId: 'race-queue', queuePosition: 0 },
{ id: 'queue-second', url: 'http://test/second', fileName: 'second', destination: '/tmp', status: 'ready', category: 'Other', dateAdded: '', queueId: 'race-queue', queuePosition: 1 }
] as any[]
});
let resolveEnqueue!: (value: { id: string; filename: string }) => void;
const enqueue = new Promise<{ id: string; filename: string }>(resolve => {
resolveEnqueue = resolve;
});
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'enqueue_download') return enqueue as never;
return Promise.resolve(undefined) as never;
});
const start = useDownloadStore.getState().startQueue('race-queue');
await vi.waitFor(() => {
expect(ipc.invokeCommand).toHaveBeenCalledWith(
'enqueue_download',
expect.objectContaining({ item: expect.objectContaining({ id: 'queue-first' }) })
);
});
const pause = useDownloadStore.getState().pauseQueue('race-queue');
resolveEnqueue({ id: 'queue-first', filename: 'first' });
await expect(pause).resolves.toBe(1);
await expect(start).resolves.toEqual([]);
expect(
vi.mocked(ipc.invokeCommand).mock.calls.filter(([command, args]) =>
command === 'enqueue_download' && (args as any)?.item?.id === 'queue-second'
)
).toHaveLength(0);
});
it('uses one atomic backend move and keeps queue positions unique around active transfers', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'active', status: 'downloading', queueId: 'move-queue', queuePosition: 0 },
{ id: 'one', status: 'queued', queueId: 'move-queue', queuePosition: 1 },
{ id: 'two', status: 'queued', queueId: 'move-queue', queuePosition: 2 },
{ id: 'three', status: 'queued', queueId: 'move-queue', queuePosition: 3 }
] as any[],
backendRegisteredIds: new Set(['three']),
pendingOrder: ['one', 'two', 'three']
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'move_many_in_queue') return ['one', 'three', 'two'];
if (command === 'get_pending_order') return ['one', 'three', 'two'];
return undefined;
});
await useDownloadStore.getState().moveInQueue('three', 'up');
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith('move_many_in_queue', {
ids: ['three'],
queueId: 'move-queue',
direction: 'up'
});
expect(vi.mocked(ipc.invokeCommand)).not.toHaveBeenCalledWith('move_in_queue', expect.anything());
const positions = useDownloadStore.getState().downloads
.filter(item => item.queueId === 'move-queue')
.map(item => item.queuePosition);
expect(new Set(positions).size).toBe(4);
});
it('rolls back and rejects a failed keyboard or header queue move', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'first', status: 'queued', queueId: 'move-failure-queue', queuePosition: 0 },
{ id: 'second', status: 'queued', queueId: 'move-failure-queue', queuePosition: 1 }
] as any[],
backendRegisteredIds: new Set(['second'])
});
vi.mocked(ipc.invokeCommand).mockRejectedValue(new Error('backend unavailable'));
await expect(
useDownloadStore.getState().moveInQueue('second', 'up')
).rejects.toThrow('backend unavailable');
expect(useDownloadStore.getState().downloads.map(item => item.queuePosition)).toEqual([0, 1]);
});
it('translates a drag target around staged rows before the atomic backend move', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'a', status: 'queued', queueId: 'drag-queue', queuePosition: 0 },
{ id: 'staged', status: 'staged', queueId: 'drag-queue', queuePosition: 1 },
{ id: 'b', status: 'queued', queueId: 'drag-queue', queuePosition: 2 },
{ id: 'c', status: 'queued', queueId: 'drag-queue', queuePosition: 3 }
] as any[],
backendRegisteredIds: new Set(['a', 'b', 'c']),
pendingOrder: ['a', 'b', 'c']
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
if (command === 'move_many_in_queue') return ['c', 'a', 'b'];
if (command === 'get_pending_order') return ['c', 'a', 'b'];
return undefined;
});
await useDownloadStore.getState().moveManyInQueueToPosition('c', 'drag-queue', 0);
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith('move_many_in_queue', {
ids: ['c'],
queueId: 'drag-queue',
direction: 'up',
targetIndex: 0
});
expect(useDownloadStore.getState().downloads.map(item => item.id).sort()).toEqual(['a', 'b', 'c', 'staged']);
expect(useDownloadStore.getState().downloads.find(item => item.id === 'c')?.queuePosition).toBe(0);
expect(useDownloadStore.getState().downloads.find(item => item.id === 'staged')?.queuePosition).toBe(2);
});
it('keeps a drag anchored when the queue changes before its serialized operation runs', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'a', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 0 },
{ id: 'b', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 1 },
{ id: 'c', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 2 }
] as any[],
backendRegisteredIds: new Set()
});
const operation = useDownloadStore.getState().moveManyInQueueToPosition(
'b',
'concurrent-drag-queue',
2,
'c'
);
useDownloadStore.setState({
downloads: [
{ id: 'a', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 0 },
{ id: 'b', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 1 },
{ id: 'new', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 2 },
{ id: 'c', status: 'queued', queueId: 'concurrent-drag-queue', queuePosition: 3 }
] as any[]
});
await operation;
expect(useDownloadStore.getState().downloads
.filter(item => item.queueId === 'concurrent-drag-queue')
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0))
.map(item => item.id)
).toEqual(['a', 'new', 'b', 'c']);
});
it('rolls back a failed drag move and rejects so the UI can report the failure', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'first', status: 'queued', queueId: 'rollback-queue', queuePosition: 0 },
{ id: 'second', status: 'queued', queueId: 'rollback-queue', queuePosition: 1 }
] as any[],
backendRegisteredIds: new Set(['first', 'second'])
});
vi.mocked(ipc.invokeCommand).mockRejectedValue(new Error('backend unavailable'));
await expect(
useDownloadStore.getState().moveManyInQueueToPosition('second', 'rollback-queue', 0)
).rejects.toThrow('backend unavailable');
expect(useDownloadStore.getState().downloads.map(item => item.queuePosition)).toEqual([0, 1]);
});
it('detaches a registered queued item through the backend before reassigning it', async () => {
useDownloadStore.setState({
downloads: [{
id: 'registered-queued',
url: 'https://example.com/file',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: 'old',
queuePosition: 0
}],
backendRegisteredIds: new Set(['registered-queued']),
pendingOrder: ['registered-queued']
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().assignToQueue(['registered-queued'], 'new');
expect(vi.mocked(ipc.invokeCommand)).toHaveBeenCalledWith(
'detach_download_for_reconfigure',
{ id: 'registered-queued' }
);
expect(useDownloadStore.getState().pendingOrder).not.toContain('registered-queued');
expect(useDownloadStore.getState().downloads[0].status).toBe('staged');
});
it('disables scheduler when its last selected queue is deleted', async () => {
const originalSettings = useSettingsStore.getState();
const setScheduler = vi.fn();
vi.mocked(useSettingsStore.getState).mockReturnValue({
scheduler: {
enabled: true,
selectedQueueIds: ['queue-a']
},
setScheduler
} as any);
useDownloadStore.setState({
queues: [
{ id: '00000000-0000-0000-0000-000000000001', name: 'Main Queue', isMain: true },
{ id: 'queue-a', name: 'Scheduled', isMain: false }
],
downloads: []
});
await useDownloadStore.getState().removeQueue('queue-a');
expect(setScheduler).toHaveBeenCalledWith(expect.objectContaining({
enabled: false,
selectedQueueIds: []
}));
vi.mocked(useSettingsStore.getState).mockReturnValue(originalSettings);
});
it('retains the UI item when backend removal fails', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'active', url: 'https://example.com/file', fileName: 'file', status: 'downloading', category: 'Other', dateAdded: '', queueId: 'main' }
] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation((command: string) => {
if (command === 'remove_download') {
return Promise.reject(new Error('writer did not stop')) as never;
}
return Promise.resolve(undefined) as never;
});
await expect(useDownloadStore.getState().removeDownload('active', true))
.rejects.toThrow('writer did not stop');
expect(useDownloadStore.getState().downloads.map(download => download.id))
.toEqual(['active']);
});
it('clears live progress when a download is removed', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'removed-progress', url: 'https://example.com/file', fileName: 'file', status: 'paused', category: 'Other', dateAdded: '' }
] as any[]
});
useDownloadProgressStore.getState().updateDownloadProgress('removed-progress', {
id: 'removed-progress',
fraction: 0.5,
speed: '1 MB/s',
eta: '10s',
size: '2 MB',
size_is_final: false
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().removeDownload('removed-progress');
expect(useDownloadProgressStore.getState().progressMap).toEqual({});
});
it('asks the backend to preserve resumable assets during replacement removal', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'paused', url: 'https://example.com/file', fileName: 'file', status: 'paused', category: 'Other', dateAdded: '' }
] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().removeDownload('paused', true, true);
expect(ipc.invokeCommand).toHaveBeenCalledWith('remove_download', {
id: 'paused',
deleteAssets: true,
preserveResumable: true
});
});
it('starts staged queue items in their persisted queue order', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'later', url: 'https://example.com/later', fileName: 'later', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 1 },
{ id: 'first', url: 'https://example.com/first', fileName: 'first', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 0 }
] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string, args?: unknown) => {
if (command === 'get_pending_order') {
return [(args as { queueId: string }).queueId === 'queue-a' ? 'first' : 'later'];
}
return undefined;
});
expect(await useDownloadStore.getState().startQueue('queue-a')).toEqual(['first', 'later']);
const enqueuedIds = vi.mocked(ipc.invokeCommand).mock.calls
.filter(call => call[0] === 'enqueue_download')
.map(call => (call[1] as any).item.id);
expect(enqueuedIds).toEqual(['first', 'later']);
expect((vi.mocked(ipc.invokeCommand).mock.calls.find(call =>
call[0] === 'enqueue_download'
)?.[1] as any).item.queue_id).toBe('queue-a');
});
it('preserves extension request headers and cookies for the Add modal', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/file.bin'],
referer: 'https://example.com/page',
silent: false,
filename: 'file.bin',
headers: 'X-Test: value',
cookies: 'session=secret',
cookie_scopes: [
{ url: 'https://mail.google.com/', cookies: 'SID=mail-session' },
{ url: 'https://accounts.google.com/', cookies: 'SID=account-session' }
],
media: false,
batch: false,
batch_name: null
});
const state = useDownloadStore.getState();
expect(state.isAddModalOpen).toBe(true);
expect(state.pendingAddUrls).toBe('https://example.com/file.bin');
expect(state.pendingAddReferer).toBe('https://example.com/page');
expect(state.pendingAddFilename).toBe('file.bin');
expect(state.pendingAddHeaders).toBe('X-Test: value');
expect(state.pendingAddCookies).toBe('session=secret');
expect(state.pendingAddRequestContexts['https://example.com/file.bin'].cookieScopes).toEqual([
{ url: 'https://mail.google.com/', cookies: 'SID=mail-session' },
{ url: 'https://accounts.google.com/', cookies: 'SID=account-session' }
]);
expect(state.pendingAddMediaUrls).toEqual([]);
});
it('does not reuse stale extension metadata for a later single-link handoff', async () => {
useDownloadStore.setState({
isAddModalOpen: true,
pendingAddUrls: '',
pendingAddReferer: 'https://old.example/page',
pendingAddFilename: '7aae36e6-00ec-4e7d-8dec-f14ace170bdb',
pendingAddHeaders: 'X-Old: value',
pendingAddCookies: 'old=session',
pendingAddMediaUrls: []
});
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg'],
referer: 'https://github.com/center2055/OnionHop/releases/tag/v3.5',
silent: false,
filename: null,
headers: 'User-Agent: Firefox Test',
cookies: null,
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
const state = useDownloadStore.getState();
expect(state.pendingAddUrls).toBe('https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg');
expect(state.pendingAddReferer).toBe('https://github.com/center2055/OnionHop/releases/tag/v3.5');
expect(state.pendingAddFilename).toBe('');
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
expect(state.pendingAddCookies).toBe('');
expect(state.pendingAddMediaUrls).toEqual([]);
});
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/downloads/report.pdf'],
referer: 'https://example.com/page',
silent: true,
filename: 'report.pdf',
headers: 'User-Agent: Test',
cookies: 'session=secret',
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
const state = useDownloadStore.getState();
expect(state.isAddModalOpen).toBe(true);
expect(state.pendingAddUrls).toBe('https://example.com/downloads/report.pdf');
expect(state.pendingAddReferer).toBe('https://example.com/page');
expect(state.pendingAddFilename).toBe('report.pdf');
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
expect(state.pendingAddCookies).toBe('session=secret');
expect(state.pendingAddMediaUrls).toEqual([]);
});
it('tracks selected-link batch context without changing ordinary multi-link handoffs', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/one.zip', 'https://example.com/two.zip'],
referer: 'https://example.com/gallery',
silent: false,
filename: null,
headers: null,
cookies: null,
cookie_scopes: null,
media: false,
batch: true,
batch_name: 'Example Gallery'
});
expect(useDownloadStore.getState().pendingAddBatch).toBe(true);
expect(useDownloadStore.getState().pendingAddBatchName).toBe('Example Gallery');
useDownloadStore.getState().toggleAddModal(false);
useDownloadStore.getState().openAddModalWithUrls(
'https://example.com/one.zip\nhttps://example.com/two.zip'
);
expect(useDownloadStore.getState().pendingAddBatch).toBe(false);
});
it('keeps each extension handoff context attached to its own URL while the Add Modal is open', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://first.example/file.zip'],
referer: 'https://first.example/page',
silent: true,
filename: 'first.zip',
headers: 'User-Agent: First Browser',
cookies: 'first=session',
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://second.example/file.zip'],
referer: 'https://second.example/page',
silent: true,
filename: 'second.zip',
headers: 'User-Agent: Second Browser',
cookies: 'second=session',
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
const state = useDownloadStore.getState();
expect(state.pendingAddUrls).toBe(
'https://first.example/file.zip\nhttps://second.example/file.zip'
);
expect(state.pendingAddRequestVersion).toBe(2);
expect(state.pendingAddRequestContexts).toEqual({
'https://first.example/file.zip': {
version: 1,
referer: 'https://first.example/page',
filename: 'first.zip',
headers: 'User-Agent: First Browser',
cookies: 'first=session',
media: false
},
'https://second.example/file.zip': {
version: 2,
referer: 'https://second.example/page',
filename: 'second.zip',
headers: 'User-Agent: Second Browser',
cookies: 'second=session',
media: false
}
});
});
it('preserves explicit extension media intent for non-allow-listed pages', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://adult.example/watch/123'],
referer: 'https://adult.example/watch/123',
silent: false,
filename: null,
headers: `Cookie: stale=${'x'.repeat(64 * 1024)}\nCookie2: stale=1\nAuthorization: Bearer stale\nProxy-Authorization: Basic stale\nSet-Cookie: stale=1\nSet-Cookie2: stale=1\nUser-Agent: Firefox Test`,
cookies: `oversized=${'x'.repeat(64 * 1024)}`,
cookie_scopes: null,
media: true,
batch: false,
batch_name: null
});
const state = useDownloadStore.getState();
expect(state.isAddModalOpen).toBe(true);
expect(state.pendingAddUrls).toBe('https://adult.example/watch/123');
expect(state.pendingAddMediaUrls).toEqual(['https://adult.example/watch/123']);
expect(state.pendingAddCookies).toBe('');
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
});
it('preserves extension cookies for ordinary captured downloads', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/private.zip'],
referer: 'https://example.com/downloads',
silent: true,
filename: 'private.zip',
headers: null,
cookies: 'session=secret',
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
expect(useDownloadStore.getState().pendingAddCookies).toBe('session=secret');
});
it('drops extension cookie scopes for explicit media captures', async () => {
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://media.example/watch/123'],
referer: 'https://media.example/watch/123',
silent: true,
filename: null,
headers: null,
cookies: null,
cookie_scopes: [
{ url: 'https://media.example/', cookies: 'session=secret' }
],
media: true,
batch: false,
batch_name: null
});
expect(useDownloadStore.getState().pendingAddRequestContexts['https://media.example/watch/123']?.cookieScopes)
.toBeUndefined();
});
it('clears stale request context when the same URL is captured without it later', async () => {
const url = 'https://example.com/file.zip';
await useDownloadStore.getState().handleExtensionDownload({
urls: [url],
referer: 'https://example.com/private',
silent: true,
filename: 'private.zip',
headers: 'Authorization: secret',
cookies: 'session=secret',
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
await useDownloadStore.getState().handleExtensionDownload({
urls: [url],
referer: null,
silent: true,
filename: null,
headers: null,
cookies: null,
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
expect(useDownloadStore.getState().pendingAddRequestContexts[url]).toEqual({
version: 2,
referer: '',
filename: '',
headers: '',
cookies: '',
media: false
});
});
it('deduplicates forced media URLs and drops stale media intent when opening fresh', async () => {
useDownloadStore.setState({
isAddModalOpen: true,
pendingAddUrls: 'https://adult.example/watch/123',
pendingAddMediaUrls: ['https://adult.example/watch/123']
});
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://adult.example/watch/123'],
referer: 'https://adult.example/watch/123',
silent: false,
filename: null,
headers: 'User-Agent: Firefox Test',
cookies: 'session=secret',
cookie_scopes: null,
media: true,
batch: false,
batch_name: null
});
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([
'https://adult.example/watch/123'
]);
useDownloadStore.setState({
isAddModalOpen: false,
pendingAddMediaUrls: ['https://stale.example/watch']
});
await useDownloadStore.getState().handleExtensionDownload({
urls: ['https://example.com/file.bin'],
referer: 'https://example.com/page',
silent: false,
filename: 'file.bin',
headers: 'User-Agent: Firefox Test',
cookies: null,
cookie_scopes: null,
media: false,
batch: false,
batch_name: null
});
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([]);
});
});