fix: add pin-snapping and coordinate feedback to net label tools

add_schematic_net_label now accepts optional componentRef + pinNumber to
snap the label directly to the exact pin endpoint via PinLocator, removing
all approximation risk.  The response always includes actual_position and,
when snapping was used, snapped_to_pin — so the caller gets confirmation
of exactly where the label landed.

connect_to_net return type changed from bool to Dict, returning
pin_location, label_location, and wire_stub on success so agents no
longer need a separate verification call to confirm placement.

connect_passthrough updated to check result.get("success") against the
new dict return.  tool_schemas.py and schematic.ts updated to match
(position is now optional, componentRef/pinNumber/labelType/orientation
added, connect_to_net schema field names corrected).

17 new unit tests in tests/test_net_label_pin_snapping.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-04-12 14:12:11 +01:00
parent 0bf73674da
commit 94125eda7f
5 changed files with 481 additions and 55 deletions

View File

@@ -59,9 +59,9 @@ class ConnectionManager:
@staticmethod @staticmethod
def connect_to_net( def connect_to_net(
schematic_path: Path, component_ref: str, pin_name: str, net_name: str schematic_path: Path, component_ref: str, pin_name: str, net_name: str
) -> bool: ) -> Dict[str, Any]:
""" """
Connect a component pin to a named net using a wire stub and label Connect a component pin to a named net using a wire stub and label.
Args: Args:
schematic_path: Path to .kicad_sch file schematic_path: Path to .kicad_sch file
@@ -70,27 +70,32 @@ class ConnectionManager:
net_name: Name of the net to connect to (e.g., "VCC", "GND", "SIGNAL_1") net_name: Name of the net to connect to (e.g., "VCC", "GND", "SIGNAL_1")
Returns: Returns:
True if successful, False otherwise Dict with keys:
success bool
pin_location [x, y] exact pin endpoint used (present on success)
label_location [x, y] where the net label was placed (present on success)
wire_stub [[x1,y1],[x2,y2]] the wire segment added (present on success)
message human-readable status
""" """
try: try:
if not WIRE_MANAGER_AVAILABLE: if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager/PinLocator not available") logger.error("WireManager/PinLocator not available")
return False return {"success": False, "message": "WireManager/PinLocator not available"}
locator = ConnectionManager.get_pin_locator() locator = ConnectionManager.get_pin_locator()
if not locator: if not locator:
logger.error("Pin locator unavailable") logger.error("Pin locator unavailable")
return False return {"success": False, "message": "Pin locator unavailable"}
# Get pin location using PinLocator # Get pin location using PinLocator
pin_loc = locator.get_pin_location(schematic_path, component_ref, pin_name) pin_loc = locator.get_pin_location(schematic_path, component_ref, pin_name)
if not pin_loc: if not pin_loc:
logger.error(f"Could not locate pin {component_ref}/{pin_name}") msg = f"Could not locate pin {component_ref}/{pin_name}"
return False logger.error(msg)
return {"success": False, "message": msg}
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing) # Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
# Stub direction follows the pin's outward angle from the PinLocator # Stub direction follows the pin's outward angle from the PinLocator
pin_angle_deg = getattr(locator, "_last_pin_angle", 0)
try: try:
pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0 pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
except Exception: except Exception:
@@ -106,26 +111,34 @@ class ConnectionManager:
# Create wire stub using WireManager # Create wire stub using WireManager
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end) wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
if not wire_success: if not wire_success:
logger.error(f"Failed to create wire stub for net connection") msg = "Failed to create wire stub for net connection"
return False logger.error(msg)
return {"success": False, "message": msg}
# Add label at the end of the stub using WireManager # Add label at the end of the stub using WireManager
label_success = WireManager.add_label( label_success = WireManager.add_label(
schematic_path, net_name, stub_end, label_type="label" schematic_path, net_name, stub_end, label_type="label"
) )
if not label_success: if not label_success:
logger.error(f"Failed to add net label '{net_name}'") msg = f"Failed to add net label '{net_name}'"
return False logger.error(msg)
return {"success": False, "message": msg}
logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'") logger.info(f"Connected {component_ref}/{pin_name} to net '{net_name}'")
return True return {
"success": True,
"message": f"Connected {component_ref}/{pin_name} to net '{net_name}'",
"pin_location": pin_loc,
"label_location": stub_end,
"wire_stub": [pin_loc, stub_end],
}
except Exception as e: except Exception as e:
logger.error(f"Error connecting to net: {e}") logger.error(f"Error connecting to net: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return {"success": False, "message": str(e)}
@staticmethod @staticmethod
def connect_passthrough( def connect_passthrough(
@@ -177,18 +190,18 @@ class ConnectionManager:
else f"{net_prefix}_{pin_num}" else f"{net_prefix}_{pin_num}"
) )
ok_src = ConnectionManager.connect_to_net( res_src = ConnectionManager.connect_to_net(
schematic_path, source_ref, pin_num, net_name schematic_path, source_ref, pin_num, net_name
) )
if not ok_src: if not res_src.get("success"):
failed.append(f"{source_ref}/{pin_num}") failed.append(f"{source_ref}/{pin_num}")
continue continue
if pin_num in tgt_pins: if pin_num in tgt_pins:
ok_tgt = ConnectionManager.connect_to_net( res_tgt = ConnectionManager.connect_to_net(
schematic_path, target_ref, pin_num, net_name schematic_path, target_ref, pin_num, net_name
) )
if not ok_tgt: if not res_tgt.get("success"):
failed.append(f"{target_ref}/{pin_num}") failed.append(f"{target_ref}/{pin_num}")
continue continue
else: else:

View File

@@ -1514,9 +1514,16 @@ class KiCADInterface:
return {"success": False, "message": str(e)} return {"success": False, "message": str(e)}
def _handle_add_schematic_net_label(self, params: Dict[str, Any]) -> Dict[str, Any]: def _handle_add_schematic_net_label(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a net label to schematic using WireManager""" """Add a net label to schematic using WireManager.
When componentRef and pinNumber are supplied the label is placed at the
exact pin endpoint retrieved via PinLocator, ignoring the provided
position. The response includes the actual coordinates used and
whether the label landed on a pin endpoint.
"""
logger.info("Adding net label to schematic") logger.info("Adding net label to schematic")
try: try:
import traceback
from pathlib import Path from pathlib import Path
from commands.wire_manager import WireManager from commands.wire_manager import WireManager
@@ -1524,13 +1531,48 @@ class KiCADInterface:
schematic_path = params.get("schematicPath") schematic_path = params.get("schematicPath")
net_name = params.get("netName") net_name = params.get("netName")
position = params.get("position") position = params.get("position")
label_type = params.get( label_type = params.get("labelType", "label")
"labelType", "label" orientation = params.get("orientation", 0)
) # 'label', 'global_label', 'hierarchical_label' component_ref = params.get("componentRef")
orientation = params.get("orientation", 0) # 0, 90, 180, 270 pin_number = params.get("pinNumber")
if not all([schematic_path, net_name, position]): if not all([schematic_path, net_name]):
return {"success": False, "message": "Missing required parameters"} return {
"success": False,
"message": "Missing required parameters: schematicPath, netName",
}
snapped_to_pin: Optional[Dict[str, Any]] = None
if component_ref and pin_number:
# Snap position to exact pin endpoint using PinLocator
from commands.pin_locator import PinLocator
locator = PinLocator()
pin_loc = locator.get_pin_location(
Path(schematic_path), component_ref, str(pin_number)
)
if pin_loc is None:
return {
"success": False,
"message": (
f"Could not locate pin {pin_number} on {component_ref}. "
"Check the reference and pin number."
),
}
position = pin_loc
snapped_to_pin = {"component": component_ref, "pin": str(pin_number)}
logger.info(
f"Snapped label '{net_name}' to pin {component_ref}/{pin_number} at {position}"
)
elif position is None:
return {
"success": False,
"message": (
"Missing position. Either provide position [x, y] or "
"componentRef + pinNumber to snap to a pin endpoint."
),
}
# Use WireManager for S-expression manipulation # Use WireManager for S-expression manipulation
success = WireManager.add_label( success = WireManager.add_label(
@@ -1541,13 +1583,22 @@ class KiCADInterface:
orientation=orientation, orientation=orientation,
) )
if success: if not success:
return {
"success": True,
"message": f"Added net label '{net_name}' at {position}",
}
else:
return {"success": False, "message": "Failed to add net label"} return {"success": False, "message": "Failed to add net label"}
response: Dict[str, Any] = {
"success": True,
"message": f"Added net label '{net_name}' at {position}",
"actual_position": position,
}
if snapped_to_pin:
response["snapped_to_pin"] = snapped_to_pin
response["message"] = (
f"Added net label '{net_name}' at exact pin endpoint "
f"{component_ref}/{pin_number} ({position[0]}, {position[1]})"
)
return response
except Exception as e: except Exception as e:
logger.error(f"Error adding net label: {str(e)}") logger.error(f"Error adding net label: {str(e)}")
import traceback import traceback
@@ -1574,17 +1625,10 @@ class KiCADInterface:
return {"success": False, "message": "Missing required parameters"} return {"success": False, "message": "Missing required parameters"}
# Use ConnectionManager with new WireManager integration # Use ConnectionManager with new WireManager integration
success = ConnectionManager.connect_to_net( result = ConnectionManager.connect_to_net(
Path(schematic_path), component_ref, pin_name, net_name Path(schematic_path), component_ref, pin_name, net_name
) )
return result
if success:
return {
"success": True,
"message": f"Connected {component_ref}/{pin_name} to net '{net_name}'",
}
else:
return {"success": False, "message": "Failed to connect to net"}
except Exception as e: except Exception as e:
logger.error(f"Error connecting to net: {str(e)}") logger.error(f"Error connecting to net: {str(e)}")
import traceback import traceback

View File

@@ -1399,7 +1399,15 @@ SCHEMATIC_TOOLS = [
{ {
"name": "add_schematic_net_label", "name": "add_schematic_net_label",
"title": "Add Net Label", "title": "Add Net Label",
"description": "Adds a net label at exact coordinates on a schematic wire or pin endpoint. WARNING: x/y must match an existing wire endpoint or pin endpoint exactly — placing the label even 0.01mm away from a pin will result in an unconnected pin ERC error. To connect a component pin to a net by reference and pin number (recommended), use connect_to_net instead.", "description": (
"Add a net label to a schematic. "
"PREFERRED: supply componentRef + pinNumber to snap the label to the exact pin endpoint — "
"this guarantees an electrical connection. "
"Alternatively supply position [x, y], but the coordinates must match the pin endpoint exactly "
"(even a 0.01 mm offset breaks the connection). "
"The response includes actual_position (coordinates actually used) and snapped_to_pin "
"(present when a pin reference was resolved)."
),
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1411,21 +1419,45 @@ SCHEMATIC_TOOLS = [
"type": "string", "type": "string",
"description": "Name of the net (e.g., VCC, GND, SDA)", "description": "Name of the net (e.g., VCC, GND, SDA)",
}, },
"x": {"type": "number", "description": "X coordinate on schematic"}, "position": {
"y": {"type": "number", "description": "Y coordinate on schematic"}, "type": "array",
"rotation": { "items": {"type": "number"},
"minItems": 2,
"maxItems": 2,
"description": "Position [x, y] for the label. Required when componentRef/pinNumber are not given.",
},
"componentRef": {
"type": "string",
"description": "Component reference to snap label to (e.g. U1, R1). Use with pinNumber.",
},
"pinNumber": {
"type": "string",
"description": "Pin number or name on componentRef (e.g. '1', 'GND'). Use with componentRef.",
},
"labelType": {
"type": "string",
"enum": ["label", "global_label", "hierarchical_label"],
"description": "Label type (default: label)",
"default": "label",
},
"orientation": {
"type": "number", "type": "number",
"description": "Rotation angle in degrees (0, 90, 180, 270)", "description": "Rotation angle in degrees (0, 90, 180, 270)",
"default": 0, "default": 0,
}, },
}, },
"required": ["schematicPath", "netName", "x", "y"], "required": ["schematicPath", "netName"],
}, },
}, },
{ {
"name": "connect_to_net", "name": "connect_to_net",
"title": "Connect Pin to Net", "title": "Connect Pin to Net",
"description": "Intelligently connects a component pin to a named net, automatically routing wires as needed.", "description": (
"Connect a component pin to a named net by adding a wire stub and net label at the exact "
"pin endpoint. The response includes pin_location (exact pin coords), label_location "
"(where the label was placed), and wire_stub (the wire segment added) so you can confirm "
"the placement without a separate verification call."
),
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1433,11 +1465,11 @@ SCHEMATIC_TOOLS = [
"type": "string", "type": "string",
"description": "Path to schematic file", "description": "Path to schematic file",
}, },
"reference": { "componentRef": {
"type": "string", "type": "string",
"description": "Component reference designator (e.g., R1, U3)", "description": "Component reference designator (e.g., R1, U3)",
}, },
"pinNumber": { "pinName": {
"type": "string", "type": "string",
"description": "Pin number or name on the component", "description": "Pin number or name on the component",
}, },
@@ -1446,7 +1478,7 @@ SCHEMATIC_TOOLS = [
"description": "Name of the net to connect to", "description": "Name of the net to connect to",
}, },
}, },
"required": ["schematicPath", "reference", "pinNumber", "netName"], "required": ["schematicPath", "componentRef", "pinName", "netName"],
}, },
}, },
{ {

View File

@@ -325,20 +325,55 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
// Add net label // Add net label
server.tool( server.tool(
"add_schematic_net_label", "add_schematic_net_label",
"Add a net label to the schematic", "Add a net label to the schematic. " +
"PREFERRED: supply componentRef + pinNumber to snap the label to the exact pin endpoint — " +
"this guarantees an electrical connection. " +
"Alternatively supply position [x, y], but the coordinates must match the pin endpoint exactly " +
"(even a 0.01 mm offset breaks the connection). " +
"The response includes actual_position (coordinates actually used) and snapped_to_pin " +
"(present when a pin reference was resolved).",
{ {
schematicPath: z.string().describe("Path to the schematic file"), schematicPath: z.string().describe("Path to the schematic file"),
netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"), netName: z.string().describe("Name of the net (e.g., VCC, GND, SIGNAL_1)"),
position: z.array(z.number()).length(2).describe("Position [x, y] for the label"), position: z
.array(z.number())
.length(2)
.optional()
.describe(
"Position [x, y] for the label. Required when componentRef/pinNumber are not given.",
),
componentRef: z
.string()
.optional()
.describe("Component reference to snap label to (e.g. U1, R1). Use with pinNumber."),
pinNumber: z
.union([z.string(), z.number()])
.optional()
.describe(
"Pin number or name on componentRef to snap label to (e.g. '1', 'GND'). Use with componentRef.",
),
labelType: z
.enum(["label", "global_label", "hierarchical_label"])
.optional()
.describe("Label type (default: label)"),
orientation: z.number().optional().describe("Rotation angle 0/90/180/270 (default: 0)"),
}, },
async (args: { schematicPath: string; netName: string; position: number[] }) => { async (args: {
schematicPath: string;
netName: string;
position?: number[];
componentRef?: string;
pinNumber?: string | number;
labelType?: string;
orientation?: number;
}) => {
const result = await callKicadScript("add_schematic_net_label", args); const result = await callKicadScript("add_schematic_net_label", args);
if (result.success) { if (result.success) {
return { return {
content: [ content: [
{ {
type: "text", type: "text",
text: `Successfully added net label '${args.netName}' at position [${args.position}]`, text: JSON.stringify(result, null, 2),
}, },
], ],
}; };
@@ -358,7 +393,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
// Connect pin to net // Connect pin to net
server.tool( server.tool(
"connect_to_net", "connect_to_net",
"Connect a component pin to a named net", "Connect a component pin to a named net by adding a wire stub and net label at the exact pin endpoint. " +
"The response includes pin_location (exact pin coords), label_location (where the label was placed), " +
"and wire_stub (the wire segment added) so you can confirm the placement.",
{ {
schematicPath: z.string().describe("Path to the schematic file"), schematicPath: z.string().describe("Path to the schematic file"),
componentRef: z.string().describe("Component reference (e.g., U1, R1)"), componentRef: z.string().describe("Component reference (e.g., U1, R1)"),
@@ -377,7 +414,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
content: [ content: [
{ {
type: "text", type: "text",
text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`, text: JSON.stringify(result, null, 2),
}, },
], ],
}; };

View File

@@ -0,0 +1,300 @@
"""
Tests for net label pin-snapping and connect_to_net richer response.
Covers:
- add_schematic_net_label with componentRef+pinNumber snaps to exact pin coords
- add_schematic_net_label without position and without pin ref returns error
- add_schematic_net_label with unknown pin returns an informative error
- connect_to_net returns pin_location, label_location, wire_stub on success
- connect_to_net returns success=False with message on failure
- connect_passthrough uses new dict return from connect_to_net correctly
- tool_schemas.py reflects new optional fields
"""
import sys
import types
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Path setup mirror existing test files
# ---------------------------------------------------------------------------
PYTHON_DIR = Path(__file__).parent.parent / "python"
sys.path.insert(0, str(PYTHON_DIR))
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_iface() -> Any:
"""Return a KiCADInterface instance with __init__ stubbed out."""
for mod in ["pcbnew", "skip"]:
sys.modules.setdefault(mod, types.ModuleType(mod))
from kicad_interface import KiCADInterface
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
return KiCADInterface.__new__(KiCADInterface)
# ---------------------------------------------------------------------------
# 1. Schema tests
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestAddNetLabelSchema:
"""Verify tool_schemas.py reflects the new add_schematic_net_label API."""
@pytest.fixture(autouse=True)
def load_schemas(self) -> Any:
from schemas.tool_schemas import SCHEMATIC_TOOLS
self.tools = {t["name"]: t for t in SCHEMATIC_TOOLS}
def test_position_is_optional(self) -> None:
schema = self.tools["add_schematic_net_label"]["inputSchema"]
assert "position" not in schema["required"], "position must not be required"
def test_component_ref_property_exists(self) -> None:
schema = self.tools["add_schematic_net_label"]["inputSchema"]
assert "componentRef" in schema["properties"]
def test_pin_number_property_exists(self) -> None:
schema = self.tools["add_schematic_net_label"]["inputSchema"]
assert "pinNumber" in schema["properties"]
def test_only_schematic_path_and_net_name_required(self) -> None:
schema = self.tools["add_schematic_net_label"]["inputSchema"]
assert set(schema["required"]) == {"schematicPath", "netName"}
@pytest.mark.unit
class TestConnectToNetSchema:
"""Verify tool_schemas.py reflects the richer connect_to_net description."""
@pytest.fixture(autouse=True)
def load_schemas(self) -> Any:
from schemas.tool_schemas import SCHEMATIC_TOOLS
self.tools = {t["name"]: t for t in SCHEMATIC_TOOLS}
def test_description_mentions_pin_location(self) -> None:
desc = self.tools["connect_to_net"]["description"]
assert "pin_location" in desc
def test_description_mentions_label_location(self) -> None:
desc = self.tools["connect_to_net"]["description"]
assert "label_location" in desc
# ---------------------------------------------------------------------------
# 2. _handle_add_schematic_net_label unit tests
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestHandleAddSchematicNetLabelSnapping:
"""Unit tests for the pin-snapping path of _handle_add_schematic_net_label."""
@pytest.fixture(autouse=True)
def setup(self) -> Any:
self.iface = _make_iface()
# -- happy-path: snap to pin -----------------------------------------
@patch("commands.wire_manager.WireManager.add_label", return_value=True)
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=[42.0, 13.5])
def test_snap_uses_pin_coords(self, mock_pin_loc: Any, mock_add_label: Any) -> None:
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"netName": "VCC",
"componentRef": "U1",
"pinNumber": "1",
}
)
assert result["success"] is True
assert result["actual_position"] == [42.0, 13.5]
assert result["snapped_to_pin"] == {"component": "U1", "pin": "1"}
# WireManager.add_label must have been called with the pin coords
mock_add_label.assert_called_once()
call_args = mock_add_label.call_args
assert call_args[0][2] == [42.0, 13.5] # position positional arg
@patch("commands.wire_manager.WireManager.add_label", return_value=True)
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=[10.0, 20.0])
def test_snap_ignores_provided_position(self, mock_pin_loc: Any, mock_add_label: Any) -> None:
"""If both position and componentRef/pinNumber are given, pin coords win."""
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"netName": "GND",
"position": [999.0, 999.0],
"componentRef": "R1",
"pinNumber": "2",
}
)
assert result["success"] is True
assert result["actual_position"] == [10.0, 20.0]
# -- error: pin not found --------------------------------------------
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=None)
def test_snap_unknown_pin_returns_error(self, mock_pin_loc: Any) -> None:
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"netName": "VCC",
"componentRef": "U99",
"pinNumber": "99",
}
)
assert result["success"] is False
assert "U99" in result["message"] or "pin" in result["message"].lower()
# -- error: no position and no pin ref --------------------------------
def test_no_position_no_ref_returns_error(self) -> None:
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"netName": "VCC",
}
)
assert result["success"] is False
assert "position" in result["message"].lower() or "componentRef" in result["message"]
# -- happy-path: explicit position ------------------------------------
@patch("commands.wire_manager.WireManager.add_label", return_value=True)
def test_explicit_position_used_when_no_ref(self, mock_add_label: Any) -> None:
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"netName": "CLK",
"position": [55.0, 77.0],
}
)
assert result["success"] is True
assert result["actual_position"] == [55.0, 77.0]
assert "snapped_to_pin" not in result
# -- missing required params -----------------------------------------
def test_missing_net_name_returns_error(self) -> None:
result = self.iface._handle_add_schematic_net_label(
{
"schematicPath": "/fake/sch.kicad_sch",
"position": [10.0, 20.0],
}
)
assert result["success"] is False
# ---------------------------------------------------------------------------
# 3. connect_to_net unit tests
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestConnectToNetRicherResponse:
"""connect_to_net now returns coordinates instead of a bare bool."""
@patch("commands.wire_manager.WireManager.add_label", return_value=True)
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
@patch("commands.pin_locator.PinLocator.get_pin_angle", return_value=0.0)
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=[100.0, 50.0])
def test_success_returns_coordinates(
self,
mock_pin_loc: Any,
mock_pin_angle: Any,
mock_add_wire: Any,
mock_add_label: Any,
) -> None:
from commands.connection_schematic import ConnectionManager
result = ConnectionManager.connect_to_net(Path("/fake/sch.kicad_sch"), "U1", "5", "VCC")
assert result["success"] is True
assert result["pin_location"] == [100.0, 50.0]
assert "label_location" in result
assert "wire_stub" in result
# wire_stub is [[pin_x, pin_y], [label_x, label_y]]
assert result["wire_stub"][0] == [100.0, 50.0]
assert result["wire_stub"][1] == result["label_location"]
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=None)
def test_unknown_pin_returns_failure_dict(self, mock_pin_loc: Any) -> None:
from commands.connection_schematic import ConnectionManager
result = ConnectionManager.connect_to_net(Path("/fake/sch.kicad_sch"), "U99", "99", "VCC")
assert result["success"] is False
assert "message" in result
@patch("commands.wire_manager.WireManager.add_wire", return_value=False)
@patch("commands.pin_locator.PinLocator.get_pin_angle", return_value=0.0)
@patch("commands.pin_locator.PinLocator.get_pin_location", return_value=[10.0, 20.0])
def test_wire_failure_returns_failure_dict(
self, mock_pin_loc: Any, mock_pin_angle: Any, mock_add_wire: Any
) -> None:
from commands.connection_schematic import ConnectionManager
result = ConnectionManager.connect_to_net(Path("/fake/sch.kicad_sch"), "R1", "1", "GND")
assert result["success"] is False
assert "message" in result
# ---------------------------------------------------------------------------
# 4. connect_passthrough uses dict return correctly
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestConnectPassthroughUsesDict:
"""connect_passthrough must handle the dict returned by connect_to_net."""
@patch(
"commands.connection_schematic.ConnectionManager.connect_to_net",
return_value={
"success": True,
"pin_location": [0, 0],
"label_location": [2.54, 0],
"wire_stub": [[0, 0], [2.54, 0]],
"message": "ok",
},
)
@patch(
"commands.pin_locator.PinLocator.get_all_symbol_pins",
side_effect=[{"1": [0.0, 0.0]}, {"1": [10.0, 10.0]}],
)
def test_passthrough_succeeds_with_dict_return(self, mock_pins: Any, mock_connect: Any) -> None:
from commands.connection_schematic import ConnectionManager
result = ConnectionManager.connect_passthrough(
Path("/fake/sch.kicad_sch"), "J1", "J2", net_prefix="PIN"
)
assert len(result["connected"]) == 1
assert len(result["failed"]) == 0
@patch(
"commands.connection_schematic.ConnectionManager.connect_to_net",
return_value={"success": False, "message": "pin not found"},
)
@patch(
"commands.pin_locator.PinLocator.get_all_symbol_pins",
side_effect=[{"1": [0.0, 0.0]}, {"1": [10.0, 10.0]}],
)
def test_passthrough_records_failure_with_dict_return(
self, mock_pins: Any, mock_connect: Any
) -> None:
from commands.connection_schematic import ConnectionManager
result = ConnectionManager.connect_passthrough(
Path("/fake/sch.kicad_sch"), "J1", "J2", net_prefix="PIN"
)
assert len(result["failed"]) >= 1