From 07ce7f966ecfb8dd2f7e08ce82779acc351f87ff Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 31 Jan 2026 01:22:49 +0530 Subject: [PATCH] Refactor integration structure and state management - Restructured integration libraries to have dedicated directories for each integration (e.g., Telegram, future Gmail) under `src/lib//`, improving organization and clarity. - Updated state management to ensure all Redux dispatches for each integration are routed through specific state functions, enhancing maintainability. - Consolidated services related to each integration within their respective folders, separating them from cross-cutting services. This update streamlines the architecture for better scalability and maintainability of integrations. --- CLAUDE.md | 4 +- .../mcp/{ => __tests__}/errorHandler.test.ts | 6 +-- .../mcp/{ => __tests__}/rateLimiter.test.ts | 4 +- .../mcp/{ => __tests__}/validation.test.ts | 2 +- src/lib/telegram/{ => __tests__}/args.test.ts | 2 +- .../telegram/{ => __tests__}/server.test.ts | 48 +++++++++---------- .../{ => __tests__}/updateManager.test.ts | 4 +- .../{ => __tests__}/telegramSelectors.test.ts | 8 ++-- .../telegram/{ => __tests__}/reducers.test.ts | 4 +- 9 files changed, 41 insertions(+), 41 deletions(-) rename src/lib/mcp/{ => __tests__}/errorHandler.test.ts (98%) rename src/lib/mcp/{ => __tests__}/rateLimiter.test.ts (99%) rename src/lib/mcp/{ => __tests__}/validation.test.ts (99%) rename src/lib/telegram/{ => __tests__}/args.test.ts (98%) rename src/lib/telegram/{ => __tests__}/server.test.ts (93%) rename src/lib/telegram/services/{ => __tests__}/updateManager.test.ts (99%) rename src/store/{ => __tests__}/telegramSelectors.test.ts (99%) rename src/store/telegram/{ => __tests__}/reducers.test.ts (99%) diff --git a/CLAUDE.md b/CLAUDE.md index f079db30a..2a2723905 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,8 +209,8 @@ Key updates from recent commits: - **Tauri IPC**: Frontend calls Rust via `invoke()` from `@tauri-apps/api/core`. Rust commands are registered in `generate_handler![]` macro. - **CORS workaround**: External HTTP requests from the WebView hit CORS. Use Rust `reqwest` via Tauri commands instead of browser `fetch()`. - **Hash Routing**: Uses HashRouter for desktop app compatibility and deep link handling. -- **State Layer**: All Redux dispatches for Telegram state go through functions in `src/lib/telegram/state/` — never import Redux actions directly from services or update handlers. State functions accept an optional `userId` param (falls back to `getCurrentUserId()`). -- **Telegram Services**: All Telegram-related services live in `src/lib/telegram/services/` (mtprotoService, updateManager, messageLoader, syncService, updateHandler, entityBuilders). Non-Telegram services remain in `src/services/`. +- **Integration Libraries**: Each integration (Telegram, future Gmail, etc.) lives under `src/lib//` with its own `state/`, `services/`, `api/` subdirectories. Domain-specific services belong in the integration folder, not in `src/services/` (which holds only cross-cutting services like socketService, apiClient). +- **State Layer**: Each integration dispatches Redux changes through state functions in `src/lib//state/` — never import Redux actions directly from services or update handlers. State functions accept an optional `userId` param (falls back to `getCurrentUserId()`). - **Unit Tests**: All unit tests live in `__tests__/` folders co-located with the code they test. ## Platform Gotchas diff --git a/src/lib/mcp/errorHandler.test.ts b/src/lib/mcp/__tests__/errorHandler.test.ts similarity index 98% rename from src/lib/mcp/errorHandler.test.ts rename to src/lib/mcp/__tests__/errorHandler.test.ts index e86d75535..5fe92f47c 100644 --- a/src/lib/mcp/errorHandler.test.ts +++ b/src/lib/mcp/__tests__/errorHandler.test.ts @@ -3,9 +3,9 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { logAndFormatError, withErrorHandling, ErrorCategory } from "./errorHandler"; -import { ValidationError } from "./validation"; -import type { MCPToolResult } from "./types"; +import { logAndFormatError, withErrorHandling, ErrorCategory } from "../errorHandler"; +import { ValidationError } from "../validation"; +import type { MCPToolResult } from "../types"; describe("logAndFormatError", () => { let consoleErrorSpy: ReturnType; diff --git a/src/lib/mcp/rateLimiter.test.ts b/src/lib/mcp/__tests__/rateLimiter.test.ts similarity index 99% rename from src/lib/mcp/rateLimiter.test.ts rename to src/lib/mcp/__tests__/rateLimiter.test.ts index 4a5fe8e3e..a2115bd41 100644 --- a/src/lib/mcp/rateLimiter.test.ts +++ b/src/lib/mcp/__tests__/rateLimiter.test.ts @@ -6,10 +6,10 @@ import { enforceRateLimit, resetRequestCallCount, RATE_LIMIT_CONFIG, -} from "./rateLimiter"; +} from "../rateLimiter"; // Mock the logger module to avoid console output during tests -vi.mock("./logger", () => ({ +vi.mock("../logger", () => ({ mcpLog: vi.fn(), mcpWarn: vi.fn(), })); diff --git a/src/lib/mcp/validation.test.ts b/src/lib/mcp/__tests__/validation.test.ts similarity index 99% rename from src/lib/mcp/validation.test.ts rename to src/lib/mcp/__tests__/validation.test.ts index 583740c77..7de454181 100644 --- a/src/lib/mcp/validation.test.ts +++ b/src/lib/mcp/__tests__/validation.test.ts @@ -5,7 +5,7 @@ import { validateIdList, validatePositiveInt, validateOptionalId, -} from "./validation"; +} from "../validation"; describe("ValidationError", () => { it("should have correct name property", () => { diff --git a/src/lib/telegram/args.test.ts b/src/lib/telegram/__tests__/args.test.ts similarity index 98% rename from src/lib/telegram/args.test.ts rename to src/lib/telegram/__tests__/args.test.ts index b64abd98b..c5f474566 100644 --- a/src/lib/telegram/args.test.ts +++ b/src/lib/telegram/__tests__/args.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { optNumber, optString } from "./args"; +import { optNumber, optString } from "../args"; describe("optNumber", () => { it("returns the number when args[key] is a valid number", () => { diff --git a/src/lib/telegram/server.test.ts b/src/lib/telegram/__tests__/server.test.ts similarity index 93% rename from src/lib/telegram/server.test.ts rename to src/lib/telegram/__tests__/server.test.ts index 827914be1..430b15b1f 100644 --- a/src/lib/telegram/server.test.ts +++ b/src/lib/telegram/__tests__/server.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import type { MCPToolResult } from "../mcp/types"; +import type { MCPToolResult } from "../../mcp/types"; // Use vi.hoisted so mock variables are available when vi.mock factories run const { @@ -21,12 +21,12 @@ const { })); // Mock the transport module — must use hoisted variables -vi.mock("../mcp/transport", () => ({ +vi.mock("../../mcp/transport", () => ({ SocketIOMCPTransportImpl: vi.fn(), })); // Mock the store -vi.mock("../../store", () => ({ +vi.mock("../../../store", () => ({ store: { getState: vi.fn(() => ({ user: { user: { _id: "u1" } }, @@ -70,7 +70,7 @@ vi.mock("../../store", () => ({ }, })); -vi.mock("../../store/telegramSelectors", () => ({ +vi.mock("../../../store/telegramSelectors", () => ({ selectTelegramUserState: vi.fn((state: any) => { const userId = state?.user?.user?._id ?? ""; return state?.telegram?.byUser?.[userId] ?? {}; @@ -80,7 +80,7 @@ vi.mock("../../store/telegramSelectors", () => ({ })); // Mock the skills module -vi.mock("../mcp/skills", () => ({ +vi.mock("../../mcp/skills", () => ({ useExtraToolDefinition: { name: "use_extra_tool", description: "meta", @@ -93,7 +93,7 @@ vi.mock("../mcp/skills", () => ({ })); // Mock rateLimiter -vi.mock("../mcp/rateLimiter", () => ({ +vi.mock("../../mcp/rateLimiter", () => ({ enforceRateLimit: vi.fn().mockResolvedValue(undefined), resetRequestCallCount: vi.fn(), isStateOnlyTool: vi.fn(() => false), @@ -102,13 +102,13 @@ vi.mock("../mcp/rateLimiter", () => ({ })); // Mock logger -vi.mock("../mcp/logger", () => ({ +vi.mock("../../mcp/logger", () => ({ mcpLog: vi.fn(), mcpWarn: vi.fn(), })); // Mock API functions used by tool handlers -vi.mock("./api/getChats", () => ({ +vi.mock("../api/getChats", () => ({ getChats: vi.fn(() => Promise.resolve({ data: [ @@ -132,7 +132,7 @@ vi.mock("./api/getChats", () => ({ ), })); -vi.mock("./api/sendMessage", () => ({ +vi.mock("../api/sendMessage", () => ({ sendMessage: vi.fn(() => Promise.resolve({ data: { id: "msg1", message: "Hello", chatId: "1", date: 1234567890 }, @@ -141,7 +141,7 @@ vi.mock("./api/sendMessage", () => ({ ), })); -vi.mock("./api/getCurrentUser", () => ({ +vi.mock("../api/getCurrentUser", () => ({ getCurrentUser: vi.fn(() => Promise.resolve({ data: { @@ -156,7 +156,7 @@ vi.mock("./api/getCurrentUser", () => ({ ), })); -vi.mock("./api/getMessages", () => ({ +vi.mock("../api/getMessages", () => ({ getMessages: vi.fn(() => Promise.resolve({ data: [ @@ -180,7 +180,7 @@ vi.mock("./api/getMessages", () => ({ ), })); -vi.mock("./api/helpers", async (importOriginal) => { +vi.mock("../api/helpers", async (importOriginal) => { const original = (await importOriginal()) as any; return { ...original, @@ -218,7 +218,7 @@ describe("TelegramMCPServer", () => { vi.clearAllMocks(); // Re-apply mock implementations (mockReset: true in config clears them between tests) - const { SocketIOMCPTransportImpl } = await import("../mcp/transport"); + const { SocketIOMCPTransportImpl } = await import("../../mcp/transport"); vi.mocked(SocketIOMCPTransportImpl).mockImplementation(function ( this: any ) { @@ -230,18 +230,18 @@ describe("TelegramMCPServer", () => { return this; } as any); - const rateLimiter = await import("../mcp/rateLimiter"); + const rateLimiter = await import("../../mcp/rateLimiter"); vi.mocked(rateLimiter.enforceRateLimit).mockResolvedValue(undefined); vi.mocked(rateLimiter.resetRequestCallCount).mockImplementation(() => {}); vi.mocked(rateLimiter.isStateOnlyTool).mockReturnValue(false); - const skills = await import("../mcp/skills"); + const skills = await import("../../mcp/skills"); vi.mocked(skills.executeExtraToolIfExists).mockReturnValue(null as any); vi.mocked(skills.getAllExtraTools).mockReturnValue([]); vi.mocked(skills.isExtraToolByName).mockReturnValue(false); // Re-apply API mocks - const getChatsApi = await import("./api/getChats"); + const getChatsApi = await import("../api/getChats"); vi.mocked(getChatsApi.getChats).mockResolvedValue({ data: [ { @@ -262,7 +262,7 @@ describe("TelegramMCPServer", () => { fromCache: true, }); - const sendMsgApi = await import("./api/sendMessage"); + const sendMsgApi = await import("../api/sendMessage"); vi.mocked(sendMsgApi.sendMessage).mockResolvedValue({ data: { id: "msg1", @@ -273,7 +273,7 @@ describe("TelegramMCPServer", () => { fromCache: false, }); - const getUserApi = await import("./api/getCurrentUser"); + const getUserApi = await import("../api/getCurrentUser"); vi.mocked(getUserApi.getCurrentUser).mockResolvedValue({ data: { id: "1", @@ -285,7 +285,7 @@ describe("TelegramMCPServer", () => { fromCache: true, }); - const getMsgsApi = await import("./api/getMessages"); + const getMsgsApi = await import("../api/getMessages"); vi.mocked(getMsgsApi.getMessages).mockResolvedValue({ data: [ { @@ -306,7 +306,7 @@ describe("TelegramMCPServer", () => { fromCache: true, }); - const helpers = await import("./api/helpers"); + const helpers = await import("../api/helpers"); vi.mocked(helpers.getChatById).mockReturnValue({ id: "1", title: "Chat One", @@ -315,7 +315,7 @@ describe("TelegramMCPServer", () => { isPinned: false, }); - const store = await import("../../store"); + const store = await import("../../../store"); vi.mocked(store.store.getState).mockReturnValue({ user: { user: { _id: "u1" } }, telegram: { @@ -335,7 +335,7 @@ describe("TelegramMCPServer", () => { } as any); // Import server after all mocks are set up - const { TelegramMCPServer } = await import("./server"); + const { TelegramMCPServer } = await import("../server"); // Create server instance — constructor calls setupHandlers() which calls // transport.on("toolCall", ...) and transport.on("listTools", ...) @@ -551,7 +551,7 @@ describe("TelegramMCPServer", () => { it("should handle rate limit errors", async () => { // Mock enforceRateLimit to reject for this call - const rateLimiter = await import("../mcp/rateLimiter"); + const rateLimiter = await import("../../mcp/rateLimiter"); vi.mocked(rateLimiter.enforceRateLimit).mockRejectedValueOnce( new Error("Rate limit exceeded") ); @@ -578,7 +578,7 @@ describe("TelegramMCPServer", () => { it("should handle API errors gracefully", async () => { // Make the send_message tool handler throw - const sendMessageApi = await import("./api/sendMessage"); + const sendMessageApi = await import("../api/sendMessage"); vi.mocked(sendMessageApi.sendMessage).mockRejectedValueOnce( new Error("Network error: Connection timeout") ); diff --git a/src/lib/telegram/services/updateManager.test.ts b/src/lib/telegram/services/__tests__/updateManager.test.ts similarity index 99% rename from src/lib/telegram/services/updateManager.test.ts rename to src/lib/telegram/services/__tests__/updateManager.test.ts index 74c8122b8..f8d90c120 100644 --- a/src/lib/telegram/services/updateManager.test.ts +++ b/src/lib/telegram/services/__tests__/updateManager.test.ts @@ -84,9 +84,9 @@ vi.mock("telegram/tl", () => ({ })); vi.mock("big-integer", () => ({ default: (v: any) => v })); -vi.mock("../lib/mcp/logger"); +vi.mock("../../../mcp/logger"); -import { UpdateManager } from "./updateManager"; +import { UpdateManager } from "../updateManager"; import { Api } from "telegram/tl"; function createMockClient(): TelegramClient { diff --git a/src/store/telegramSelectors.test.ts b/src/store/__tests__/telegramSelectors.test.ts similarity index 99% rename from src/store/telegramSelectors.test.ts rename to src/store/__tests__/telegramSelectors.test.ts index a73766537..3fce2e7d7 100644 --- a/src/store/telegramSelectors.test.ts +++ b/src/store/__tests__/telegramSelectors.test.ts @@ -16,10 +16,10 @@ import { selectSelectedChat, selectFilteredChats, selectChatLatestMessage, -} from "./telegramSelectors"; -import type { TelegramState, TelegramChat, TelegramMessage } from "./telegram/types"; -import { initialState } from "./telegram/types"; -import type { RootState } from "./index"; +} from "../telegramSelectors"; +import type { TelegramState, TelegramChat, TelegramMessage } from "../telegram/types"; +import { initialState } from "../telegram/types"; +import type { RootState } from "../index"; /** * Helper to build a mock RootState with customizable telegram state diff --git a/src/store/telegram/reducers.test.ts b/src/store/telegram/__tests__/reducers.test.ts similarity index 99% rename from src/store/telegram/reducers.test.ts rename to src/store/telegram/__tests__/reducers.test.ts index 8a1b23a40..b8ca5b734 100644 --- a/src/store/telegram/reducers.test.ts +++ b/src/store/telegram/__tests__/reducers.test.ts @@ -18,7 +18,7 @@ vi.mock("redux-persist", async () => { }); // Mock the main store exports to prevent store initialization -vi.mock("../index", () => ({ +vi.mock("../../index", () => ({ store: { getState: vi.fn(), dispatch: vi.fn(), subscribe: vi.fn(), replaceReducer: vi.fn() }, persistor: { purge: vi.fn(), flush: vi.fn() }, })); @@ -60,7 +60,7 @@ import telegramReducer, { setCurrentUser, setUsers, addUsers, -} from "./index"; +} from "../index"; function createStore() { return configureStore({ reducer: { telegram: telegramReducer } });