import { beforeEach, describe, expect, it, vi } from "vitest"; beforeEach(() => vi.clearAllMocks()); vi.mock("./search", () => ({ searchText: vi.fn(), searchImages: vi.fn(), })); vi.mock("./history", () => ({ getCurrentSearchRunId: vi.fn(() => "run-1"), saveChatMessageForQuery: vi.fn(() => Promise.resolve()), updateSearchResults: vi.fn(() => Promise.resolve()), })); vi.mock("./pubSub", () => ({ updateImageSearchResults: vi.fn(), updateLlmTextSearchResults: vi.fn(), updateTextSearchResults: vi.fn(), updateTextSearchStale: vi.fn(), })); vi.mock("./relatedSearchQuery", () => ({ generateRelatedSearchQuery: vi.fn(() => Promise.resolve("")), })); vi.mock("./logEntries", () => ({ addLogEntry: vi.fn(), })); vi.mock("./textGenerationUtilities", () => ({ searchResultsToConsider: 6, })); import { persistChatMessages, refreshImageSearchResults, refreshTextSearchResults, runFollowUpSearch, } from "./chatHelpers"; import { saveChatMessageForQuery } from "./history"; import { updateImageSearchResults, updateLlmTextSearchResults, updateTextSearchResults, updateTextSearchStale, } from "./pubSub"; import { searchImages, searchText } from "./search"; import type { ImageSearchResults, TextSearchResults } from "./types"; const mockSearchText = vi.mocked(searchText); const mockSearchImages = vi.mocked(searchImages); const mockSaveChat = vi.mocked(saveChatMessageForQuery); const mockUpdateText = vi.mocked(updateTextSearchResults); const mockUpdateImage = vi.mocked(updateImageSearchResults); const mockUpdateLlmText = vi.mocked(updateLlmTextSearchResults); const mockUpdateTextStale = vi.mocked(updateTextSearchStale); describe("refreshTextSearchResults", () => { it("deduplicates by URL and appends fresh results", async () => { const existing: TextSearchResults = [ ["Existing", "snippet", "https://existing.com"], ]; const fresh: TextSearchResults = [ ["New", "snippet", "https://new.com"], ["Duplicate", "snippet", "https://existing.com"], ]; mockSearchText.mockResolvedValueOnce({ results: fresh, stale: false }); await refreshTextSearchResults("query", 10, existing); expect(mockUpdateText).toHaveBeenCalledWith([ ["Existing", "snippet", "https://existing.com"], ["New", "snippet", "https://new.com"], ]); expect(mockUpdateTextStale).toHaveBeenCalledWith(false); }); it("skips updates when all fresh results are duplicates", async () => { const existing: TextSearchResults = [ ["Title", "snippet", "https://dup.com"], ]; const fresh: TextSearchResults = [["Title", "snippet", "https://dup.com"]]; mockSearchText.mockResolvedValueOnce({ results: fresh, stale: false }); await refreshTextSearchResults("query", 10, existing); expect(mockUpdateText).not.toHaveBeenCalled(); }); it("skips updates when there are no fresh results", async () => { mockSearchText.mockResolvedValueOnce({ results: [], stale: false }); await refreshTextSearchResults("query", 10, []); expect(mockUpdateText).not.toHaveBeenCalled(); expect(mockUpdateLlmText).not.toHaveBeenCalled(); expect(mockUpdateTextStale).not.toHaveBeenCalled(); }); it("raises the stale banner when the fresh batch was served from the cache", async () => { mockSearchText.mockResolvedValueOnce({ results: [["New", "snippet", "https://new.com"]], stale: true, }); await refreshTextSearchResults("query", 10, []); expect(mockUpdateTextStale).toHaveBeenCalledWith(true); }); }); describe("refreshImageSearchResults", () => { it("deduplicates by URL and prepends fresh results", async () => { const existing: ImageSearchResults = [ ["Existing", "https://existing.com", "thumb", "source"], ]; const fresh: ImageSearchResults = [ ["New", "https://new.com", "thumb", "source"], ["Duplicate", "https://existing.com", "thumb", "source"], ]; mockSearchImages.mockResolvedValueOnce({ results: fresh, stale: false }); await refreshImageSearchResults("query", 10, existing); expect(mockUpdateImage).toHaveBeenCalledWith([ ["New", "https://new.com", "thumb", "source"], ["Existing", "https://existing.com", "thumb", "source"], ]); }); it("skips updates when there are no fresh results", async () => { mockSearchImages.mockResolvedValueOnce({ results: [], stale: false }); await refreshImageSearchResults("query", 10, []); expect(mockUpdateImage).not.toHaveBeenCalled(); }); }); describe("persistChatMessages", () => { it("saves user and assistant messages", async () => { await persistChatMessages("my query", "user msg", "assistant msg"); expect(mockSaveChat).toHaveBeenCalledWith("my query", "user", "user msg"); expect(mockSaveChat).toHaveBeenCalledWith( "my query", "assistant", "assistant msg", ); }); }); describe("runFollowUpSearch", () => { const makeSettings = (overrides = {}) => ({ enableTextSearch: true, enableImageSearch: true, searchResultsLimit: 10, ...overrides, }) as unknown as Parameters[2]; it("refreshes text results when enabled", async () => { mockSearchText.mockResolvedValueOnce({ results: [["Title", "snippet", "https://new.com"]], stale: false, }); await runFollowUpSearch( [{ role: "user", content: "hello" }], "hello", makeSettings(), [], [], ); expect(mockSearchText).toHaveBeenCalled(); }); it("skips text search when disabled", async () => { await runFollowUpSearch( [{ role: "user", content: "hello" }], "hello", makeSettings({ enableTextSearch: false }), [], [], ); expect(mockSearchText).not.toHaveBeenCalled(); }); it("skips image search when disabled", async () => { await runFollowUpSearch( [{ role: "user", content: "hello" }], "hello", makeSettings({ enableImageSearch: false }), [], [], ); expect(mockSearchImages).not.toHaveBeenCalled(); }); });