🏗️ Build and Test JellyTau / Run Tests (push) Successful in 29m23s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 44s
📱 Test APK / Build test APK (push) Successful in 43m47s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 6m33s
Traceability Validation / Check Requirement Traces (push) Successful in 14s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 5m21s
Two frontend halves of the version-compatibility work.
The login flow now renders ServerCompatibility. A server below the floor blocks
with a message naming the minimum; a server newer than this build gets a
non-blocking note and proceeds; an unreadable version says nothing at all,
because refusing — or even warning — on a version string we could not parse would
punish the user for a limitation of ours.
The frontend never receives a version number to reason about, only the opaque
verdict, for the same reason it never receives an item-type list. Rust decides
whether the server is usable; the frontend decides only how that reads.
Separately, imageCache.getCachedImageUrl is deleted. It built
${serverUrl}/Items/${itemId}/Images/${imageType} in Svelte — a Jellyfin route in
the presentation layer, which is domain logic by this project's own litmus test
(would it change if Jellyfin changed its API?). check:boundary does not catch it:
the tripwire flags item-type array literals, not route strings.
It was also entirely unused. Nothing outside its own file and test ever called
it; the live path is CachedImage.svelte -> commands.imageGetUrl -> Rust, which
was already correct. So the leak was in dead code and the fix is a deletion
rather than a migration.
One consequence left deliberately unacted: that function was the last
convertFileSrc caller, so the asset-protocol grant narrowed to
$APPDATA/thumbnails/** under DR-198 now has no caller at all. Dropping a
capability grant is a security change that deserves its own commit and its own
testing on Android, not a side effect of deleting dead code. Noted in the file.
TRACES: UR-012, UR-085 | DR-285, DR-286
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
/**
|
|
* Image cache service tests
|
|
*
|
|
* TRACES: UR-007 | DR-016
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import {
|
|
getCacheStats,
|
|
setCacheLimit,
|
|
clearCache,
|
|
deleteItemCache,
|
|
formatBytes,
|
|
gbToBytes,
|
|
bytesToGb,
|
|
} from "./imageCache";
|
|
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: vi.fn(async (command: string, args?: any) => {
|
|
if (command === "thumbnail_get_cached") {
|
|
return null; // No cached image
|
|
}
|
|
if (command === "thumbnail_get_stats") {
|
|
return {
|
|
totalSizeBytes: 1024 * 1024,
|
|
itemCount: 10,
|
|
limitBytes: 1024 * 1024 * 1024,
|
|
};
|
|
}
|
|
if (command === "thumbnail_set_limit") {
|
|
return undefined;
|
|
}
|
|
if (command === "thumbnail_clear_cache") {
|
|
return undefined;
|
|
}
|
|
if (command === "thumbnail_delete_item") {
|
|
return undefined;
|
|
}
|
|
if (command === "thumbnail_save") {
|
|
return undefined;
|
|
}
|
|
return null;
|
|
}),
|
|
convertFileSrc: vi.fn((path: string) => `asset://path/${path}`),
|
|
}));
|
|
|
|
describe("image cache service", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("cache statistics", () => {
|
|
it("should get cache statistics", async () => {
|
|
const stats = await getCacheStats();
|
|
expect(stats).toHaveProperty("totalSizeBytes");
|
|
expect(stats).toHaveProperty("itemCount");
|
|
expect(stats).toHaveProperty("limitBytes");
|
|
expect(typeof stats.totalSizeBytes).toBe("number");
|
|
expect(typeof stats.itemCount).toBe("number");
|
|
});
|
|
|
|
it("should set cache limit", async () => {
|
|
const limit = 1024 * 1024 * 1024 * 5; // 5GB
|
|
await setCacheLimit(limit);
|
|
|
|
const { invoke } = await import("@tauri-apps/api/core");
|
|
const invokeSpy = vi.mocked(invoke);
|
|
const setLimitCall = invokeSpy.mock.calls.find((call) => call[0] === "thumbnail_set_limit");
|
|
expect(setLimitCall).toBeDefined();
|
|
expect(setLimitCall![1]).toHaveProperty("limitBytes", limit);
|
|
});
|
|
});
|
|
|
|
describe("cache clearing", () => {
|
|
it("should clear all cached thumbnails", async () => {
|
|
await clearCache();
|
|
|
|
const { invoke } = await import("@tauri-apps/api/core");
|
|
const invokeSpy = vi.mocked(invoke);
|
|
expect(invokeSpy).toHaveBeenCalledWith("thumbnail_clear_cache");
|
|
});
|
|
|
|
it("should delete cache for specific item", async () => {
|
|
await deleteItemCache("item-456");
|
|
|
|
const { invoke } = await import("@tauri-apps/api/core");
|
|
const invokeSpy = vi.mocked(invoke);
|
|
const deleteCall = invokeSpy.mock.calls.find((call) => call[0] === "thumbnail_delete_item");
|
|
expect(deleteCall).toBeDefined();
|
|
expect(deleteCall![1]).toHaveProperty("itemId", "item-456");
|
|
});
|
|
});
|
|
|
|
describe("byte formatting", () => {
|
|
it("should format bytes", () => {
|
|
expect(formatBytes(512)).toContain("B");
|
|
expect(formatBytes(512)).not.toContain("KB");
|
|
});
|
|
|
|
it("should format kilobytes", () => {
|
|
expect(formatBytes(1024)).toContain("KB");
|
|
});
|
|
|
|
it("should format megabytes", () => {
|
|
expect(formatBytes(1024 * 1024)).toContain("MB");
|
|
});
|
|
|
|
it("should format gigabytes", () => {
|
|
expect(formatBytes(1024 * 1024 * 1024)).toContain("GB");
|
|
});
|
|
|
|
it("should format with correct precision", () => {
|
|
const result = formatBytes(1024 * 1.5);
|
|
expect(result).toMatch(/\d+\.\d+ KB/);
|
|
});
|
|
});
|
|
|
|
describe("unit conversion", () => {
|
|
it("should convert gigabytes to bytes", () => {
|
|
const bytes = gbToBytes(1);
|
|
expect(bytes).toBe(1024 * 1024 * 1024);
|
|
});
|
|
|
|
it("should convert bytes to gigabytes", () => {
|
|
const gb = bytesToGb(1024 * 1024 * 1024);
|
|
expect(gb).toBe(1);
|
|
});
|
|
|
|
it("should handle fractional conversions", () => {
|
|
const bytes = gbToBytes(0.5);
|
|
expect(bytes).toBe(512 * 1024 * 1024);
|
|
|
|
const gb = bytesToGb(512 * 1024 * 1024);
|
|
expect(gb).toBe(0.5);
|
|
});
|
|
});
|
|
});
|