Skip to main content
Glama
touchDesignerClientAndWebServer.test.ts12.9 kB
import { afterAll, beforeAll, describe, expect, test } from "vitest"; import type { TdNode } from "../../src/gen/endpoints/TouchDesignerAPI"; import { TouchDesignerClient } from "../../src/tdClient/touchDesignerClient"; const PROJECT_PATH = "/project1"; const SANDBOX_NAME = "test_base_comp"; const SANDBOX_PATH = `${PROJECT_PATH}/${SANDBOX_NAME}`; /** * Verify if a node exists */ async function verifyNodeExists(params: { client: TouchDesignerClient; nodeName: string; }): Promise<boolean> { try { const response = await params.client.execNodeMethod<{ result: TdNode[]; }>({ args: [params.nodeName], kwargs: {}, method: "ops", nodePath: SANDBOX_PATH, }); return response.success ? response.data.result.length > 0 : false; } catch (_err) { return false; } } const tdClient = new TouchDesignerClient(); describe("TouchDesigner Client E2E Tests", () => { beforeAll(async () => { process.env.TD_WEB_SERVER_HOST = "http://127.0.0.1"; process.env.TD_WEB_SERVER_PORT = "9981"; await tdClient.createNode({ nodeName: SANDBOX_NAME, nodeType: "baseCOMP", parentPath: PROJECT_PATH, }); }); afterAll(async () => { await tdClient.deleteNode({ nodePath: SANDBOX_PATH }); }); test("TouchDesigner info endpoint should return server information", async () => { const response = await tdClient.getTdInfo(); expect(response).toBeDefined(); expect(response.success).toBe(true); if (response.success) { expect(response.data).toBeDefined(); } else { expect.fail(`getTdInfo failed: ${response.error}`); } }); test("Python classes list endpoint should return available classes", async () => { const response = await tdClient.getClasses(); expect(response).toBeDefined(); if (!response.success) { throw new Error(`failed: ${response.error}`); } expect(response.data).toBeDefined(); expect(response.success).toBe(true); const classes = response.data.classes || []; const hasValidClass = classes.some( (c) => typeof c.name === "string" && c.name.length > 0, ); expect(hasValidClass).toBe(true); }); test("Python class details endpoint should return class structure", async () => { const classNames = ["OP", "op"]; for (const className of classNames) { const response = await tdClient.getClassDetails(className); expect(response).toBeDefined(); if (!response.success) { throw new Error(`failed: ${response.error}`); } expect(response.data).toBeDefined(); expect(response.success).toBe(true); expect(response.data?.name).toBe(className); } }); test("Node method execute should create a node", async () => { const parentPath = SANDBOX_PATH; const nodeType = "textTOP"; const nodeName = `api_text_top_${Date.now()}`; const nodePath = `${parentPath}/${nodeName}`; const response = await tdClient.execNodeMethod({ args: [nodeType, nodeName], kwargs: { initialize: true }, method: "create", nodePath: parentPath, }); expect(response).toBeDefined(); if (!response.success) { throw new Error(`failed: ${response.error}`); } expect(response.data).toBeDefined(); expect(response.success).toBe(true); const exists = await verifyNodeExists({ client: tdClient, nodeName, }); expect(exists).toBe(true); await tdClient.deleteNode({ nodePath }); }); test("Complete node update flow should work", async () => { const parentPath = SANDBOX_PATH; const nodeType = "textTOP"; const nodeName = `test_update_${Date.now()}`; const nodePath = `${parentPath}/${nodeName}`; const createResponse = await tdClient.createNode({ nodeName, nodeType, parentPath, }); expect(createResponse).toBeDefined(); if (!createResponse.success) { throw new Error(`failed: ${createResponse.error}`); } expect(createResponse.success).toBe(true); expect(createResponse.data?.result?.name).toBe(nodeName); const initialProps = await tdClient.getNodeDetail({ nodePath, }); expect(initialProps).toBeDefined(); const updateProps = { fontsizex: 24, text: "Updated via API!", }; const updateResponse = await tdClient.updateNode({ nodePath, properties: updateProps, }); expect(updateResponse).toBeDefined(); if (!updateResponse.success) { throw new Error(`failed: ${updateResponse.error}`); } expect(updateResponse.success).toBe(true); expect(updateResponse.data?.updated).toBeInstanceOf(Array); expect(updateResponse.data?.updated).toContain("text"); expect(updateResponse.data?.updated).toContain("fontsizex"); const updatedProps = await tdClient.getNodeDetail({ nodePath, }); expect(updatedProps).toBeDefined(); if (!updatedProps.success) { throw new Error(`failed: ${updatedProps.error}`); } expect(updatedProps.success).toBe(true); expect(updatedProps.data?.properties.fontsizex).toBe(updateProps.fontsizex); expect(updatedProps.data?.properties.text).toBe(updateProps.text); await tdClient.deleteNode({ nodePath }); }); test("Get nodes should return filtered nodes by pattern", async () => { const parentPath = SANDBOX_PATH; const testNodes = [ { name: `test_filter_a_${Date.now()}`, type: "textTOP" }, { name: `test_filter_b_${Date.now() + 1}`, type: "textTOP" }, ]; for (const node of testNodes) { await tdClient.createNode({ nodeName: node.name, nodeType: node.type, parentPath, }); } const allNodesResponse = await tdClient.getNodes({ parentPath, }); expect(allNodesResponse).toBeDefined(); if (!allNodesResponse.success) { throw new Error(`failed: ${allNodesResponse.error}`); } expect(allNodesResponse.success).toBe(true); expect(allNodesResponse.data?.nodes).toBeInstanceOf(Array); const filterPattern = "test_filter_*"; const filteredNodesResponse = await tdClient.getNodes({ parentPath, pattern: filterPattern, }); expect(filteredNodesResponse).toBeDefined(); if (!filteredNodesResponse.success) { throw new Error(`failed: ${filteredNodesResponse.error}`); } expect(filteredNodesResponse.success).toBe(true); expect(filteredNodesResponse.data?.nodes).toBeInstanceOf(Array); const filteredNodes = filteredNodesResponse.data?.nodes || []; expect(filteredNodes.length).toBeGreaterThanOrEqual(2); for (const node of filteredNodes) { expect(node.name.startsWith("test_filter_")).toBe(true); } for (const node of testNodes) { await tdClient.deleteNode({ nodePath: `${parentPath}/${node.name}` }); } }); test("Node creation and deletion should work correctly", async () => { const parentPath = SANDBOX_PATH; const nodeType = "constantTOP"; const nodeName = `test_create_delete_${Date.now()}`; const nodePath = `${parentPath}/${nodeName}`; const createResponse = await tdClient.createNode({ nodeName, nodeType, parentPath, }); expect(createResponse).toBeDefined(); if (!createResponse.success) { throw new Error(`failed: ${createResponse.error}`); } expect(createResponse.success).toBe(true); expect(createResponse.data?.result).toBeDefined(); const exists = await verifyNodeExists({ client: tdClient, nodeName, }); expect(exists).toBe(true); const deleteResponse = await tdClient.deleteNode({ nodePath, }); expect(deleteResponse).toBeDefined(); if (!deleteResponse.success) { throw new Error(`failed: ${deleteResponse.error}`); } expect(deleteResponse.success).toBe(true); expect(deleteResponse.data?.deleted).toBe(true); expect(deleteResponse.data?.node?.path).toBe(nodePath); const stillExists = await verifyNodeExists({ client: tdClient, nodeName, }); expect(stillExists).toBe(false); }); test("Python script execution should create nodes", async () => { const nodeName = `exec_test_${Date.now()}`; const nodePath = `${SANDBOX_PATH}/${nodeName}`; const execResponse = await tdClient.execPythonScript<{ result: TdNode; }>({ script: `op('${SANDBOX_PATH}').create('nullDAT', '${nodeName}')`, }); expect(execResponse).toBeDefined(); if (!execResponse.success) { throw new Error(`failed: ${execResponse.error}`); } expect(execResponse.success).toBe(true); expect(execResponse.data).toBeDefined(); const exists = await verifyNodeExists({ client: tdClient, nodeName, }); expect(exists).toBe(true); await tdClient.deleteNode({ nodePath }); }); test("Can catch errors", async () => { const nodeName = `exec_test_${Date.now()}`; const nodePath = `${SANDBOX_PATH}/${nodeName}`; const execResponse = await tdClient.execPythonScript({ script: `op('${SANDBOX_PATH}').error()`, }); expect(execResponse).toBeDefined(); if (execResponse.success) { throw new Error(`failed: ${execResponse}`); } expect(execResponse.success).toBe(false); expect(execResponse.error).toStrictEqual( new Error( "Handler for 'exec_python_script' failed: OP attribute error is deprecated has been replaced by errors and addError.", ), ); await tdClient.deleteNode({ nodePath }); }); test("Node error check should return error lists", async () => { const response = await tdClient.getNodeErrors({ nodePath: SANDBOX_PATH, }); expect(response).toBeDefined(); if (!response.success) { throw new Error(`failed: ${response.error}`); } expect(response.success).toBe(true); expect(response.data).toBeDefined(); expect(Array.isArray(response.data?.errors)).toBe(true); }); test("add TOP errors should be detected by getNodeErrors", async () => { const nodeName = "add_error"; const addNodePath = `${SANDBOX_PATH}/${nodeName}`; const parentPath = SANDBOX_PATH; const nodeType = "addTOP"; const setupResponse = await tdClient.createNode({ nodeName, nodeType, parentPath, }); if (!setupResponse.success) { throw new Error(`failed: ${setupResponse.error}`); } // Force cook to trigger error detection const execResponse = await tdClient.execPythonScript<{ result: TdNode; }>({ script: `op('${SANDBOX_PATH}').cook(recurse=True)\n`, }); expect(execResponse).toBeDefined(); if (!execResponse.success) { throw new Error(`failed: ${execResponse.error}`); } expect(execResponse.success).toBe(true); const response = await tdClient.getNodeErrors({ nodePath: SANDBOX_PATH, }); expect(response).toBeDefined(); if (!response.success) { throw new Error(`failed: ${response.error}`); } const errors = response.data?.errors ?? []; expect(errors.length).toBeGreaterThan(0); expect( errors.some( (msg) => msg.message === `${addNodePath}: Error: Not enough sources specified`, ), ).toBe(true); }); test("Module help should return documentation for TouchDesigner classes", async () => { // Test with common TouchDesigner class const response = await tdClient.getModuleHelp({ moduleName: "noiseCHOP", }); expect(response).toBeDefined(); if (!response.success) { throw new Error("getModuleHelp failed"); } expect(response.success).toBe(true); expect(response.data).toBeDefined(); expect(response.data.moduleName).toBe("noiseCHOP"); expect(response.data.helpText).toBeDefined(); const { helpText } = response.data; if (!helpText) { throw new Error("helpText is undefined"); } expect(helpText.length).toBeGreaterThan(0); expect(helpText).toContain("class noiseCHOP"); }); test("Module help should work with td. prefix", async () => { const response = await tdClient.getModuleHelp({ moduleName: "td.noiseCHOP", }); expect(response).toBeDefined(); if (!response.success) { throw new Error("getModuleHelp failed"); } expect(response.success).toBe(true); expect(response.data.moduleName).toBe("td.noiseCHOP"); expect(response.data.helpText).toBeDefined(); const { helpText } = response.data; if (!helpText) { throw new Error("helpText is undefined"); } expect(helpText.length).toBeGreaterThan(0); }); test("Module help should work with utility modules", async () => { const response = await tdClient.getModuleHelp({ moduleName: "tdu", }); expect(response).toBeDefined(); if (!response.success) { throw new Error("getModuleHelp failed"); } expect(response.success).toBe(true); expect(response.data.moduleName).toBe("tdu"); expect(response.data.helpText).toBeDefined(); const { helpText } = response.data; if (!helpText) { throw new Error("helpText is undefined"); } expect(helpText.length).toBeGreaterThan(0); expect(helpText).toContain("TDU"); }); test("Module help should handle non-existent modules gracefully", async () => { const response = await tdClient.getModuleHelp({ moduleName: "nonExistentModule123", }); expect(response).toBeDefined(); expect(response.success).toBe(false); if (response.success) { throw new Error("Expected failure for non-existent module"); } // Error is available on ErrorResult type }); });

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