Skip to content

Commit 0397887

Browse files
authored
test: Consolidate pytest suite to MCPForUnity and improve test infrastructure (#332)
* Update github-repo-stats.yml * pytest: make harness MCPForUnity-only; remove UnityMcpBridge paths from tests; route tools.manage_script via unity_connection for reliable monkeypatching; fix ctx usage; all tests green (39 pass, 5 skip, 7 xpass) * Add missing meta for MaterialMeshInstantiationTests.cs (Assets) * bridge/tools/manage_script: fix missing unity_connection prefix in validate_script; tests: tidy manage_script_uri unused symbols and arg names * tests: rename to script_apply_edits_module; extract DummyContext to tests/test_helpers and import; add telemetry stubs in tests to avoid pyproject I/O * tests: import cleanup and helper extraction; telemetry: prefer plain config and opt-in env override; test stubs and CWD fixes; exclude Bridge from pytest discovery * chore: remove unintended .wt-origin-main gitlink and ignore folder * tests: nit fixes (unused-arg stubs, import order, path-normalized ignore hook); telemetry: validate config endpoint; read_console: action optional * Add development dependencies to pyproject.toml - Add [project.optional-dependencies] section with dev group - Include pytest>=8.0.0 and pytest-anyio>=0.6.0 - Add Development Setup section to README-DEV.md with installation and testing instructions * Revert "Update github-repo-stats.yml" This reverts commit 8ae595d. * test: improve test clarity and modernize asyncio usage - Add explanation for 200ms timeout in backpressure test - Replace manual event loop creation with asyncio.run() - Add assertion message with actual elapsed time for easier debugging * refactor: remove duplicate DummyContext definitions across test files Replace 7 duplicate DummyContext class definitions with imports from tests.test_helpers. This follows DRY principles and ensures consistency across the test suite. * chore: remove unused _load function from test_edit_strict_and_warnings.py Dead code cleanup - function was no longer used after refactoring to dynamic tool registration. * docs: add comment explaining CWD manipulation in telemetry test Clarify why os.chdir() is necessary: telemetry.py calls get_package_version() at module load time, which reads pyproject.toml using a relative path. Acknowledges the fragility while explaining why it's currently required.
1 parent a81e130 commit 0397887

27 files changed

+535
-143
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ TestProjects/UnityMCPTests/Packages/packages-lock.json
3939
# Backup artifacts
4040
*.backup
4141
*.backup.meta
42+
43+
.wt-origin-main/

MCPForUnity/UnityMcpServer~/src/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ dependencies = [
1111
"tomli>=2.3.0",
1212
]
1313

14+
[project.optional-dependencies]
15+
dev = [
16+
"pytest>=8.0.0",
17+
"pytest-anyio>=0.6.0",
18+
]
19+
1420
[build-system]
1521
requires = ["setuptools>=64.0.0", "wheel"]
1622
build-backend = "setuptools.build_meta"

MCPForUnity/UnityMcpServer~/src/telemetry.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ def __init__(self):
9393
"""
9494
server_config = None
9595
for modname in (
96+
# Prefer plain module to respect test-time overrides and sys.path injection
97+
"config",
98+
"src.config",
9699
"MCPForUnity.UnityMcpServer~.src.config",
97100
"MCPForUnity.UnityMcpServer.src.config",
98-
"src.config",
99-
"config",
100101
):
101102
try:
102103
mod = importlib.import_module(modname)
@@ -116,10 +117,13 @@ def __init__(self):
116117
server_config, "telemetry_endpoint", None)
117118
default_ep = cfg_default or "https://api-prod.coplay.dev/telemetry/events"
118119
self.default_endpoint = default_ep
119-
self.endpoint = self._validated_endpoint(
120-
os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT", default_ep),
121-
default_ep,
122-
)
120+
# Prefer config default; allow explicit env override only when set
121+
env_ep = os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT")
122+
if env_ep is not None and env_ep != "":
123+
self.endpoint = self._validated_endpoint(env_ep, default_ep)
124+
else:
125+
# Validate config-provided default as well to enforce scheme/host rules
126+
self.endpoint = self._validated_endpoint(default_ep, default_ep)
123127
try:
124128
logger.info(
125129
"Telemetry configured: endpoint=%s (default=%s), timeout_env=%s",

MCPForUnity/UnityMcpServer~/src/tools/manage_script.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mcp.server.fastmcp import FastMCP, Context
77

88
from registry import mcp_for_unity_tool
9-
from unity_connection import send_command_with_retry
9+
import unity_connection
1010

1111

1212
def _split_uri(uri: str) -> tuple[str, str]:
@@ -103,7 +103,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
103103
warnings: list[str] = []
104104
if _needs_normalization(edits):
105105
# Read file to support index->line/col conversion when needed
106-
read_resp = send_command_with_retry("manage_script", {
106+
read_resp = unity_connection.send_command_with_retry("manage_script", {
107107
"action": "read",
108108
"name": name,
109109
"path": directory,
@@ -304,7 +304,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
304304
"options": opts,
305305
}
306306
params = {k: v for k, v in params.items() if v is not None}
307-
resp = send_command_with_retry("manage_script", params)
307+
resp = unity_connection.send_command_with_retry("manage_script", params)
308308
if isinstance(resp, dict):
309309
data = resp.setdefault("data", {})
310310
data.setdefault("normalizedEdits", normalized_edits)
@@ -336,7 +336,7 @@ def _flip_async():
336336
st = _latest_status()
337337
if st and st.get("reloading"):
338338
return
339-
send_command_with_retry(
339+
unity_connection.send_command_with_retry(
340340
"execute_menu_item",
341341
{"menuPath": "MCP/Flip Reload Sentinel"},
342342
max_retries=0,
@@ -386,7 +386,7 @@ def create_script(
386386
contents.encode("utf-8")).decode("utf-8")
387387
params["contentsEncoded"] = True
388388
params = {k: v for k, v in params.items() if v is not None}
389-
resp = send_command_with_retry("manage_script", params)
389+
resp = unity_connection.send_command_with_retry("manage_script", params)
390390
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
391391

392392

@@ -401,7 +401,7 @@ def delete_script(
401401
if not directory or directory.split("/")[0].lower() != "assets":
402402
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
403403
params = {"action": "delete", "name": name, "path": directory}
404-
resp = send_command_with_retry("manage_script", params)
404+
resp = unity_connection.send_command_with_retry("manage_script", params)
405405
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
406406

407407

@@ -426,7 +426,7 @@ def validate_script(
426426
"path": directory,
427427
"level": level,
428428
}
429-
resp = send_command_with_retry("manage_script", params)
429+
resp = unity_connection.send_command_with_retry("manage_script", params)
430430
if isinstance(resp, dict) and resp.get("success"):
431431
diags = resp.get("data", {}).get("diagnostics", []) or []
432432
warnings = sum(1 for d in diags if str(
@@ -473,7 +473,7 @@ def manage_script(
473473

474474
params = {k: v for k, v in params.items() if v is not None}
475475

476-
response = send_command_with_retry("manage_script", params)
476+
response = unity_connection.send_command_with_retry("manage_script", params)
477477

478478
if isinstance(response, dict):
479479
if response.get("success"):
@@ -541,7 +541,7 @@ def get_sha(
541541
try:
542542
name, directory = _split_uri(uri)
543543
params = {"action": "get_sha", "name": name, "path": directory}
544-
resp = send_command_with_retry("manage_script", params)
544+
resp = unity_connection.send_command_with_retry("manage_script", params)
545545
if isinstance(resp, dict) and resp.get("success"):
546546
data = resp.get("data", {})
547547
minimal = {"sha256": data.get(

MCPForUnity/UnityMcpServer~/src/tools/read_console.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
def read_console(
1515
ctx: Context,
16-
action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."],
16+
action: Annotated[Literal['get', 'clear'], "Get or clear the Unity Editor console."] | None = None,
1717
types: Annotated[list[Literal['error', 'warning',
1818
'log', 'all']], "Message types to get"] | None = None,
1919
count: Annotated[int, "Max messages to return"] | None = None,

TestProjects/UnityMCPTests/Assets/Temp.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialMeshInstantiationTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def pytest_ignore_collect(path, config):
2+
# Avoid duplicate import mismatches between Bridge and MCPForUnity copies
3+
p = str(path)
4+
return p.endswith("test_telemetry.py")

UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mcp.server.fastmcp import FastMCP, Context
77

88
from registry import mcp_for_unity_tool
9-
from unity_connection import send_command_with_retry
9+
import unity_connection
1010

1111

1212
def _split_uri(uri: str) -> tuple[str, str]:
@@ -103,7 +103,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool:
103103
warnings: list[str] = []
104104
if _needs_normalization(edits):
105105
# Read file to support index->line/col conversion when needed
106-
read_resp = send_command_with_retry("manage_script", {
106+
read_resp = unity_connection.send_command_with_retry("manage_script", {
107107
"action": "read",
108108
"name": name,
109109
"path": directory,
@@ -304,7 +304,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool:
304304
"options": opts,
305305
}
306306
params = {k: v for k, v in params.items() if v is not None}
307-
resp = send_command_with_retry("manage_script", params)
307+
resp = unity_connection.send_command_with_retry("manage_script", params)
308308
if isinstance(resp, dict):
309309
data = resp.setdefault("data", {})
310310
data.setdefault("normalizedEdits", normalized_edits)
@@ -336,7 +336,7 @@ def _flip_async():
336336
st = _latest_status()
337337
if st and st.get("reloading"):
338338
return
339-
send_command_with_retry(
339+
unity_connection.send_command_with_retry(
340340
"execute_menu_item",
341341
{"menuPath": "MCP/Flip Reload Sentinel"},
342342
max_retries=0,
@@ -386,7 +386,7 @@ def create_script(
386386
contents.encode("utf-8")).decode("utf-8")
387387
params["contentsEncoded"] = True
388388
params = {k: v for k, v in params.items() if v is not None}
389-
resp = send_command_with_retry("manage_script", params)
389+
resp = unity_connection.send_command_with_retry("manage_script", params)
390390
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
391391

392392

@@ -401,7 +401,7 @@ def delete_script(
401401
if not directory or directory.split("/")[0].lower() != "assets":
402402
return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."}
403403
params = {"action": "delete", "name": name, "path": directory}
404-
resp = send_command_with_retry("manage_script", params)
404+
resp = unity_connection.send_command_with_retry("manage_script", params)
405405
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
406406

407407

@@ -426,7 +426,7 @@ def validate_script(
426426
"path": directory,
427427
"level": level,
428428
}
429-
resp = send_command_with_retry("manage_script", params)
429+
resp = unity_connection.send_command_with_retry("manage_script", params)
430430
if isinstance(resp, dict) and resp.get("success"):
431431
diags = resp.get("data", {}).get("diagnostics", []) or []
432432
warnings = sum(1 for d in diags if str(
@@ -473,7 +473,7 @@ def manage_script(
473473

474474
params = {k: v for k, v in params.items() if v is not None}
475475

476-
response = send_command_with_retry("manage_script", params)
476+
response = unity_connection.send_command_with_retry("manage_script", params)
477477

478478
if isinstance(response, dict):
479479
if response.get("success"):
@@ -541,7 +541,7 @@ def get_sha(
541541
try:
542542
name, directory = _split_uri(uri)
543543
params = {"action": "get_sha", "name": name, "path": directory}
544-
resp = send_command_with_retry("manage_script", params)
544+
resp = unity_connection.send_command_with_retry("manage_script", params)
545545
if isinstance(resp, dict) and resp.get("success"):
546546
data = resp.get("data", {})
547547
minimal = {"sha256": data.get(

docs/README-DEV.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@
55

66
Welcome to the MCP for Unity development environment! This directory contains tools and utilities to streamline MCP for Unity core development.
77

8+
## 🛠️ Development Setup
9+
10+
### Installing Development Dependencies
11+
12+
To contribute or run tests, you need to install the development dependencies:
13+
14+
```bash
15+
# Navigate to the server source directory
16+
cd MCPForUnity/UnityMcpServer~/src
17+
18+
# Install the package in editable mode with dev dependencies
19+
pip install -e .[dev]
20+
```
21+
22+
This installs:
23+
- **Runtime dependencies**: `httpx`, `mcp[cli]`, `pydantic`, `tomli`
24+
- **Development dependencies**: `pytest`, `pytest-anyio`
25+
26+
### Running Tests
27+
28+
```bash
29+
# From the repo root
30+
pytest tests/ -v
31+
```
32+
33+
Or if you prefer using Python module syntax:
34+
```bash
35+
python -m pytest tests/ -v
36+
```
37+
838
## 🚀 Available Development Features
939

1040
### ✅ Development Deployment Scripts

0 commit comments

Comments
 (0)