Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 2 additions & 32 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

190 changes: 190 additions & 0 deletions desktop/src/apps/LibraryApp.storage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { LibraryApp } from "./LibraryApp";

const MOCK_ITEMS = [
{
id: "lib-item-1",
title: "YouTube Video",
source_type: "youtube",
source_url: "https://youtube.com/watch?v=1",
source_id: "yt-1",
author: "YT Author",
summary: "Summary 1",
content: "Content 1",
media_path: null,
thumbnail: null,
categories: [],
tags: [],
metadata: {},
status: "ready",
monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" },
created_at: 1700000000,
updated_at: 1700000000,
},
{
id: "lib-item-2",
title: "Reddit Post",
source_type: "reddit",
source_url: "https://reddit.com/r/test",
source_id: "rp-1",
author: "Redditor",
summary: "Summary 2",
content: "Content 2",
media_path: null,
thumbnail: null,
categories: [],
tags: [],
metadata: {},
status: "ready",
monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" },
created_at: 1700003600,
updated_at: 1700003600,
},
{
id: "lib-item-3",
title: "GitHub Repo",
source_type: "github",
source_url: "https://github.com/test/repo",
source_id: "gh-1",
author: "Dev",
summary: "Summary 3",
content: "Content 3",
media_path: null,
thumbnail: null,
categories: [],
tags: [],
metadata: {},
status: "processing",
monitor: { current_interval: 0, frequency: 0, decay_rate: 0, pinned: false, last_poll: null, last_hash: "" },
created_at: 1700007200,
updated_at: 1700007200,
},
];

const MOCK_AGENTS = [
{ name: "Agent 1", color: "#ff0000" },
{ name: "Agent 2", color: "#00ff00" },
];

function createFetchMock(overrides: Record<string, Promise<Response>> = {}) {
return vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith("/api/knowledge/items")) {
for (const [prefix, response] of Object.entries(overrides)) {
if (url.startsWith(prefix)) return response;
}
return Promise.resolve({
ok: true,
status: 200,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve({ items: MOCK_ITEMS, count: MOCK_ITEMS.length }),
} as Response);
}
if (url === "/api/agents") {
return Promise.resolve({
ok: true,
status: 200,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve(MOCK_AGENTS),
} as Response);
}
if (url === "/api/knowledge/subscriptions") {
return Promise.resolve({
ok: true,
status: 200,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve({ subscriptions: [] }),
} as Response);
}
return Promise.resolve({
ok: false,
status: 404,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve({}),
} as Response);
});
}

describe("LibraryApp storage view", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it("renders the view mode toggle and switches to storage view", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The storage view has a branch for items.length === 0 showing "No items to account for", but there's no test for it. Zero-coverage edge case.

🩹 The Fix: Add a test:

Suggested change
it("renders the view mode toggle and switches to storage view", async () => {
it("shows empty state when no items exist", async () => {
const emptyOverrides: Record<string, Promise<Response>> = {
"/api/knowledge/items": Promise.resolve({
ok: true,
status: 200,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve({ items: [], count: 0 }),
} as Response),
};
vi.stubGlobal("fetch", createFetchMock(emptyOverrides) as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);
await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));
await waitFor(() => screen.getByText("No items to account for"));
});

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The storageViewUI has a branch for items.length === 0 (line 1000-1009) showing "No items to account for", but no test covers it. Untested code is broken code waiting to happen.

🩹 The Fix: Add a test that renders LibraryApp with empty items and asserts the empty state message appears in storage view.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);

await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));

await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 });
});

it("shows per-source totals sorted by bytes descending", async () => {
vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);

await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));

await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 });
expect(screen.getAllByText("YouTube").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Reddit").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("GitHub").length).toBeGreaterThanOrEqual(1);
});

it("shows per-item rows sorted by bytes descending", async () => {
vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);

await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));

await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 });
expect(screen.getByText("YouTube Video")).toBeInTheDocument();
expect(screen.getByText("Reddit Post")).toBeInTheDocument();
expect(screen.getByText("GitHub Repo")).toBeInTheDocument();
});

it("does not show paused-at-cap when total is under cap", async () => {
vi.stubGlobal("fetch", createFetchMock() as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);

await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));

await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 });
expect(screen.queryByText(/Paused at cap/)).toBeNull();
});

it("shows paused-at-cap warning when total exceeds cap", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: This test creates 60 mock items and assumes their total mock bytes will exceed the 50GB cap. But mockItemBytes generates 100MB–4.1GB per item deterministically via a hash of the ID. With 60 items at minimum 100MB each, that's only 6GB — nowhere near 50GB. At maximum it's 246GB. Whether this test passes depends entirely on the hash values of overflow-0 through overflow-59. This is a flaky test waiting to happen.

🩹 The Fix:

Suggested change
it("shows paused-at-cap warning when total exceeds cap", async () => {
const manyItems = Array.from({ length: 60 }, (_, i) => ({
...MOCK_ITEMS[0],
id: `overflow-${i}`,
title: `Overflow Item ${i}`,
source_type: "youtube",
created_at: 1700000000 + i,
updated_at: 1700000000 + i,
// Force large mock bytes by using IDs that hash to max values
// Or better: mock deriveMockStorageData directly in the test
}));

Better approach: Mock the fetch to return items, then spy on deriveMockStorageData or just override the storage calculation in the test. But since it's not exported, the simplest fix is to use IDs that you've verified produce large hash values, or increase the item count significantly (e.g., 600 items guarantees >50GB even at minimum).

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: This test creates 60 items to exceed the 50GB cap, but mockItemBytes returns a minimum of 100MB per item (100 * 1024 * 1024). 60 × 100MB = 6GB — an order of magnitude short of 50GB. Whether this passes depends entirely on hash lottery of the overflow-{i} IDs. This is a flaky time bomb.

🩹 The Fix: Either mock deriveMockStorageData directly to return a controlled total_bytes > cap_bytes, or increase the item count to ~500+, or override mockItemBytes in the test.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const manyItems = Array.from({ length: 60 }, (_, i) => ({
...MOCK_ITEMS[0],
id: `overflow-${i}`,
title: `Overflow Item ${i}`,
source_type: "youtube",
created_at: 1700000000 + i,
updated_at: 1700000000 + i,
}));

const overrides: Record<string, Promise<Response>> = {
"/api/knowledge/items": Promise.resolve({
ok: true,
status: 200,
headers: new Map([["content-type", "application/json"]]),
json: () => Promise.resolve({ items: manyItems, count: manyItems.length }),
} as Response),
};

vi.stubGlobal("fetch", createFetchMock(overrides) as unknown as typeof fetch);
render(<LibraryApp windowId="test-win" />);

await waitFor(() => screen.getByRole("radio", { name: "storage" }));
fireEvent.click(screen.getByRole("radio", { name: "storage" }));

await waitFor(() => screen.getByText("Storage Accounting"), { timeout: 5000 });
expect(screen.getByText(/Paused at cap/)).toBeInTheDocument();
});
});
Loading
Loading