## Phase 1 (Fixed): Agent Management UI - Исправлена авторизация: agents переведены на publicProcedure - AgentDetailModal: 5 вкладок (General, LLM Params, Tools, History, Stats) - Полное редактирование: model, provider, temperature, topP, maxTokens, frequencyPenalty, presencePenalty, systemPrompt - Управление allowedTools и allowedDomains через теги - AgentCreateModal: создание агентов с выбором модели из Ollama API - Кнопка Metrics на каждой карточке агента ## Phase 2+3: Tool Binding System - server/tools.ts: реестр из 10 инструментов (http_get, http_post, shell_exec, file_read, file_write, docker_list, docker_exec, docker_logs, browser_navigate, browser_screenshot) - Безопасное выполнение: проверка allowedTools агента, accessControl из БД - tools.execute tRPC endpoint - Tools.tsx: страница управления инструментами с тест-выполнением - Добавлен пункт "Инструменты" в sidebar навигацию ## Phase 4: Metrics & History - AgentMetrics.tsx: детальная страница метрик по агенту - Request Timeline: bar chart по часам (success/error) - Conversation Log: история диалогов с пагинацией - Raw Metrics Table: все метрики с токенами и временем - Time range selector: 6h/24h/48h/7d - Маршрут /agents/:id/metrics ## Tests: 24/24 passed - server/auth.logout.test.ts (1) - server/agents.test.ts (7) - server/tools.test.ts (13) - server/ollama.test.ts (3)
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { getAllTools, getToolById, executeTool, TOOL_REGISTRY } from "./tools";
|
|
|
|
// Mock agents module
|
|
vi.mock("./agents", () => ({
|
|
getAgentById: vi.fn(),
|
|
getAgentAccessControl: vi.fn(),
|
|
}));
|
|
|
|
import { getAgentById, getAgentAccessControl } from "./agents";
|
|
|
|
const mockAgent = {
|
|
id: 1,
|
|
name: "Test Agent",
|
|
model: "gpt-4",
|
|
provider: "OpenAI",
|
|
allowedTools: ["http_get", "http_post", "file_read"],
|
|
temperature: "0.7",
|
|
maxTokens: 2048,
|
|
};
|
|
|
|
describe("Tool Registry", () => {
|
|
it("should return all tools", () => {
|
|
const tools = getAllTools();
|
|
expect(tools.length).toBeGreaterThan(0);
|
|
expect(tools.every((t) => t.id && t.name && t.category)).toBe(true);
|
|
});
|
|
|
|
it("should have required fields for each tool", () => {
|
|
const tools = getAllTools();
|
|
for (const tool of tools) {
|
|
expect(tool.id).toBeTruthy();
|
|
expect(tool.name).toBeTruthy();
|
|
expect(tool.description).toBeTruthy();
|
|
expect(tool.category).toBeTruthy();
|
|
expect(typeof tool.dangerous).toBe("boolean");
|
|
expect(typeof tool.parameters).toBe("object");
|
|
}
|
|
});
|
|
|
|
it("should find tool by ID", () => {
|
|
const tool = getToolById("http_get");
|
|
expect(tool).toBeDefined();
|
|
expect(tool?.name).toBe("HTTP GET");
|
|
expect(tool?.category).toBe("http");
|
|
});
|
|
|
|
it("should return undefined for unknown tool ID", () => {
|
|
const tool = getToolById("nonexistent_tool");
|
|
expect(tool).toBeUndefined();
|
|
});
|
|
|
|
it("should have http tools", () => {
|
|
const httpTools = TOOL_REGISTRY.filter((t) => t.category === "http");
|
|
expect(httpTools.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("should have shell tools", () => {
|
|
const shellTools = TOOL_REGISTRY.filter((t) => t.category === "shell");
|
|
expect(shellTools.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("should have docker tools", () => {
|
|
const dockerTools = TOOL_REGISTRY.filter((t) => t.category === "docker");
|
|
expect(dockerTools.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
it("should mark dangerous tools correctly", () => {
|
|
const shellExec = getToolById("shell_exec");
|
|
expect(shellExec?.dangerous).toBe(true);
|
|
|
|
const httpGet = getToolById("http_get");
|
|
expect(httpGet?.dangerous).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Tool Execution", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
(getAgentById as any).mockResolvedValue(mockAgent);
|
|
(getAgentAccessControl as any).mockResolvedValue([]);
|
|
});
|
|
|
|
it("should return error for non-existent agent", async () => {
|
|
(getAgentById as any).mockResolvedValue(null);
|
|
const result = await executeTool(999, "http_get", { url: "https://example.com" });
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Agent not found");
|
|
});
|
|
|
|
it("should return error for unknown tool", async () => {
|
|
const result = await executeTool(1, "nonexistent_tool", {});
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Unknown tool");
|
|
});
|
|
|
|
it("should block tool not in agent's allowed list", async () => {
|
|
const agentWithLimitedTools = { ...mockAgent, allowedTools: ["http_get"] };
|
|
(getAgentById as any).mockResolvedValue(agentWithLimitedTools);
|
|
|
|
const result = await executeTool(1, "shell_exec", { command: "echo hello" });
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("not in agent's allowed tools list");
|
|
});
|
|
|
|
it("should block tool explicitly denied in access control", async () => {
|
|
(getAgentAccessControl as any).mockResolvedValue([
|
|
{ tool: "http_get", isAllowed: false },
|
|
]);
|
|
const agentWithAllTools = { ...mockAgent, allowedTools: [] };
|
|
(getAgentById as any).mockResolvedValue(agentWithAllTools);
|
|
|
|
const result = await executeTool(1, "http_get", { url: "https://example.com" });
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("blocked");
|
|
});
|
|
|
|
it("should include executionTimeMs in result", async () => {
|
|
// Agent with no allowed tools (empty = all allowed)
|
|
const agentAllTools = { ...mockAgent, allowedTools: [] };
|
|
(getAgentById as any).mockResolvedValue(agentAllTools);
|
|
|
|
// This will fail (no actual HTTP), but should still return executionTimeMs
|
|
const result = await executeTool(1, "http_get", { url: "https://httpbin.org/get" });
|
|
expect(typeof result.executionTimeMs).toBe("number");
|
|
expect(result.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|