fix: close post-release lifecycle state gaps

This commit is contained in:
NimBold
2026-07-15 10:13:53 +03:30
parent 9224117d77
commit 82f2914077
4 changed files with 139 additions and 4 deletions
+70
View File
@@ -589,6 +589,44 @@ describe('useDownloadStore', () => {
});
});
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: 'file.bin' }];
}
if (cmd === 'get_pending_order') throw new Error('queue state unavailable');
if (cmd === 'resume_download') return true;
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().backendRegisteredIds.has('startup-accepted')).toBe(true);
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
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('redownloads fallback media without requiring a format selector', async () => {
useDownloadStore.setState({
downloads: [{
@@ -620,6 +658,38 @@ describe('useDownloadStore', () => {
);
});
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('starts and pauses all items regardless of legacy missing queue ids', async () => {
useDownloadStore.setState({
downloads: [
+14 -3
View File
@@ -818,6 +818,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
get().unregisterBackendIds([id]);
} catch (e) {
console.warn("Could not remove old download from backend", e);
throw e;
}
get().updateDownload(id, {
@@ -1175,9 +1176,12 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
.filter(result => !result.success)
.map(result => [result.id, result.error || 'Backend rejected the queued download.'])
);
const order = await invoke('get_pending_order', { queueId: null });
// Commit backend ownership as soon as enqueue_many accepts an item.
// The order query is a separate best-effort view read; if it fails,
// forgetting these registrations would let a later queue start
// enqueue the same backend lifecycle a second time.
set(state => ({
pendingOrder: order,
backendRegisteredIds: new Set([
...state.backendRegisteredIds,
...registeredIds
@@ -1189,11 +1193,18 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
status: 'failed' as const,
lastError: failedErrors.get(download.id)
}
: registeredIds.includes(download.id)
: registeredIds.includes(download.id)
? { ...download, hasBeenDispatched: true, lastError: undefined }
: download
)
}));
try {
const order = await invoke('get_pending_order', { queueId: null });
set({ pendingOrder: order });
} catch (e) {
console.error("Failed to refresh pending order after auto-resume:", e);
}
} catch (e) {
console.error("Failed to auto-resume active downloads:", e);
}