-
Notifications
You must be signed in to change notification settings - Fork 0
Chore/pytest harness tests only #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f8946c2
15bd3f3
a729600
b0edffd
282f4d8
2d50bf4
386a6cc
8cdcda3
ddd3159
a607041
3213d46
51750f0
dcd6907
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -93,10 +93,11 @@ def __init__(self): | |
| """ | ||
| server_config = None | ||
| for modname in ( | ||
| # Prefer plain module to respect test-time overrides and sys.path injection | ||
| "config", | ||
| "src.config", | ||
| "MCPForUnity.UnityMcpServer~.src.config", | ||
| "MCPForUnity.UnityMcpServer.src.config", | ||
| "src.config", | ||
| "config", | ||
| ): | ||
| try: | ||
| mod = importlib.import_module(modname) | ||
|
|
@@ -116,10 +117,13 @@ def __init__(self): | |
| server_config, "telemetry_endpoint", None) | ||
| default_ep = cfg_default or "https://api-prod.coplay.dev/telemetry/events" | ||
| self.default_endpoint = default_ep | ||
| self.endpoint = self._validated_endpoint( | ||
| os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT", default_ep), | ||
| default_ep, | ||
| ) | ||
| # Prefer config default; allow explicit env override only when set | ||
| env_ep = os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT") | ||
| if env_ep is not None and env_ep != "": | ||
| self.endpoint = self._validated_endpoint(env_ep, default_ep) | ||
| else: | ||
| # Validate config-provided default as well to enforce scheme/host rules | ||
| self.endpoint = self._validated_endpoint(default_ep, default_ep) | ||
|
Comment on lines
+120
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Validation fallback uses potentially invalid config value. The endpoint validation logic has a flaw: when validating Attack scenario: A malicious or misconfigured Apply this diff to use the hardcoded safe default as the fallback: + # Safe hardcoded fallback
+ SAFE_DEFAULT = "https://api-prod.coplay.dev/telemetry/events"
# Telemetry endpoint (Cloud Run default; override via env)
cfg_default = None if server_config is None else getattr(
server_config, "telemetry_endpoint", None)
- default_ep = cfg_default or "https://api-prod.coplay.dev/telemetry/events"
+ default_ep = cfg_default or SAFE_DEFAULT
self.default_endpoint = default_ep
# Prefer config default; allow explicit env override only when set
env_ep = os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT")
if env_ep is not None and env_ep != "":
- self.endpoint = self._validated_endpoint(env_ep, default_ep)
+ self.endpoint = self._validated_endpoint(env_ep, SAFE_DEFAULT)
else:
# Validate config-provided default as well to enforce scheme/host rules
- self.endpoint = self._validated_endpoint(default_ep, default_ep)
+ self.endpoint = self._validated_endpoint(default_ep, SAFE_DEFAULT)
🤖 Prompt for AI Agents |
||
| try: | ||
| logger.info( | ||
| "Telemetry configured: endpoint=%s (default=%s), timeout_env=%s", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| from mcp.server.fastmcp import FastMCP, Context | ||
|
|
||
| from registry import mcp_for_unity_tool | ||
| from unity_connection import send_command_with_retry | ||
| import unity_connection | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: This import pattern differs from other tool files in the same directory (e.g.,
Prompt To Fix With AIThis is a comment left during a code review.
Path: MCPForUnity/UnityMcpServer~/src/tools/manage_script.py
Line: 9:9
Comment:
**style:** This import pattern differs from other tool files in the same directory (e.g., `script_apply_edits.py`, `read_console.py`, `manage_editor.py`) which still use `from unity_connection import send_command_with_retry`. While the module import works correctly and enables better test mocking, the inconsistency across the codebase could be confusing. Consider either:
1. Applying this pattern to all tool files for consistency
2. Documenting why `manage_script.py` needs this special import pattern
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
|
|
||
| def _split_uri(uri: str) -> tuple[str, str]: | ||
|
|
@@ -103,7 +103,7 @@ def _needs_normalization(arr: list[dict[str, Any]]) -> bool: | |
| warnings: list[str] = [] | ||
| if _needs_normalization(edits): | ||
| # Read file to support index->line/col conversion when needed | ||
| read_resp = send_command_with_retry("manage_script", { | ||
| read_resp = unity_connection.send_command_with_retry("manage_script", { | ||
| "action": "read", | ||
| "name": name, | ||
| "path": directory, | ||
|
|
@@ -304,7 +304,7 @@ def _le(a: tuple[int, int], b: tuple[int, int]) -> bool: | |
| "options": opts, | ||
| } | ||
| params = {k: v for k, v in params.items() if v is not None} | ||
| resp = send_command_with_retry("manage_script", params) | ||
| resp = unity_connection.send_command_with_retry("manage_script", params) | ||
| if isinstance(resp, dict): | ||
| data = resp.setdefault("data", {}) | ||
| data.setdefault("normalizedEdits", normalized_edits) | ||
|
|
@@ -336,7 +336,7 @@ def _flip_async(): | |
| st = _latest_status() | ||
| if st and st.get("reloading"): | ||
| return | ||
| send_command_with_retry( | ||
| unity_connection.send_command_with_retry( | ||
| "execute_menu_item", | ||
| {"menuPath": "MCP/Flip Reload Sentinel"}, | ||
| max_retries=0, | ||
|
|
@@ -386,7 +386,7 @@ def create_script( | |
| contents.encode("utf-8")).decode("utf-8") | ||
| params["contentsEncoded"] = True | ||
| params = {k: v for k, v in params.items() if v is not None} | ||
| resp = send_command_with_retry("manage_script", params) | ||
| resp = unity_connection.send_command_with_retry("manage_script", params) | ||
| return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} | ||
|
|
||
|
|
||
|
|
@@ -401,7 +401,7 @@ def delete_script( | |
| if not directory or directory.split("/")[0].lower() != "assets": | ||
| return {"success": False, "code": "path_outside_assets", "message": "URI must resolve under 'Assets/'."} | ||
| params = {"action": "delete", "name": name, "path": directory} | ||
| resp = send_command_with_retry("manage_script", params) | ||
| resp = unity_connection.send_command_with_retry("manage_script", params) | ||
| return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} | ||
|
|
||
|
|
||
|
|
@@ -426,7 +426,7 @@ def validate_script( | |
| "path": directory, | ||
| "level": level, | ||
| } | ||
| resp = send_command_with_retry("manage_script", params) | ||
| resp = unity_connection.send_command_with_retry("manage_script", params) | ||
| if isinstance(resp, dict) and resp.get("success"): | ||
| diags = resp.get("data", {}).get("diagnostics", []) or [] | ||
| warnings = sum(1 for d in diags if str( | ||
|
|
@@ -473,7 +473,7 @@ def manage_script( | |
|
|
||
| params = {k: v for k, v in params.items() if v is not None} | ||
|
|
||
| response = send_command_with_retry("manage_script", params) | ||
| response = unity_connection.send_command_with_retry("manage_script", params) | ||
|
|
||
| if isinstance(response, dict): | ||
| if response.get("success"): | ||
|
|
@@ -541,7 +541,7 @@ def get_sha( | |
| try: | ||
| name, directory = _split_uri(uri) | ||
| params = {"action": "get_sha", "name": name, "path": directory} | ||
| resp = send_command_with_retry("manage_script", params) | ||
| resp = unity_connection.send_command_with_retry("manage_script", params) | ||
| if isinstance(resp, dict) and resp.get("success"): | ||
| data = resp.get("data", {}) | ||
| minimal = {"sha256": data.get( | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| def pytest_ignore_collect(path, config): | ||
| # Avoid duplicate import mismatches between Bridge and MCPForUnity copies | ||
| p = str(path) | ||
| return p.endswith("test_telemetry.py") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [pytest] | ||
| testpaths = tests | ||
| norecursedirs = UnityMcpBridge MCPForUnity | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that plain module names don't introduce unintended imports in production.
The addition of plain module names (
"config","src.config") before package-qualified paths enables test-time sys.path injection, which aligns with the test harness changes in this PR. However, these plain names could theoretically match unintended modules if sys.path is polluted in production environments.Run the following script to verify that no unintended
configorsrc.configmodules exist in common Python paths that could shadow the intended module:Based on learnings
🏁 Script executed:
Length of output: 245
🏁 Script executed:
Length of output: 4782
🏁 Script executed:
Length of output: 5509
🏁 Script executed:
Length of output: 1299
Address circular fallback in endpoint validation; verify module import resolution in production.
Two issues identified:
Endpoint validation bypass (lines 120-126): When
cfg_defaultis invalid,_validated_endpoint(default_ep, default_ep)fails validation but returns the same invalid value as fallback. The hardcoded default ("https://api-prod.coplay.dev/telemetry/events") should be the ultimate fallback instead. Replace line 126 with:Module import resolution (lines 96-98): Plain module names enable test-time sys.path injection, but
UnityMcpBridge/UnityMcpServer~/src/config.pyexists alongside the target module, creating collision risk. While fully-qualified fallbacks (lines 99-100) provide MCPForUnity-specific safety for production, verify that plain imports resolve to the intended module in both test and production environments. Consider adding debug logging to confirm which config module was loaded.