feat: Tier-1 Model Manager with Hot-Swapping, HF Download Engine, bug fixes, and test repairs
- Phase 4: Hugging Face Download Engine — streaming GGUF downloads with progress, cancellation, and managed-model storage - Phase 5: Model Source Validation — source-mode compatibility, path/file validation, inline error UI with recovery guidance - Hot-swap: stop runtime → update model_path → restart atomic command - Model library: folder open, delete model file, local model management - SettingsPage unified hub: local models list + HF search with 3-state action buttons - Bug fixes: send button text restored (4 failing tests), reasoning-toggle aria-label, Rust warnings - All 89 tests passing, cargo check clean
This commit is contained in:
1 parent
01b629cb4a
commit
9a093dca15
23 files changed
+1798
-344
No files matched your search
@@ -0,0 +1,156 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { HUGGING_FACE_DOWNLOAD_EVENT, type HuggingFaceDownloadProgress } from "./api";
|
||||
import { useModelDownload } from "./useModelDownload";
|
||||
|
||||
const invokeMock = vi.fn();
|
||||
const listenMock = vi.fn();
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: (...args: unknown[]) => invokeMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/event", () => ({
|
||||
listen: (...args: unknown[]) => listenMock(...args),
|
||||
}));
|
||||
|
||||
function Harness({ token = "hf_token" }: { token?: string }) {
|
||||
const state = useModelDownload(token);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<output id="is-downloading">{String(state.isDownloading)}</output>
|
||||
<output id="progress">{state.progress.toFixed(1)}</output>
|
||||
<output id="speed">{state.speed.toFixed(1)}</output>
|
||||
<output id="downloaded">{state.downloaded.toFixed(1)}</output>
|
||||
<output id="total">{state.total.toFixed(1)}</output>
|
||||
<output id="active-file">{state.activeFile ?? ""}</output>
|
||||
<output id="error">{state.error ?? ""}</output>
|
||||
<button id="start" onClick={() => void state.startDownload("microsoft/phi-4", "phi-4-q4.gguf")}>
|
||||
Start
|
||||
</button>
|
||||
<button id="cancel" onClick={() => void state.cancelDownload()}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("useModelDownload", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let progressListener: ((event: { payload: HuggingFaceDownloadProgress }) => void) | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error React test environment flag
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
invokeMock.mockReset();
|
||||
listenMock.mockReset();
|
||||
progressListener = null;
|
||||
listenMock.mockImplementation(async (eventName, callback) => {
|
||||
if (eventName === HUGGING_FACE_DOWNLOAD_EVENT) {
|
||||
progressListener = callback as (event: { payload: HuggingFaceDownloadProgress }) => void;
|
||||
}
|
||||
return () => undefined;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
// @ts-expect-error React test environment flag
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = false;
|
||||
});
|
||||
|
||||
it("starts a Hugging Face download with the active token and tracks progress events", async () => {
|
||||
invokeMock.mockResolvedValue("download-1");
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Harness token="hf_secret" />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
(container.querySelector("#start") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(invokeMock).toHaveBeenCalledWith("start_hugging_face_download_command", {
|
||||
repo: "microsoft/phi-4",
|
||||
fileName: "phi-4-q4.gguf",
|
||||
token: "hf_secret",
|
||||
});
|
||||
expect(container.querySelector("#is-downloading")?.textContent).toBe("true");
|
||||
expect(container.querySelector("#active-file")?.textContent).toBe("microsoft/phi-4:phi-4-q4.gguf");
|
||||
|
||||
await act(async () => {
|
||||
progressListener?.({
|
||||
payload: {
|
||||
downloadId: "download-1",
|
||||
repo: "microsoft/phi-4",
|
||||
fileName: "phi-4-q4.gguf",
|
||||
phase: "progress",
|
||||
progressPercentage: 37.5,
|
||||
downloadedMB: 768,
|
||||
totalMB: 2048,
|
||||
downloadSpeed: 12.4,
|
||||
destinationPath: null,
|
||||
message: null,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(container.querySelector("#progress")?.textContent).toBe("37.5");
|
||||
expect(container.querySelector("#speed")?.textContent).toBe("12.4");
|
||||
expect(container.querySelector("#downloaded")?.textContent).toBe("768.0");
|
||||
expect(container.querySelector("#total")?.textContent).toBe("2048.0");
|
||||
});
|
||||
|
||||
it("ignores progress for stale downloads and cancels the active one", async () => {
|
||||
invokeMock.mockResolvedValueOnce("download-1").mockResolvedValueOnce(undefined);
|
||||
|
||||
await act(async () => {
|
||||
root.render(<Harness />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
(container.querySelector("#start") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
progressListener?.({
|
||||
payload: {
|
||||
downloadId: "download-stale",
|
||||
repo: "other/repo",
|
||||
fileName: "other.gguf",
|
||||
phase: "progress",
|
||||
progressPercentage: 90,
|
||||
downloadedMB: 90,
|
||||
totalMB: 100,
|
||||
downloadSpeed: 4,
|
||||
destinationPath: null,
|
||||
message: null,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(container.querySelector("#progress")?.textContent).toBe("0.0");
|
||||
|
||||
await act(async () => {
|
||||
(container.querySelector("#cancel") as HTMLButtonElement).click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(invokeMock).toHaveBeenLastCalledWith("cancel_hugging_face_download_command", {
|
||||
downloadId: "download-1",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user