Files
2026-07-13 12:49:17 +08:00

673 lines
22 KiB
Python

from __future__ import annotations
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from services.tools.manage_probuilder import (
manage_probuilder,
ALL_ACTIONS,
SHAPE_ACTIONS,
MESH_ACTIONS,
VERTEX_ACTIONS,
SELECTION_ACTIONS,
UV_MATERIAL_ACTIONS,
QUERY_ACTIONS,
SMOOTHING_ACTIONS,
UTILITY_ACTIONS,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_unity(monkeypatch):
"""Patch Unity transport layer and return captured call dict."""
captured: dict[str, object] = {}
async def fake_send(send_fn, unity_instance, tool_name, params):
captured["unity_instance"] = unity_instance
captured["tool_name"] = tool_name
captured["params"] = params
return {"success": True, "message": "ok"}
monkeypatch.setattr(
"services.tools.manage_probuilder.get_unity_instance_from_context",
AsyncMock(return_value="unity-instance-1"),
)
monkeypatch.setattr(
"services.tools.manage_probuilder.send_with_unity_instance",
fake_send,
)
return captured
# ---------------------------------------------------------------------------
# Action list completeness
# ---------------------------------------------------------------------------
def test_all_actions_is_union_of_sub_lists():
expected = set(
["ping"] + SHAPE_ACTIONS + MESH_ACTIONS + VERTEX_ACTIONS + SELECTION_ACTIONS
+ UV_MATERIAL_ACTIONS + QUERY_ACTIONS + SMOOTHING_ACTIONS + UTILITY_ACTIONS
)
assert set(ALL_ACTIONS) == expected
def test_no_duplicate_actions():
assert len(ALL_ACTIONS) == len(set(ALL_ACTIONS))
# ---------------------------------------------------------------------------
# Invalid / missing action
# ---------------------------------------------------------------------------
def test_unknown_action_returns_error(mock_unity):
result = asyncio.run(
manage_probuilder(SimpleNamespace(), action="nonexistent_action")
)
assert result["success"] is False
assert "Unknown action" in result["message"]
assert "tool_name" not in mock_unity # Should NOT call Unity
def test_empty_action_returns_error(mock_unity):
result = asyncio.run(
manage_probuilder(SimpleNamespace(), action="")
)
assert result["success"] is False
# ---------------------------------------------------------------------------
# Shape creation
# ---------------------------------------------------------------------------
def test_create_shape_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="create_shape",
properties={"shapeType": "Cube", "size": [2, 2, 2]},
)
)
assert result["success"] is True
assert mock_unity["tool_name"] == "manage_probuilder"
assert mock_unity["params"]["action"] == "create_shape"
assert mock_unity["params"]["properties"]["shapeType"] == "Cube"
def test_create_shape_with_target(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="create_shape",
target="MyParent",
properties={"shapeType": "Torus"},
)
)
assert result["success"] is True
assert mock_unity["params"]["target"] == "MyParent"
def test_create_poly_shape_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="create_poly_shape",
properties={
"points": [[0, 0, 0], [5, 0, 0], [5, 0, 5], [0, 0, 5]],
"extrudeHeight": 3.0,
},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "create_poly_shape"
assert mock_unity["params"]["properties"]["extrudeHeight"] == 3.0
# ---------------------------------------------------------------------------
# Mesh editing
# ---------------------------------------------------------------------------
def test_extrude_faces_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="extrude_faces",
target="MyCube",
properties={"faceIndices": [0, 1], "distance": 1.5},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "extrude_faces"
assert mock_unity["params"]["target"] == "MyCube"
assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1]
assert mock_unity["params"]["properties"]["distance"] == 1.5
def test_bevel_edges_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="bevel_edges",
target="MyCube",
properties={"edgeIndices": [0, 2], "amount": 0.2},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "bevel_edges"
assert mock_unity["params"]["properties"]["amount"] == 0.2
def test_delete_faces_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="delete_faces",
target="MyCube",
properties={"faceIndices": [3]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "delete_faces"
def test_subdivide_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="subdivide",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "subdivide"
def test_combine_meshes_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="combine_meshes",
properties={"targets": ["Cube1", "Cube2"]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "combine_meshes"
# ---------------------------------------------------------------------------
# Vertex operations
# ---------------------------------------------------------------------------
def test_move_vertices_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="move_vertices",
target="MyCube",
properties={"vertexIndices": [0, 1, 2], "offset": [0, 1, 0]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "move_vertices"
assert mock_unity["params"]["properties"]["offset"] == [0, 1, 0]
# ---------------------------------------------------------------------------
# UV & materials
# ---------------------------------------------------------------------------
def test_set_face_material_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="set_face_material",
target="MyCube",
properties={"faceIndices": [0], "materialPath": "Assets/Materials/Red.mat"},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "set_face_material"
assert mock_unity["params"]["properties"]["materialPath"] == "Assets/Materials/Red.mat"
def test_set_face_uvs_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="set_face_uvs",
target="MyCube",
properties={"faceIndices": [0, 1], "scale": [2, 2], "rotation": 45},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "set_face_uvs"
# ---------------------------------------------------------------------------
# Query
# ---------------------------------------------------------------------------
def test_get_mesh_info_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="get_mesh_info",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "get_mesh_info"
assert mock_unity["params"]["target"] == "MyCube"
def test_convert_to_probuilder_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="convert_to_probuilder",
target="StandardMesh",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "convert_to_probuilder"
# ---------------------------------------------------------------------------
# Search method passthrough
# ---------------------------------------------------------------------------
def test_search_method_passed_through(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="get_mesh_info",
target="-12345",
search_method="by_id",
)
)
assert result["success"] is True
assert mock_unity["params"]["searchMethod"] == "by_id"
# ---------------------------------------------------------------------------
# Ping
# ---------------------------------------------------------------------------
def test_ping_sends_to_unity(mock_unity):
result = asyncio.run(
manage_probuilder(SimpleNamespace(), action="ping")
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "ping"
# ---------------------------------------------------------------------------
# All actions are lowercase-normalized
# ---------------------------------------------------------------------------
def test_action_case_insensitive(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="Create_Shape",
properties={"shapeType": "Cube"},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "create_shape"
# ---------------------------------------------------------------------------
# Non-dict result from Unity
# ---------------------------------------------------------------------------
def test_non_dict_result_wrapped(monkeypatch):
async def fake_send(send_fn, unity_instance, tool_name, params):
return "unexpected string result"
monkeypatch.setattr(
"services.tools.manage_probuilder.get_unity_instance_from_context",
AsyncMock(return_value="unity-instance-1"),
)
monkeypatch.setattr(
"services.tools.manage_probuilder.send_with_unity_instance",
fake_send,
)
result = asyncio.run(
manage_probuilder(SimpleNamespace(), action="ping")
)
assert result["success"] is False
assert "unexpected string result" in result["message"]
# ---------------------------------------------------------------------------
# New action categories
# ---------------------------------------------------------------------------
def test_smoothing_actions_in_all():
for action in SMOOTHING_ACTIONS:
assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"
def test_utility_actions_in_all():
for action in UTILITY_ACTIONS:
assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"
# ---------------------------------------------------------------------------
# Smoothing actions
# ---------------------------------------------------------------------------
def test_auto_smooth_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="auto_smooth",
target="MyCube",
properties={"angleThreshold": 45},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "auto_smooth"
assert mock_unity["params"]["target"] == "MyCube"
assert mock_unity["params"]["properties"]["angleThreshold"] == 45
def test_set_smoothing_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="set_smoothing",
target="MyCube",
properties={"faceIndices": [0, 1, 2], "smoothingGroup": 1},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "set_smoothing"
assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1, 2]
assert mock_unity["params"]["properties"]["smoothingGroup"] == 1
# ---------------------------------------------------------------------------
# Mesh utility actions
# ---------------------------------------------------------------------------
def test_center_pivot_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="center_pivot",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "center_pivot"
assert mock_unity["params"]["target"] == "MyCube"
def test_freeze_transform_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="freeze_transform",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "freeze_transform"
def test_validate_mesh_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="validate_mesh",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "validate_mesh"
def test_repair_mesh_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="repair_mesh",
target="MyCube",
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "repair_mesh"
# ---------------------------------------------------------------------------
# get_mesh_info include parameter passthrough
# ---------------------------------------------------------------------------
def test_get_mesh_info_include_param_passthrough(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="get_mesh_info",
target="MyCube",
properties={"include": "faces"},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "get_mesh_info"
assert mock_unity["params"]["properties"]["include"] == "faces"
# ---------------------------------------------------------------------------
# New actions: mesh editing additions
# ---------------------------------------------------------------------------
def test_duplicate_and_flip_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="duplicate_and_flip",
target="MyCube",
properties={"faceIndices": [0, 1]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "duplicate_and_flip"
assert mock_unity["params"]["properties"]["faceIndices"] == [0, 1]
def test_create_polygon_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="create_polygon",
target="MyCube",
properties={"vertexIndices": [0, 1, 2, 3], "unordered": True},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "create_polygon"
assert mock_unity["params"]["properties"]["vertexIndices"] == [0, 1, 2, 3]
# ---------------------------------------------------------------------------
# New actions: vertex operations
# ---------------------------------------------------------------------------
def test_weld_vertices_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="weld_vertices",
target="MyCube",
properties={"vertexIndices": [0, 1, 2], "radius": 0.05},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "weld_vertices"
assert mock_unity["params"]["properties"]["radius"] == 0.05
def test_insert_vertex_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="insert_vertex",
target="MyCube",
properties={"edge": {"a": 0, "b": 1}, "point": [0.5, 0, 0]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "insert_vertex"
assert mock_unity["params"]["properties"]["edge"] == {"a": 0, "b": 1}
def test_append_vertices_to_edge_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="append_vertices_to_edge",
target="MyCube",
properties={"edgeIndices": [0, 1], "count": 3},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "append_vertices_to_edge"
assert mock_unity["params"]["properties"]["count"] == 3
# ---------------------------------------------------------------------------
# New actions: selection
# ---------------------------------------------------------------------------
def test_select_faces_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="select_faces",
target="MyCube",
properties={"direction": "up", "tolerance": 0.9},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "select_faces"
assert mock_unity["params"]["properties"]["direction"] == "up"
assert mock_unity["params"]["properties"]["tolerance"] == 0.9
def test_selection_actions_in_all():
for action in SELECTION_ACTIONS:
assert action in ALL_ACTIONS, f"{action} should be in ALL_ACTIONS"
# ---------------------------------------------------------------------------
# New actions: utility
# ---------------------------------------------------------------------------
def test_set_pivot_sends_correct_params(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="set_pivot",
target="MyCube",
properties={"position": [1.5, 0, 2.3]},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "set_pivot"
assert mock_unity["params"]["properties"]["position"] == [1.5, 0, 2.3]
# ---------------------------------------------------------------------------
# Edge specification by vertex pairs
# ---------------------------------------------------------------------------
def test_bevel_edges_with_vertex_pairs(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="bevel_edges",
target="MyCube",
properties={"edges": [{"a": 0, "b": 1}, {"a": 2, "b": 3}], "amount": 0.15},
)
)
assert result["success"] is True
assert mock_unity["params"]["action"] == "bevel_edges"
assert mock_unity["params"]["properties"]["edges"] == [{"a": 0, "b": 1}, {"a": 2, "b": 3}]
def test_extrude_edges_with_vertex_pairs(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="extrude_edges",
target="MyCube",
properties={"edges": [{"a": 0, "b": 1}], "distance": 0.5},
)
)
assert result["success"] is True
assert mock_unity["params"]["properties"]["edges"] == [{"a": 0, "b": 1}]
# ---------------------------------------------------------------------------
# Detach faces with deleteSourceFaces
# ---------------------------------------------------------------------------
def test_detach_faces_with_delete_source(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="detach_faces",
target="MyCube",
properties={"faceIndices": [0], "deleteSourceFaces": True},
)
)
assert result["success"] is True
assert mock_unity["params"]["properties"]["deleteSourceFaces"] is True
# ---------------------------------------------------------------------------
# Bridge edges with allowNonManifold
# ---------------------------------------------------------------------------
def test_bridge_edges_with_allow_non_manifold(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="bridge_edges",
target="MyCube",
properties={
"edgeA": {"a": 0, "b": 1},
"edgeB": {"a": 2, "b": 3},
"allowNonManifold": True,
},
)
)
assert result["success"] is True
assert mock_unity["params"]["properties"]["allowNonManifold"] is True
# ---------------------------------------------------------------------------
# Merge vertices with collapseToFirst
# ---------------------------------------------------------------------------
def test_merge_vertices_with_collapse_to_first(mock_unity):
result = asyncio.run(
manage_probuilder(
SimpleNamespace(),
action="merge_vertices",
target="MyCube",
properties={"vertexIndices": [0, 1], "collapseToFirst": True},
)
)
assert result["success"] is True
assert mock_unity["params"]["properties"]["collapseToFirst"] is True