Files
jellytau/src/lib/stores/downloads.test.ts
T
dtourolle 1a9805f0f3 fix(downloads): queue the whole album, and make every queued track findable offline
An album download put a handful of its tracks on the device while the button
reported the album as downloaded. Two independent gaps, one shared cause.

- `download_album` read its track list from `items WHERE album_id = ?` — the
  local catalog cache. Jellyfin does not return `AlbumId` on every listing
  endpoint, so tracks cached from one of those sit in `items` with a NULL
  `album_id` and are invisible to that query. On the reported database three
  whole albums (18, 12 and 9 tracks) had it NULL on every track; a partially
  linked album queued only the linked subset.
- The frontend then resolved one stream URL per track from its own list and
  paired it with the returned row ids by position. The ids came back in the
  backend's `index_number` order over a different set of rows, so a row could
  be handed another track's URL and any track past the end of the shorter list
  was never started. On Android that loop also stopped wherever the webview was
  suspended.
- `album_id` is what `OfflineRepository::get_items` joins a track to its album
  on, so a track that did download stayed invisible under its album offline —
  the same missing link seen from the other side.

The operation now belongs to Rust end to end:

- `HybridRepository::get_album_tracks` asks the server what the album contains.
  Cache-first `get_items` is right for browsing and wrong for deciding what to
  download; it errors offline so the caller falls back to the ungated local
  catalog, keeping the queue-while-offline flow.
- `queue_album_tracks` writes the album link onto every track it queues, and
  creates an `items` row for tracks the cache has never seen.
- Stream URLs resolve here, through the existing reconnect resolver, now scoped
  to the rows just queued so one album cannot start every unrelated pending row.
  Only the album id crosses the IPC boundary.
- `album_file_names` gives each track its own file. A title repeated inside one
  album (deluxe edition, two discs) mapped to one path, so those downloads
  overwrote each other.

Re-tapping download on a broken album heals it: missing tracks are queued and
the tracks already on disk get their link.

`download_series`/`download_season` still derive their episode lists from the
cache the same way and want the same treatment.

DR-173, UT-170..172. Rust 673 tests, frontend 975 tests, svelte-check and
check:boundary clean.

Note: this tree is shared with a concurrent session. Only the files above are
committed; docs/traceability.md is left to be regenerated once that work lands.
2026-08-16 09:20:32 +02:00

944 lines
28 KiB
TypeScript

