Skip to content

Commit 6bf489b

Browse files
authored
Merge pull request #266 from Oxygen-Low/feature/llm-model-finder-app-14585450540918251285
Add LLM Model Finder App
2 parents f67e9fc + cbbdfa3 commit 6bf489b

3 files changed

Lines changed: 564 additions & 2 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/** @vitest-environment jsdom */
2+
import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
3+
import { LlmModelFinderApp } from "./LlmModelFinder";
4+
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
5+
6+
// Mock ResizeObserver for Radix UI
7+
class MockResizeObserver {
8+
observe() {}
9+
unobserve() {}
10+
disconnect() {}
11+
}
12+
global.ResizeObserver = MockResizeObserver as any;
13+
14+
// Mock global fetch
15+
const mockFetch = vi.fn();
16+
global.fetch = mockFetch;
17+
18+
// Mock the ScrollArea component to avoid Radix UI issues in tests
19+
vi.mock("@/components/ui/scroll-area", () => ({
20+
ScrollArea: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
21+
}));
22+
23+
// Mock the Slider component to avoid Radix UI useSize issues
24+
vi.mock("@/components/ui/slider", () => ({
25+
Slider: ({ value, onValueChange, min, max }: any) => (
26+
<input
27+
type="range"
28+
min={min}
29+
max={max}
30+
value={value[0]}
31+
onChange={(e) => onValueChange([parseInt(e.target.value)])}
32+
/>
33+
),
34+
}));
35+
36+
describe("LlmModelFinderApp", () => {
37+
beforeEach(() => {
38+
mockFetch.mockClear();
39+
});
40+
41+
afterEach(() => {
42+
cleanup();
43+
});
44+
45+
it("renders the hardware config section", () => {
46+
render(<LlmModelFinderApp />);
47+
expect(screen.getByText("Hardware Config")).toBeDefined();
48+
expect(screen.getByText("System RAM")).toBeDefined();
49+
expect(screen.getByText("GPU Acceleration")).toBeDefined();
50+
});
51+
52+
it("fetches and displays models based on hardware specs", async () => {
53+
const mockModels = [
54+
{
55+
id: "meta-llama/Llama-2-7b-chat-hf",
56+
downloads: 1000000,
57+
likes: 5000,
58+
lastModified: "2023-10-01T00:00:00Z",
59+
tags: ["text-generation", "conversational", "llama-2", "params:7B"],
60+
}
61+
];
62+
63+
mockFetch.mockResolvedValueOnce({
64+
ok: true,
65+
json: async () => mockModels,
66+
});
67+
68+
render(<LlmModelFinderApp />);
69+
70+
const findButton = screen.getByText("Find Compatible Models");
71+
fireEvent.click(findButton);
72+
73+
await waitFor(() => {
74+
expect(screen.getByText("Llama-2-7b-chat-hf")).toBeDefined();
75+
expect(screen.getByText("meta-llama")).toBeDefined();
76+
});
77+
});
78+
79+
it("filters out models that exceed RAM limit", async () => {
80+
const mockModels = [
81+
{
82+
id: "meta-llama/Llama-2-70b-chat-hf",
83+
downloads: 500000,
84+
likes: 2000,
85+
lastModified: "2023-10-01T00:00:00Z",
86+
tags: ["text-generation", "params:70B"],
87+
},
88+
{
89+
id: "meta-llama/Llama-2-7b-chat-hf",
90+
downloads: 1000000,
91+
likes: 5000,
92+
lastModified: "2023-10-01T00:00:00Z",
93+
tags: ["text-generation", "params:7B"],
94+
}
95+
];
96+
97+
mockFetch.mockResolvedValueOnce({
98+
ok: true,
99+
json: async () => mockModels,
100+
});
101+
102+
render(<LlmModelFinderApp />);
103+
104+
const findButton = screen.getByText("Find Compatible Models");
105+
fireEvent.click(findButton);
106+
107+
await waitFor(() => {
108+
expect(screen.getByText("Llama-2-7b-chat-hf")).toBeDefined();
109+
});
110+
111+
expect(screen.queryByText("Llama-2-70b-chat-hf")).toBeNull();
112+
});
113+
114+
it("handles GPU mode toggle and VRAM filtering", async () => {
115+
const mockModels = [
116+
{
117+
id: "heavy-gpu-model",
118+
downloads: 100,
119+
likes: 10,
120+
lastModified: "2023-10-01T00:00:00Z",
121+
tags: ["text-generation", "params:40B"], // ~29GB RAM
122+
}
123+
];
124+
125+
mockFetch.mockResolvedValueOnce({
126+
ok: true,
127+
json: async () => mockModels,
128+
});
129+
130+
render(<LlmModelFinderApp />);
131+
132+
// Enable GPU
133+
const gpuSwitch = screen.getByLabelText("GPU Acceleration");
134+
fireEvent.click(gpuSwitch);
135+
136+
// Default VRAM is 8GB. 40B model won't fit.
137+
const findButton = screen.getByText("Find Compatible Models");
138+
fireEvent.click(findButton);
139+
140+
await waitFor(() => {
141+
expect(screen.queryByText("heavy-gpu-model")).toBeNull();
142+
});
143+
144+
// Increase VRAM to 40GB
145+
const vramSlider = screen.getAllByRole("slider").find(s => s.getAttribute("max") === "48");
146+
if (vramSlider) {
147+
fireEvent.change(vramSlider, { target: { value: "40" } });
148+
}
149+
150+
mockFetch.mockResolvedValueOnce({
151+
ok: true,
152+
json: async () => mockModels,
153+
});
154+
fireEvent.click(findButton);
155+
156+
await waitFor(() => {
157+
expect(screen.getByText("heavy-gpu-model")).toBeDefined();
158+
});
159+
});
160+
161+
it("filters models by search query", async () => {
162+
const mockModels = [
163+
{ id: "owner/llama", downloads: 10, likes: 1, lastModified: "2023", tags: ["text-generation", "params:7B"] },
164+
{ id: "owner/mistral", downloads: 10, likes: 1, lastModified: "2023", tags: ["text-generation", "params:7B"] }
165+
];
166+
167+
mockFetch.mockResolvedValue({ ok: true, json: async () => mockModels });
168+
169+
render(<LlmModelFinderApp />);
170+
171+
const input = screen.getByPlaceholderText("Filter by name...");
172+
fireEvent.change(input, { target: { value: "llama" } });
173+
174+
fireEvent.click(screen.getByText("Find Compatible Models"));
175+
176+
await waitFor(() => {
177+
expect(screen.getByText("llama")).toBeDefined();
178+
expect(screen.queryByText("mistral")).toBeNull();
179+
});
180+
});
181+
182+
it("handles fetch errors gracefully", async () => {
183+
mockFetch.mockRejectedValueOnce(new Error("API Down"));
184+
185+
render(<LlmModelFinderApp />);
186+
fireEvent.click(screen.getByText("Find Compatible Models"));
187+
188+
await waitFor(() => {
189+
expect(screen.getByText("Search Failed")).toBeDefined();
190+
expect(screen.getByText("API Down")).toBeDefined();
191+
});
192+
});
193+
194+
it("filters out models with unknown size (ramRequired === 0)", async () => {
195+
const mockModels = [
196+
{ id: "owner/unknown", downloads: 10, likes: 1, lastModified: "2023", tags: ["text-generation"] } // No params
197+
];
198+
199+
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockModels });
200+
201+
render(<LlmModelFinderApp />);
202+
fireEvent.click(screen.getByText("Find Compatible Models"));
203+
204+
await waitFor(() => {
205+
expect(screen.queryByText("unknown")).toBeNull();
206+
expect(screen.getByText("No models found")).toBeDefined();
207+
});
208+
});
209+
});

0 commit comments

Comments
 (0)