diff --git a/python/commands/connection_schematic.py b/python/commands/connection_schematic.py index 69f8ceb..f46ea99 100644 --- a/python/commands/connection_schematic.py +++ b/python/commands/connection_schematic.py @@ -59,9 +59,9 @@ class ConnectionManager: @staticmethod def connect_to_net( 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: 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") 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: if not WIRE_MANAGER_AVAILABLE: logger.error("WireManager/PinLocator not available") - return False + return {"success": False, "message": "WireManager/PinLocator not available"} locator = ConnectionManager.get_pin_locator() if not locator: logger.error("Pin locator unavailable") - return False + return {"success": False, "message": "Pin locator unavailable"} # Get pin location using PinLocator pin_loc = locator.get_pin_location(schematic_path, component_ref, pin_name) if not pin_loc: - logger.error(f"Could not locate pin {component_ref}/{pin_name}") - return False + msg = f"Could not locate pin {component_ref}/{pin_name}" + logger.error(msg) + return {"success": False, "message": msg} # 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 - pin_angle_deg = getattr(locator, "_last_pin_angle", 0) try: pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0 except Exception: @@ -106,26 +111,34 @@ class ConnectionManager: # Create wire stub using WireManager wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end) if not wire_success: - logger.error(f"Failed to create wire stub for net connection") - return False + msg = "Failed to create wire stub for net connection" + logger.error(msg) + return {"success": False, "message": msg} # Add label at the end of the stub using WireManager label_success = WireManager.add_label( schematic_path, net_name, stub_end, label_type="label" ) if not label_success: - logger.error(f"Failed to add net label '{net_name}'") - return False + msg = f"Failed to add net label '{net_name}'" + logger.error(msg) + return {"success": False, "message": msg} 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: logger.error(f"Error connecting to net: {e}") import traceback logger.error(traceback.format_exc()) - return False + return {"success": False, "message": str(e)} @staticmethod def connect_passthrough( @@ -177,18 +190,18 @@ class ConnectionManager: 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 ) - if not ok_src: + if not res_src.get("success"): failed.append(f"{source_ref}/{pin_num}") continue 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 ) - if not ok_tgt: + if not res_tgt.get("success"): failed.append(f"{target_ref}/{pin_num}") continue else: diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 7fd00b7..8f2d3d2 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -1514,9 +1514,16 @@ class KiCADInterface: return {"success": False, "message": str(e)} 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") try: + import traceback from pathlib import Path from commands.wire_manager import WireManager @@ -1524,13 +1531,48 @@ class KiCADInterface: schematic_path = params.get("schematicPath") net_name = params.get("netName") position = params.get("position") - label_type = params.get( - "labelType", "label" - ) # 'label', 'global_label', 'hierarchical_label' - orientation = params.get("orientation", 0) # 0, 90, 180, 270 + label_type = params.get("labelType", "label") + orientation = params.get("orientation", 0) + component_ref = params.get("componentRef") + pin_number = params.get("pinNumber") - if not all([schematic_path, net_name, position]): - return {"success": False, "message": "Missing required parameters"} + if not all([schematic_path, net_name]): + 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 success = WireManager.add_label( @@ -1541,13 +1583,22 @@ class KiCADInterface: orientation=orientation, ) - if success: - return { - "success": True, - "message": f"Added net label '{net_name}' at {position}", - } - else: + if not success: 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: logger.error(f"Error adding net label: {str(e)}") import traceback @@ -1574,17 +1625,10 @@ class KiCADInterface: return {"success": False, "message": "Missing required parameters"} # 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 ) - - 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"} + return result except Exception as e: logger.error(f"Error connecting to net: {str(e)}") import traceback diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index 94de080..2f8f429 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -1399,7 +1399,15 @@ SCHEMATIC_TOOLS = [ { "name": "add_schematic_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": { "type": "object", "properties": { @@ -1411,21 +1419,45 @@ SCHEMATIC_TOOLS = [ "type": "string", "description": "Name of the net (e.g., VCC, GND, SDA)", }, - "x": {"type": "number", "description": "X coordinate on schematic"}, - "y": {"type": "number", "description": "Y coordinate on schematic"}, - "rotation": { + "position": { + "type": "array", + "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", "description": "Rotation angle in degrees (0, 90, 180, 270)", "default": 0, }, }, - "required": ["schematicPath", "netName", "x", "y"], + "required": ["schematicPath", "netName"], }, }, { "name": "connect_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": { "type": "object", "properties": { @@ -1433,11 +1465,11 @@ SCHEMATIC_TOOLS = [ "type": "string", "description": "Path to schematic file", }, - "reference": { + "componentRef": { "type": "string", "description": "Component reference designator (e.g., R1, U3)", }, - "pinNumber": { + "pinName": { "type": "string", "description": "Pin number or name on the component", }, @@ -1446,7 +1478,7 @@ SCHEMATIC_TOOLS = [ "description": "Name of the net to connect to", }, }, - "required": ["schematicPath", "reference", "pinNumber", "netName"], + "required": ["schematicPath", "componentRef", "pinName", "netName"], }, }, { diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index eec672a..a68ff83 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -325,20 +325,55 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp // Add net label server.tool( "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"), 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); if (result.success) { return { content: [ { 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 server.tool( "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"), 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: [ { type: "text", - text: `Successfully connected ${args.componentRef}/${args.pinName} to net '${args.netName}'`, + text: JSON.stringify(result, null, 2), }, ], }; diff --git a/tests/test_net_label_pin_snapping.py b/tests/test_net_label_pin_snapping.py new file mode 100644 index 0000000..d4b1f93 --- /dev/null +++ b/tests/test_net_label_pin_snapping.py @@ -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