Files
GoClaw/server/gateway-proxy.test.ts

225 lines
7.5 KiB
TypeScript

/**
* Tests for gateway-proxy.ts — Go Gateway integration layer
*
* These tests verify the proxy functions that bridge Node.js tRPC
* procedures to the Go Gateway service.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// ─── Mock fetch globally ───────────────────────────────────────────────────────
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
// Helper to create a mock Response
function mockResponse(body: unknown, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => body,
text: async () => JSON.stringify(body),
} as Response;
}
// Import after mock setup
import {
checkGatewayHealth,
isGatewayAvailable,
getGatewayOrchestratorConfig,
getGatewayTools,
type GatewayToolDef,
} from "./gateway-proxy";
describe("Go Gateway Proxy", () => {
beforeEach(() => {
mockFetch.mockReset();
// Default: gateway is available
process.env.GATEWAY_URL = "http://localhost:18789";
});
afterEach(() => {
vi.clearAllMocks();
});
// ─── checkGatewayHealth ──────────────────────────────────────────────────────
describe("checkGatewayHealth", () => {
it("returns connected=true when gateway responds with ok status", async () => {
mockFetch.mockResolvedValueOnce(
mockResponse({
status: "ok",
service: "goclaw-gateway",
version: "1.0.0",
ollama: { connected: true, latencyMs: 5 },
})
);
const result = await checkGatewayHealth();
expect(result.connected).toBe(true);
expect(result.latencyMs).toBeGreaterThanOrEqual(0);
});
it("returns connected=false when gateway is unreachable", async () => {
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
const result = await checkGatewayHealth();
expect(result.connected).toBe(false);
expect(result.latencyMs).toBe(0);
});
it("returns connected=false when gateway returns non-ok status", async () => {
mockFetch.mockResolvedValueOnce(mockResponse({ error: "Internal Server Error" }, 500));
const result = await checkGatewayHealth();
expect(result.connected).toBe(false);
});
it("includes llm health info when gateway is healthy", async () => {
mockFetch.mockResolvedValueOnce(
mockResponse({
status: "ok",
ollama: { connected: true, latencyMs: 12 },
})
);
const result = await checkGatewayHealth();
expect(result.connected).toBe(true);
expect(result.llm).toBeDefined();
expect(result.llm?.connected).toBe(true);
});
});
// ─── isGatewayAvailable ──────────────────────────────────────────────────────
describe("isGatewayAvailable", () => {
it("returns true when gateway health check succeeds", async () => {
mockFetch.mockResolvedValueOnce(mockResponse({ status: "ok" }));
const available = await isGatewayAvailable();
expect(available).toBe(true);
});
it("returns false when gateway is unreachable", async () => {
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
const available = await isGatewayAvailable();
expect(available).toBe(false);
});
});
// ─── getGatewayOrchestratorConfig ───────────────────────────────────────────
describe("getGatewayOrchestratorConfig", () => {
it("returns orchestrator config when gateway responds", async () => {
const mockConfig = {
id: 1,
name: "Orchestrator",
model: "qwen2.5:7b",
temperature: 0.5,
maxTokens: 8192,
allowedTools: ["shell_exec", "file_read", "file_list"],
systemPromptPreview: "You are GoClaw Orchestrator.",
};
mockFetch.mockResolvedValueOnce(mockResponse(mockConfig));
const config = await getGatewayOrchestratorConfig();
expect(config).not.toBeNull();
expect(config?.model).toBe("qwen2.5:7b");
expect(config?.allowedTools).toContain("shell_exec");
expect(config?.temperature).toBe(0.5);
});
it("returns null when gateway is unreachable", async () => {
mockFetch.mockRejectedValueOnce(new Error("Network error"));
const config = await getGatewayOrchestratorConfig();
expect(config).toBeNull();
});
it("returns null when gateway returns non-ok status", async () => {
mockFetch.mockResolvedValueOnce(mockResponse({ error: "not found" }, 404));
const config = await getGatewayOrchestratorConfig();
expect(config).toBeNull();
});
});
// ─── getGatewayTools ────────────────────────────────────────────────────────
describe("getGatewayTools", () => {
it("maps OpenAI-format tools to GatewayToolDef", async () => {
// Go Gateway returns OpenAI format: {type: "function", function: {...}}
const openAITools = {
count: 2,
tools: [
{
type: "function",
function: {
name: "shell_exec",
description: "Execute a bash command on the host system.",
parameters: {
type: "object",
properties: { command: { type: "string" } },
required: ["command"],
},
},
},
{
type: "function",
function: {
name: "file_read",
description: "Read a file from the filesystem.",
parameters: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"],
},
},
},
],
};
mockFetch.mockResolvedValueOnce(mockResponse(openAITools));
const tools = await getGatewayTools();
expect(tools).not.toBeNull();
expect(tools).toHaveLength(2);
const shellTool = tools![0] as GatewayToolDef;
expect(shellTool.name).toBe("shell_exec");
expect(shellTool.description).toContain("bash command");
expect(shellTool.parameters).toBeDefined();
});
it("also handles flat format tools (name, description, parameters)", async () => {
const flatTools = {
count: 1,
tools: [
{
name: "file_list",
description: "List files at a path.",
parameters: { type: "object", properties: { path: { type: "string" } } },
},
],
};
mockFetch.mockResolvedValueOnce(mockResponse(flatTools));
const tools = await getGatewayTools();
expect(tools).not.toBeNull();
expect(tools![0].name).toBe("file_list");
});
it("returns null when gateway is unreachable", async () => {
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
const tools = await getGatewayTools();
expect(tools).toBeNull();
});
it("returns null when response has no tools array", async () => {
mockFetch.mockResolvedValueOnce(mockResponse({ error: "no tools" }));
const tools = await getGatewayTools();
expect(tools).toBeNull();
});
});
});