mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-29 14:02:19 +00:00
Refactor integration structure and state management
- Restructured integration libraries to have dedicated directories for each integration (e.g., Telegram, future Gmail) under `src/lib/<integration>/`, 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.
This commit is contained in:
@@ -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/<integration>/` 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/<integration>/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
|
||||
|
||||
@@ -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<typeof vi.spyOn>;
|
||||
@@ -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(),
|
||||
}));
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
validateIdList,
|
||||
validatePositiveInt,
|
||||
validateOptionalId,
|
||||
} from "./validation";
|
||||
} from "../validation";
|
||||
|
||||
describe("ValidationError", () => {
|
||||
it("should have correct name property", () => {
|
||||
@@ -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", () => {
|
||||
@@ -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")
|
||||
);
|
||||
+2
-2
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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 } });
|
||||
Reference in New Issue
Block a user