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
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:

View File

@@ -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

View File

@@ -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"],
},
},
{

View File

@@ -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),
},
],
};

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