diff --git a/src/__tests__/ChatView.test.tsx b/src/__tests__/ChatView.test.tsx
deleted file mode 100644
index 4f02d98..0000000
--- a/src/__tests__/ChatView.test.tsx
+++ /dev/null
@@ -1,395 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { ChatView, VIEW_TYPE_COI_CHAT } from "../ChatView";
-import type { WorkspaceLeaf, App, TFile } from "obsidian";
-import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
-import type { ModelChatMessage, ContextItems } from "@/types";
-
-vi.mock("obsidian", () => ({
- TextFileView: class TextFileView {
- leaf: any;
- app: any;
- constructor(leaf: any) {
- this.leaf = leaf;
- }
- registerEvent = vi.fn();
- requestSave = vi.fn();
- },
- Notice: vi.fn(),
- TFile: class TFile {
- path: string;
- basename: string;
- constructor(path: string, basename: string) {
- this.path = path;
- this.basename = basename;
- }
- },
-}));
-
-vi.mock("solid-js/web", () => ({
- render: vi.fn(() => ({ dispose: vi.fn() })),
-}));
-
-vi.mock("@/CoiChatApp", () => ({
- CoiChatApp: vi.fn(),
-}));
-
-vi.mock("@/utils/notes", () => ({
- serializeCoiNote: vi.fn(),
- deserializeCoiNote: vi.fn(),
- renameNote: vi.fn(),
- isActiveCoiNote: vi.fn(),
- deserializeCoiNoteContent: vi.fn(),
- serializeCoiNoteContent: vi.fn(),
-}));
-
-describe("ChatView", () => {
- let app: App;
- let plugin: CoIntelligencePlugin;
- let leaf: WorkspaceLeaf;
- let chatView: ChatView;
- let mockFile: TFile;
-
- beforeEach(() => {
- vi.useFakeTimers();
-
- mockFile = {
- path: "test.md",
- basename: "test",
- } as TFile;
-
- app = {
- workspace: {
- getActiveFile: vi.fn().mockReturnValue(mockFile),
- on: vi.fn(),
- },
- vault: {
- cachedRead: vi.fn().mockResolvedValue("# Test Content"),
- process: vi.fn(),
- },
- fileManager: {
- processFrontMatter: vi.fn(),
- },
- } as any;
-
- plugin = {
- app,
- registry: {
- availableModels: [],
- },
- settings: {
- defaultModel: "",
- systemPromptFolder: "",
- defaultSystemPromptNote: "",
- },
- } as any;
-
- leaf = {} as WorkspaceLeaf;
-
- chatView = new ChatView(leaf, plugin, app);
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- describe("constructor", () => {
- it("should initialize with correct properties", () => {
- expect(chatView.plugin).toBe(plugin);
- expect(chatView.app).toBe(app);
- expect(chatView.file).toBe(mockFile);
- expect(chatView.icon).toBe("bot-message-square");
- expect(chatView.messages).toEqual([]);
- expect(chatView.contextItems).toEqual({ notes: [], tags: [], sources: [] });
- expect(chatView.sources).toEqual([]);
- });
-
- it("should register settings change event", () => {
- expect(chatView.registerEvent).toHaveBeenCalled();
- expect(app.workspace.on).toHaveBeenCalledWith(
- "co-intelligence:settings-changed",
- expect.any(Function)
- );
- });
- });
-
- describe("getViewType", () => {
- it("should return correct view type", () => {
- expect(chatView.getViewType()).toBe(VIEW_TYPE_COI_CHAT);
- });
- });
-
- describe("getDisplayText", () => {
- it("should return file basename when file exists", () => {
- expect(chatView.getDisplayText()).toBe("test");
- });
-
- it("should return default text when no file", () => {
- chatView.file = null;
- expect(chatView.getDisplayText()).toBe("Co-Intelligence Chat");
- });
- });
-
- describe("debounceUpdateViewData", () => {
- it("should debounce multiple calls", async () => {
- const updateSpy = vi.spyOn(chatView, "updateViewData");
-
- await chatView.debounceUpdateViewData();
- await chatView.debounceUpdateViewData();
- await chatView.debounceUpdateViewData();
-
- expect(updateSpy).not.toHaveBeenCalled();
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(updateSpy).toHaveBeenCalledTimes(1);
- });
-
- it("should clear previous timeout", async () => {
- const clearTimeoutSpy = vi.spyOn(window, "clearTimeout");
-
- await chatView.debounceUpdateViewData();
- const firstTimeout = chatView["debounceTimeout"];
-
- await chatView.debounceUpdateViewData();
-
- expect(clearTimeoutSpy).toHaveBeenCalledWith(firstTimeout);
- });
- });
-
- describe("updateViewData", () => {
- it("should update view data with serialized content", async () => {
- const { serializeCoiNoteContent } = await import("@/utils/notes");
- const mockSerializedContent = "Serialized content";
- vi.mocked(serializeCoiNoteContent).mockResolvedValue(mockSerializedContent);
-
- chatView.messages = [
- { role: "user", content: "Hello" },
- { role: "assistant", content: "Hi there!" },
- ] as ModelChatMessage[];
-
- await chatView.updateViewData();
-
- expect(app.vault.cachedRead).toHaveBeenCalledWith(mockFile);
- expect(serializeCoiNoteContent).toHaveBeenCalledWith(
- "# Test Content",
- app,
- chatView.messages,
- chatView.contextItems
- );
- expect(chatView.data).toBe(mockSerializedContent);
- expect(chatView.requestSave).toHaveBeenCalled();
- });
-
- it("should handle no file gracefully", async () => {
- chatView.file = null;
-
- const result = await chatView.updateViewData();
-
- expect(result).toBe("");
- expect(app.vault.cachedRead).not.toHaveBeenCalled();
- });
-
- });
-
- describe("getViewData", () => {
- it("should return current data", () => {
- chatView.data = "test data";
- expect(chatView.getViewData()).toBe("test data");
- });
- });
-
- describe("setViewData", () => {
- it("should set data and deserialize content", async () => {
- const { deserializeCoiNoteContent } = await import("@/utils/notes");
- const mockDeserializedData = {
- messages: [{ role: "user", content: "Deserialized" }],
- contextItems: { notes: ["note.md"], tags: [], sources: [] },
- sources: [],
- };
- vi.mocked(deserializeCoiNoteContent).mockResolvedValue(mockDeserializedData);
-
- app.metadataCache = {
- getFileCache: vi.fn().mockReturnValue({}),
- } as any;
-
- await chatView.setViewData("new data", false);
-
- expect(chatView.data).toBe("new data");
- expect(chatView.messages).toEqual(mockDeserializedData.messages);
- expect(chatView.contextItems).toEqual(mockDeserializedData.contextItems);
- });
-
- it("should handle null file gracefully", async () => {
- const { deserializeCoiNoteContent } = await import("@/utils/notes");
- const Notice = vi.mocked((await import("obsidian")).Notice);
- chatView.file = null;
-
- vi.mocked(deserializeCoiNoteContent).mockResolvedValue({
- messages: [],
- contextItems: { notes: [], tags: [], sources: [] },
- sources: []
- });
-
- await chatView.setViewData("data", false);
-
- expect(Notice).toHaveBeenCalledWith("Error: file is null while trying to set view data");
- });
-
- it("should clear when clear parameter is true", async () => {
- const clearSpy = vi.spyOn(chatView, "clear");
- app.metadataCache = {
- getFileCache: vi.fn().mockReturnValue({}),
- } as any;
-
- await chatView.setViewData("data", true);
-
- expect(clearSpy).toHaveBeenCalled();
- });
- });
-
- describe("handleChatChange", () => {
- it("should serialize note with new data", async () => {
- const { serializeCoiNote } = await import("@/utils/notes");
- const newMessages: ModelChatMessage[] = [
- { role: "user", content: "New message" },
- ];
- const newContextItems: ContextItems = {
- notes: ["note1.md"],
- tags: ["#tag1"],
- sources: [],
- };
-
- await chatView.handleChatChange({
- newMessages,
- newTitle: "test",
- contextItems: newContextItems,
- sources: [],
- lastModelId: "gpt-4",
- });
-
- expect(serializeCoiNote).toHaveBeenCalledWith(
- mockFile,
- app,
- newMessages,
- newContextItems,
- "gpt-4",
- []
- );
- });
-
- it("should rename note when title changes", async () => {
- const { renameNote } = await import("@/utils/notes");
-
- await chatView.handleChatChange({
- newMessages: [],
- newTitle: "New Title",
- contextItems: null,
- lastModelId: null,
- });
-
- expect(renameNote).toHaveBeenCalledWith(mockFile, "New Title", app, plugin);
- });
-
- it("should not rename when title is same as basename", async () => {
- const { renameNote } = await import("@/utils/notes");
-
- await chatView.handleChatChange({
- newMessages: [],
- newTitle: "test",
- contextItems: null,
- lastModelId: null,
- });
-
- expect(renameNote).not.toHaveBeenCalled();
- });
-
- it("should handle errors during serialization", async () => {
- const { serializeCoiNote } = await import("@/utils/notes");
- const Notice = vi.mocked((await import("obsidian")).Notice);
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
- vi.mocked(serializeCoiNote).mockRejectedValue(new Error("Serialization failed"));
-
- await chatView.handleChatChange({
- newMessages: [],
- newTitle: "",
- contextItems: null,
- lastModelId: null,
- });
-
- expect(Notice).toHaveBeenCalledWith("Error serializing CoiNote: Error: Serialization failed");
- expect(chatView["updating"]).toBe(false);
-
- consoleSpy.mockRestore();
- });
-
- it("should handle null file", async () => {
- const Notice = vi.mocked((await import("obsidian")).Notice);
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
- chatView.file = null;
-
- await chatView.handleChatChange({
- newMessages: [],
- newTitle: "",
- contextItems: null,
- lastModelId: null,
- });
-
- expect(Notice).toHaveBeenCalledWith("Error: file is null while trying to handle chat change");
-
- consoleSpy.mockRestore();
- });
- });
-
- describe("refresh", () => {
- it("should refresh view when active", async () => {
- const { isActiveCoiNote, deserializeCoiNote } = await import("@/utils/notes");
- vi.mocked(isActiveCoiNote).mockReturnValue(true);
- vi.mocked(deserializeCoiNote).mockResolvedValue({
- messages: [],
- contextItems: { notes: [], tags: [], sources: [] },
- sources: []
- });
-
- app.metadataCache = {
- getFileCache: vi.fn().mockReturnValue({}),
- } as any;
-
- chatView.containerEl = {
- children: [null, document.createElement("div")],
- } as any;
-
- const onOpenSpy = vi.spyOn(chatView, "onOpen");
-
- await chatView.refresh();
-
- expect(isActiveCoiNote).toHaveBeenCalledWith(mockFile, app);
- expect(onOpenSpy).toHaveBeenCalled();
- });
-
-
- it("should handle no file", async () => {
- chatView.file = null;
-
- await expect(chatView.refresh()).resolves.not.toThrow();
- });
- });
-
- describe("clear", () => {
- it("should reset all data", () => {
- chatView.messages = [{ role: "user", content: "test" }] as ModelChatMessage[];
- chatView.contextItems = { notes: ["note.md"], tags: [], sources: [] };
- chatView.sources = [{ title: "Source", url: "https://example.com" }];
- chatView.dispose = vi.fn();
-
- chatView.clear();
-
- expect(chatView.messages).toEqual([]);
- expect(chatView.contextItems).toEqual({ notes: [], tags: [], sources: [] });
- expect(chatView.sources).toEqual([]);
- expect(chatView.dispose).toHaveBeenCalled();
- });
- });
-
-
-});
\ No newline at end of file
diff --git a/src/__tests__/CoIntelligencePlugin.test.tsx b/src/__tests__/CoIntelligencePlugin.test.tsx
deleted file mode 100644
index 441dfe5..0000000
--- a/src/__tests__/CoIntelligencePlugin.test.tsx
+++ /dev/null
@@ -1,315 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { CoIntelligencePlugin } from "../CoIntelligencePlugin";
-import { CoIntelligenceSettingsTab, DEFAULT_SETTINGS } from "../settings";
-import { ModelRegistry } from "@/services/model-registry";
-import { VIEW_TYPE_COI_CHAT } from "@/ChatView";
-import type { App, PluginManifest, WorkspaceLeaf, TFile, Menu, ViewState } from "obsidian";
-
-vi.mock("obsidian", () => ({
- Plugin: class Plugin {
- app: any;
- manifest: any;
- constructor(app: any, manifest: any) {
- this.app = app;
- this.manifest = manifest;
- }
- addSettingTab = vi.fn();
- addCommand = vi.fn();
- registerView = vi.fn();
- addRibbonIcon = vi.fn();
- registerEvent = vi.fn();
- register = vi.fn();
- loadData = vi.fn();
- saveData = vi.fn();
- },
- TFile: class TFile {
- path: string;
- basename: string;
- constructor(path: string, basename: string) {
- this.path = path;
- this.basename = basename;
- }
- },
- around: vi.fn((target, patches) => {
- return () => {};
- }),
-}));
-
-vi.mock("../settings", () => ({
- CoIntelligenceSettingsTab: vi.fn(),
- DEFAULT_SETTINGS: {
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- perplexityApiKey: "",
- defaultFolder: "coi",
- defaultModel: "",
- renamingModel: "",
- systemPromptFolder: "coi/prompts",
- defaultSystemPromptNote: "",
- },
-}));
-
-vi.mock("@/services/model-registry", () => ({
- ModelRegistry: {
- getInstance: vi.fn(),
- },
-}));
-
-vi.mock("@/commands/new-chat", () => ({
- NewChatCommand: vi.fn(),
-}));
-
-vi.mock("@/commands/toggle-chat-view", () => ({
- ToggleChatViewCommand: vi.fn(),
-}));
-
-vi.mock("@/ChatView", () => ({
- ChatView: vi.fn(),
- VIEW_TYPE_COI_CHAT: "co-intelligence-chat",
-}));
-
-vi.mock("@/utils/notes", () => ({
- isCoiNote: vi.fn(),
- isPathActiveCoiNote: vi.fn(),
- isActiveCoiNote: vi.fn(),
- createCOINote: vi.fn(),
- waitForMetadataCache: vi.fn(),
-}));
-
-describe("CoIntelligencePlugin", () => {
- let app: App;
- let manifest: PluginManifest;
- let plugin: CoIntelligencePlugin;
- let mockRegistry: ModelRegistry;
-
- beforeEach(() => {
- vi.clearAllMocks();
-
- app = {
- workspace: {
- on: vi.fn(),
- onLayoutReady: vi.fn((callback) => callback()),
- getActiveFile: vi.fn(),
- },
- vault: {
- on: vi.fn(),
- },
- fileManager: {
- processFrontMatter: vi.fn(),
- },
- commands: {
- executeCommandById: vi.fn(),
- },
- } as any;
-
- manifest = {
- id: "co-intelligence",
- name: "Co-Intelligence",
- version: "1.0.0",
- } as PluginManifest;
-
- mockRegistry = {} as ModelRegistry;
- vi.mocked(ModelRegistry.getInstance).mockReturnValue(mockRegistry);
-
- plugin = new CoIntelligencePlugin(app, manifest);
- });
-
- describe("constructor", () => {
- it("should initialize with default settings", () => {
- expect(plugin.settings).toEqual(DEFAULT_SETTINGS);
- expect(plugin.isPerformingAutomaticRename).toBe(false);
- });
- });
-
- describe("onload", () => {
- it("should initialize plugin components", async () => {
- plugin.loadData = vi.fn().mockResolvedValue({});
- await plugin.onload();
-
- expect(plugin.registry).toBe(mockRegistry);
- expect(plugin.addSettingTab).toHaveBeenCalled();
- expect(plugin.addCommand).toHaveBeenCalledTimes(2);
- expect(plugin.registerView).toHaveBeenCalledWith(
- VIEW_TYPE_COI_CHAT,
- expect.any(Function)
- );
- expect(plugin.addRibbonIcon).toHaveBeenCalledWith(
- "bot-message-square",
- "New COI chat",
- expect.any(Function)
- );
- });
-
- it("should register event handlers", async () => {
- await plugin.onload();
-
- expect(plugin.registerEvent).toHaveBeenCalledWith(
- app.workspace.on("file-open", expect.any(Function))
- );
- expect(plugin.registerEvent).toHaveBeenCalledWith(
- app.vault.on("rename", expect.any(Function))
- );
- expect(plugin.registerEvent).toHaveBeenCalledWith(
- app.workspace.on("file-menu", expect.any(Function))
- );
- });
-
- it("should call onLayoutReady", async () => {
- await plugin.onload();
-
- expect(app.workspace.onLayoutReady).toHaveBeenCalledWith(expect.any(Function));
- });
- });
-
- describe("loadSettings", () => {
- it("should merge loaded data with default settings", async () => {
- const customSettings = {
- openaiApiKey: "test-key",
- defaultFolder: "custom-folder",
- };
- plugin.loadData = vi.fn().mockResolvedValue(customSettings);
-
- await plugin.loadSettings();
-
- expect(plugin.settings).toEqual({
- ...DEFAULT_SETTINGS,
- ...customSettings,
- });
- });
-
- it("should use default settings when no data is loaded", async () => {
- plugin.loadData = vi.fn().mockResolvedValue(null);
-
- await plugin.loadSettings();
-
- expect(plugin.settings).toEqual(DEFAULT_SETTINGS);
- });
- });
-
- describe("saveSettings", () => {
- it("should save current settings", async () => {
- plugin.saveData = vi.fn();
- plugin.settings = { ...DEFAULT_SETTINGS, openaiApiKey: "test-key" };
-
- await plugin.saveSettings();
-
- expect(plugin.saveData).toHaveBeenCalledWith(plugin.settings);
- });
- });
-
- describe("handleFileRename", () => {
- let mockFile: TFile;
-
- beforeEach(async () => {
- const { TFile } = await import("obsidian");
- mockFile = new TFile("test.md", "test");
- });
-
- it("should mark COI notes as renamed when not automatic", async () => {
- const { isCoiNote, waitForMetadataCache } = await import("@/utils/notes");
- vi.mocked(isCoiNote).mockReturnValue(true);
-
- plugin.isPerformingAutomaticRename = false;
- const processFrontMatterCallback = vi.fn();
-
- app.fileManager.processFrontMatter = vi.fn(async (file, callback) => {
- const frontmatter = {};
- callback(frontmatter);
- processFrontMatterCallback(frontmatter);
- });
-
- await plugin["handleFileRename"](mockFile, "old-path.md");
-
- expect(waitForMetadataCache).toHaveBeenCalledWith(app, mockFile);
- expect(processFrontMatterCallback).toHaveBeenCalledWith(
- expect.objectContaining({ "note-renamed": true })
- );
- });
-
- it("should not mark as renamed when automatic rename", async () => {
- const { isCoiNote, waitForMetadataCache } = await import("@/utils/notes");
- vi.mocked(isCoiNote).mockReturnValue(true);
-
- plugin.isPerformingAutomaticRename = true;
-
- await plugin["handleFileRename"](mockFile, "old-path.md");
-
- expect(waitForMetadataCache).toHaveBeenCalledWith(app, mockFile);
- expect(app.fileManager.processFrontMatter).not.toHaveBeenCalled();
- expect(plugin.isPerformingAutomaticRename).toBe(false);
- });
-
- it("should ignore non-TFile objects", async () => {
- const notAFile = { path: "not-a-file" } as any;
-
- await plugin["handleFileRename"](notAFile, "old-path");
-
- expect(app.fileManager.processFrontMatter).not.toHaveBeenCalled();
- });
-
- it("should ignore non-COI notes", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- vi.mocked(isCoiNote).mockReturnValue(false);
-
- await plugin["handleFileRename"](mockFile, "old-path.md");
-
- expect(app.fileManager.processFrontMatter).not.toHaveBeenCalled();
- });
- });
-
- describe("onFileMenuHandler", () => {
- let mockMenu: Menu;
- let mockFile: TFile;
- let mockLeaf: WorkspaceLeaf;
-
- beforeEach(async () => {
- mockMenu = {
- addItem: vi.fn(),
- } as any;
-
- const { TFile } = await import("obsidian");
- mockFile = new TFile("test.md", "test");
-
- mockLeaf = {} as WorkspaceLeaf;
- });
-
-
- it("should not add menu item for non-COI notes", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- vi.mocked(isCoiNote).mockReturnValue(false);
-
- await plugin["onFileMenuHandler"](mockMenu, mockFile, "file-explorer", mockLeaf);
-
- expect(mockMenu.addItem).not.toHaveBeenCalled();
- });
-
- it("should not add menu item for active COI notes", async () => {
- const { isCoiNote, isActiveCoiNote } = await import("@/utils/notes");
- vi.mocked(isCoiNote).mockReturnValue(true);
- vi.mocked(isActiveCoiNote).mockReturnValue(true);
-
- await plugin["onFileMenuHandler"](mockMenu, mockFile, "file-explorer", mockLeaf);
-
- expect(mockMenu.addItem).not.toHaveBeenCalled();
- });
- });
-
-
- describe("ribbon icon", () => {
- it("should create COI note when ribbon icon is clicked", async () => {
- const { createCOINote } = await import("@/utils/notes");
- let ribbonClickHandler: () => void;
-
- plugin.addRibbonIcon = vi.fn((icon, title, callback) => {
- ribbonClickHandler = callback;
- });
-
- await plugin.onload();
-
- ribbonClickHandler!();
-
- expect(createCOINote).toHaveBeenCalledWith(app, plugin);
- });
- });
-});
\ No newline at end of file
diff --git a/src/__tests__/CoiChatApp.test.tsx b/src/__tests__/CoiChatApp.test.tsx
deleted file mode 100644
index 77880ac..0000000
--- a/src/__tests__/CoiChatApp.test.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen } from "@solidjs/testing-library";
-import {
- CoiChatApp,
- PluginContext,
- AppContext,
- FileContext,
-} from "../CoiChatApp";
-import type { App, TFile } from "obsidian";
-import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
-import type { ModelChatMessage, ContextItems, Source } from "@/types";
-import { useContext } from "solid-js";
-
-vi.mock("@/components/ChatInterface", () => ({
- ChatInterface: (props: any) => {
- // Test component that verifies props and context
- const plugin = useContext(PluginContext);
- const app = useContext(AppContext);
- const file = useContext(FileContext);
-
- return (
-
-
- {JSON.stringify(props.initialMessages)}
-
-
- {JSON.stringify(props.initialContext)}
-
-
- {JSON.stringify(props.initialSources)}
-
-
{plugin ? "true" : "false"}
-
{app ? "true" : "false"}
-
{file ? "true" : "false"}
-
-
- );
- },
-}));
-
-describe("CoiChatApp", () => {
- let app: App;
- let file: TFile;
- let plugin: CoIntelligencePlugin;
- let onChange: vi.Mock;
- let initialMessages: ModelChatMessage[];
- let initialContext: ContextItems;
- let initialSources: Source[];
-
- beforeEach(() => {
- app = { name: "MockApp" } as App;
- file = { path: "test.md", basename: "test" } as TFile;
- plugin = { name: "MockPlugin" } as CoIntelligencePlugin;
- onChange = vi.fn();
- initialMessages = [
- { role: "user", content: "Hello" },
- { role: "assistant", content: "Hi there!" },
- ];
- initialContext = {
- notes: ["note1.md"],
- tags: ["#tag1"],
- sources: [],
- };
- initialSources = [{ title: "Source 1", url: "https://example.com" }];
- });
-
- it("should render ChatInterface with correct props", () => {
- render(() => (
-
- ));
-
- expect(screen.getByTestId("chat-interface")).toBeInTheDocument();
- expect(screen.getByTestId("initial-messages").textContent).toBe(
- JSON.stringify(initialMessages),
- );
- expect(screen.getByTestId("initial-context").textContent).toBe(
- JSON.stringify(initialContext),
- );
- expect(screen.getByTestId("initial-sources").textContent).toBe(
- JSON.stringify(initialSources),
- );
- });
-
- it("should provide contexts to child components", () => {
- render(() => (
-
- ));
-
- expect(screen.getByTestId("has-plugin").textContent).toBe("true");
- expect(screen.getByTestId("has-app").textContent).toBe("true");
- expect(screen.getByTestId("has-file").textContent).toBe("true");
- });
-
- it("should pass onChange callback to ChatInterface", () => {
- render(() => (
-
- ));
-
- const button = screen.getByText("Trigger Change");
- button.click();
-
- expect(onChange).toHaveBeenCalledWith({
- newMessages: [{ role: "user", content: "test" }],
- newTitle: "Test Title",
- contextItems: null,
- lastModelId: "test-model",
- });
- });
-
- it("should handle undefined initialSources with default value", () => {
- render(() => (
-
- ));
-
- expect(screen.getByTestId("initial-sources").textContent).toBe(
- JSON.stringify([]),
- );
- });
-
- it("should wrap content in coi-app div", () => {
- render(() => (
-
- ));
-
- const wrapper = screen.getByTestId("chat-interface").parentElement;
- expect(wrapper).toHaveClass("coi-app");
- });
-
- it("should handle empty initial messages", () => {
- render(() => (
-
- ));
-
- expect(screen.getByTestId("initial-messages").textContent).toBe("[]");
- });
-
- it("should handle complex context items", () => {
- const complexContext: ContextItems = {
- notes: ["note1.md", "note2.md", "folder/note3.md"],
- tags: ["#tag1", "#tag2", "#nested/tag"],
- sources: ["https://source1.com", "https://source2.com"],
- };
-
- render(() => (
-
- ));
-
- expect(screen.getByTestId("initial-context").textContent).toBe(
- JSON.stringify(complexContext),
- );
- });
-});
diff --git a/src/__tests__/settings.test.ts b/src/__tests__/settings.test.ts
deleted file mode 100644
index 0a7bf88..0000000
--- a/src/__tests__/settings.test.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { CoIntelligenceSettingsTab, DEFAULT_SETTINGS } from "../settings";
-import type { App, Setting } from "obsidian";
-import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
-
-vi.mock("obsidian", () => ({
- PluginSettingTab: class PluginSettingTab {
- app: any;
- plugin: any;
- containerEl: any;
- constructor(app: any, plugin: any) {
- this.app = app;
- this.plugin = plugin;
- this.containerEl = {
- empty: vi.fn(),
- createEl: vi.fn(),
- createDiv: vi.fn(),
- querySelectorAll: vi.fn(),
- };
- }
- },
- Setting: vi.fn().mockImplementation(() => ({
- setName: vi.fn().mockReturnThis(),
- setDesc: vi.fn().mockReturnThis(),
- addText: vi.fn().mockReturnThis(),
- addDropdown: vi.fn().mockReturnThis(),
- })),
- normalizePath: vi.fn((path: string) =>
- path.replace(/\/+/g, "/").replace(/\/$/, ""),
- ),
-}));
-
-describe("settings", () => {
- describe("DEFAULT_SETTINGS", () => {
- it("should have all required fields with default values", () => {
- expect(DEFAULT_SETTINGS).toEqual({
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- perplexityApiKey: "",
- defaultFolder: "coi",
- defaultModel: "",
- renamingModel: "",
- systemPromptFolder: "coi/prompts",
- defaultSystemPromptNote: "",
- });
- });
- });
-
- describe("CoIntelligenceSettingsTab", () => {
- let app: App;
- let plugin: CoIntelligencePlugin;
- let settingsTab: CoIntelligenceSettingsTab;
- let mockElement: any;
-
- beforeEach(() => {
- vi.useFakeTimers();
-
- mockElement = {
- empty: vi.fn(),
- createEl: vi.fn().mockReturnValue({
- createEl: vi.fn().mockReturnValue({
- createEl: vi.fn(),
- appendChild: vi.fn(),
- setAttribute: vi.fn(),
- }),
- }),
- createDiv: vi.fn().mockReturnValue({
- createDiv: vi.fn().mockReturnValue({
- createEl: vi.fn().mockReturnValue({
- createEl: vi.fn(),
- }),
- }),
- createEl: vi.fn().mockReturnValue({
- createEl: vi.fn(),
- appendChild: vi.fn(),
- }),
- }),
- querySelectorAll: vi.fn().mockReturnValue([]),
- };
-
- app = {
- vault: {
- getMarkdownFiles: vi.fn().mockReturnValue([
- { path: "coi/prompts/prompt1.md", basename: "prompt1" },
- { path: "coi/prompts/prompt2.md", basename: "prompt2" },
- ]),
- },
- workspace: {
- trigger: vi.fn(),
- },
- } as any;
-
- plugin = {
- settings: { ...DEFAULT_SETTINGS },
- saveSettings: vi.fn().mockResolvedValue(undefined),
- registry: {
- availableModels: [
- { id: "gpt-4", name: "GPT-4", renaming: true },
- { id: "claude-3", name: "Claude 3", renaming: true },
- { id: "gemini-pro", name: "Gemini Pro", renaming: false },
- ],
- reinitialize: vi.fn(),
- },
- } as any;
-
- settingsTab = new CoIntelligenceSettingsTab(app, plugin);
- settingsTab.containerEl = mockElement;
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- describe("createDebouncedChangeHandler", () => {
- it("should debounce multiple changes", async () => {
- const handler = settingsTab["createDebouncedChangeHandler"](
- "openaiApiKey",
- true,
- false,
- );
-
- handler("key1");
- handler("key2");
- handler("key3");
-
- expect(plugin.saveSettings).not.toHaveBeenCalled();
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(plugin.saveSettings).toHaveBeenCalledTimes(1);
- expect(plugin.settings.openaiApiKey).toBe("key3");
- });
-
- it("should reinitialize registry when required", async () => {
- const handler = settingsTab["createDebouncedChangeHandler"](
- "anthropicApiKey",
- true,
- false,
- );
-
- handler("new-key");
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(plugin.registry.reinitialize).toHaveBeenCalled();
- });
-
- it("should trigger settings-changed event", async () => {
- const handler = settingsTab["createDebouncedChangeHandler"](
- "defaultFolder",
- false,
- false,
- );
-
- handler("new-folder");
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(app.workspace.trigger).toHaveBeenCalledWith(
- "co-intelligence:settings-changed",
- );
- });
-
- it("should normalize folder paths", async () => {
- const handler = settingsTab["createDebouncedChangeHandler"](
- "defaultFolder",
- false,
- false,
- );
-
- handler("folder//with///slashes/");
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(plugin.settings.defaultFolder).toBe("folder/with/slashes");
- });
-
- it("should handle multiple pending changes for different settings", async () => {
- const handler1 = settingsTab["createDebouncedChangeHandler"](
- "openaiApiKey",
- true,
- false,
- );
- const handler2 = settingsTab["createDebouncedChangeHandler"](
- "anthropicApiKey",
- true,
- false,
- );
-
- handler1("openai-key");
- handler2("anthropic-key");
-
- vi.advanceTimersByTime(500);
- await vi.runAllTimersAsync();
-
- expect(plugin.settings.openaiApiKey).toBe("openai-key");
- expect(plugin.settings.anthropicApiKey).toBe("anthropic-key");
- expect(plugin.saveSettings).toHaveBeenCalledTimes(1);
- expect(plugin.registry.reinitialize).toHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/src/commands/__tests__/new-chat.test.ts b/src/commands/__tests__/new-chat.test.ts
deleted file mode 100644
index 3d882a9..0000000
--- a/src/commands/__tests__/new-chat.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import { NewChatCommand } from "../new-chat";
-import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
-import type { App } from "obsidian";
-
-vi.mock("@/utils/notes", () => ({
- createCOINote: vi.fn(),
-}));
-
-describe("NewChatCommand", () => {
- let app: App;
- let plugin: CoIntelligencePlugin;
- let command: NewChatCommand;
-
- beforeEach(() => {
- app = {} as App;
- plugin = { app } as CoIntelligencePlugin;
- command = new NewChatCommand(plugin);
- });
-
- it("should have correct id and name", () => {
- expect(command.id).toBe("new-chat");
- expect(command.name).toBe("New chat");
- });
-
- it("should call createCOINote when callback is executed", async () => {
- const { createCOINote } = await import("@/utils/notes");
- const mockedCreateCOINote = vi.mocked(createCOINote);
-
- await command.callback();
-
- expect(mockedCreateCOINote).toHaveBeenCalledWith(app, plugin);
- expect(mockedCreateCOINote).toHaveBeenCalledTimes(1);
- });
-
- it("should store app and plugin references", () => {
- expect(command["app"]).toBe(app);
- expect(command["plugin"]).toBe(plugin);
- });
-});
\ No newline at end of file
diff --git a/src/commands/__tests__/toggle-chat-view.test.ts b/src/commands/__tests__/toggle-chat-view.test.ts
deleted file mode 100644
index 16c14ee..0000000
--- a/src/commands/__tests__/toggle-chat-view.test.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { ToggleChatViewCommand } from "../toggle-chat-view";
-import type CoIntelligencePlugin from "@/CoIntelligencePlugin";
-import type { App, TFile, WorkspaceLeaf, View, Notice } from "obsidian";
-import { VIEW_TYPE_COI_CHAT } from "@/ChatView";
-
-vi.mock("@/utils/notes", () => ({
- isCoiNote: vi.fn(),
- isActiveCoiNote: vi.fn(),
-}));
-
-vi.mock("obsidian", async (importOriginal) => {
- const actual = await importOriginal() as any;
- return {
- ...actual,
- Notice: vi.fn(),
- SuggestModal: vi.fn().mockImplementation(() => ({
- open: vi.fn(),
- close: vi.fn(),
- })),
- };
-});
-
-describe("ToggleChatViewCommand", () => {
- let app: App;
- let plugin: CoIntelligencePlugin;
- let command: ToggleChatViewCommand;
- let mockFile: TFile;
- let mockLeaf: WorkspaceLeaf;
- let mockView: View;
-
- beforeEach(() => {
- vi.useFakeTimers();
-
- mockView = {
- getState: vi.fn().mockReturnValue({ mode: "source" }),
- } as any;
-
- mockLeaf = {
- view: mockView,
- setViewState: vi.fn().mockResolvedValue(undefined),
- } as any;
-
- mockFile = {
- path: "test.md",
- basename: "test",
- } as TFile;
-
- app = {
- workspace: {
- getActiveFile: vi.fn(),
- getMostRecentLeaf: vi.fn().mockReturnValue(mockLeaf),
- },
- fileManager: {
- processFrontMatter: vi.fn(),
- },
- } as any;
-
- plugin = { app } as CoIntelligencePlugin;
- command = new ToggleChatViewCommand(plugin);
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- it("should have correct id and name", () => {
- expect(command.id).toBe("toggle-chat-view");
- expect(command.name).toBe("Toggle chat view");
- });
-
- describe("checkCallback", () => {
- it("should return false when checking with no active file", () => {
- app.workspace.getActiveFile = vi.fn().mockReturnValue(null);
-
- const result = command.checkCallback(true);
-
- expect(result).toBe(false);
- });
-
- it("should return false when checking non-COI note", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- const mockedIsCoiNote = vi.mocked(isCoiNote);
-
- app.workspace.getActiveFile = vi.fn().mockReturnValue(mockFile);
- mockedIsCoiNote.mockReturnValue(false);
-
- const result = command.checkCallback(true);
-
- expect(result).toBe(false);
- expect(mockedIsCoiNote).toHaveBeenCalledWith(mockFile, app);
- });
-
- it("should return true when checking COI note", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- const mockedIsCoiNote = vi.mocked(isCoiNote);
-
- app.workspace.getActiveFile = vi.fn().mockReturnValue(mockFile);
- mockedIsCoiNote.mockReturnValue(true);
-
- const result = command.checkCallback(true);
-
- expect(result).toBe(true);
- });
-
- it("should perform toggle when not checking and file is COI note", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- const mockedIsCoiNote = vi.mocked(isCoiNote);
- const performToggleSpy = vi.spyOn(command as any, "performToggle");
-
- app.workspace.getActiveFile = vi.fn().mockReturnValue(mockFile);
- mockedIsCoiNote.mockReturnValue(true);
-
- command.checkCallback(false);
-
- expect(performToggleSpy).toHaveBeenCalledWith(mockFile);
- });
-
- it("should not perform toggle when file is not COI note", async () => {
- const { isCoiNote } = await import("@/utils/notes");
- const mockedIsCoiNote = vi.mocked(isCoiNote);
- const performToggleSpy = vi.spyOn(command as any, "performToggle");
-
- app.workspace.getActiveFile = vi.fn().mockReturnValue(mockFile);
- mockedIsCoiNote.mockReturnValue(false);
-
- command.checkCallback(false);
-
- expect(performToggleSpy).not.toHaveBeenCalled();
- });
- });
-
- describe("performToggle", () => {
- it("should toggle from active to source view", async () => {
- const { isActiveCoiNote } = await import("@/utils/notes");
- const mockedIsActiveCoiNote = vi.mocked(isActiveCoiNote);
- mockedIsActiveCoiNote.mockReturnValue(true);
-
- const processFrontMatterCallback = vi.fn();
- app.fileManager.processFrontMatter = vi.fn().mockImplementation(async (file, callback) => {
- const frontmatter = { "coi-chat-view": true };
- callback(frontmatter);
- processFrontMatterCallback(frontmatter);
- return Promise.resolve();
- });
-
- const togglePromise = command["performToggle"](mockFile);
- await vi.runAllTimersAsync();
- await togglePromise;
-
- expect(processFrontMatterCallback).toHaveBeenCalledWith(
- expect.objectContaining({ "coi-chat-view": false })
- );
-
- expect(mockLeaf.setViewState).toHaveBeenCalledWith(
- {
- type: "markdown",
- state: { mode: "source" },
- popstate: true,
- },
- { focus: true }
- );
- });
-
- it("should toggle from source to chat view", async () => {
- const { isActiveCoiNote } = await import("@/utils/notes");
- const mockedIsActiveCoiNote = vi.mocked(isActiveCoiNote);
- mockedIsActiveCoiNote.mockReturnValue(false);
-
- const processFrontMatterCallback = vi.fn();
- app.fileManager.processFrontMatter = vi.fn().mockImplementation(async (file, callback) => {
- const frontmatter = { "coi-chat-view": false };
- callback(frontmatter);
- processFrontMatterCallback(frontmatter);
- return Promise.resolve();
- });
-
- const togglePromise = command["performToggle"](mockFile);
- await vi.runAllTimersAsync();
- await togglePromise;
-
- expect(processFrontMatterCallback).toHaveBeenCalledWith(
- expect.objectContaining({ "coi-chat-view": true })
- );
-
- expect(mockLeaf.setViewState).toHaveBeenCalledWith(
- {
- type: VIEW_TYPE_COI_CHAT,
- state: { mode: "source" },
- popstate: true,
- },
- { focus: true }
- );
- });
- });
-
- describe("error handling", () => {
- it("should show notice when no leaf found for default editor", async () => {
- const Notice = vi.mocked((await import("obsidian")).Notice);
- app.workspace.getMostRecentLeaf = vi.fn().mockReturnValue(null);
-
- await command["openInDefaultEditor"](mockFile);
-
- expect(Notice).toHaveBeenCalledWith(
- "Error: no leaf found while opening chat in default editor"
- );
- });
-
- it("should show notice when no leaf found for chat view", async () => {
- const Notice = vi.mocked((await import("obsidian")).Notice);
- app.workspace.getMostRecentLeaf = vi.fn().mockReturnValue(null);
-
- await command["openInChatView"](mockFile);
-
- expect(Notice).toHaveBeenCalledWith(
- "Error: no leaf found while opening chat in chat view"
- );
- });
-
- it("should log error to console when no leaf found", async () => {
- const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation();
- app.workspace.getMostRecentLeaf = vi.fn().mockReturnValue(null);
-
- await command["openInDefaultEditor"](mockFile);
-
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- "No leaf found while opening chat in default editor"
- );
-
- consoleErrorSpy.mockRestore();
- });
- });
-});
\ No newline at end of file
diff --git a/src/components/__tests__/LucideIcon.test.tsx b/src/components/__tests__/LucideIcon.test.tsx
deleted file mode 100644
index 18e43e9..0000000
--- a/src/components/__tests__/LucideIcon.test.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen } from '@solidjs/testing-library';
-import { LucideIcon } from '@/components/LucideIcon';
-import { render } from '../../../test/utils/render';
-
-// Mock the setIcon function from Obsidian
-vi.mock('obsidian', () => ({
- setIcon: vi.fn(),
-}));
-
-describe('LucideIcon', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('should render with correct CSS class', () => {
- render(() => );
-
- expect(document.querySelector('.coi-lucide-icon')).toBeInTheDocument();
- });
-
- it('should call setIcon with correct parameters on mount', async () => {
- const { setIcon } = await import('obsidian');
- render(() => );
-
- // Wait for onMount to be called
- await new Promise(resolve => setTimeout(resolve, 0));
-
- expect(setIcon).toHaveBeenCalledTimes(1);
- expect(setIcon).toHaveBeenCalledWith(
- expect.any(HTMLElement),
- 'home'
- );
- });
-
- it('should render different icons with different names', async () => {
- const { setIcon } = await import('obsidian');
- const { unmount } = render(() => );
-
- await new Promise(resolve => setTimeout(resolve, 0));
- expect(setIcon).toHaveBeenCalledWith(expect.any(HTMLElement), 'star');
-
- unmount();
- vi.clearAllMocks();
-
- render(() => );
-
- await new Promise(resolve => setTimeout(resolve, 0));
- expect(setIcon).toHaveBeenCalledWith(expect.any(HTMLElement), 'heart');
- });
-});
\ No newline at end of file
diff --git a/src/components/__tests__/ModelSelector.test.tsx b/src/components/__tests__/ModelSelector.test.tsx
deleted file mode 100644
index 342bf1d..0000000
--- a/src/components/__tests__/ModelSelector.test.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen } from '@solidjs/testing-library';
-import userEvent from '@testing-library/user-event';
-import { createSignal } from 'solid-js';
-import { ModelSelector } from '@/components/ModelSelector';
-import { Model, ModelId } from '@/types';
-import { render, createMockPlugin } from '../../../test/utils/render';
-
-// Mock ModelRegistry
-vi.mock('@/services/model-registry', () => ({
- ModelRegistry: {
- getInstance: vi.fn(),
- }
-}));
-
-// Mock LucideIcon component
-vi.mock('@/components/LucideIcon', () => ({
- LucideIcon: ({ name }: { name: string }) =>
-}));
-
-describe('ModelSelector', () => {
- const mockModels: Model[] = [
- {
- id: 'openai:gpt-4' as ModelId,
- provider: 'openai',
- name: 'GPT-4',
- renaming: false,
- toggleWebSearch: true,
- streaming: true,
- },
- {
- id: 'anthropic:claude-3-5-sonnet' as ModelId,
- provider: 'anthropic',
- name: 'Claude 3.5 Sonnet',
- renaming: false,
- toggleWebSearch: false,
- streaming: true,
- }
- ];
-
- let mockRegistry: any;
-
- beforeEach(async () => {
- mockRegistry = {
- availableModels: [],
- getModel: vi.fn(),
- };
-
- const { ModelRegistry } = await import('@/services/model-registry');
- vi.mocked(ModelRegistry.getInstance).mockReturnValue(mockRegistry);
- });
-
- it('should render with no models available', () => {
- mockRegistry.availableModels = [];
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- expect(screen.getByRole('combobox')).toBeDisabled();
- expect(screen.getByText('No available models')).toBeInTheDocument();
- expect(screen.getByRole('combobox')).toHaveAttribute('aria-label', 'No models available');
- });
-
- it('should render available models', () => {
- mockRegistry.availableModels = mockModels;
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- expect(screen.getByRole('combobox')).not.toBeDisabled();
- expect(screen.getByText('GPT-4')).toBeInTheDocument();
- expect(screen.getByText('Claude 3.5 Sonnet')).toBeInTheDocument();
- });
-
- it('should call onModelChange when selection changes', async () => {
- mockRegistry.availableModels = mockModels;
- mockRegistry.getModel.mockImplementation((id: ModelId) =>
- mockModels.find(model => model.id === id) || null
- );
-
- const user = userEvent.setup();
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- const select = screen.getByRole('combobox');
- await user.selectOptions(select, 'openai:gpt-4');
-
- expect(mockRegistry.getModel).toHaveBeenCalledWith('openai:gpt-4');
- expect(onModelChange).toHaveBeenCalledWith(mockModels[0]);
- });
-
- it('should display selected model', () => {
- mockRegistry.availableModels = mockModels;
- const [selectedModel, setSelectedModel] = createSignal(mockModels[0]);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- expect(screen.getByRole('combobox')).toHaveValue('openai:gpt-4');
- });
-
- it('should show label when showLabel is true', () => {
- mockRegistry.availableModels = [];
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- expect(screen.getByText('Choose Model')).toBeInTheDocument();
- expect(screen.getByText('Choose Model')).toHaveClass('model-selector-label');
- });
-
- it('should hide label when showLabel is false', () => {
- mockRegistry.availableModels = [];
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- // The label should exist but be visually hidden with sr-only class
- const hiddenLabel = document.querySelector('label.sr-only');
- expect(hiddenLabel).toBeInTheDocument();
- expect(hiddenLabel).toHaveClass('sr-only');
- expect(hiddenLabel).toHaveTextContent('Choose Model');
- });
-
- it('should render bot icon', () => {
- mockRegistry.availableModels = [];
- const [selectedModel, setSelectedModel] = createSignal(null);
- const onModelChange = vi.fn();
-
- render(() =>
- ,
- { plugin: createMockPlugin() }
- );
-
- expect(screen.getByTestId('icon-bot-message-square')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/components/__tests__/ProcessingIndicator.test.tsx b/src/components/__tests__/ProcessingIndicator.test.tsx
deleted file mode 100644
index f60ee01..0000000
--- a/src/components/__tests__/ProcessingIndicator.test.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { describe, it, expect, vi } from 'vitest';
-import { screen } from '@solidjs/testing-library';
-import userEvent from '@testing-library/user-event';
-import { ProcessingIndicator } from '@/components/ProcessingIndicator';
-import { render } from '../../../test/utils/render';
-
-describe('ProcessingIndicator', () => {
- it('should render processing indicator with text', () => {
- render(() => );
-
- expect(screen.getByText('Generating response...')).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
- });
-
- it('should call onCancel when cancel button is clicked', async () => {
- const user = userEvent.setup();
- const onCancel = vi.fn();
-
- render(() => );
-
- const cancelButton = screen.getByRole('button', { name: 'Cancel' });
- await user.click(cancelButton);
-
- expect(onCancel).toHaveBeenCalledTimes(1);
- });
-
- it('should render without onCancel prop', () => {
- render(() => );
-
- const cancelButton = screen.getByRole('button', { name: 'Cancel' });
- expect(cancelButton).toBeInTheDocument();
- });
-
- it('should have correct CSS classes', () => {
- render(() => );
-
- expect(document.querySelector('.coi-processing-indicator')).toBeInTheDocument();
- expect(document.querySelector('.coi-processing-spinner')).toBeInTheDocument();
- expect(document.querySelector('.coi-processing-text')).toBeInTheDocument();
- expect(document.querySelector('.coi-cancel-button')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/components/__tests__/UserInput.test.tsx b/src/components/__tests__/UserInput.test.tsx
deleted file mode 100644
index bfa42e7..0000000
--- a/src/components/__tests__/UserInput.test.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { screen } from '@solidjs/testing-library';
-import userEvent from '@testing-library/user-event';
-import { createSignal } from 'solid-js';
-import { UserInput } from '@/components/UserInput';
-import { Model, ModelId } from '@/types';
-import { render, createMockPlugin, createMockApp } from '../../../test/utils/render';
-
-// Mock ModelRegistry
-vi.mock('@/services/model-registry', () => ({
- ModelRegistry: {
- getInstance: vi.fn(() => ({
- availableModels: [mockModel],
- })),
- }
-}));
-
-// Mock child components to isolate UserInput behavior
-vi.mock('@/components/ModelSelector', () => ({
- ModelSelector: () => Model Selector
-}));
-
-vi.mock('@/components/SystemPromptSelector', () => ({
- SystemPromptSelector: () => System Prompt Selector
-}));
-
-vi.mock('@/components/NoteLinkSuggestionModal', () => ({
- NoteLinkSuggestionModal: class {
- constructor() {}
- open() {}
- onClose = vi.fn();
- }
-}));
-
-vi.mock('@/components/TagSuggestionModal', () => ({
- TagSuggestionModal: class {
- constructor() {}
- open() {}
- onClose = vi.fn();
- }
-}));
-
-const mockModel: Model = {
- id: 'openai:gpt-4' as ModelId,
- provider: 'openai',
- name: 'GPT-4',
- renaming: false,
- toggleWebSearch: true,
- streaming: true,
-};
-
-describe('UserInput', () => {
- let mockProps: any;
-
- beforeEach(() => {
- const [currentModel, setCurrentModel] = createSignal(mockModel);
-
- mockProps = {
- triggerChange: vi.fn(),
- onSubmit: vi.fn(),
- currentModel,
- updateModel: vi.fn(),
- onLinkNote: vi.fn(),
- onAddTag: vi.fn(),
- initialSystemPrompt: '',
- };
- });
-
- it('should render textarea with correct placeholder', () => {
- render(() => , {
- app: createMockApp(),
- plugin: createMockPlugin()
- });
-
- expect(screen.getByPlaceholderText('Type your message...')).toBeInTheDocument();
- expect(screen.getByRole('textbox')).toBeInTheDocument();
- });
-
- it('should render child components', () => {
- render(() => , {
- app: createMockApp(),
- plugin: createMockPlugin()
- });
-
- expect(screen.getByTestId('model-selector')).toBeInTheDocument();
- expect(screen.getByTestId('system-prompt-selector')).toBeInTheDocument();
- });
-
- it('should submit message on Enter key', async () => {
- const user = userEvent.setup();
-
- render(() => , {
- app: createMockApp(),
- plugin: createMockPlugin()
- });
-
- const textarea = screen.getByRole('textbox');
- await user.type(textarea, 'Test message{Enter}');
-
- expect(mockProps.onSubmit).toHaveBeenCalled();
- });
-
- it('should show web search checkbox when model supports it', () => {
- render(() => , {
- app: createMockApp(),
- plugin: createMockPlugin()
- });
-
- expect(screen.getByRole('checkbox')).toBeInTheDocument();
- expect(screen.getByText('Web search')).toBeInTheDocument();
- });
-
- it('should toggle web search checkbox', async () => {
- const user = userEvent.setup();
-
- render(() => , {
- app: createMockApp(),
- plugin: createMockPlugin()
- });
-
- const checkbox = screen.getByRole('checkbox');
- expect(checkbox).not.toBeChecked();
-
- await user.click(checkbox);
- expect(checkbox).toBeChecked();
- });
-});
\ No newline at end of file
diff --git a/src/services/__tests__/model-registry.test.ts b/src/services/__tests__/model-registry.test.ts
deleted file mode 100644
index f32766a..0000000
--- a/src/services/__tests__/model-registry.test.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { ModelRegistry } from "@/services/model-registry";
-import type { CoIntelligencePlugin } from "@/CoIntelligencePlugin";
-
-// Mock the AI SDK providers
-vi.mock("@ai-sdk/openai", () => ({
- createOpenAI: vi.fn(() => "mocked-openai-provider"),
-}));
-
-vi.mock("@ai-sdk/anthropic", () => ({
- createAnthropic: vi.fn(() => "mocked-anthropic-provider"),
-}));
-
-vi.mock("@ai-sdk/google", () => ({
- createGoogleGenerativeAI: vi.fn(() => "mocked-google-provider"),
-}));
-
-vi.mock("@ai-sdk/perplexity", () => ({
- createPerplexity: vi.fn(() => "mocked-perplexity-provider"),
-}));
-
-vi.mock("ai", () => ({
- createProviderRegistry: vi.fn((providers) => ({
- providers,
- languageModel: vi.fn((modelId: string) => {
- if (modelId.startsWith("openai:")) return { provider: "openai" };
- if (modelId.startsWith("anthropic:")) return { provider: "anthropic" };
- if (modelId.startsWith("google:")) return { provider: "google" };
- if (modelId.startsWith("perplexity:")) return { provider: "perplexity" };
- return null;
- }),
- })),
-}));
-
-vi.mock("obsidian", () => ({
- Notice: vi.fn(),
-}));
-
-describe("ModelRegistry", () => {
- let mockPlugin: CoIntelligencePlugin;
-
- beforeEach(() => {
- // Reset the singleton instance before each test
- (ModelRegistry as any).instance = null;
-
- mockPlugin = {
- settings: {
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- perplexityApiKey: "",
- },
- } as CoIntelligencePlugin;
- });
-
- afterEach(() => {
- vi.clearAllMocks();
- // Clean up singleton instance
- (ModelRegistry as any).instance = null;
- });
-
- describe("singleton pattern", () => {
- it("should create a new instance when none exists and plugin provided", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- expect(registry).toBeInstanceOf(ModelRegistry);
- });
-
- it("should return the same instance on subsequent calls", () => {
- const registry1 = ModelRegistry.getInstance(mockPlugin);
- const registry2 = ModelRegistry.getInstance();
-
- expect(registry1).toBe(registry2);
- });
-
- it("should throw error when no instance exists and no plugin provided", () => {
- expect(() => {
- ModelRegistry.getInstance();
- }).toThrow(
- "No existing registry instance and no plugin instance provided.",
- );
- });
- });
-
- describe("provider initialization", () => {
- it("should initialize no providers when no API keys provided", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- expect(registry.availableModels).toHaveLength(0);
- expect(registry.hasInitializedProviders()).toBe(false);
- });
-
- it("should initialize OpenAI provider when API key provided", () => {
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const openaiModels = registry.availableModels.filter(
- (m) => m.provider === "openai",
- );
- expect(openaiModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
-
- it("should initialize Anthropic provider when API key provided", () => {
- mockPlugin.settings.anthropicApiKey = "sk-ant-test-key";
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const anthropicModels = registry.availableModels.filter(
- (m) => m.provider === "anthropic",
- );
- expect(anthropicModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
-
- it("should initialize Google provider when API key provided", () => {
- mockPlugin.settings.googleApiKey = "google-test-key";
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const googleModels = registry.availableModels.filter(
- (m) => m.provider === "google",
- );
- expect(googleModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
-
- it("should initialize Perplexity provider when API key provided", () => {
- mockPlugin.settings.perplexityApiKey = "pplx-test-key";
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const perplexityModels = registry.availableModels.filter(
- (m) => m.provider === "perplexity",
- );
- expect(perplexityModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
-
- it("should initialize multiple providers when multiple API keys provided", () => {
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- mockPlugin.settings.anthropicApiKey = "sk-ant-test-key";
-
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const openaiModels = registry.availableModels.filter(
- (m) => m.provider === "openai",
- );
- const anthropicModels = registry.availableModels.filter(
- (m) => m.provider === "anthropic",
- );
-
- expect(openaiModels.length).toBeGreaterThan(0);
- expect(anthropicModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
- });
-
- describe("model retrieval", () => {
- beforeEach(() => {
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- mockPlugin.settings.anthropicApiKey = "sk-ant-test-key";
- });
-
- it("should retrieve a model by ID", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const model = registry.getModel("openai:gpt-4o");
-
- expect(model).toBeDefined();
- expect(model.id).toBe("openai:gpt-4o");
- expect(model.provider).toBe("openai");
- expect(model.name).toBe("OpenAI GPT-4o");
- });
-
- it("should throw error for non-existent model", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- expect(() => {
- registry.getModel("nonexistent:model" as any);
- }).toThrow("Model not found: nonexistent:model");
- });
-
- it("should get language model from provider registry", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const languageModel = registry.getLanguageModel("openai:gpt-4o");
-
- expect(languageModel).toBeDefined();
- expect(languageModel.provider).toBe("openai");
- });
-
- it("should return default model when available", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const defaultModel = registry.getDefaultModel();
-
- expect(defaultModel).toBeDefined();
- expect(registry.availableModels).toContain(defaultModel);
- });
-
- it("should return null as default model when no providers initialized", () => {
- // Create registry without API keys
- mockPlugin.settings = {
- openaiApiKey: "",
- anthropicApiKey: "",
- googleApiKey: "",
- perplexityApiKey: "",
- defaultFolder: "",
- defaultModel: "" as any,
- renamingModel: "" as any,
- systemPromptFolder: "",
- defaultSystemPromptNote: ""
- };
-
- const registry = ModelRegistry.getInstance(mockPlugin);
- const defaultModel = registry.getDefaultModel();
-
- expect(defaultModel).toBeNull();
- });
- });
-
- describe("reinitialization", () => {
- it("should reinitialize providers when reinitialize is called", () => {
- // Start with no API keys
- const registry = ModelRegistry.getInstance(mockPlugin);
- expect(registry.availableModels).toHaveLength(0);
-
- // Add API key and reinitialize
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- registry.reinitialize();
-
- expect(registry.availableModels.length).toBeGreaterThan(0);
- expect(registry.hasInitializedProviders()).toBe(true);
- });
-
- it("should update available models after reinitialization", () => {
- // Start with OpenAI only
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- const registry = ModelRegistry.getInstance(mockPlugin);
-
- const initialModelCount = registry.availableModels.length;
- expect(
- registry.availableModels.every((m) => m.provider === "openai"),
- ).toBe(true);
-
- // Add Anthropic and reinitialize
- mockPlugin.settings.anthropicApiKey = "sk-ant-test-key";
- registry.reinitialize();
-
- expect(registry.availableModels.length).toBeGreaterThan(
- initialModelCount,
- );
- expect(
- registry.availableModels.some((m) => m.provider === "anthropic"),
- ).toBe(true);
- });
- });
-
- describe("model properties", () => {
- beforeEach(() => {
- mockPlugin.settings.openaiApiKey = "sk-test-key";
- mockPlugin.settings.anthropicApiKey = "sk-ant-test-key";
- });
-
- it("should have correct model properties for OpenAI models", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const gpt4o = registry.getModel("openai:gpt-4o");
-
- expect(gpt4o.renaming).toBe(true);
- expect(gpt4o.toggleWebSearch).toBe(true);
- expect(gpt4o.streaming).toBe(true);
- });
-
- it("should have correct model properties for O1 models (no web search)", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const o1 = registry.getModel("openai:o1");
-
- expect(o1.renaming).toBe(false);
- expect(o1.toggleWebSearch).toBe(false);
- expect(o1.streaming).toBe(true);
- });
-
- it("should have correct model properties for Claude models", () => {
- const registry = ModelRegistry.getInstance(mockPlugin);
- const claude = registry.getModel("anthropic:claude-4-sonnet-20250514");
-
- expect(claude.renaming).toBe(true);
- expect(claude.toggleWebSearch).toBe(false);
- expect(claude.streaming).toBe(true);
- });
- });
-});
diff --git a/src/services/__tests__/model-service.test.ts b/src/services/__tests__/model-service.test.ts
deleted file mode 100644
index 4bd3feb..0000000
--- a/src/services/__tests__/model-service.test.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import {
- generateChatResponse,
- cancelChatResponse,
- deleteAbortControllerForRequest,
- generateChatTitle
-} from '@/services/model-service';
-import { ModelRegistry } from '@/services/model-registry';
-import { ChatRequest, ModelChatMessage, ContextItemContent } from '@/types';
-import type { CoIntelligencePlugin } from '@/CoIntelligencePlugin';
-
-// Mock the AI SDK
-vi.mock('ai', async () => {
- const actual = await vi.importActual('ai');
- return {
- ...actual,
- streamText: vi.fn(),
- generateText: vi.fn()
- };
-});
-
-// Mock the openai SDK
-vi.mock('@ai-sdk/openai', () => ({
- openai: {
- tools: {
- webSearchPreview: vi.fn(() => ({ type: 'web_search_preview' }))
- }
- }
-}));
-
-// Mock the model context utility
-vi.mock('@/utils/model-context', () => ({
- makeContext: vi.fn((contexts: ContextItemContent[]) =>
- contexts.map(c => `--- Note: ${c.title} ---\n${c.content}\n---`).join('\n\n')
- )
-}));
-
-// Mock ModelRegistry to avoid singleton instance issues in tests
-let mockRegistryInstance: any;
-vi.mock('@/services/model-registry', () => ({
- ModelRegistry: {
- getInstance: vi.fn(() => mockRegistryInstance)
- }
-}));
-
-import { streamText, generateText } from 'ai';
-
-describe('model-service', () => {
- let mockRegistry: ModelRegistry;
- let mockPlugin: CoIntelligencePlugin;
- let mockModel: any;
- let baseRequest: ChatRequest;
-
- beforeEach(() => {
- // Reset mocks
- vi.clearAllMocks();
-
- // Create mock model
- mockModel = {
- provider: 'openai',
- doStream: vi.fn().mockResolvedValue({
- stream: async function* () {
- yield { type: 'text-delta', textDelta: 'Hello' };
- yield { type: 'text-delta', textDelta: ' world' };
- yield { type: 'finish', finishReason: 'stop', usage: {
- promptTokens: 10,
- completionTokens: 2,
- totalTokens: 12
- }};
- }
- })
- };
-
- // Create mock registry
- mockRegistry = {
- getLanguageModel: vi.fn().mockReturnValue(mockModel),
- getModel: vi.fn().mockReturnValue({
- id: 'openai:gpt-4o',
- provider: 'openai',
- name: 'OpenAI GPT-4o',
- renaming: true,
- toggleWebSearch: true,
- streaming: true
- })
- } as any;
-
- // Set the mock registry instance for the mocked ModelRegistry.getInstance
- mockRegistryInstance = mockRegistry;
-
- // Create mock plugin
- mockPlugin = {
- settings: {
- renamingModel: 'openai:gpt-4o'
- }
- } as any;
-
- // Base request object
- baseRequest = {
- requestID: 'test-request-123',
- modelId: 'openai:gpt-4o' as any,
- messages: [{ role: 'user', content: 'Hello' }],
- webSearch: false
- };
-
- // Mock streamText to return a mock result
- const mockStreamResult = {
- textStream: (async function* () {
- yield 'Hello';
- yield ' world';
- })(),
- text: 'Hello world',
- finishReason: 'stop',
- usage: { promptTokens: 10, completionTokens: 2, totalTokens: 12 }
- };
-
- (streamText as any).mockResolvedValue(mockStreamResult);
- });
-
- afterEach(() => {
- vi.clearAllMocks();
- });
-
- describe('generateChatResponse', () => {
- it('should generate a basic chat response', async () => {
- const result = await generateChatResponse(baseRequest, mockRegistry);
-
- expect(mockRegistry.getLanguageModel).toHaveBeenCalledWith('openai:gpt-4o');
- expect(mockRegistry.getModel).toHaveBeenCalledWith('openai:gpt-4o');
- expect(streamText).toHaveBeenCalled();
- expect(result).toBeDefined();
- });
-
- it('should use default system prompt when none provided', async () => {
- await generateChatResponse(baseRequest, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.system).toBe('You are a helpful assistant.');
- });
-
- it('should use custom system prompt when provided', async () => {
- const requestWithSystem = {
- ...baseRequest,
- systemPrompt: 'You are a helpful coding assistant.'
- };
-
- await generateChatResponse(requestWithSystem, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.system).toBe('You are a helpful coding assistant.');
- });
-
- it('should disable web search for models that do not support it', async () => {
- mockRegistry.getModel = vi.fn().mockReturnValue({
- id: 'openai:o1',
- provider: 'openai',
- name: 'OpenAI O1',
- renaming: false,
- toggleWebSearch: false,
- streaming: true
- });
-
- const requestWithWebSearch = {
- ...baseRequest,
- modelId: 'openai:o1' as any,
- webSearch: true
- };
-
- await generateChatResponse(requestWithWebSearch, mockRegistry);
-
- // The webSearch should be set to false internally
- expect(requestWithWebSearch.webSearch).toBe(false);
- });
-
- it('should add web search system prompt when web search enabled', async () => {
- const requestWithWebSearch = {
- ...baseRequest,
- webSearch: true,
- systemPrompt: 'Custom prompt.'
- };
-
- await generateChatResponse(requestWithWebSearch, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.system).toContain('Custom prompt.');
- expect(streamTextCall.system).toContain('You should search the web for current information');
- });
-
- it('should add OpenAI web search tool when web search enabled for OpenAI models', async () => {
- const requestWithWebSearch = {
- ...baseRequest,
- webSearch: true
- };
-
- await generateChatResponse(requestWithWebSearch, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.tools).toBeDefined();
- expect(streamTextCall.tools.web_search_preview).toEqual({ type: 'web_search_preview' });
- });
-
- it('should add reasoning summary for O3 models', async () => {
- const o3Request = {
- ...baseRequest,
- modelId: 'openai:o3' as any
- };
-
- await generateChatResponse(o3Request, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.providerOptions?.openai?.reasoningSummary).toBe('detailed');
- });
-
- it('should add anthropic headers for anthropic models', async () => {
- const anthropicModel = {
- provider: 'anthropic'
- };
-
- mockRegistry.getLanguageModel = vi.fn().mockReturnValue(anthropicModel);
- mockRegistry.getModel = vi.fn().mockReturnValue({
- id: 'anthropic:claude-4-sonnet-20250514',
- provider: 'anthropic',
- name: 'Anthropic Claude 4 Sonnet',
- renaming: true,
- toggleWebSearch: false,
- streaming: true
- });
-
- const anthropicRequest = {
- ...baseRequest,
- modelId: 'anthropic:claude-4-sonnet-20250514' as any
- };
-
- await generateChatResponse(anthropicRequest, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.headers).toEqual({
- 'anthropic-dangerous-direct-browser-access': 'true'
- });
- });
-
- it('should configure Google models for web search', async () => {
- const mockGoogleModel = {
- provider: 'google',
- settings: {} as any
- };
-
- mockRegistry.getLanguageModel = vi.fn().mockReturnValue(mockGoogleModel);
- mockRegistry.getModel = vi.fn().mockReturnValue({
- id: 'google:gemini-2.0-flash',
- provider: 'google',
- name: 'Google Gemini 2.0 Flash',
- renaming: false,
- toggleWebSearch: true,
- streaming: true
- });
-
- const googleRequest = {
- ...baseRequest,
- modelId: 'google:gemini-2.0-flash' as any,
- webSearch: true
- };
-
- await generateChatResponse(googleRequest, mockRegistry);
-
- expect(mockGoogleModel.settings.useSearchGrounding).toBe(true);
- });
-
- it('should handle context injection', async () => {
- const contextItems: ContextItemContent[] = [
- { title: 'Note 1', content: 'Content of note 1' },
- { title: 'Note 2', content: 'Content of note 2' }
- ];
-
- const requestWithContext = {
- ...baseRequest,
- context: contextItems,
- systemPrompt: 'Base prompt.'
- };
-
- await generateChatResponse(requestWithContext, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.system).toContain('Base prompt.');
- expect(streamTextCall.system).toContain('The following documents provide additional context');
- expect(streamTextCall.system).toContain('--- Note: Note 1 ---');
- expect(streamTextCall.system).toContain('Content of note 1');
- });
-
- it('should handle errors in streaming', async () => {
- const errorMessage = 'Streaming error';
- (streamText as any).mockRejectedValue(new Error(errorMessage));
-
- await expect(generateChatResponse(baseRequest, mockRegistry)).rejects.toThrow(errorMessage);
- });
-
- it('should create and store abort controller', async () => {
- await generateChatResponse(baseRequest, mockRegistry);
-
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.abortSignal).toBeInstanceOf(AbortSignal);
- });
- });
-
- describe('abort controller functionality', () => {
- it('should cancel chat response', async () => {
- // Start a request
- const responsePromise = generateChatResponse(baseRequest, mockRegistry);
-
- // Cancel it
- cancelChatResponse(baseRequest);
-
- // The abort signal should be triggered
- const streamTextCall = (streamText as any).mock.calls[0][0];
- expect(streamTextCall.abortSignal).toBeInstanceOf(AbortSignal);
-
- await responsePromise;
- });
-
- it('should delete abort controller for request', async () => {
- await generateChatResponse(baseRequest, mockRegistry);
-
- // Should not throw
- deleteAbortControllerForRequest(baseRequest);
- });
-
- it('should handle canceling non-existent request', () => {
- const nonExistentRequest = {
- ...baseRequest,
- requestID: 'non-existent-request'
- };
-
- // Should not throw
- expect(() => cancelChatResponse(nonExistentRequest)).not.toThrow();
- });
-
- it('should handle deleting non-existent abort controller', () => {
- const nonExistentRequest = {
- ...baseRequest,
- requestID: 'non-existent-request'
- };
-
- // Should not throw
- expect(() => deleteAbortControllerForRequest(nonExistentRequest)).not.toThrow();
- });
- });
-
- describe('generateChatTitle', () => {
- const testMessages: ModelChatMessage[] = [
- { role: 'user', content: 'How do I implement a binary search algorithm?' },
- { role: 'assistant', content: 'A binary search algorithm works by...' }
- ];
-
-
- it('should generate a chat title', async () => {
- const mockGenerateResult = {
- text: 'Binary Search Implementation Guide'
- };
- (generateText as any).mockResolvedValue(mockGenerateResult);
-
- const title = await generateChatTitle(testMessages, mockPlugin);
-
- expect(title).toBe('Binary Search Implementation Guide (Chat)');
- expect(generateText).toHaveBeenCalled();
- });
-
- it('should return empty string when no renaming model configured', async () => {
- mockPlugin.settings.renamingModel = "" as any;
-
- const title = await generateChatTitle(testMessages, mockPlugin);
-
- expect(title).toBe('');
- expect(generateText).not.toHaveBeenCalled();
- });
-
- it('should sanitize forbidden characters in title', async () => {
- const mockGenerateResult = {
- text: 'How to use / \\ and : characters'
- };
- (generateText as any).mockResolvedValue(mockGenerateResult);
-
- const title = await generateChatTitle(testMessages, mockPlugin);
-
- expect(title).toBe('How to use - - and - characters (Chat)');
- });
-
- it('should truncate long titles', async () => {
- const mockGenerateResult = {
- text: 'This is a very long title that exceeds the fifty character limit and should be truncated'
- };
- (generateText as any).mockResolvedValue(mockGenerateResult);
-
- const title = await generateChatTitle(testMessages, mockPlugin);
-
- expect(title.length).toBeLessThanOrEqual(57); // 50 chars + ' (Chat)' = 57
- });
-
- it('should handle anthropic models with special headers', async () => {
- mockRegistry.getLanguageModel = vi.fn().mockReturnValue({
- provider: 'anthropic.claude-4-sonnet-20250514'
- });
-
- const mockGenerateResult = {
- text: 'Anthropic Chat Title'
- };
- (generateText as any).mockResolvedValue(mockGenerateResult);
-
- await generateChatTitle(testMessages, mockPlugin);
-
- const generateTextCall = (generateText as any).mock.calls[0][0];
- expect(generateTextCall.headers).toEqual({
- 'anthropic-dangerous-direct-browser-access': 'true'
- });
- });
-
- it('should handle errors gracefully', async () => {
- (generateText as any).mockRejectedValue(new Error('Title generation failed'));
-
- const title = await generateChatTitle(testMessages, mockPlugin);
-
- expect(title).toBe('');
- });
-
- it('should use correct system prompt for title generation', async () => {
- const mockGenerateResult = {
- text: 'Generated Title'
- };
- (generateText as any).mockResolvedValue(mockGenerateResult);
-
- await generateChatTitle(testMessages, mockPlugin);
-
- const generateTextCall = (generateText as any).mock.calls[0][0];
- expect(generateTextCall.system).toContain('Summarize the following conversation');
- expect(generateTextCall.system).toContain('six words or less');
- expect(generateTextCall.system).toContain('must not contain the characters /, \\, or :');
- });
- });
-});
\ No newline at end of file
diff --git a/src/utils/__tests__/model-context.test.ts b/src/utils/__tests__/model-context.test.ts
deleted file mode 100644
index b7d69bc..0000000
--- a/src/utils/__tests__/model-context.test.ts
+++ /dev/null
@@ -1,377 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { TFile, App } from 'obsidian';
-import {
- getContext,
- makeContext,
- contextTokenEstimate
-} from '@/utils/model-context';
-import { ContextItems, ContextItemContent, Source } from '@/types';
-
-// Mock Obsidian functions
-vi.mock('obsidian', async () => {
- const actual = await vi.importActual('obsidian');
- return {
- ...actual,
- requestUrl: vi.fn(),
- htmlToMarkdown: vi.fn(),
- TFile: class MockTFile {
- name: string;
- path: string;
- basename: string;
- extension: string;
-
- constructor(path: string) {
- this.name = path.split('/').pop() || '';
- this.path = path;
- this.basename = this.name.replace(/\.[^/.]+$/, '');
- this.extension = path.split('.').pop() || '';
- }
- }
- };
-});
-
-// Mock the notes utility
-vi.mock('@/utils/notes', () => ({
- getFilesWithTag: vi.fn()
-}));
-
-import { requestUrl, htmlToMarkdown } from 'obsidian';
-import { getFilesWithTag } from '@/utils/notes';
-
-describe('model-context utilities', () => {
- let mockApp: App;
- let mockFile1: TFile;
- let mockFile2: TFile;
-
- beforeEach(() => {
- vi.clearAllMocks();
-
- mockFile1 = {
- name: 'note1.md',
- path: 'notes/note1.md',
- basename: 'note1',
- extension: 'md',
- vault: null,
- parent: null,
- stat: { ctime: 0, mtime: 0, size: 0 }
- } as any;
-
- mockFile2 = {
- name: 'note2.md',
- path: 'notes/note2.md',
- basename: 'note2',
- extension: 'md',
- vault: null,
- parent: null,
- stat: { ctime: 0, mtime: 0, size: 0 }
- } as any;
-
- mockApp = {
- vault: {
- cachedRead: vi.fn()
- },
- metadataCache: {
- getFileCache: vi.fn(),
- getCache: vi.fn()
- }
- } as any;
- });
-
- describe('makeContext', () => {
- it('should create context string from array of context items', () => {
- const contextItems: ContextItemContent[] = [
- { title: 'Note 1', content: 'Content of first note' },
- { title: 'Note 2', content: 'Content of second note' }
- ];
-
- const result = makeContext(contextItems);
-
- expect(result).toBe(
- '--- Note: Note 1 ---\nContent of first note\n---\n\n--- Note: Note 2 ---\nContent of second note\n---'
- );
- });
-
- it('should return empty string for empty array', () => {
- const result = makeContext([]);
- expect(result).toBe('');
- });
-
- it('should handle single context item', () => {
- const contextItems: ContextItemContent[] = [
- { title: 'Single Note', content: 'Single note content' }
- ];
-
- const result = makeContext(contextItems);
-
- expect(result).toBe('--- Note: Single Note ---\nSingle note content\n---');
- });
-
- it('should handle context items with special characters', () => {
- const contextItems: ContextItemContent[] = [
- { title: 'Note with: Special & Characters', content: 'Content with\n\nmultiple\nlines' }
- ];
-
- const result = makeContext(contextItems);
-
- expect(result).toContain('--- Note: Note with: Special & Characters ---');
- expect(result).toContain('Content with\n\nmultiple\nlines');
- });
-
- it('should handle empty content', () => {
- const contextItems: ContextItemContent[] = [
- { title: 'Empty Note', content: '' }
- ];
-
- const result = makeContext(contextItems);
-
- expect(result).toBe('--- Note: Empty Note ---\n\n---');
- });
- });
-
- describe('getContext', () => {
- it('should return empty array when items is null', async () => {
- const result = await getContext(null, mockApp);
- expect(result).toEqual([]);
- });
-
- it('should process notes from ContextItems', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1, mockFile2],
- tags: [],
- sources: []
- };
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('Content of note 1')
- .mockResolvedValueOnce('Content of note 2');
-
- const result = await getContext(contextItems, mockApp);
-
- expect(result).toHaveLength(2);
- expect(result[0]).toEqual({
- title: 'note1.md',
- content: 'Content of note 1'
- });
- expect(result[1]).toEqual({
- title: 'note2.md',
- content: 'Content of note 2'
- });
- });
-
- it('should process tags and get associated files', async () => {
- const contextItems: ContextItems = {
- notes: [],
- tags: ['#important', '#work'],
- sources: []
- };
-
- const tagFile1 = { ...mockFile1, name: 'tagged1.md' };
- const tagFile2 = { ...mockFile2, name: 'tagged2.md' };
-
- (getFilesWithTag as any)
- .mockReturnValueOnce([tagFile1])
- .mockReturnValueOnce([tagFile2]);
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('Content of tagged note 1')
- .mockResolvedValueOnce('Content of tagged note 2');
-
- const result = await getContext(contextItems, mockApp);
-
- expect(getFilesWithTag).toHaveBeenCalledWith('#important', mockApp);
- expect(getFilesWithTag).toHaveBeenCalledWith('#work', mockApp);
- expect(result).toHaveLength(2);
- });
-
- it('should process sources and fetch web content', async () => {
- const contextItems: ContextItems = {
- notes: [],
- tags: [],
- sources: [
- { url: 'https://example.com', title: 'Example Site' },
- { url: 'https://test.com' } // No title provided
- ]
- };
-
- (requestUrl as any)
- .mockResolvedValueOnce({ text: 'Example content' })
- .mockResolvedValueOnce({ text: 'Test content' });
-
- (htmlToMarkdown as any)
- .mockReturnValueOnce('Example content')
- .mockReturnValueOnce('Test content');
-
- const result = await getContext(contextItems, mockApp);
-
- expect(requestUrl).toHaveBeenCalledWith({ url: 'https://example.com' });
- expect(requestUrl).toHaveBeenCalledWith({ url: 'https://test.com' });
- expect(htmlToMarkdown).toHaveBeenCalledTimes(2);
-
- expect(result).toHaveLength(2);
- expect(result[0]).toEqual({
- title: 'Example Site',
- content: 'Example content'
- });
- expect(result[1]).toEqual({
- title: 'https://test.com',
- content: 'Test content'
- });
- });
-
- it('should handle mixed context items (notes, tags, sources)', async () => {
- const taggedFile = { ...mockFile1, name: 'tagged.md' };
-
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: ['#important'],
- sources: [{ url: 'https://example.com', title: 'Example' }]
- };
-
- (getFilesWithTag as any).mockReturnValueOnce([taggedFile]);
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('Direct note content')
- .mockResolvedValueOnce('Tagged note content');
-
- (requestUrl as any).mockResolvedValueOnce({ text: 'Web content' });
- (htmlToMarkdown as any).mockReturnValueOnce('Web content');
-
- const result = await getContext(contextItems, mockApp);
-
- expect(result).toHaveLength(3);
- expect(result[0].title).toBe('note1.md');
- expect(result[1].title).toBe('tagged.md');
- expect(result[2].title).toBe('Example');
- });
-
- it('should handle duplicate files from notes and tags', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: ['#important'],
- sources: []
- };
-
- // Tag search returns the same file that's already in notes
- (getFilesWithTag as any).mockReturnValueOnce([mockFile1]);
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('Note content first time')
- .mockResolvedValueOnce('Note content second time');
-
- const result = await getContext(contextItems, mockApp);
-
- // Should include the file twice since it's in both notes and found via tags
- expect(result).toHaveLength(2);
- expect(result[0].title).toBe('note1.md');
- expect(result[1].title).toBe('note1.md');
- });
-
- it('should handle empty file content', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: [],
- sources: []
- };
-
- mockApp.vault.cachedRead = vi.fn().mockResolvedValueOnce('');
-
- const result = await getContext(contextItems, mockApp);
-
- expect(result).toHaveLength(1);
- expect(result[0]).toEqual({
- title: 'note1.md',
- content: ''
- });
- });
-
- it('should handle vault read failures gracefully', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: [],
- sources: []
- };
-
- mockApp.vault.cachedRead = vi.fn().mockResolvedValueOnce(null);
-
- const result = await getContext(contextItems, mockApp);
-
- expect(result).toHaveLength(1);
- expect(result[0]).toEqual({
- title: 'note1.md',
- content: ''
- });
- });
- });
-
- describe('contextTokenEstimate', () => {
- it('should estimate tokens for context items', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: [],
- sources: []
- };
-
- // Mock content that's 100 characters long
- const content = 'a'.repeat(100);
- mockApp.vault.cachedRead = vi.fn().mockResolvedValueOnce(content);
-
- const result = await contextTokenEstimate(contextItems, mockApp);
-
- // Should be approximately content.length / 4 = 100 / 4 = 25
- expect(result).toBe(25);
- });
-
- it('should sum tokens from multiple context items', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1, mockFile2],
- tags: [],
- sources: []
- };
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('a'.repeat(80)) // 20 tokens
- .mockResolvedValueOnce('b'.repeat(120)); // 30 tokens
-
- const result = await contextTokenEstimate(contextItems, mockApp);
-
- expect(result).toBe(50); // 20 + 30
- });
-
- it('should handle empty content', async () => {
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: [],
- sources: []
- };
-
- mockApp.vault.cachedRead = vi.fn().mockResolvedValueOnce('');
-
- const result = await contextTokenEstimate(contextItems, mockApp);
-
- expect(result).toBe(0);
- });
-
- it('should include tokens from all context types', async () => {
- const taggedFile = { ...mockFile1, name: 'tagged.md' };
-
- const contextItems: ContextItems = {
- notes: [mockFile1],
- tags: ['#test'],
- sources: [{ url: 'https://example.com', title: 'Example' }]
- };
-
- (getFilesWithTag as any).mockReturnValueOnce([taggedFile]);
-
- mockApp.vault.cachedRead = vi.fn()
- .mockResolvedValueOnce('a'.repeat(40)) // 10 tokens
- .mockResolvedValueOnce('b'.repeat(80)); // 20 tokens
-
- (requestUrl as any).mockResolvedValueOnce({ text: 'content' });
- (htmlToMarkdown as any).mockReturnValueOnce('c'.repeat(120)); // 30 tokens
-
- const result = await contextTokenEstimate(contextItems, mockApp);
-
- expect(result).toBe(60); // 10 + 20 + 30
- });
- });
-});
\ No newline at end of file
diff --git a/src/utils/__tests__/notes.test.ts b/src/utils/__tests__/notes.test.ts
deleted file mode 100644
index 89270e8..0000000
--- a/src/utils/__tests__/notes.test.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import {
- isCoiNote,
- isPathCoiNote,
- isActiveCoiNote,
- isPathActiveCoiNote,
- sanitizeForTagName,
- serializeCoiNoteContent,
- deserializeCoiNoteContent
-} from '../notes';
-import { TFile, App } from 'obsidian';
-
-describe('notes utilities', () => {
- let mockApp: App;
- let mockFile: TFile;
-
- beforeEach(() => {
- mockApp = new App();
- mockFile = new TFile('test.md');
- });
-
- describe('isCoiNote', () => {
- it('should return true for COI notes', () => {
- mockApp.metadataCache.getFileCache = vi.fn().mockReturnValue({
- frontmatter: { 'is-coi-chat': true }
- });
-
- const result = isCoiNote(mockFile, mockApp);
- expect(result).toBe(true);
- });
-
- it('should return false for non-COI notes', () => {
- mockApp.metadataCache.getFileCache = vi.fn().mockReturnValue({
- frontmatter: { 'is-coi-chat': false }
- });
-
- const result = isCoiNote(mockFile, mockApp);
- expect(result).toBe(false);
- });
-
- it('should return false when frontmatter is missing', () => {
- mockApp.metadataCache.getFileCache = vi.fn().mockReturnValue(null);
-
- const result = isCoiNote(mockFile, mockApp);
- expect(result).toBe(false);
- });
- });
-
- describe('isPathCoiNote', () => {
- it('should return true for COI note paths', () => {
- mockApp.metadataCache.getCache = vi.fn().mockReturnValue({
- frontmatter: { 'is-coi-chat': true }
- });
-
- const result = isPathCoiNote('test.md', mockApp);
- expect(result).toBe(true);
- });
-
- it('should return false for empty path', () => {
- const result = isPathCoiNote('', mockApp);
- expect(result).toBe(false);
- });
- });
-
- describe('isActiveCoiNote', () => {
- it('should return true for active COI notes', () => {
- mockApp.metadataCache.getFileCache = vi.fn().mockReturnValue({
- frontmatter: {
- 'is-coi-chat': true,
- 'coi-chat-view': true
- }
- });
-
- const result = isActiveCoiNote(mockFile, mockApp);
- expect(result).toBe(true);
- });
-
- it('should return false for inactive COI notes', () => {
- mockApp.metadataCache.getFileCache = vi.fn().mockReturnValue({
- frontmatter: {
- 'is-coi-chat': true,
- 'coi-chat-view': false
- }
- });
-
- const result = isActiveCoiNote(mockFile, mockApp);
- expect(result).toBe(false);
- });
- });
-
- describe('isPathActiveCoiNote', () => {
- it('should return true for active COI note paths', () => {
- mockApp.metadataCache.getCache = vi.fn().mockReturnValue({
- frontmatter: {
- 'is-coi-chat': true,
- 'coi-chat-view': true
- }
- });
-
- const result = isPathActiveCoiNote('test.md', mockApp);
- expect(result).toBe(true);
- });
-
- it('should return false for empty path', () => {
- const result = isPathActiveCoiNote('', mockApp);
- expect(result).toBe(false);
- });
- });
-
- describe('sanitizeForTagName', () => {
- it('should sanitize special characters', () => {
- const result = sanitizeForTagName('Hello World!@#$%');
- expect(result).toBe('hello-world');
- });
-
- it('should preserve alphanumeric and allowed characters', () => {
- const result = sanitizeForTagName('test_tag-123');
- expect(result).toBe('test_tag-123');
- });
-
- it('should remove leading and trailing dashes', () => {
- const result = sanitizeForTagName('--test-tag--');
- expect(result).toBe('test-tag');
- });
-
- it('should collapse multiple dashes', () => {
- const result = sanitizeForTagName('test---multiple--dashes');
- expect(result).toBe('test-multiple-dashes');
- });
-
- it('should handle empty string', () => {
- const result = sanitizeForTagName('');
- expect(result).toBe('');
- });
-
- it('should trim whitespace', () => {
- const result = sanitizeForTagName(' test tag ');
- expect(result).toBe('test-tag');
- });
- });
-
- describe('serializeCoiNoteContent', () => {
- it('should serialize basic chat messages', async () => {
- const currentContent = '\n\n';
- const messages = [
- { role: 'user' as const, content: 'Hello' },
- { role: 'assistant' as const, content: 'Hi there!' }
- ];
-
- const result = await serializeCoiNoteContent(currentContent, mockApp, messages, null);
-
- expect(result).toContain('## user:\n\nHello');
- expect(result).toContain('## assistant:\n\nHi there!');
- });
-
- it('should include sources when provided', async () => {
- const currentContent = '\n\n';
- const messages = [
- { role: 'user' as const, content: 'Question' }
- ];
- const sources = [
- { title: 'Test Source', url: 'https://example.com' }
- ];
-
- const result = await serializeCoiNoteContent(currentContent, mockApp, messages, null, sources);
-
- expect(result).toContain('## Sources');
- expect(result).toContain('1. [Test Source](https://example.com)');
- });
-
- it('should preserve content before and after chat section', async () => {
- const currentContent = 'Before\n\n\n\nAfter';
- const messages = [
- { role: 'user' as const, content: 'Test' }
- ];
-
- const result = await serializeCoiNoteContent(currentContent, mockApp, messages, null);
-
- expect(result.startsWith('Before')).toBe(true);
- expect(result.endsWith('After')).toBe(true);
- });
-
- it('should adjust header levels in content', async () => {
- const currentContent = '\n\n';
- const messages = [
- { role: 'user' as const, content: '# Main Header\n## Sub Header' }
- ];
-
- const result = await serializeCoiNoteContent(currentContent, mockApp, messages, null);
-
- expect(result).toContain('### Main Header');
- expect(result).toContain('#### Sub Header');
- });
- });
-
- describe('deserializeCoiNoteContent', () => {
- it('should deserialize basic chat messages', async () => {
- const content = `
-
-## user:
-
-Hello
-
-## assistant:
-
-Hi there!
-
- `.trim();
-
- const result = await deserializeCoiNoteContent(content, null, mockApp);
-
- expect(result.messages).toHaveLength(2);
- expect(result.messages[0]).toEqual({ role: 'user', content: 'Hello' });
- expect(result.messages[1]).toEqual({ role: 'assistant', content: 'Hi there!' });
- });
-
- it('should deserialize sources', async () => {
- const content = `
-
-## user:
-
-Question
-
-## Sources
-
-1. [Test Source](https://example.com)
-2. [Another Source](https://test.com)
-
- `.trim();
-
- const result = await deserializeCoiNoteContent(content, null, mockApp);
-
- expect(result.sources).toHaveLength(2);
- expect(result.sources[0]).toEqual({ title: 'Test Source', url: 'https://example.com' });
- expect(result.sources[1]).toEqual({ title: 'Another Source', url: 'https://test.com' });
- });
-
- it('should return empty result when no chat section found', async () => {
- const content = 'Regular note content without chat section';
-
- const result = await deserializeCoiNoteContent(content, null, mockApp);
-
- expect(result.messages).toHaveLength(0);
- expect(result.sources).toHaveLength(0);
- expect(result.contextItems).toEqual({ notes: [], tags: [], sources: [] });
- });
-
- it('should handle context items from frontmatter', async () => {
- const content = '\n';
- const metadata = {
- frontmatter: {
- 'linked-notes': ['note1.md', 'note2.md'],
- 'linked-tags': ['#tag1', '#tag2']
- }
- };
-
- // Mock files for linked notes
- mockApp.vault.getAbstractFileByPath = vi.fn()
- .mockReturnValueOnce(new TFile('note1.md'))
- .mockReturnValueOnce(new TFile('note2.md'));
-
- const result = await deserializeCoiNoteContent(content, metadata, mockApp);
-
- expect(result.contextItems.notes).toHaveLength(2);
- expect(result.contextItems.tags).toEqual(['#tag1', '#tag2']);
- });
- });
-});
\ No newline at end of file
diff --git a/src/utils/__tests__/url.test.ts b/src/utils/__tests__/url.test.ts
deleted file mode 100644
index 3dc0e2f..0000000
--- a/src/utils/__tests__/url.test.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { getDomainFromUrl, ensureSourceTitle } from '../url';
-
-describe('url utilities', () => {
- describe('getDomainFromUrl', () => {
- it('should extract domain from valid HTTPS URL', () => {
- const result = getDomainFromUrl('https://www.example.com/path/to/page');
- expect(result).toBe('www.example.com');
- });
-
- it('should extract domain from valid HTTP URL', () => {
- const result = getDomainFromUrl('http://example.org/some/path');
- expect(result).toBe('example.org');
- });
-
- it('should extract domain without protocol', () => {
- const result = getDomainFromUrl('example.com/path');
- expect(result).toBe('example.com');
- });
-
- it('should handle URLs with www prefix', () => {
- const result = getDomainFromUrl('www.test-site.co.uk');
- expect(result).toBe('test-site.co.uk');
- });
-
- it('should handle URLs with ports', () => {
- const result = getDomainFromUrl('https://localhost:3000/api/endpoint');
- expect(result).toBe('localhost');
- });
-
- it('should fallback to manual parsing for invalid URLs', () => {
- const result = getDomainFromUrl('not-a-valid-url');
- expect(result).toBe('not-a-valid-url');
- });
-
- it('should handle empty string', () => {
- const result = getDomainFromUrl('');
- expect(result).toBe('');
- });
-
- it('should handle complex URLs with subdomains', () => {
- const result = getDomainFromUrl('https://api.subdomain.example.com/v1/data');
- expect(result).toBe('api.subdomain.example.com');
- });
- });
-
- describe('ensureSourceTitle', () => {
- it('should keep existing title when provided', () => {
- const source = { url: 'https://example.com', title: 'My Title' };
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://example.com',
- title: 'My Title'
- });
- });
-
- it('should trim whitespace from existing title', () => {
- const source = { url: 'https://example.com', title: ' My Title ' };
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://example.com',
- title: 'My Title'
- });
- });
-
- it('should use domain as fallback when title is empty', () => {
- const source = { url: 'https://www.example.com/page', title: '' };
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://www.example.com/page',
- title: 'www.example.com'
- });
- });
-
- it('should use domain as fallback when title is only whitespace', () => {
- const source = { url: 'https://example.org/path', title: ' ' };
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://example.org/path',
- title: 'example.org'
- });
- });
-
- it('should use domain as fallback when title is missing', () => {
- const source = { url: 'https://test.com/api/data' };
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://test.com/api/data',
- title: 'test.com'
- });
- });
-
- it('should preserve other properties from source', () => {
- const source = {
- url: 'https://example.com',
- title: 'Test',
- customProp: 'value'
- } as any;
- const result = ensureSourceTitle(source);
-
- expect(result).toEqual({
- url: 'https://example.com',
- title: 'Test',
- customProp: 'value'
- });
- });
- });
-});
\ No newline at end of file
diff --git a/test/component-testing-guide.md b/test/component-testing-guide.md
deleted file mode 100644
index 598eb4e..0000000
--- a/test/component-testing-guide.md
+++ /dev/null
@@ -1,328 +0,0 @@
-# Component Testing Guide
-
-This guide provides patterns and best practices for testing Solid.js components in the Co-Intelligence AI plugin.
-
-## Overview
-
-Component testing in this project uses:
-- **@solidjs/testing-library** - Solid-specific testing utilities
-- **@testing-library/user-event** - User interaction simulation
-- **Custom render function** - Provides necessary contexts and mocks
-
-## Testing Patterns
-
-### 1. Simple Components
-
-For components with minimal dependencies and straightforward behavior:
-
-```typescript
-// Example: ProcessingIndicator.test.tsx
-import { render } from '../../../test/utils/render';
-
-describe('ProcessingIndicator', () => {
- it('should render processing indicator with text', () => {
- render(() => );
-
- expect(screen.getByText('Generating response...')).toBeInTheDocument();
- });
-});
-```
-
-### 2. Components with Obsidian Dependencies
-
-For components that use Obsidian APIs (like `setIcon`):
-
-```typescript
-// Mock Obsidian functions
-const mockSetIcon = vi.fn();
-vi.mock('obsidian', async () => {
- const actual = await vi.importActual('obsidian');
- return {
- ...actual,
- setIcon: mockSetIcon,
- };
-});
-
-describe('LucideIcon', () => {
- it('should call setIcon with correct parameters', async () => {
- render(() => );
-
- await new Promise(resolve => setTimeout(resolve, 0)); // Wait for onMount
-
- expect(mockSetIcon).toHaveBeenCalledWith(
- expect.any(HTMLElement),
- 'home'
- );
- });
-});
-```
-
-### 3. Components with Context Dependencies
-
-For components that use context (App, Plugin):
-
-```typescript
-import { render, createMockPlugin, createMockApp } from '../../../test/utils/render';
-
-describe('ModelSelector', () => {
- it('should render with plugin context', () => {
- render(() =>
- ,
- {
- plugin: createMockPlugin(),
- app: createMockApp()
- }
- );
- });
-});
-```
-
-### 4. Complex Components with Services
-
-For components that interact with services:
-
-```typescript
-// Mock the service
-const mockModelRegistry = {
- availableModels: [],
- getInstance: vi.fn(),
-};
-
-vi.mock('@/services/model-registry', () => ({
- ModelRegistry: mockModelRegistry
-}));
-
-describe('UserInput', () => {
- beforeEach(() => {
- mockModelRegistry.getInstance.mockReturnValue(mockModelRegistry);
- });
-});
-```
-
-### 5. Testing User Interactions
-
-Use `@testing-library/user-event` for realistic interactions:
-
-```typescript
-import userEvent from '@testing-library/user-event';
-
-it('should submit on Enter key', async () => {
- const user = userEvent.setup();
- const onSubmit = vi.fn();
-
- render(() => );
-
- const textarea = screen.getByRole('textbox');
- await user.type(textarea, 'Test message{Enter}');
-
- expect(onSubmit).toHaveBeenCalledWith('Test message', false, '');
-});
-```
-
-### 6. Testing Reactive State
-
-For components with Solid.js signals:
-
-```typescript
-import { createSignal } from 'solid-js';
-
-it('should display selected model', () => {
- const [selectedModel, setSelectedModel] = createSignal(mockModel);
-
- render(() =>
-
- );
-
- expect(screen.getByRole('combobox')).toHaveValue('openai:gpt-4');
-});
-```
-
-## Mock Strategies
-
-### 1. Service Mocking
-
-Always mock external services to isolate component behavior:
-
-```typescript
-vi.mock('@/services/model-registry', () => ({
- ModelRegistry: {
- getInstance: () => mockRegistry,
- }
-}));
-```
-
-### 2. Child Component Mocking
-
-Mock complex child components to focus on the component under test:
-
-```typescript
-vi.mock('@/components/ModelSelector', () => ({
- ModelSelector: ({ onModelChange }: any) => (
-
- )
-}));
-```
-
-### 3. Obsidian API Mocking
-
-Mock only the Obsidian APIs your component actually uses:
-
-```typescript
-class MockNoteLinkSuggestionModal {
- constructor(public app: any, public query: string, public onSelect: Function) {}
- open() { this.onSelect(new TFile('/test.md')); }
- onClose = vi.fn();
-}
-
-vi.mock('@/components/NoteLinkSuggestionModal', () => ({
- NoteLinkSuggestionModal: MockNoteLinkSuggestionModal
-}));
-```
-
-## Best Practices
-
-### 1. Test User-Facing Behavior
-
-Focus on what users see and interact with, not implementation details:
-
-```typescript
-// Good - tests user-facing behavior
-expect(screen.getByRole('button')).toBeInTheDocument();
-
-// Bad - tests implementation details
-expect(wrapper.find('.internal-class')).toExist();
-```
-
-### 2. Use Meaningful Test Names
-
-Test names should describe the expected behavior:
-
-```typescript
-// Good
-it('should disable textarea when no models are available')
-
-// Bad
-it('should work correctly')
-```
-
-### 3. Arrange-Act-Assert Pattern
-
-Structure tests clearly:
-
-```typescript
-it('should toggle web search checkbox', async () => {
- // Arrange
- const user = userEvent.setup();
- render(() => );
-
- // Act
- const checkbox = screen.getByRole('checkbox');
- await user.click(checkbox);
-
- // Assert
- expect(checkbox).toBeChecked();
-});
-```
-
-### 4. Clean Up Between Tests
-
-Use `beforeEach` to reset mocks and state:
-
-```typescript
-beforeEach(() => {
- mockService.method.mockClear();
- mockService.availableItems = [];
-});
-```
-
-### 5. Test Error Conditions
-
-Don't forget to test error states and edge cases:
-
-```typescript
-it('should throw error when plugin context is not available', () => {
- expect(() =>
- render(() => ),
- { plugin: null }
- ).toThrow('Plugin Context is not available');
-});
-```
-
-### 6. Use Accessibility-Friendly Queries
-
-Prefer queries that match how users interact with your app:
-
-```typescript
-// Good - uses semantic role
-screen.getByRole('textbox')
-screen.getByRole('button', { name: 'Submit' })
-
-// Good - uses label text
-screen.getByLabelText('Model selector')
-
-// Okay - uses visible text
-screen.getByText('Cancel')
-
-// Avoid - uses test IDs (only when necessary)
-screen.getByTestId('complex-component')
-```
-
-## Common Pitfalls
-
-### 1. Over-Mocking
-Don't mock everything - focus on external dependencies and complex children.
-
-### 2. Testing Implementation Details
-Test what users see, not how the code works internally.
-
-### 3. Ignoring Async Operations
-Always await user interactions and effects:
-
-```typescript
-await user.click(button);
-await waitFor(() => expect(screen.getByText('Success')).toBeInTheDocument());
-```
-
-### 4. Not Cleaning Up
-Reset mocks and clear state between tests to avoid test interdependence.
-
-### 5. Overly Complex Tests
-Keep tests focused on one behavior at a time. Split complex scenarios into multiple tests.
-
-## File Organization
-
-```
-src/components/
-├── ComponentName.tsx
-└── __tests__/
- └── ComponentName.test.tsx
-```
-
-Each component should have its test file in a `__tests__` directory within the same folder as the component.
-
-## Running Tests
-
-```bash
-# Run all component tests
-npm run test src/components
-
-# Run tests with UI
-npm run test:ui
-
-# Run tests with coverage
-npm run test:coverage
-```
-
-## Examples
-
-See the following test files for complete examples:
-- `src/components/__tests__/ProcessingIndicator.test.tsx` - Simple component
-- `src/components/__tests__/LucideIcon.test.tsx` - Component with Obsidian dependency
-- `src/components/__tests__/ModelSelector.test.tsx` - Component with service dependency
-- `src/components/__tests__/UserInput.test.tsx` - Complex component with multiple interactions
-- `src/components/__tests__/ChatInterface.test.tsx` - Integration-style component test
\ No newline at end of file
diff --git a/test/mocks/obsidian.ts b/test/mocks/obsidian.ts
deleted file mode 100644
index 8660903..0000000
--- a/test/mocks/obsidian.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-import { vi } from 'vitest';
-
-// Minimal Obsidian API mocks - only implement what we actually use
-
-export class TFile {
- path: string;
- basename: string;
- extension: string;
- name: string;
- parent: TFolder | null = null;
-
- constructor(path: string) {
- this.path = path;
- this.name = path.split('/').pop() || '';
- this.basename = this.name.replace(/\.[^/.]+$/, '');
- this.extension = path.split('.').pop() || '';
-
- // Set parent if path has directories
- const parentPath = path.split('/').slice(0, -1).join('/');
- if (parentPath) {
- this.parent = new TFolder(parentPath);
- }
- }
-}
-
-export class TFolder {
- path: string;
- name: string;
-
- constructor(path: string) {
- this.path = path;
- this.name = path.split('/').pop() || '';
- }
-}
-
-export class Plugin {
- app: App;
- manifest: any;
-
- constructor(app: App, manifest: any) {
- this.app = app;
- this.manifest = manifest;
- }
-
- addCommand = vi.fn().mockReturnValue(this);
- addSettingTab = vi.fn().mockReturnValue(this);
- registerView = vi.fn().mockReturnValue(this);
- saveData = vi.fn().mockResolvedValue(undefined);
- loadData = vi.fn().mockResolvedValue({});
- onload = vi.fn();
- onunload = vi.fn();
-}
-
-export class App {
- vault = {
- create: vi.fn().mockResolvedValue(new TFile('test.md')),
- modify: vi.fn().mockResolvedValue(undefined),
- read: vi.fn().mockResolvedValue(''),
- cachedRead: vi.fn().mockResolvedValue(''),
- getMarkdownFiles: vi.fn().mockReturnValue([]),
- delete: vi.fn().mockResolvedValue(undefined),
- exists: vi.fn().mockReturnValue(true),
- getAbstractFileByPath: vi.fn(),
- createFolder: vi.fn().mockResolvedValue(new TFolder('test')),
- };
-
- workspace = {
- getLeaf: vi.fn().mockReturnValue({
- view: null,
- setViewState: vi.fn().mockResolvedValue(undefined),
- openFile: vi.fn().mockResolvedValue(undefined),
- }),
- activeLeaf: null,
- on: vi.fn(),
- off: vi.fn(),
- trigger: vi.fn(),
- getLeavesOfType: vi.fn().mockReturnValue([]),
- detachLeavesOfType: vi.fn(),
- };
-
- metadataCache = {
- getFileCache: vi.fn().mockReturnValue(null),
- getCache: vi.fn().mockReturnValue(null),
- on: vi.fn(),
- off: vi.fn(),
- };
-
- fileManager = {
- processFrontMatter: vi.fn(),
- generateMarkdownLink: vi.fn(),
- renameFile: vi.fn().mockResolvedValue(undefined),
- };
-}
-
-export class WorkspaceLeaf {
- view: any = null;
-
- setViewState = vi.fn().mockResolvedValue(undefined);
- detach = vi.fn();
- getViewState = vi.fn().mockReturnValue({});
-}
-
-export class ItemView {
- app: App;
- leaf: WorkspaceLeaf;
-
- constructor(leaf: WorkspaceLeaf) {
- this.leaf = leaf;
- this.app = leaf.view?.app || new App();
- }
-
- onOpen = vi.fn().mockResolvedValue(undefined);
- onClose = vi.fn().mockResolvedValue(undefined);
- getDisplayText = vi.fn().mockReturnValue('Test View');
- getViewType = vi.fn().mockReturnValue('test-view');
-}
-
-export class TextFileView extends ItemView {
- file: TFile | null = null;
-
- constructor(leaf: WorkspaceLeaf) {
- super(leaf);
- }
-
- onLoadFile = vi.fn().mockResolvedValue(undefined);
- onUnloadFile = vi.fn().mockResolvedValue(undefined);
- getViewData = vi.fn().mockReturnValue('');
- setViewData = vi.fn();
- clear = vi.fn();
-}
-
-export class PluginSettingTab {
- app: App;
- plugin: Plugin;
-
- constructor(app: App, plugin: Plugin) {
- this.app = app;
- this.plugin = plugin;
- }
-
- display = vi.fn();
- hide = vi.fn();
-}
-
-export class Setting {
- constructor(containerEl: HTMLElement) {}
-
- setName = vi.fn().mockReturnThis();
- setDesc = vi.fn().mockReturnThis();
- addText = vi.fn().mockReturnThis();
- addToggle = vi.fn().mockReturnThis();
- addDropdown = vi.fn().mockReturnThis();
-}
-
-export class SuggestModal {
- app: App;
-
- constructor(app: App) {
- this.app = app;
- }
-
- open = vi.fn();
- close = vi.fn();
- getSuggestions = vi.fn().mockReturnValue([]);
- renderSuggestion = vi.fn();
- onChooseSuggestion = vi.fn();
-}
-
-export class Modal {
- app: App;
-
- constructor(app: App) {
- this.app = app;
- }
-
- open = vi.fn();
- close = vi.fn();
- onOpen = vi.fn();
- onClose = vi.fn();
-}
-
-// Mock global functions
-export const addIcon = vi.fn();
-export const debounce = vi.fn((fn: Function, delay: number) => fn);
-export const normalizePath = vi.fn((path: string) => path);
-export const parseFrontMatterEntry = vi.fn();
-export const parseFrontMatterStringArray = vi.fn();
-export const parseFrontMatterTags = vi.fn();
-
-// Default export for compatibility
-export default {
- TFile,
- TFolder,
- Plugin,
- App,
- WorkspaceLeaf,
- ItemView,
- TextFileView,
- PluginSettingTab,
- Setting,
- SuggestModal,
- Modal,
- addIcon,
- debounce,
- normalizePath,
- parseFrontMatterEntry,
- parseFrontMatterStringArray,
- parseFrontMatterTags,
-};
\ No newline at end of file
diff --git a/test/setup.ts b/test/setup.ts
deleted file mode 100644
index d17a2af..0000000
--- a/test/setup.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { vi, afterEach, expect } from "vitest";
-import '@testing-library/jest-dom';
-
-// Global test setup and configuration
-global.console = {
- ...console,
- // Suppress console.log in tests unless needed for debugging
- log: vi.fn(),
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: console.error, // Keep error logging for debugging
-};
-
-// Add Obsidian extensions to built-in types
-declare global {
- interface String {
- contains(searchString: string): boolean;
- }
-}
-
-// Polyfill for Obsidian's String.contains method
-String.prototype.contains = function (searchString: string): boolean {
- return this.includes(searchString);
-};
-
-// Clean up after each test
-afterEach(() => {
- vi.clearAllMocks();
- vi.clearAllTimers();
- vi.useRealTimers();
-});
diff --git a/test/testing.md b/test/testing.md
deleted file mode 100644
index 3c35966..0000000
--- a/test/testing.md
+++ /dev/null
@@ -1,273 +0,0 @@
-# Testing Guide for Co-Intelligence AI
-
-This document provides instructions for running tests and understanding the testing infrastructure for the Co-Intelligence AI Obsidian plugin.
-
-## Quick Start
-
-```bash
-# Run all tests
-npm test
-
-# Run tests in watch mode (interactive)
-npm test
-
-# Run tests once and exit
-npm run test:run
-
-# Run tests with coverage report
-npm run test:coverage
-
-# Open interactive test UI in browser
-npm run test:ui
-```
-
-## Testing Commands
-
-### Basic Testing
-- `npm test` - Run tests in watch mode (recommended for development)
-- `npm run test:run` - Run all tests once and exit (used in CI)
-
-### Coverage & Analysis
-- `npm run test:coverage` - Generate detailed coverage report (HTML + terminal)
-- `npm run test:ui` - Open Vitest UI for interactive test exploration
-
-### Development Tips
-- Use `npm test` during development - it watches for file changes and re-runs tests automatically
-- Use `npm run test:ui` for debugging complex test scenarios with the visual interface
-- Check `coverage/index.html` after running coverage command for detailed HTML report
-
-## Directory Structure
-
-```
-test/
-├── testing.md # This guide
-├── setup.ts # Global test setup and configuration
-├── mocks/ # Mock implementations
-│ └── obsidian.ts # Obsidian API mocks
-├── fixtures/ # Test data (planned)
-│ ├── chat-data.ts # Sample chat messages
-│ ├── ai-responses.ts # Mock AI responses
-│ └── notes.ts # Sample Obsidian notes
-└── utils/ # Test utilities
- ├── test-helpers.ts # Common helper functions
- └── render.tsx # Custom Solid.js render utilities
-
-src/
-├── **/__tests__/ # Unit tests (co-located with source)
-│ ├── *.test.ts # TypeScript/utility tests
-│ └── *.test.tsx # Component tests
-└── ...
-```
-
-## Testing Approach
-
-### 1. Unit Tests (Current Focus)
-**Location**: `src/**/__tests__/*.test.ts`
-**Purpose**: Test individual functions and utilities in isolation
-
-Examples:
-- `src/utils/__tests__/debounce.test.ts` - Debounce function logic
-- `src/utils/__tests__/url.test.ts` - URL parsing and validation
-- `src/utils/__tests__/notes.test.ts` - Note serialization/deserialization
-
-### 2. Service Tests (Phase 2)
-**Location**: `src/services/__tests__/*.test.ts`
-**Purpose**: Test core business logic with mocked dependencies
-
-Planned tests:
-- Model registry behavior
-- AI streaming with abort handling
-- Context preparation for prompts
-
-### 3. Component Tests (Phase 3)
-**Location**: `src/components/__tests__/*.test.tsx`
-**Purpose**: Test Solid.js components with user interactions
-
-Planned tests:
-- User input handling
-- Message rendering
-- Model selector behavior
-
-## Mock Strategy
-
-### Obsidian API Mocks
-We use minimal mocks in `test/mocks/obsidian.ts` that only implement methods actually used by our code:
-
-```typescript
-// Example: Mock only what you need
-export class App {
- vault = {
- create: vi.fn().mockResolvedValue(new TFile('test.md')),
- read: vi.fn().mockResolvedValue(''),
- // ... only mocked methods we use
- };
-}
-```
-
-### AI SDK Testing
-The AI SDK provides built-in testing utilities:
-```typescript
-import { MockLanguageModelV2, simulateReadableStream } from 'ai/test';
-```
-
-### Obsidian Extensions
-We polyfill Obsidian-specific extensions in `test/setup.ts`:
-```typescript
-// Adds String.contains() method used by Obsidian
-String.prototype.contains = function(searchString: string): boolean {
- return this.includes(searchString);
-};
-```
-
-## Writing Tests
-
-### Basic Test Structure
-```typescript
-import { describe, it, expect, vi } from 'vitest';
-import { functionToTest } from '../source-file';
-
-describe('functionToTest', () => {
- it('should do something specific', () => {
- const result = functionToTest('input');
- expect(result).toBe('expected');
- });
-});
-```
-
-### Testing Async Functions
-```typescript
-it('should handle async operations', async () => {
- const result = await asyncFunction();
- expect(result).toBeDefined();
-});
-```
-
-### Mocking Dependencies
-```typescript
-import { vi } from 'vitest';
-
-const mockFn = vi.fn().mockReturnValue('mocked result');
-const mockAsync = vi.fn().mockResolvedValue('async result');
-```
-
-### Testing Solid.js Components (Phase 3)
-```typescript
-import { render, screen } from '@solidjs/testing-library';
-import { MyComponent } from '../MyComponent';
-
-it('should render correctly', () => {
- render(() => );
- expect(screen.getByText('Expected Text')).toBeInTheDocument();
-});
-```
-
-## Configuration Files
-
-### vitest.config.ts
-Main test configuration with:
-- Solid.js plugin integration
-- Path aliases matching main Vite config
-- Obsidian mock aliasing
-- Coverage settings
-- jsdom environment for DOM testing
-
-### test/setup.ts
-Global test setup that runs before all tests:
-- Console suppression for cleaner test output
-- Obsidian API extensions (String.contains, etc.)
-- Mock cleanup between tests
-
-## Coverage Goals
-
-### Current Coverage (Phase 1)
-- ✅ **debounce.ts**: 100% coverage
-- ✅ **url.ts**: 100% coverage
-- ✅ **notes.ts**: 46% coverage (core functions tested)
-
-### Target Coverage (Future Phases)
-- **Services**: 90%+ coverage
-- **Components**: 70%+ coverage
-- **Overall**: 60%+ coverage
-
-Coverage reports help identify untested code paths and ensure quality.
-
-## Best Practices
-
-### DO:
-- ✅ Test behavior, not implementation details
-- ✅ Use descriptive test names: `should handle empty input gracefully`
-- ✅ Mock external dependencies (Obsidian API, network calls)
-- ✅ Test edge cases and error conditions
-- ✅ Keep tests fast and independent
-
-### DON'T:
-- ❌ Test framework internals (Solid.js reactivity, Obsidian's behavior)
-- ❌ Create elaborate mocks that mirror entire APIs
-- ❌ Write slow tests with real file I/O or network requests
-- ❌ Make tests depend on each other's state
-
-## Troubleshooting
-
-### Common Issues
-
-#### Tests Not Finding Obsidian Imports
-**Solution**: Check that `vitest.config.ts` has the obsidian alias:
-```typescript
-resolve: {
- alias: {
- 'obsidian': path.resolve(__dirname, './test/mocks/obsidian.ts')
- }
-}
-```
-
-#### Solid.js Components Not Rendering
-**Solution**: Ensure jsdom environment is configured:
-```typescript
-test: {
- environment: 'jsdom'
-}
-```
-
-#### Missing Mock Methods
-**Solution**: Add methods to `test/mocks/obsidian.ts` as you discover them:
-```typescript
-// Add missing methods as needed
-someObsidianClass = {
- newMethod: vi.fn().mockReturnValue('default'),
- // ...
-}
-```
-
-#### String.contains() Not Found
-This is handled in `test/setup.ts` but if you see this error, ensure setup files are properly configured in `vitest.config.ts`.
-
-## Future Phases
-
-### Phase 2: Core Services Testing
-- Model registry singleton behavior
-- AI streaming with real response simulation
-- Context injection and preparation
-- Error handling and abort scenarios
-
-### Phase 3: Component Testing
-- User interaction simulation
-- State management testing
-- Integration between components
-- Accessibility testing
-
-### Phase 4: Integration & E2E
-- Complete workflow testing
-- Plugin lifecycle testing
-- Settings persistence testing
-- CI/CD pipeline integration
-
-## Resources
-
-- [Vitest Documentation](https://vitest.dev/)
-- [Solid.js Testing Library](https://github.com/solidjs/solid-testing-library)
-- [AI SDK Testing Guide](https://sdk.vercel.ai/docs)
-- [Testing Implementation Plan](../testing-implementation.md) - Full implementation strategy
-
----
-
-For questions or issues with testing, refer to the main implementation plan or create an issue in the repository.
\ No newline at end of file
diff --git a/test/utils/render.tsx b/test/utils/render.tsx
deleted file mode 100644
index 5258348..0000000
--- a/test/utils/render.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { render as solidRender } from '@solidjs/testing-library';
-import { JSX, createContext } from 'solid-js';
-import { App } from 'obsidian';
-import { vi } from 'vitest';
-
-// Mock contexts that components might need
-export const MockAppContext = createContext();
-export const MockPluginContext = createContext();
-
-// Mock App instance for testing
-export const createMockApp = (): App => ({
- vault: {
- create: vi.fn(),
- modify: vi.fn(),
- read: vi.fn(),
- getMarkdownFiles: vi.fn(() => []),
- },
- workspace: {
- getLeaf: vi.fn(),
- activeLeaf: null,
- },
- metadataCache: {
- getFileCache: vi.fn(),
- },
-} as any);
-
-// Mock Plugin instance for testing
-export const createMockPlugin = () => ({
- app: createMockApp(),
- manifest: {},
- addCommand: vi.fn(),
- addSettingTab: vi.fn(),
- registerView: vi.fn(),
- saveData: vi.fn(() => Promise.resolve()),
- loadData: vi.fn(() => Promise.resolve({})),
-});
-
-// Custom render function with providers
-export const render = (
- component: () => JSX.Element,
- options?: {
- app?: App;
- plugin?: any;
- }
-) => {
- const mockApp = options?.app || createMockApp();
- const mockPlugin = options?.plugin || createMockPlugin();
-
- const WrappedComponent = () => (
-
-
- {component()}
-
-
- );
-
- return solidRender(WrappedComponent);
-};
\ No newline at end of file
diff --git a/test/utils/test-helpers.ts b/test/utils/test-helpers.ts
deleted file mode 100644
index db2da01..0000000
--- a/test/utils/test-helpers.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { vi } from 'vitest';
-
-// Common test utilities
-
-export const mockFn = any>() => vi.fn, ReturnType>();
-
-export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
-export const createMockFile = (path: string) => ({
- path,
- basename: path.split('/').pop()?.replace(/\.[^/.]+$/, '') || '',
- extension: path.split('.').pop() || '',
- name: path.split('/').pop() || '',
-});
\ No newline at end of file