@dpantani/tdmcp 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/cli/agent.d.ts +13 -0
- package/dist/cli/agent.js +12324 -1726
- package/dist/cli/agent.js.map +1 -1
- package/dist/index.js +27930 -14302
- package/dist/index.js.map +1 -1
- package/dist/recipes/animus_rings_visualizer.json +37 -0
- package/dist/recipes/body_tracking_reactive.json +127 -0
- package/dist/recipes/mediapipe_body_dots.json +86 -0
- package/dist/recipes/pose_skeleton_mediapipe.json +67 -0
- package/package.json +16 -5
- package/recipes/animus_rings_visualizer.json +37 -0
- package/recipes/body_tracking_reactive.json +127 -0
- package/recipes/mediapipe_body_dots.json +86 -0
- package/recipes/pose_skeleton_mediapipe.json +67 -0
- package/scripts/fetch-shader-park-td.mjs +79 -0
- package/scripts/tdmcp-lops.mjs +52 -0
- package/td/modules/utils/version.py +1 -1
- package/td/tests/test_api_controller.py +0 -307
- package/td/tests/test_services.py +0 -209
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
"""Unit tests for the bridge service layer: parameter validation + connect guards.
|
|
2
|
-
|
|
3
|
-
Exercises api_service.update_parameters / create_node and batch_service.connect
|
|
4
|
-
off-TD using lightweight fakes for TD node objects. These lock in the fixes for
|
|
5
|
-
two silent-failure bugs:
|
|
6
|
-
|
|
7
|
-
* setting an unknown parameter name (e.g. `gain` on a levelTOP) used to be
|
|
8
|
-
silently dropped; it now raises (update) or warns (create).
|
|
9
|
-
* wiring two operators in different containers used to silently no-op; it now
|
|
10
|
-
raises with an actionable message.
|
|
11
|
-
|
|
12
|
-
Stdlib only. Run: `python3 -m unittest discover -s td/tests` (or `npm run test:bridge`).
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import os
|
|
16
|
-
import sys
|
|
17
|
-
import types
|
|
18
|
-
import unittest
|
|
19
|
-
from unittest import mock
|
|
20
|
-
|
|
21
|
-
# --- Make the bridge importable without TouchDesigner --------------------------
|
|
22
|
-
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
-
_MODULES = os.path.abspath(os.path.join(_HERE, "..", "modules"))
|
|
24
|
-
if _MODULES not in sys.path:
|
|
25
|
-
sys.path.insert(0, _MODULES)
|
|
26
|
-
|
|
27
|
-
# `mcp.services.*` bind `op/app/project` from `td` at import time; a stub suffices.
|
|
28
|
-
_td_stub = types.ModuleType("td")
|
|
29
|
-
_td_stub.op = mock.MagicMock(name="op")
|
|
30
|
-
_td_stub.app = mock.MagicMock(name="app")
|
|
31
|
-
_td_stub.project = mock.MagicMock(name="project")
|
|
32
|
-
sys.modules.setdefault("td", _td_stub)
|
|
33
|
-
|
|
34
|
-
from mcp.services import analysis_service, api_service, batch_service # noqa: E402
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# --- Lightweight fakes for TD node objects -------------------------------------
|
|
38
|
-
class FakePar:
|
|
39
|
-
def __init__(self, name, val=None):
|
|
40
|
-
self.name = name
|
|
41
|
-
self.val = val
|
|
42
|
-
|
|
43
|
-
def eval(self):
|
|
44
|
-
return self.val
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class FakeParCollection:
|
|
48
|
-
"""getattr(self, name, None) -> FakePar for known names, None otherwise."""
|
|
49
|
-
|
|
50
|
-
def __init__(self, names):
|
|
51
|
-
for n in names:
|
|
52
|
-
setattr(self, n, FakePar(n))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class FakeConnector:
|
|
56
|
-
def __init__(self):
|
|
57
|
-
self.connected_to = None
|
|
58
|
-
|
|
59
|
-
def connect(self, other):
|
|
60
|
-
self.connected_to = other
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class FakeNode:
|
|
64
|
-
def __init__(self, path, par_names=(), parent_path="/project1", n_in=1, n_out=1):
|
|
65
|
-
self.path = path
|
|
66
|
-
self.name = path.rsplit("/", 1)[-1]
|
|
67
|
-
self.OPType = "fakeTOP"
|
|
68
|
-
self.par = FakeParCollection(par_names)
|
|
69
|
-
self.inputs = []
|
|
70
|
-
self.outputs = []
|
|
71
|
-
self.inputConnectors = [FakeConnector() for _ in range(n_in)]
|
|
72
|
-
self.outputConnectors = [FakeConnector() for _ in range(n_out)]
|
|
73
|
-
self._parent_path = parent_path
|
|
74
|
-
|
|
75
|
-
def pars(self):
|
|
76
|
-
return [v for v in vars(self.par).values() if isinstance(v, FakePar)]
|
|
77
|
-
|
|
78
|
-
def parent(self):
|
|
79
|
-
return types.SimpleNamespace(path=self._parent_path)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class UpdateParametersTests(unittest.TestCase):
|
|
83
|
-
def test_rejects_unknown_param_atomically(self):
|
|
84
|
-
node = FakeNode("/project1/lvl", ["brightness1"])
|
|
85
|
-
with mock.patch.object(api_service, "op", lambda p: node):
|
|
86
|
-
with self.assertRaises(ValueError) as cm:
|
|
87
|
-
api_service.update_parameters("/project1/lvl", {"gain": 0.5, "brightness1": 0.9})
|
|
88
|
-
self.assertIn("gain", str(cm.exception))
|
|
89
|
-
# Atomic: a valid sibling param in the same call is NOT applied either.
|
|
90
|
-
self.assertIsNone(node.par.brightness1.val)
|
|
91
|
-
|
|
92
|
-
def test_applies_known_params(self):
|
|
93
|
-
node = FakeNode("/project1/lvl", ["brightness1", "opacity"])
|
|
94
|
-
with mock.patch.object(api_service, "op", lambda p: node):
|
|
95
|
-
detail = api_service.update_parameters(
|
|
96
|
-
"/project1/lvl", {"brightness1": 0.85, "opacity": 0.5}
|
|
97
|
-
)
|
|
98
|
-
self.assertEqual(node.par.brightness1.val, 0.85)
|
|
99
|
-
self.assertEqual(node.par.opacity.val, 0.5)
|
|
100
|
-
self.assertEqual(detail["path"], "/project1/lvl")
|
|
101
|
-
|
|
102
|
-
def test_missing_node_raises(self):
|
|
103
|
-
with mock.patch.object(api_service, "op", lambda p: None):
|
|
104
|
-
with self.assertRaises(LookupError):
|
|
105
|
-
api_service.update_parameters("/nope", {"x": 1})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class CreateNodeWarningTests(unittest.TestCase):
|
|
109
|
-
def test_failed_params_become_warnings_not_silent(self):
|
|
110
|
-
node = FakeNode("/project1/lvl", ["brightness1"])
|
|
111
|
-
parent = mock.MagicMock(name="parent")
|
|
112
|
-
parent.create.return_value = node
|
|
113
|
-
with mock.patch.object(api_service, "op", lambda p: parent), mock.patch.object(
|
|
114
|
-
api_service, "_resolve_type", lambda t: object
|
|
115
|
-
):
|
|
116
|
-
ref = api_service.create_node(
|
|
117
|
-
"/project1", "fakeTOP", "lvl", {"gain": 1, "brightness1": 0.5}
|
|
118
|
-
)
|
|
119
|
-
# Node is still created; the bad param surfaces as a warning, not silence.
|
|
120
|
-
self.assertEqual(ref["path"], "/project1/lvl")
|
|
121
|
-
self.assertIn("gain", ref.get("parameter_warnings", []))
|
|
122
|
-
# The valid param still applied.
|
|
123
|
-
self.assertEqual(node.par.brightness1.val, 0.5)
|
|
124
|
-
|
|
125
|
-
def test_no_warnings_when_all_params_valid(self):
|
|
126
|
-
node = FakeNode("/project1/lvl", ["brightness1"])
|
|
127
|
-
parent = mock.MagicMock(name="parent")
|
|
128
|
-
parent.create.return_value = node
|
|
129
|
-
with mock.patch.object(api_service, "op", lambda p: parent), mock.patch.object(
|
|
130
|
-
api_service, "_resolve_type", lambda t: object
|
|
131
|
-
):
|
|
132
|
-
ref = api_service.create_node("/project1", "fakeTOP", "lvl", {"brightness1": 0.5})
|
|
133
|
-
self.assertNotIn("parameter_warnings", ref)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class ConnectGuardTests(unittest.TestCase):
|
|
137
|
-
def _patch_op(self, nodes):
|
|
138
|
-
return mock.patch.object(batch_service, "op", lambda p: nodes.get(p))
|
|
139
|
-
|
|
140
|
-
def test_rejects_cross_container(self):
|
|
141
|
-
src = FakeNode("/project1/a/x", parent_path="/project1/a")
|
|
142
|
-
dst = FakeNode("/project1/b/y", parent_path="/project1/b")
|
|
143
|
-
with self._patch_op({"/project1/a/x": src, "/project1/b/y": dst}):
|
|
144
|
-
with self.assertRaises(ValueError) as cm:
|
|
145
|
-
batch_service.connect("/project1/a/x", "/project1/b/y")
|
|
146
|
-
self.assertIn("across containers", str(cm.exception))
|
|
147
|
-
# No phantom wire was made.
|
|
148
|
-
self.assertIsNone(dst.inputConnectors[0].connected_to)
|
|
149
|
-
|
|
150
|
-
def test_same_parent_wires(self):
|
|
151
|
-
src = FakeNode("/project1/x", parent_path="/project1")
|
|
152
|
-
dst = FakeNode("/project1/y", parent_path="/project1")
|
|
153
|
-
with self._patch_op({"/project1/x": src, "/project1/y": dst}):
|
|
154
|
-
batch_service.connect("/project1/x", "/project1/y")
|
|
155
|
-
self.assertIs(dst.inputConnectors[0].connected_to, src.outputConnectors[0])
|
|
156
|
-
|
|
157
|
-
def test_bad_connector_index_raises(self):
|
|
158
|
-
src = FakeNode("/project1/x", parent_path="/project1")
|
|
159
|
-
dst = FakeNode("/project1/y", parent_path="/project1", n_in=1)
|
|
160
|
-
with self._patch_op({"/project1/x": src, "/project1/y": dst}):
|
|
161
|
-
with self.assertRaises(IndexError):
|
|
162
|
-
batch_service.connect("/project1/x", "/project1/y", target_input=3)
|
|
163
|
-
|
|
164
|
-
def test_missing_node_raises(self):
|
|
165
|
-
with self._patch_op({}):
|
|
166
|
-
with self.assertRaises(LookupError):
|
|
167
|
-
batch_service.connect("/a", "/b")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class _PerfNode:
|
|
171
|
-
def __init__(self, path, cook_time=0.0, cook_count=0):
|
|
172
|
-
self.path = path
|
|
173
|
-
self.cookTime = cook_time
|
|
174
|
-
self.cookCount = cook_count
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class _PerfRoot:
|
|
178
|
-
"""Returns direct children at depth=1, all descendants otherwise — like a TD COMP."""
|
|
179
|
-
|
|
180
|
-
def __init__(self, direct, nested):
|
|
181
|
-
self._direct = direct
|
|
182
|
-
self._nested = nested
|
|
183
|
-
|
|
184
|
-
def findChildren(self, depth=None):
|
|
185
|
-
return self._direct if depth == 1 else self._nested
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class PerformanceRecursiveTests(unittest.TestCase):
|
|
189
|
-
def _run(self, recursive):
|
|
190
|
-
direct = [_PerfNode("/p/a", 1.0)]
|
|
191
|
-
nested = [_PerfNode("/p/a", 1.0), _PerfNode("/p/sys/inner", 2.0)]
|
|
192
|
-
root = _PerfRoot(direct, nested)
|
|
193
|
-
with mock.patch.object(analysis_service, "op", lambda path: root):
|
|
194
|
-
return analysis_service.performance("/p", recursive=recursive)
|
|
195
|
-
|
|
196
|
-
def test_shallow_measures_only_direct_children(self):
|
|
197
|
-
result = self._run(recursive=False)
|
|
198
|
-
self.assertEqual([n["path"] for n in result["nodes"]], ["/p/a"])
|
|
199
|
-
self.assertEqual(result["total_cook_time_ms"], 1.0)
|
|
200
|
-
|
|
201
|
-
def test_recursive_measures_nested_nodes_too(self):
|
|
202
|
-
result = self._run(recursive=True)
|
|
203
|
-
self.assertEqual(sorted(n["path"] for n in result["nodes"]), ["/p/a", "/p/sys/inner"])
|
|
204
|
-
# Nested node's cook time is now counted in the total.
|
|
205
|
-
self.assertEqual(result["total_cook_time_ms"], 3.0)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if __name__ == "__main__":
|
|
209
|
-
unittest.main()
|