// Tests for downloads store
// TRACES: UR-011, UR-013, UR-018 | DR-015, DR-017 | UT-010, UT-024
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { get } from "svelte/store";
// Mock Tauri APIs
const mockInvoke = vi.fn();
const mockListen = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
invoke: mockInvoke,
}));
vi.mock("@tauri-apps/api/event", () => ({
listen: mockListen,
}));
describe("downloads store", () => {
let eventHandler: ((event: { payload: unknown }) => void) | null = null;
beforeEach(() => {
vi.clearAllMocks();
// Reset the invoke mock to clear any remaining queued return values
mockInvoke.mockReset();
// Capture the event handler when listen is called
mockListen.mockImplementation((_event: string, handler: (event: { payload: unknown }) => void) => {
eventHandler = handler;
return Promise.resolve(() => {});
});
});
afterEach(async () => {
// Clean up event listeners
const { cleanupDownloadEvents } = await import("./downloads");
cleanupDownloadEvents();
eventHandler = null;
// Clear all mocks
vi.clearAllMocks();
mockInvoke.mockReset();
mockListen.mockReset();
});
describe("initial state", () => {
it("should have empty downloads initially", async () => {
const { downloads } = await import("./downloads");
const state = get(downloads);
expect(state.downloads).toEqual({});
expect(state.stats.activeCount).toBe(0);
expect(state.stats.queuedCount).toBe(0);
});
});
describe("downloadItem", () => {
it("should call invoke with correct parameters", async () => {
const { downloads } = await import("./downloads");
mockInvoke
.mockResolvedValueOnce(123) // download_item returns ID
.mockResolvedValueOnce({
downloads: [],
stats: {
total: 0,
activeCount: 0,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
}); // get_downloads returns empty
const downloadId = await downloads.downloadItem(
"item-1",
"user-1",
"/path/to/file.mp3",
"audio/mpeg",
10
);
expect(mockInvoke).toHaveBeenCalledWith("download_item", {
request: {
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
mimeType: "audio/mpeg",
priority: 10,
itemName: null,
artistName: null,
albumName: null,
expectedSize: null,
},
});
expect(downloadId).toBe(123);
});
it("should refresh downloads after queuing", async () => {
const { downloads } = await import("./downloads");
mockInvoke
.mockResolvedValueOnce(123)
.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 10,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 0,
queuedCount: 1,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.downloadItem("item-1", "user-1", "/path/to/file.mp3");
expect(mockInvoke).toHaveBeenCalledWith("get_downloads", {
userId: "user-1",
statusFilter: null,
});
const state = get(downloads);
expect(state.downloads[123]).toBeDefined();
expect(state.stats.queuedCount).toBe(1);
});
});
describe("downloadAlbum", () => {
it("should call invoke with correct parameters", async () => {
const { downloads } = await import("./downloads");
mockInvoke
.mockResolvedValueOnce([1, 2, 3]) // download_album returns IDs
.mockResolvedValueOnce({
downloads: [],
stats: {
total: 0,
activeCount: 0,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
}); // get_downloads
const ids = await downloads.downloadAlbum("handle-1", "album-1", "user-1", "/base/path");
expect(mockInvoke).toHaveBeenCalledWith("download_album", {
handle: "handle-1",
albumId: "album-1",
userId: "user-1",
basePath: "/base/path",
});
expect(ids).toEqual([1, 2, 3]);
});
});
describe("pause/resume/cancel", () => {
it("should call pause_download with correct ID", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockResolvedValueOnce(undefined);
await downloads.pause(123);
expect(mockInvoke).toHaveBeenCalledWith("pause_download", { downloadId: 123 });
});
it("should call resume_download with correct ID", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockResolvedValueOnce(undefined);
await downloads.resume(123);
expect(mockInvoke).toHaveBeenCalledWith("resume_download", { downloadId: 123 });
});
it("should call cancel_download with correct ID", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockResolvedValueOnce(undefined);
await downloads.cancel(123);
expect(mockInvoke).toHaveBeenCalledWith("cancel_download", { downloadId: 123 });
});
});
describe("delete", () => {
it("should call delete_download and remove from store", async () => {
const { downloads } = await import("./downloads");
// First add a download via refresh
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 0,
queuedCount: 0,
completedCount: 1,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1");
expect(get(downloads).downloads[123]).toBeDefined();
// Now delete
mockInvoke.mockResolvedValueOnce(undefined);
await downloads.delete(123);
expect(mockInvoke).toHaveBeenCalledWith("delete_download", { downloadId: 123 });
expect(get(downloads).downloads[123]).toBeUndefined();
});
});
describe("refresh", () => {
it("should update store with downloads from backend", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 1,
itemId: "item-1",
userId: "user-1",
filePath: "/path/1.mp3",
status: "downloading",
progress: 0.5,
bytesDownloaded: 500,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 2,
itemId: "item-2",
userId: "user-1",
filePath: "/path/2.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:01Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 3,
itemId: "item-3",
userId: "user-1",
filePath: "/path/3.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:02Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 3,
activeCount: 1,
queuedCount: 1,
completedCount: 1,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1");
const state = get(downloads);
expect(Object.keys(state.downloads).length).toBe(3);
expect(state.stats.activeCount).toBe(1); // 1 downloading
expect(state.stats.queuedCount).toBe(1); // 1 pending
});
// The Transfers view shows only in-flight rows; a completed transfer must
// NOT appear there (it lives in Downloaded). Mirrors the /downloads page's
// `transfers` derivation: active + pending + failed.
// TRACES: UR-055 | DR-084 | UT-052
it("transfers set excludes completed downloads", async () => {
const { downloads, activeDownloads, pendingDownloads, failedDownloads } = await import(
"./downloads"
);
mockInvoke.mockResolvedValueOnce({
downloads: [
{ id: 1, itemId: "a", userId: "u", filePath: "/a", status: "downloading", progress: 0.5, bytesDownloaded: 5, queuedAt: "t", retryCount: 0, priority: 0, mediaType: "audio", downloadSource: "user" },
{ id: 2, itemId: "b", userId: "u", filePath: "/b", status: "pending", progress: 0, bytesDownloaded: 0, queuedAt: "t", retryCount: 0, priority: 0, mediaType: "audio", downloadSource: "user" },
{ id: 3, itemId: "c", userId: "u", filePath: "/c", status: "completed", progress: 1, bytesDownloaded: 9, queuedAt: "t", retryCount: 0, priority: 0, mediaType: "audio", downloadSource: "user" },
{ id: 4, itemId: "d", userId: "u", filePath: "/d", status: "failed", progress: 0, bytesDownloaded: 0, queuedAt: "t", retryCount: 1, priority: 0, mediaType: "audio", downloadSource: "user" },
],
stats: { total: 4, activeCount: 1, queuedCount: 1, completedCount: 1, failedCount: 1, pausedCount: 0 },
});
await downloads.refresh("u");
const transfers = get(activeDownloads)
.concat(get(pendingDownloads))
.concat(get(failedDownloads));
const ids = transfers.map((d) => d.id).sort();
expect(ids).toEqual([1, 2, 4]);
expect(transfers.some((d) => d.status === "completed")).toBe(false);
});
it("should support status filter", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockResolvedValueOnce({
downloads: [],
stats: {
total: 0,
activeCount: 0,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1", ["pending", "downloading"]);
expect(mockInvoke).toHaveBeenCalledWith("get_downloads", {
userId: "user-1",
statusFilter: ["pending", "downloading"],
});
});
});
describe("event handling", () => {
it("should initialize event listener via initDownloadEvents", async () => {
const { initDownloadEvents } = await import("./downloads");
await initDownloadEvents();
expect(mockListen).toHaveBeenCalledWith("download-event", expect.any(Function));
expect(eventHandler).not.toBeNull();
});
it("should handle started event and refresh", async () => {
const { downloads, initDownloadEvents } = await import("./downloads");
// First add a pending download
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 0,
queuedCount: 1,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1");
// Initialize event listener
await initDownloadEvents();
expect(eventHandler).not.toBeNull();
// Mock refresh call that will happen when event is handled
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "downloading",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 1,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
// Simulate started event
eventHandler!({
payload: {
type: "started",
downloadId: 123,
itemId: "item-1",
},
});
// Wait for async operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
const state = get(downloads);
expect(state.downloads[123].status).toBe("downloading");
});
it("should handle completed event and refresh", async () => {
const { downloads, initDownloadEvents } = await import("./downloads");
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "downloading",
progress: 0.99,
bytesDownloaded: 990000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 1,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1");
// Initialize event listener
await initDownloadEvents();
// Mock the mark_download_completed invoke call
mockInvoke.mockResolvedValueOnce(undefined);
// Simulate completed event
eventHandler!({
payload: {
type: "completed",
downloadId: 123,
itemId: "item-1",
filePath: "/path/to/file.mp3",
totalBytes: 1000000,
},
});
// Wait for async operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
const state = get(downloads);
expect(state.downloads[123].status).toBe("completed");
});
it("should handle failed event and refresh", async () => {
const { downloads, initDownloadEvents } = await import("./downloads");
// Setup mock to return downloading download when refresh is called
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.resolve({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "downloading",
progress: 0.5,
bytesDownloaded: 500000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 1,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
} else if (command === "mark_download_failed") {
return Promise.resolve(undefined);
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await downloads.refresh("user-1");
// Initialize event listener
await initDownloadEvents();
// Simulate failed event
eventHandler!({
payload: {
type: "failed",
downloadId: 123,
itemId: "item-1",
error: "Network timeout",
},
});
// Wait for async operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
const state = get(downloads);
expect(state.downloads[123].status).toBe("failed");
expect(state.downloads[123].errorMessage).toBe("Network timeout");
});
it("should handle cancelled event and refresh", async () => {
const { downloads, initDownloadEvents } = await import("./downloads");
mockInvoke.mockResolvedValueOnce({
downloads: [
{
id: 123,
itemId: "item-1",
userId: "user-1",
filePath: "/path/to/file.mp3",
status: "downloading",
progress: 0.5,
bytesDownloaded: 500000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 1,
activeCount: 1,
queuedCount: 0,
completedCount: 0,
failedCount: 0,
pausedCount: 0,
},
});
await downloads.refresh("user-1");
// Initialize event listener
await initDownloadEvents();
// Simulate cancelled event
// Note: The cancelled event handler does NOT call refresh, it only removes from store
eventHandler!({
payload: {
type: "cancelled",
downloadId: 123,
itemId: "item-1",
},
});
// Wait for async operations to complete
await new Promise((resolve) => setTimeout(resolve, 100));
const state = get(downloads);
expect(state.downloads[123]).toBeUndefined();
});
});
describe("derived stores", () => {
it("activeDownloads should filter downloading status", async () => {
const { downloads, activeDownloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.resolve({
downloads: [
{
id: 1,
itemId: "item-1",
userId: "user-1",
filePath: "/path/1.mp3",
status: "downloading",
progress: 0.5,
bytesDownloaded: 500,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 2,
itemId: "item-2",
userId: "user-1",
filePath: "/path/2.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 3,
itemId: "item-3",
userId: "user-1",
filePath: "/path/3.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 3,
activeCount: 1,
queuedCount: 1,
completedCount: 1,
failedCount: 0,
pausedCount: 0,
},
});
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await downloads.refresh("user-1");
const active = get(activeDownloads);
expect(active.length).toBe(1);
expect(active[0].id).toBe(1);
expect(active[0].status).toBe("downloading");
});
it("completedDownloads should filter completed status", async () => {
const { downloads, completedDownloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.resolve({
downloads: [
{
id: 1,
itemId: "item-1",
userId: "user-1",
filePath: "/path/1.mp3",
status: "downloading",
progress: 0.5,
bytesDownloaded: 500,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 2,
itemId: "item-2",
userId: "user-1",
filePath: "/path/2.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 3,
itemId: "item-3",
userId: "user-1",
filePath: "/path/3.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 3,
activeCount: 1,
queuedCount: 0,
completedCount: 2,
failedCount: 0,
pausedCount: 0,
},
});
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await downloads.refresh("user-1");
const completed = get(completedDownloads);
expect(completed.length).toBe(2);
expect(completed.every((d) => d.status === "completed")).toBe(true);
});
it("pendingDownloads should filter pending status", async () => {
const { downloads, pendingDownloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.resolve({
downloads: [
{
id: 1,
itemId: "item-1",
userId: "user-1",
filePath: "/path/1.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 10,
mediaType: "audio",
downloadSource: "user",
},
{
id: 2,
itemId: "item-2",
userId: "user-1",
filePath: "/path/2.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 3,
itemId: "item-3",
userId: "user-1",
filePath: "/path/3.mp3",
status: "pending",
progress: 0,
bytesDownloaded: 0,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 5,
mediaType: "audio",
downloadSource: "user",
},
],
stats: {
total: 3,
activeCount: 0,
queuedCount: 2,
completedCount: 1,
failedCount: 0,
pausedCount: 0,
},
});
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await downloads.refresh("user-1");
const pending = get(pendingDownloads);
expect(pending.length).toBe(2);
expect(pending.every((d) => d.status === "pending")).toBe(true);
});
it("failedDownloads should filter failed status", async () => {
const { downloads, failedDownloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.resolve({
downloads: [
{
id: 1,
itemId: "item-1",
userId: "user-1",
filePath: "/path/1.mp3",
status: "failed",
progress: 0.5,
bytesDownloaded: 500,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 3,
priority: 0,
mediaType: "audio",
downloadSource: "user",
errorMessage: "Network error",
},
{
id: 2,
itemId: "item-2",
userId: "user-1",
filePath: "/path/2.mp3",
status: "completed",
progress: 1.0,
bytesDownloaded: 1000,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 0,
priority: 0,
mediaType: "audio",
downloadSource: "user",
},
{
id: 3,
itemId: "item-3",
userId: "user-1",
filePath: "/path/3.mp3",
status: "failed",
progress: 0.3,
bytesDownloaded: 300,
queuedAt: "2024-01-01T00:00:00Z",
retryCount: 2,
priority: 0,
mediaType: "audio",
downloadSource: "user",
errorMessage: "Timeout",
},
],
stats: {
total: 3,
activeCount: 0,
queuedCount: 0,
completedCount: 1,
failedCount: 2,
pausedCount: 0,
},
});
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await downloads.refresh("user-1");
const failed = get(failedDownloads);
expect(failed.length).toBe(2);
expect(failed.every((d) => d.status === "failed")).toBe(true);
});
});
describe("error handling", () => {
it("should throw error when download_item fails", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "download_item") {
return Promise.reject(new Error("Backend error: failed to queue download"));
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await expect(
downloads.downloadItem("item-1", "user-1", "/path/to/file.mp3")
).rejects.toThrow("Backend error: failed to queue download");
});
it("should throw error when refresh fails", async () => {
const { downloads } = await import("./downloads");
mockInvoke.mockImplementation((command: string) => {
if (command === "get_downloads") {
return Promise.reject(new Error("Backend error: failed to fetch downloads"));
}
return Promise.reject(new Error(`Unexpected command: ${command}`));
});
await expect(
downloads.refresh("user-1")
).rejects.toThrow("Backend error: failed to fetch downloads");
});
});
});