From ddf7c02d1ee5de76a1cddbf9f88dca1dfe572ccb Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:01:24 -0400 Subject: [PATCH 1/3] parol6: register plugin tools in every process registry; variant TCP honors rpy register_plugin_tools() runs at Controller init, motion_planner_main (spawn re-imports the registry), and Robot init (SelectToolCmd validates client-side before sending); _PLUGIN_KEYS keeps _build_tools native-only regardless of construction order. get_tool_transform's variant branch overrides origin/rpy field-independently to match the client path; _plugin_tool_transform uses waldoctl.resolve_variant_tcp and warns before identity on unknown keys. _ToolsCollection replaced by waldoctl.ToolsCollection. Cold-start home timeout 30s; variant-rpy unit regression. Co-Authored-By: Claude Opus 4.8 (1M context) --- parol6/robot.py | 86 +++++++++++++---------- parol6/server/controller.py | 5 ++ parol6/server/motion_planner.py | 6 ++ parol6/tools.py | 72 +++++++++++++++++-- tests/integration/conftest.py | 3 +- tests/integration/test_tool_operations.py | 5 +- tests/unit/test_tcp_offset.py | 37 ++++++++++ 7 files changed, 165 insertions(+), 49 deletions(-) diff --git a/parol6/robot.py b/parol6/robot.py index e0ebf36..26c87e8 100644 --- a/parol6/robot.py +++ b/parol6/robot.py @@ -41,9 +41,10 @@ PositionLimits, Robot as _RobotABC, ToolSpec, - ToolsSpec, + ToolsCollection, ToolStatus, ToolType, + resolve_variant_tcp, ) from parol6.client.async_client import AsyncRobotClient @@ -56,6 +57,8 @@ ElectricGripperConfig, PneumaticGripperConfig, get_registry, + plugin_tool_keys, + register_plugin_tools, ) from parol6.utils.ik import check_limits, solve_ik @@ -385,38 +388,6 @@ def channel_descriptors(self) -> tuple[ChannelDescriptor, ...]: ) -class _ToolsCollection(ToolsSpec): - """Concrete ToolsSpec for PAROL6.""" - - def __init__(self, tools: tuple[ToolSpec, ...]) -> None: - self._tools = tools - self._by_key = {t.key: t for t in tools} - - @property - def available(self) -> tuple[ToolSpec, ...]: - return self._tools - - @property - def default(self) -> ToolSpec: - return self._by_key.get("NONE", self._tools[0]) - - def __getitem__(self, key: str) -> ToolSpec: - return self._by_key[key] - - def __contains__(self, item: object) -> bool: - # ToolType is a StrEnum, so test it before the plain-str branch — - # otherwise a category like ToolType.GRIPPER would misroute to the - # by-key lookup (keyed by tool key, not category) and return False. - if isinstance(item, ToolType): - return any(t.tool_type == item for t in self._tools) - if isinstance(item, str): - return item in self._by_key - return False - - def by_type(self, tool_type: str | ToolType) -> tuple[ToolSpec, ...]: - return tuple(t for t in self._tools if t.tool_type == tool_type) - - # =========================================================================== # Helper builders # =========================================================================== @@ -462,10 +433,15 @@ def _decompose_transform( return origin, rpy -def _build_tools() -> _ToolsCollection: - """Build typed tool specs from the parol6 tool registry.""" +def _build_tools() -> ToolsCollection: + """Build typed tool specs from the parol6 tool registry. Plugin-registered + keys are excluded — they reach ``robot.tools`` through waldoctl composition + as their own ToolSpec classes, not as registry-derived natives.""" + plugin_keys = plugin_tool_keys() tools: list[ToolSpec] = [] for key, cfg in get_registry().items(): + if key in plugin_keys: + continue origin, rpy = _decompose_transform(cfg.transform) common = dict( key=key, @@ -493,7 +469,7 @@ def _build_tools() -> _ToolsCollection: else: tools.append(_ToolImpl(**common, tool_type=ToolType.NONE)) - return _ToolsCollection(tuple(tools)) + return ToolsCollection(tuple(tools), default_key="NONE") def _resolve_urdf_path() -> str: @@ -555,9 +531,12 @@ def __init__( self._timeout = timeout self._manager = _ServerManager(normalize_logs=normalize_logs) - # Build configuration eagerly + # Build configuration eagerly. Native tools snapshot first, then plugin + # tools join this process's registry so client-side paths (e.g. the + # SelectToolCmd wire validation) accept them. self._joints = _build_joints() self._tools = _build_tools() + register_plugin_tools() self._urdf_path = _resolve_urdf_path() self._mesh_dir = _resolve_mesh_dir() self._motion_profiles = tuple(p.value.upper() for p in ProfileType) @@ -596,7 +575,9 @@ def joints(self) -> JointsSpec: return self._joints @property - def tools(self) -> _ToolsCollection: + def _native_tools(self) -> ToolsCollection: + """PAROL6's built-in tools. The waldoctl ``Robot.tools`` property + composes these with any plugin tools registered via ``waldoctl.tools``.""" return self._tools @property @@ -689,7 +670,12 @@ def set_active_tool( """ from parol6.tools import get_tool_transform - T_tool = get_tool_transform(tool_key, variant_key=variant_key) + try: + T_tool = get_tool_transform(tool_key, variant_key=variant_key) + except ValueError: + # Plugin tool (waldoctl.tools) not in PAROL6's registry — derive its + # TCP from the ToolSpec instead. + T_tool = self._plugin_tool_transform(tool_key, variant_key) if tcp_offset_m is not None and any(v != 0 for v in tcp_offset_m): T_offset = np.eye(4) @@ -703,6 +689,28 @@ def set_active_tool( else: self._pinokin.clear_tool_transform() + def _plugin_tool_transform( + self, tool_key: str, variant_key: str | None + ) -> NDArray[np.float64]: + """Flange→TCP transform for a plugin tool, from its ToolSpec's + ``tcp_origin``/``tcp_rpy`` (variant overrides win). Identity (with a + warning) if the key is unknown to both registries.""" + try: + spec = self.tools[tool_key] + except KeyError: + logger.warning( + "Unknown tool %r; using identity TCP. Available: %s", + tool_key, + [t.key for t in self.tools.available], + ) + return np.eye(4) + origin, rpy = resolve_variant_tcp( + spec.tcp_origin, spec.tcp_rpy, spec.variants, variant_key + ) + T = np.zeros((4, 4), dtype=np.float64) + se3_from_rpy(origin[0], origin[1], origin[2], rpy[0], rpy[1], rpy[2], T) + return T + def fk( self, q_rad: NDArray[np.float64], out: NDArray[np.float64] ) -> NDArray[np.float64]: diff --git a/parol6/server/controller.py b/parol6/server/controller.py index 44d6a5d..6910667 100644 --- a/parol6/server/controller.py +++ b/parol6/server/controller.py @@ -111,6 +111,11 @@ def __init__(self, config: ControllerConfig): self.shutdown_event = threading.Event() self._initialized = False + # Register plugin tools (waldoctl.tools) before any SELECT_TOOL. + from parol6.tools import register_plugin_tools + + register_plugin_tools() + # Core components self.state_manager = StateManager() self.udp_transport: UDPTransport | None = None diff --git a/parol6/server/motion_planner.py b/parol6/server/motion_planner.py index 82fe06f..4261751 100644 --- a/parol6/server/motion_planner.py +++ b/parol6/server/motion_planner.py @@ -527,9 +527,15 @@ def motion_planner_main( """Worker process main loop — compute trajectories and forward inline commands.""" signal.signal(signal.SIGINT, signal.SIG_IGN) from parol6.server import set_pdeathsig + from parol6.tools import register_plugin_tools set_pdeathsig() + # Spawn-mode subprocess: the registry is freshly imported with only native + # tools, so plugin tools must be registered here too or SELECT_TOOL/SyncTool + # of a plugin tool fails in apply_tool. + register_plugin_tools() + worker = PlannerWorker(segment_queue) logger.debug( diff --git a/parol6/tools.py b/parol6/tools.py index c2f890d..42f0b2f 100644 --- a/parol6/tools.py +++ b/parol6/tools.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable import numpy as np -from pinokin import se3_from_trans +from pinokin import se3_from_rpy, se3_from_trans from waldoctl import ( CameraSpec, LinearMotion, @@ -27,7 +27,7 @@ ) if TYPE_CHECKING: - from waldoctl import ToolStatus + from waldoctl import ToolSpec, ToolStatus from parol6.commands.base import MotionCommand from parol6.commands.gripper_commands import ( @@ -385,15 +385,66 @@ def list_tools() -> list[str]: return list(_TOOL_REGISTRY.keys()) +def _tool_config_from_spec(spec: "ToolSpec") -> ToolConfig: + """Build a minimal controller :class:`ToolConfig` from a waldoctl ToolSpec + (TCP transform only; base no-op status/command suit non-actuated tools).""" + transform = np.zeros((4, 4), dtype=np.float64) + ox, oy, oz = spec.tcp_origin + rx, ry, rz = spec.tcp_rpy + se3_from_rpy(ox, oy, oz, rx, ry, rz, transform) + return ToolConfig( + name=spec.display_name, + description=spec.description, + transform=transform, + meshes=spec.meshes, + motions=spec.motions, + variants=spec.variants, + camera_spec=spec.camera_spec, + ) + + +_PLUGIN_KEYS: set[str] = set() + + +def plugin_tool_keys() -> frozenset[str]: + """Keys added by :func:`register_plugin_tools` (vs. native registrations).""" + return frozenset(_PLUGIN_KEYS) + + +def register_plugin_tools() -> int: + """Register ``waldoctl.tools`` entry-point tools into the controller registry + so ``SELECT_TOOL`` resolves their TCP. Idempotent; no-op when none installed; + on collision the first registration wins (natives register at import time). + Returns the number added.""" + from waldoctl.discovery import iter_plugin_tool_specs + + count = 0 + for spec in iter_plugin_tool_specs(): + if spec.key != spec.key.upper(): + logger.warning( + "Plugin tool key %r is not uppercase; SELECT_TOOL uppercases " + "names on the wire, so this tool cannot be selected", + spec.key, + ) + if spec.key in _TOOL_REGISTRY: + continue + _TOOL_REGISTRY[spec.key] = _tool_config_from_spec(spec) + _PLUGIN_KEYS.add(spec.key) + count += 1 + if count: + logger.info("Registered %d plugin tool(s) into the controller registry", count) + return count + + def get_tool_transform( tool_name: str, variant_key: str | None = None, ) -> np.ndarray: """Get the 4x4 transformation matrix for a tool or variant. - When *variant_key* is given and the matching variant has a - ``tcp_origin``, returns a transform built from the variant's TCP - instead of the tool-level transform. + When *variant_key* is given and matches, the variant's ``tcp_origin`` / + ``tcp_rpy`` override the tool-level transform field-independently + (matching the client-side ToolSpec semantics). Raises ValueError if *tool_name* is not registered. """ @@ -402,8 +453,15 @@ def get_tool_transform( raise ValueError(f"Unknown tool '{tool_name}'. Available: {list_tools()}") if variant_key: for v in cfg.variants: - if v.key == variant_key and v.tcp_origin is not None: - return _make_tcp_transform(*v.tcp_origin) + if v.key == variant_key: + out = cfg.transform.copy() + if v.tcp_rpy is not None: + rot = np.zeros((4, 4), dtype=np.float64) + se3_from_rpy(0.0, 0.0, 0.0, *v.tcp_rpy, rot) + out[:3, :3] = rot[:3, :3] + if v.tcp_origin is not None: + out[:3, 3] = v.tcp_origin + return out logger.warning("Variant '%s' not found for tool '%s'", variant_key, tool_name) return cfg.transform diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ef9003c..345c240 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -16,5 +16,6 @@ def clean_state(server_proc, client): client.select_profile("LINEAR") idx = client.home() assert idx >= 0, "Home command failed to send" - assert client.wait_command(idx, timeout=5.0), "Home did not complete" + # Generous timeout: the first home cold-JITs the numba motion pipeline. + assert client.wait_command(idx, timeout=30.0), "Home did not complete" return client diff --git a/tests/integration/test_tool_operations.py b/tests/integration/test_tool_operations.py index f9b9f9e..c3527de 100644 --- a/tests/integration/test_tool_operations.py +++ b/tests/integration/test_tool_operations.py @@ -217,9 +217,10 @@ def test_registry_completeness(self): robot = Robot() tools = robot.tools - # 5 tools registered + # Native count stays 5; robot.tools may compose plugin tools on top. + native_keys = [t.key for t in robot._native_tools.available] + assert len(native_keys) == 5 keys = [t.key for t in tools.available] - assert len(keys) == 5 for expected in ("NONE", "PNEUMATIC", "SSG-48", "MSG", "VACUUM"): assert expected in keys, f"{expected} not in {keys}" diff --git a/tests/unit/test_tcp_offset.py b/tests/unit/test_tcp_offset.py index 950ca1a..ff290fa 100644 --- a/tests/unit/test_tcp_offset.py +++ b/tests/unit/test_tcp_offset.py @@ -98,6 +98,43 @@ def test_dry_run_select_tool_resets_tcp_offset(): assert client.tcp_offset() == [0.0, 0.0, 0.0] +# ── Variant TCP resolution ─────────────────────────────────────────────── + + +def test_get_tool_transform_variant_honors_rpy(): + """Variant tcp_origin/tcp_rpy override the tool transform field-independently + (matching the client-side ToolSpec semantics).""" + from waldoctl import ToolVariant + + from parol6.tools import _TOOL_REGISTRY, ToolConfig, get_tool_transform + + cfg = ToolConfig( + name="Test", + description="", + transform=np.eye(4), + variants=( + ToolVariant( + key="angled", + display_name="Angled", + tcp_origin=(0.012, 0.0, 0.09), + tcp_rpy=(0.0, 0.26, 0.0), + ), + ToolVariant(key="rpy_only", display_name="RPY", tcp_rpy=(0.0, 0.26, 0.0)), + ), + ) + _TOOL_REGISTRY["_TEST_VARIANT_TOOL"] = cfg + try: + T = get_tool_transform("_TEST_VARIANT_TOOL", variant_key="angled") + np.testing.assert_allclose(T[:3, 3], (0.012, 0.0, 0.09), atol=1e-12) + assert np.isclose(T[0, 0], np.cos(0.26), atol=1e-9) + + T = get_tool_transform("_TEST_VARIANT_TOOL", variant_key="rpy_only") + np.testing.assert_allclose(T[:3, 3], (0.0, 0.0, 0.0), atol=1e-12) + assert np.isclose(T[0, 0], np.cos(0.26), atol=1e-9) + finally: + del _TOOL_REGISTRY["_TEST_VARIANT_TOOL"] + + # ── Planner routing (regression: SetTcpOffsetCmd must reach the planner) ── From a1d5407d50d65b436b0dd7112538abed84fa48a0 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:03:38 -0400 Subject: [PATCH 2/3] parol6: drop tautological tcp-offset struct tests, add CLAUDE.md rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the three "wire round-trip" tests in test_tcp_offset.py that only construct SetTcpOffsetCmd/TcpOffsetResultStruct and assert the fields back — they restate the struct definitions, exercise no encode/decode, catch no bug. The planner-routing and apply_tool behavioral tests stay. Add the no-tautological-tests rule to CLAUDE.md Testing Guidelines. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 1 + tests/unit/test_tcp_offset.py | 26 +------------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 815e6c3..dfc312a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,6 +144,7 @@ For streamable commands (`streamable = True`), `do_setup()` also runs at high fr **When CI tests fail, fix them.** Don't waste time analyzing whether failures are "related to your changes" — just fix all failing tests. The goal is a green CI, not attribution. Prefer fewer, comprehensive integration tests that mimic manual testing over a large number of unit tests. We have no code coverage requirements—the goal is working features, not metrics. +- **No tautological tests.** Don't assert what's true by construction — e.g. a freshly built object's default field values. Test behavior, not the class's initializers. - **Test results are in `test-results.xml`.** Pytest writes JUnit XML to `test-results.xml` automatically. When diagnosing failures, read this file — it contains test names, durations, failure messages, and captured output. This is more reliable than parsing console output. - **NEVER run parol6 and web commander test suites in parallel** — no proper isolation, they share resources and have timing issues when resource-constrained. Always run sequentially. - **NEVER allow subagents to run tests.** Many tests are timing-sensitive and the system doesn't have enough resources for agents and tests to run simultaneously. Only the main conversation should run tests, and only after all agents have completed. diff --git a/tests/unit/test_tcp_offset.py b/tests/unit/test_tcp_offset.py index ff290fa..239f29f 100644 --- a/tests/unit/test_tcp_offset.py +++ b/tests/unit/test_tcp_offset.py @@ -2,31 +2,7 @@ import numpy as np -from parol6.protocol.wire import SelectToolCmd, SetTcpOffsetCmd, TcpOffsetResultStruct - - -# ── Wire round-trip ────────────────────────────────────────────────────── - - -def test_set_tcp_offset_cmd_fields(): - cmd = SetTcpOffsetCmd(x=1.0, y=2.0, z=-190.0) - assert cmd.x == 1.0 - assert cmd.y == 2.0 - assert cmd.z == -190.0 - - -def test_set_tcp_offset_cmd_defaults(): - cmd = SetTcpOffsetCmd() - assert cmd.x == 0.0 - assert cmd.y == 0.0 - assert cmd.z == 0.0 - - -def test_tcp_offset_result_struct(): - result = TcpOffsetResultStruct(x=10.0, y=20.0, z=-190.0) - assert result.x == 10.0 - assert result.y == 20.0 - assert result.z == -190.0 +from parol6.protocol.wire import SelectToolCmd, SetTcpOffsetCmd # ── apply_tool with tcp_offset_m ───────────────────────────────────────── From 3bde58f77d70e774164d5ad00e1df51b58dc4424 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:40:13 -0400 Subject: [PATCH 3/3] parol6: purge remaining tautological tests (round 2) Remove construct-echo/default tests in test_movecart_command.py (5), test_reset_command.py (2, dropping the now-empty TestResetCommandParsing), test_query_commands_actions.py (2), and the hardware-config assertions in test_gripper_ramp.py (2). The validation tests (bad-input raising), execute_step side-effect tests, pose-conversion tests, and tick-range derivation stay. Broaden the CLAUDE.md rule to name the categories. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- tests/unit/test_gripper_ramp.py | 14 ----- tests/unit/test_movecart_command.py | 73 ----------------------- tests/unit/test_query_commands_actions.py | 19 ------ tests/unit/test_reset_command.py | 17 ------ 5 files changed, 1 insertion(+), 124 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dfc312a..6eba9e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,7 +144,7 @@ For streamable commands (`streamable = True`), `do_setup()` also runs at high fr **When CI tests fail, fix them.** Don't waste time analyzing whether failures are "related to your changes" — just fix all failing tests. The goal is a green CI, not attribution. Prefer fewer, comprehensive integration tests that mimic manual testing over a large number of unit tests. We have no code coverage requirements—the goal is working features, not metrics. -- **No tautological tests.** Don't assert what's true by construction — e.g. a freshly built object's default field values. Test behavior, not the class's initializers. +- **No tautological tests.** Assert behavior, not what's true by construction — not default fields, constructor args echoed back, enum literals, `isinstance`/frozen-raises, or stub-raises-`NotImplementedError`. Drive a method/workflow and assert the outcome. - **Test results are in `test-results.xml`.** Pytest writes JUnit XML to `test-results.xml` automatically. When diagnosing failures, read this file — it contains test names, durations, failure messages, and captured output. This is more reliable than parsing console output. - **NEVER run parol6 and web commander test suites in parallel** — no proper isolation, they share resources and have timing issues when resource-constrained. Always run sequentially. - **NEVER allow subagents to run tests.** Many tests are timing-sensitive and the system doesn't have enough resources for agents and tests to run simultaneously. Only the main conversation should run tests, and only after all agents have completed. diff --git a/tests/unit/test_gripper_ramp.py b/tests/unit/test_gripper_ramp.py index 86444a5..65f064b 100644 --- a/tests/unit/test_gripper_ramp.py +++ b/tests/unit/test_gripper_ramp.py @@ -163,20 +163,6 @@ def test_ssg48_full_travel_time(self): class TestElectricGripperConfigSpecs: """Verify physical specs on tool configs match expected values.""" - def test_ssg48_config(self): - cfg = get_registry()["SSG-48"] - assert isinstance(cfg, ElectricGripperConfig) - assert cfg.encoder_cpr == 16_384 - assert cfg.gear_pd_mm == 12.0 - assert cfg.firmware_speed_range_tps == (40, 80_000) - - def test_msg_config(self): - cfg = get_registry()["MSG"] - assert isinstance(cfg, ElectricGripperConfig) - assert cfg.encoder_cpr == 16_384 - assert cfg.gear_pd_mm == pytest.approx(16.67, abs=0.01) - assert cfg.firmware_speed_range_tps == (500, 60_000) - def test_ssg48_tick_range_derivation(self): """Tick range derived from gear PD + travel_m should match expected ~10,432.""" cfg = get_registry()["SSG-48"] diff --git a/tests/unit/test_movecart_command.py b/tests/unit/test_movecart_command.py index a41f775..f714b96 100644 --- a/tests/unit/test_movecart_command.py +++ b/tests/unit/test_movecart_command.py @@ -7,74 +7,12 @@ import msgspec import pytest -from parol6.commands.cartesian_commands import MoveLCommand from parol6.protocol.wire import MoveLCmd class TestMoveLCommandParsing: """Test MoveLCmd struct parsing and validation.""" - def test_parse_with_speed(self): - """Parse with explicit speed.""" - # Create params struct - params = MoveLCmd( - pose=[100.0, 200.0, 300.0, 0.0, 0.0, 0.0], - speed=0.5, - accel=0.75, - ) - - cmd = MoveLCommand(params) - - assert cmd.p.pose == [100.0, 200.0, 300.0, 0.0, 0.0, 0.0] - assert cmd.p.duration == 0.0 # default - assert cmd.p.speed == 0.5 - assert cmd.p.accel == 0.75 - - def test_parse_accel_default(self): - """Default acceleration should be 1.0.""" - params = MoveLCmd( - pose=[100.0, 200.0, 300.0, 0.0, 0.0, 0.0], - speed=0.5, - ) - - cmd = MoveLCommand(params) - - assert cmd.p.accel == 1.0 # default - - def test_parse_with_duration(self): - """Parse with duration instead of speed.""" - params = MoveLCmd( - pose=[100.0, 200.0, 300.0, 0.0, 0.0, 0.0], - duration=2.5, - accel=0.8, - ) - - cmd = MoveLCommand(params) - - assert cmd.p.duration == 2.5 - assert cmd.p.speed == 0.0 # default - assert cmd.p.accel == 0.8 - - def test_parse_full_accel_range(self): - """Test acceleration values at boundaries.""" - # Min accel (must be > 0) - params1 = MoveLCmd( - pose=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - speed=0.5, - accel=0.001, - ) - cmd1 = MoveLCommand(params1) - assert cmd1.p.accel == 0.001 - - # Max accel - params2 = MoveLCmd( - pose=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - speed=0.5, - accel=1.0, - ) - cmd2 = MoveLCommand(params2) - assert cmd2.p.accel == 1.0 - def test_validation_requires_duration_or_speed(self): """Must have either duration > 0 or speed > 0.""" with pytest.raises((ValueError, msgspec.ValidationError)): @@ -105,14 +43,3 @@ def test_validation_pose_length(self): with pytest.raises(msgspec.ValidationError): decode_command(raw) - - def test_command_init(self): - """Test that MoveLCommand initializes correctly.""" - params = MoveLCmd( - pose=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - speed=0.5, - ) - cmd = MoveLCommand(params) - - assert cmd.p is params - assert not cmd.is_finished diff --git a/tests/unit/test_query_commands_actions.py b/tests/unit/test_query_commands_actions.py index d5ef7b8..56075e8 100644 --- a/tests/unit/test_query_commands_actions.py +++ b/tests/unit/test_query_commands_actions.py @@ -14,7 +14,6 @@ CurrentActionResultStruct, ActivityCmd, QueueCmd, - QueryType, QueueResultStruct, ResponseMsg, decode_message, @@ -28,15 +27,6 @@ def _unpack_response(data: bytes): return msg.result -def test_activity_command_init(): - """Test that ACTIVITY command initializes correctly.""" - cmd = ActivityCommand(ActivityCmd()) - - assert not cmd.is_finished - assert cmd.PARAMS_TYPE is not None - assert cmd.QUERY_TYPE == QueryType.CURRENT_ACTION - - def test_activity_returns_details(): """Test that ACTIVITY compute() returns correct data.""" state = SimpleNamespace( @@ -77,15 +67,6 @@ def test_activity_with_idle_state(): assert result.params == "" -def test_queue_command_init(): - """Test that QUEUE command initializes correctly.""" - cmd = QueueCommand(QueueCmd()) - - assert not cmd.is_finished - assert cmd.PARAMS_TYPE is not None - assert cmd.QUERY_TYPE == QueryType.QUEUE - - def test_queue_returns_details(): """Test that QUEUE compute() returns correct data.""" state = SimpleNamespace( diff --git a/tests/unit/test_reset_command.py b/tests/unit/test_reset_command.py index 4c6fc34..b631369 100644 --- a/tests/unit/test_reset_command.py +++ b/tests/unit/test_reset_command.py @@ -8,23 +8,6 @@ from parol6.server.state import ControllerState -class TestResetCommandParsing: - """Test ResetCommand initialization.""" - - def test_init(self): - """RESET takes no parameters.""" - cmd = ResetCommand(ResetCmd()) - - assert not cmd.is_finished - assert cmd.p is not None - - def test_struct_has_no_params(self): - """ResetCmd struct should have no fields.""" - params = ResetCmd() - # Struct should be valid with no fields - assert params is not None - - class TestResetCommandExecution: """Test ResetCommand.tick resets state correctly."""