Skip to main content
Glama
touchDesignerClient.mock.test.ts29.3 kB
import { AxiosError } from "axios"; import { beforeEach, describe, expect, test, vi } from "vitest"; import type { ILogger } from "../../src/core/logger"; import * as version from "../../src/core/version"; import * as touchDesignerAPI from "../../src/gen/endpoints/TouchDesignerAPI"; import { ERROR_CACHE_TTL_MS, type ITouchDesignerApi, SUCCESS_CACHE_TTL_MS, TouchDesignerClient, } from "../../src/tdClient/touchDesignerClient"; vi.mock("../../src/gen/endpoints/TouchDesignerAPI", async () => { return { createNode: vi.fn(), deleteNode: vi.fn(), execNodeMethod: vi.fn(), execPythonScript: vi.fn(), getModuleHelp: vi.fn(), getNodeDetail: vi.fn(), getNodeErrors: vi.fn(), getNodes: vi.fn(), getTdInfo: vi.fn(), getTdPythonClassDetails: vi.fn(), getTdPythonClasses: vi.fn(), updateNode: vi.fn(), }; }); vi.mock("../../src/core/version", async () => { return { getMcpServerVersion: vi.fn(() => "1.3.1"), getMinCompatibleApiVersion: vi.fn(() => "1.3.0"), MCP_SERVER_VERSION: "1.3.1", MIN_COMPATIBLE_API_VERSION: "1.3.0", }; }); const nullLogger: ILogger = { sendLog: () => {}, }; const compatibilityResponse = { data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }; describe("TouchDesignerClient with mocks", () => { beforeEach(() => { vi.clearAllMocks(); // Reset version mocks to default values // Individual tests can override these as needed using vi.mocked().mockReturnValue() vi.mocked(version.getMcpServerVersion).mockReturnValue("1.3.1"); vi.mocked(version.getMinCompatibleApiVersion).mockReturnValue("1.3.0"); vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue( compatibilityResponse, ); }); test("getTdInfo should handle successful response", async () => { const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result).toBeDefined(); expect(result.success).toBe(true); if (result.success) { expect(result.data).toBeDefined(); expect(result.data.server).toBe("TouchDesigner"); expect(result.data.version).toBe("2023.11050"); expect(result.data.osName).toBe("macOS"); expect(result.data.osVersion).toBe("12.6.1"); } }); test("getTdInfo should handle error response", async () => { const errorResponse = { data: null, error: "Failed to connect to server", success: false, }; vi.mocked(touchDesignerAPI.getTdInfo) .mockResolvedValueOnce(compatibilityResponse) .mockResolvedValueOnce(errorResponse); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); if (result.success) { throw new Error("Expected success to be false"); } expect(result.success).toBe(false); expect(result.error).toBeInstanceOf(Error); expect(result.error.message).toBe("Failed to connect to server"); }); test("getTdInfo should handle missing data response", async () => { const mockResponse = { data: null, error: null, success: true, }; vi.mocked(touchDesignerAPI.getTdInfo) .mockResolvedValueOnce(compatibilityResponse) .mockResolvedValueOnce(mockResponse); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); if (result.success) { throw new Error("Expected success to be false"); } expect(result.error).toBeInstanceOf(Error); expect(result.error.message).toBe("No data received"); }); test("should re-check compatibility when getTdInfo is called after cache warmup", async () => { const legacyResponse = { data: { mcpApiVersion: "", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }; const getTdInfoMock = vi.mocked(touchDesignerAPI.getTdInfo); getTdInfoMock.mockReset(); try { getTdInfoMock .mockResolvedValueOnce(compatibilityResponse) // Initial compatibility check .mockResolvedValueOnce(compatibilityResponse) // First getTdInfo call .mockResolvedValueOnce(legacyResponse) // Revalidation triggered by second getTdInfo .mockResolvedValueOnce(legacyResponse); // Actual second call should never execute after revalidation fails const client = new TouchDesignerClient({ logger: nullLogger }); await client.getTdInfo(); await expect(client.getTdInfo()).rejects.toThrow( "Version Information Missing", ); } finally { getTdInfoMock.mockReset(); getTdInfoMock.mockResolvedValue(compatibilityResponse); } }); describe("Semantic Version Compatibility", () => { test("should accept same MAJOR with different PATCH", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.3.5", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); }); test("should expose compatibility notice for MINOR warnings", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.4.0", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); expect(client.getAdditionalToolResultContents()).not.toBeNull(); expect(client.getAdditionalToolResultContents()?.[0].text).toContain( "Update Recommended", ); vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue( compatibilityResponse, ); await client.getTdInfo(); expect(client.getAdditionalToolResultContents()).toBeNull(); }); test("should not surface compatibility notice for PATCH differences", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.3.2", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); expect(client.getAdditionalToolResultContents()).toBeNull(); vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue( compatibilityResponse, ); await client.getTdInfo(); expect(client.getAdditionalToolResultContents()).toBeNull(); }); test("should reject different MAJOR version", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "2.0.0", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); await expect(client.getTdInfo()).rejects.toThrow("MAJOR version"); }); test("should reject version below minimum compatible version", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.2.99", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); await expect(client.getTdInfo()).rejects.toThrow( "TouchDesigner API Server Update Required", ); }); test("should reject legacy TOX files without mcpApiVersion", async () => { const mockLogger: ILogger = { sendLog: vi.fn(), }; vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { // Legacy TOX - empty mcpApiVersion to simulate v1.2.x or earlier mcpApiVersion: "", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "099.2025.31550", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: mockLogger }); await expect(client.getTdInfo()).rejects.toThrow( "Version Information Missing", ); expect(mockLogger.sendLog).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ message: expect.stringContaining("Version information is required"), }), level: "error", logger: "TouchDesignerClient", }), ); }); test("should warn when MCP is newer MINOR", async () => { const mockLogger: ILogger = { sendLog: vi.fn(), }; vi.mocked(version.getMcpServerVersion).mockReturnValue("1.4.0"); vi.mocked(version).MCP_SERVER_VERSION = "1.4.0"; const client = new TouchDesignerClient({ logger: mockLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); expect(mockLogger.sendLog).toHaveBeenCalledWith( expect.objectContaining({ level: "warning", logger: "TouchDesignerClient", }), ); }); test("should allow API server with newer MINOR", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.5.0", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); }); test("should accept same version with v-prefix", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "v1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); }); test("should reject invalid semver version format", async () => { vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "Invalid semver version", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: nullLogger }); await expect(client.getTdInfo()).rejects.toThrow( "Invalid semver version", ); }); test("should log error with structured data for MAJOR mismatch", async () => { const mockLogger: ILogger = { sendLog: vi.fn(), }; vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "2.0.0", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: mockLogger }); await expect(client.getTdInfo()).rejects.toThrow(); expect(mockLogger.sendLog).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ apiVersion: "2.0.0", mcpVersion: expect.any(String), minRequired: expect.any(String), }), level: "error", logger: "TouchDesignerClient", }), ); }); test("should log error with structured data for BELOW_MIN_VERSION", async () => { const mockLogger: ILogger = { sendLog: vi.fn(), }; vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue({ data: { mcpApiVersion: "1.2.9", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const client = new TouchDesignerClient({ logger: mockLogger }); await expect(client.getTdInfo()).rejects.toThrow( "TouchDesigner API Server Update Required", ); expect(mockLogger.sendLog).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ apiVersion: "1.2.9", mcpVersion: expect.any(String), minRequired: expect.any(String), }), level: "error", logger: "TouchDesignerClient", }), ); }); }); test("createNode should handle successful creation", async () => { const mockResponse = { data: { result: { id: 123, name: "testNode", opType: "nullCOMP", path: "/project1/testNode", properties: {}, }, }, error: null, success: true, }; vi.mocked(touchDesignerAPI.createNode).mockResolvedValue(mockResponse); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.createNode({ nodeName: "testNode", nodeType: "nullCOMP", parentPath: "/project1", }); if (!result.success) { throw new Error("Expected success to be true"); } expect(result).toBeDefined(); expect(result.success).toBe(true); expect(result.data.result?.name).toBe("testNode"); }); test("execPythonScript should handle successful execution", async () => { const mockResponse = { data: { result: { value: "Script executed successfully" }, // Adjusted structure }, error: null, success: true, }; vi.mocked(touchDesignerAPI.execPythonScript).mockResolvedValue( mockResponse as unknown as touchDesignerAPI.ExecPythonScript200Response, ); const client = new TouchDesignerClient({ logger: nullLogger }); const result = await client.execPythonScript<{ result: { value: string }; }>({ script: 'print("Hello")', }); if (!result.success) { throw new Error("Expected success to be true"); } expect(result).toBeDefined(); expect(result.success).toBe(true); expect(result.data).toBeDefined(); expect(result.data?.result?.value).toBe("Script executed successfully"); }); test("TouchDesignerClient should accept custom logger", async () => { const mockLogger: ILogger = { sendLog: vi.fn(), }; const mockResponse = { data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }; vi.mocked(touchDesignerAPI.getTdInfo).mockResolvedValue(mockResponse); const client = new TouchDesignerClient({ logger: mockLogger }); const result = await client.getTdInfo(); expect(result.success).toBe(true); expect(mockLogger.sendLog).toHaveBeenCalledWith( expect.objectContaining({ level: "debug" }), ); }); test("TouchDesignerClient should accept custom httpClient", async () => { const mockHttpClient = { getTdInfo: vi.fn().mockResolvedValue({ data: { mcpApiVersion: "1.3.1", server: "CustomServer", status: "CustomStatus", version: "CustomVersion", }, success: true, }), }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); const result = await client.getTdInfo(); expect(result.success).toBe(true); if (result.success) { expect(result.data?.server).toBe("CustomServer"); } expect(mockHttpClient.getTdInfo).toHaveBeenCalled(); }); test("should cache compatibility check and not call getTdInfo multiple times", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const mockHttpClient = { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); // First call should trigger compatibility check await client.createNode({ nodeName: "test1", nodeType: "null", parentPath: "/", }); // Second call should use cached compatibility result await client.createNode({ nodeName: "test2", nodeType: "null", parentPath: "/", }); // getTdInfo should only be called once (during first compatibility check) expect(mockGetTdInfo).toHaveBeenCalledTimes(1); // createNode should be called twice expect(mockCreateNode).toHaveBeenCalledTimes(2); }); test("should re-check compatibility when success cache TTL expires", async () => { vi.useFakeTimers(); try { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockGetNodes = vi.fn().mockResolvedValue({ data: { nodes: [] }, error: null, success: true, }); const mockHttpClient = { getNodes: mockGetNodes, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await client.getNodes({ parentPath: "/" }); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(SUCCESS_CACHE_TTL_MS - 1000); await client.getNodes({ parentPath: "/project1" }); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(2000); await client.getNodes({ parentPath: "/project1" }); expect(mockGetTdInfo).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); } }); test("should re-check compatibility after error", async () => { const mockGetTdInfo = vi .fn() .mockResolvedValueOnce({ // First call fails compatibility data: { mcpApiVersion: "2.0.0", // Major version mismatch osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }) .mockResolvedValueOnce({ // Second call succeeds data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const mockHttpClient = { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); // First call should fail due to version mismatch await expect( client.createNode({ nodeName: "test1", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); // After error, cached error should be thrown immediately await expect( client.createNode({ nodeName: "test2", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); // getTdInfo should only be called once (error is cached) expect(mockGetTdInfo).toHaveBeenCalledTimes(1); // createNode should never be called expect(mockCreateNode).toHaveBeenCalledTimes(0); }); describe("Connection error handling", () => { test("should format ECONNREFUSED error with helpful message", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "connect ECONNREFUSED 127.0.0.1:9981", success: false, }); const mockHttpClient = { getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow( /TouchDesigner is not running/, ); }); test("should format ETIMEDOUT error with helpful message", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "connect ETIMEDOUT", success: false, }); const mockHttpClient = { getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow(/Connection Timeout/); }); test("should format ENOTFOUND error with helpful message", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "getaddrinfo ENOTFOUND invalid-host", success: false, }); const mockHttpClient = { getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow( /Invalid Host Configuration/, ); }); test("should handle lowercase error codes", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "getaddrinfo enotfound invalid-host", success: false, }); const mockHttpClient = { getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow( /Invalid Host Configuration/, ); }); test("should handle mixed case connection refused error", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "Connect ECONNREFUSED 127.0.0.1:9981", success: false, }); const mockHttpClient = { getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow( /TouchDesigner Connection Failed/, ); }); test("should retry after error cache TTL expires", async () => { vi.useFakeTimers(); const mockGetTdInfo = vi .fn() .mockResolvedValueOnce({ // First call fails data: null, error: "connect ECONNREFUSED 127.0.0.1:9981", success: false, }) .mockResolvedValueOnce({ // Second call (after TTL) succeeds data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const mockHttpClient = { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); // First call should fail await expect( client.createNode({ nodeName: "test1", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); // Advance time past the ERROR_CACHE_TTL_MS vi.advanceTimersByTime(ERROR_CACHE_TTL_MS + 1000); // Second call should retry and succeed const result = await client.createNode({ nodeName: "test2", nodeType: "null", parentPath: "/", }); expect(result.success).toBe(true); expect(mockGetTdInfo).toHaveBeenCalledTimes(2); expect(mockCreateNode).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); test("should clear cached error when compatibility cache is invalidated", async () => { const mockGetTdInfo = vi .fn() .mockResolvedValueOnce({ data: { mcpApiVersion: "", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }) .mockResolvedValue({ data: { mcpApiVersion: "1.3.1", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const client = new TouchDesignerClient({ httpClient: { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, } as unknown as ITouchDesignerApi, logger: nullLogger, }); await expect( client.createNode({ nodeName: "test", nodeType: "null", parentPath: "/", }), ).rejects.toThrow("Version Information Missing"); expect(mockCreateNode).not.toHaveBeenCalled(); const infoResult = await client.getTdInfo(); expect(infoResult.success).toBe(true); expect(mockGetTdInfo).toHaveBeenCalledTimes(3); }); test("should not retry before error cache TTL expires", async () => { vi.useFakeTimers(); const mockGetTdInfo = vi.fn().mockResolvedValue({ data: null, error: "connect ECONNREFUSED 127.0.0.1:9981", success: false, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const mockHttpClient = { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); // First call should fail await expect( client.createNode({ nodeName: "test1", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); // Before TTL expires vi.advanceTimersByTime(ERROR_CACHE_TTL_MS - 1000); // Second call should use cached error await expect( client.createNode({ nodeName: "test2", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); // getTdInfo should still be called only once expect(mockGetTdInfo).toHaveBeenCalledTimes(1); expect(mockCreateNode).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1000); // After TTL expires, next call should retry await expect( client.createNode({ nodeName: "test3", nodeType: "null", parentPath: "/", }), ).rejects.toThrow(); // getTdInfo should be called again expect(mockGetTdInfo).toHaveBeenCalledTimes(2); expect(mockCreateNode).toHaveBeenCalledTimes(0); vi.useRealTimers(); }); test("should cache BELOW_MIN_VERSION errors", async () => { const mockGetTdInfo = vi.fn().mockResolvedValue({ data: { mcpApiVersion: "1.2.9", osName: "macOS", osVersion: "12.6.1", server: "TouchDesigner", version: "2023.11050", }, error: null, success: true, }); const mockCreateNode = vi.fn().mockResolvedValue({ data: { result: { name: "test" } }, error: null, success: true, }); const mockHttpClient = { createNode: mockCreateNode, getTdInfo: mockGetTdInfo, }; const client = new TouchDesignerClient({ httpClient: mockHttpClient as unknown as ITouchDesignerApi, logger: nullLogger, }); // First call should fail with BELOW_MIN_VERSION await expect( client.createNode({ nodeName: "test1", nodeType: "null", parentPath: "/", }), ).rejects.toThrow("TouchDesigner API Server Update Required"); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); // Second call should use cached error without calling getTdInfo again await expect( client.createNode({ nodeName: "test2", nodeType: "null", parentPath: "/", }), ).rejects.toThrow("TouchDesigner API Server Update Required"); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); expect(mockCreateNode).toHaveBeenCalledTimes(0); }); test("should format connection error when getTdInfo rejects with AxiosError", async () => { // AxiosError constructor: (message, code, config, request, response) const axiosError = new AxiosError( "connect ECONNREFUSED 127.0.0.1:9981", // message "ECONNREFUSED", // code ); const mockGetTdInfo = vi.fn().mockRejectedValue(axiosError); const mockHttpClient = { getTdInfo: mockGetTdInfo, } as Partial<ITouchDesignerApi>; const client = new TouchDesignerClient({ httpClient: mockHttpClient as ITouchDesignerApi, logger: nullLogger, }); await expect(client.getTdInfo()).rejects.toThrow( /TouchDesigner Connection Failed/, ); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); }); test("should propagate programming errors (non-AxiosError)", async () => { const mockGetTdInfo = vi .fn() .mockRejectedValue( new TypeError("Cannot read property 'x' of undefined"), ); const mockHttpClient = { getTdInfo: mockGetTdInfo, } as Partial<ITouchDesignerApi>; const client = new TouchDesignerClient({ httpClient: mockHttpClient as ITouchDesignerApi, logger: nullLogger, }); // Programming errors should propagate with their original message await expect(client.getTdInfo()).rejects.toThrow( "Cannot read property 'x' of undefined", ); expect(mockGetTdInfo).toHaveBeenCalledTimes(1); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8beeeaaat/touchdesigner-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server