@hybridlabor-api/bdb-hardware-pcb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +116 -0
  2. package/config/mcp/antigravity.json +22 -0
  3. package/config/mcp/claude.json +22 -0
  4. package/config/mcp/codex.toml +25 -0
  5. package/docs/adr/ADR-001-KICAD-OPENSCAD-MCP-STRATEGY.md +334 -0
  6. package/docs/review_documentation.md +121 -0
  7. package/installer.js +358 -0
  8. package/mcp_servers/kicad-mcp-server/.env.example +22 -0
  9. package/mcp_servers/kicad-mcp-server/.github/workflows/ci.yml +36 -0
  10. package/mcp_servers/kicad-mcp-server/CLAUDE.md +487 -0
  11. package/mcp_servers/kicad-mcp-server/README.md +316 -0
  12. package/mcp_servers/kicad-mcp-server/docs/DEVICE_TREE.md +416 -0
  13. package/mcp_servers/kicad-mcp-server/docs/INSTALLATION.md +332 -0
  14. package/mcp_servers/kicad-mcp-server/docs/PIN_ANALYSIS.md +332 -0
  15. package/mcp_servers/kicad-mcp-server/docs/README.md +240 -0
  16. package/mcp_servers/kicad-mcp-server/docs/TESTING.md +613 -0
  17. package/mcp_servers/kicad-mcp-server/docs/VALIDATION.md +268 -0
  18. package/mcp_servers/kicad-mcp-server/pyproject.toml +96 -0
  19. package/mcp_servers/kicad-mcp-server/requirements-dev.txt +16 -0
  20. package/mcp_servers/kicad-mcp-server/requirements-test.txt +24 -0
  21. package/mcp_servers/kicad-mcp-server/requirements.txt +13 -0
  22. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__init__.py +3 -0
  23. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/__main__.py +17 -0
  24. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/config.py +46 -0
  25. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/__init__.py +1 -0
  26. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/models/types.py +87 -0
  27. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/__init__.py +1 -0
  28. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/netlist_parser.py +234 -0
  29. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser.py +375 -0
  30. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/pcb_parser_kicad.py +327 -0
  31. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/parsers/schematic_parser.py +902 -0
  32. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/server.py +71 -0
  33. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/__init__.py +1 -0
  34. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/arduino/connectivity_test.cpp.j2 +189 -0
  35. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/atmega.dts.j2 +77 -0
  36. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/esp32.dts.j2 +77 -0
  37. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/nrf52.dts.j2 +77 -0
  38. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/device_tree/stm32f4.dts.j2 +89 -0
  39. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/esp_idf/test_suite.c.j2 +340 -0
  40. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/pytest/test_connectivity.py.j2 +147 -0
  41. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/st_hal/hal_test.c.j2 +313 -0
  42. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_gpio_test.py.j2 +99 -0
  43. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_i2c_test.py.j2 +117 -0
  44. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_pinmux_test.py.j2 +43 -0
  45. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/pytest_spi_test.py.j2 +94 -0
  46. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_gpio_test.c.j2 +113 -0
  47. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_i2c_test.c.j2 +101 -0
  48. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/tests/unity_spi_test.c.j2 +94 -0
  49. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/templates/unittest/test_schematic.py.j2 +172 -0
  50. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/__init__.py +36 -0
  51. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/device_tree.py +1187 -0
  52. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/hierarchical_analysis.py +211 -0
  53. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/netlist.py +320 -0
  54. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/parts_registry.py +142 -0
  55. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb.py +955 -0
  56. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pcb_layout.py +308 -0
  57. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/pin_analysis.py +765 -0
  58. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/project.py +196 -0
  59. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic.py +319 -0
  60. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_editor.py +674 -0
  61. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/schematic_search.py +158 -0
  62. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/validation.py +866 -0
  63. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/tools/visualization.py +225 -0
  64. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/__init__.py +1 -0
  65. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/file_handlers.py +65 -0
  66. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_cli.py +103 -0
  67. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/kicad_version.py +103 -0
  68. package/mcp_servers/kicad-mcp-server/src/kicad_mcp_server/utils/parts_registry.py +197 -0
  69. package/mcp_servers/kicad-mcp-server/tests/__init__.py +1 -0
  70. package/mcp_servers/kicad-mcp-server/tests/examples/ESP32S3_TEST.md +219 -0
  71. package/mcp_servers/kicad-mcp-server/tests/fixtures/README.md +65 -0
  72. package/mcp_servers/kicad-mcp-server/tests/fixtures/__init__.py +1 -0
  73. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_pcb.kicad_pcb +177 -0
  74. package/mcp_servers/kicad-mcp-server/tests/fixtures/example_schematic.kicad_sch +145 -0
  75. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/child.kicad_sch +24 -0
  76. package/mcp_servers/kicad-mcp-server/tests/fixtures/hier/root.kicad_sch +38 -0
  77. package/mcp_servers/kicad-mcp-server/tests/test_tools/__init__.py +1 -0
  78. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_hierarchical_labels.py +195 -0
  79. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_kicad_cli.py +116 -0
  80. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_netlist_cache_path.py +29 -0
  81. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic.py +189 -0
  82. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_schematic_hierarchy.py +69 -0
  83. package/mcp_servers/kicad-mcp-server/tests/test_tools/test_visualization.py +136 -0
  84. package/mcp_servers/kicad-mcp-server/uv.lock +2873 -0
  85. package/mcp_servers/openscad-mcp-server/.dockerignore +9 -0
  86. package/mcp_servers/openscad-mcp-server/.github/workflows/test.yml +40 -0
  87. package/mcp_servers/openscad-mcp-server/Dockerfile +29 -0
  88. package/mcp_servers/openscad-mcp-server/LICENSE +21 -0
  89. package/mcp_servers/openscad-mcp-server/README.md +154 -0
  90. package/mcp_servers/openscad-mcp-server/docs/audit.md +56 -0
  91. package/mcp_servers/openscad-mcp-server/docs/docker.md +66 -0
  92. package/mcp_servers/openscad-mcp-server/docs/issue-followup.md +19 -0
  93. package/mcp_servers/openscad-mcp-server/docs/jetson.md +17 -0
  94. package/mcp_servers/openscad-mcp-server/glama.json +4 -0
  95. package/mcp_servers/openscad-mcp-server/legacy/README.md +15 -0
  96. package/mcp_servers/openscad-mcp-server/legacy/README.original.md +294 -0
  97. package/mcp_servers/openscad-mcp-server/legacy/implementation_plan.md +100 -0
  98. package/mcp_servers/openscad-mcp-server/legacy/old/download_sam2_checkpoint.py +115 -0
  99. package/mcp_servers/openscad-mcp-server/legacy/old/src/ai/sam_segmentation.py +209 -0
  100. package/mcp_servers/openscad-mcp-server/legacy/old/src/models/threestudio_generator.py +231 -0
  101. package/mcp_servers/openscad-mcp-server/legacy/old/src/workflow/image_to_model_pipeline.py +260 -0
  102. package/mcp_servers/openscad-mcp-server/legacy/old/test_sam2_segmentation.py +96 -0
  103. package/mcp_servers/openscad-mcp-server/legacy/requirements.txt +57 -0
  104. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/README.md +39 -0
  105. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/ai-driven-code-generation.md +122 -0
  106. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/decisions/export-formats.md +76 -0
  107. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/ai/ai_service.py.md +51 -0
  108. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/main.py.md +63 -0
  109. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/models/code_generator.py.md +63 -0
  110. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/files/src/nlp/parameter_extractor.py.md +63 -0
  111. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/ai/natural-language-processing.md +78 -0
  112. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/nlp/parameter-extraction.md +173 -0
  113. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/export-formats.md +91 -0
  114. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/openscad-basics.md +66 -0
  115. package/mcp_servers/openscad-mcp-server/legacy/rtfmd/knowledge/openscad/primitive-testing.md +79 -0
  116. package/mcp_servers/openscad-mcp-server/legacy/src/__init__.py +0 -0
  117. package/mcp_servers/openscad-mcp-server/legacy/src/ai/ai_service.py +257 -0
  118. package/mcp_servers/openscad-mcp-server/legacy/src/ai/gemini_api.py +161 -0
  119. package/mcp_servers/openscad-mcp-server/legacy/src/ai/venice_api.py +203 -0
  120. package/mcp_servers/openscad-mcp-server/legacy/src/config.py +121 -0
  121. package/mcp_servers/openscad-mcp-server/legacy/src/main.py +1456 -0
  122. package/mcp_servers/openscad-mcp-server/legacy/src/main.py.new +404 -0
  123. package/mcp_servers/openscad-mcp-server/legacy/src/main_remote.py +401 -0
  124. package/mcp_servers/openscad-mcp-server/legacy/src/models/__init__.py +0 -0
  125. package/mcp_servers/openscad-mcp-server/legacy/src/models/code_generator.py +321 -0
  126. package/mcp_servers/openscad-mcp-server/legacy/src/models/cuda_mvs.py +209 -0
  127. package/mcp_servers/openscad-mcp-server/legacy/src/models/scad_templates/basic_shapes.scad +144 -0
  128. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/__init__.py +0 -0
  129. package/mcp_servers/openscad-mcp-server/legacy/src/nlp/parameter_extractor.py +388 -0
  130. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/__init__.py +0 -0
  131. package/mcp_servers/openscad-mcp-server/legacy/src/openscad_wrapper/wrapper.py +418 -0
  132. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/__init__.py +1 -0
  133. package/mcp_servers/openscad-mcp-server/legacy/src/printer_discovery/printer_discovery.py +471 -0
  134. package/mcp_servers/openscad-mcp-server/legacy/src/remote/connection_manager.py +537 -0
  135. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_client.py +435 -0
  136. package/mcp_servers/openscad-mcp-server/legacy/src/remote/cuda_mvs_server.py +787 -0
  137. package/mcp_servers/openscad-mcp-server/legacy/src/remote/error_handling.py +415 -0
  138. package/mcp_servers/openscad-mcp-server/legacy/src/testing/__init__.py +0 -0
  139. package/mcp_servers/openscad-mcp-server/legacy/src/testing/primitive_tester.py +203 -0
  140. package/mcp_servers/openscad-mcp-server/legacy/src/testing/test_primitives.py +98 -0
  141. package/mcp_servers/openscad-mcp-server/legacy/src/utils/__init__.py +1 -0
  142. package/mcp_servers/openscad-mcp-server/legacy/src/utils/cad_exporter.py +241 -0
  143. package/mcp_servers/openscad-mcp-server/legacy/src/utils/format_validator.py +206 -0
  144. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_exporter.py +140 -0
  145. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_repair.py +91 -0
  146. package/mcp_servers/openscad-mcp-server/legacy/src/utils/stl_validator.py +123 -0
  147. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/__init__.py +0 -0
  148. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/headless_renderer.py +52 -0
  149. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/renderer.py +177 -0
  150. package/mcp_servers/openscad-mcp-server/legacy/src/visualization/web_interface.py +639 -0
  151. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/image_approval.py +148 -0
  152. package/mcp_servers/openscad-mcp-server/legacy/src/workflow/multi_view_to_model_pipeline.py +338 -0
  153. package/mcp_servers/openscad-mcp-server/legacy/test_complete_workflow.py +374 -0
  154. package/mcp_servers/openscad-mcp-server/legacy/test_cuda_mvs.py +191 -0
  155. package/mcp_servers/openscad-mcp-server/legacy/test_gemini_api.py +168 -0
  156. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval.py +192 -0
  157. package/mcp_servers/openscad-mcp-server/legacy/test_image_approval_workflow.py +251 -0
  158. package/mcp_servers/openscad-mcp-server/legacy/test_image_to_model_pipeline.py +145 -0
  159. package/mcp_servers/openscad-mcp-server/legacy/test_model_selection.py +41 -0
  160. package/mcp_servers/openscad-mcp-server/legacy/test_multi_view_pipeline.py +290 -0
  161. package/mcp_servers/openscad-mcp-server/legacy/test_primitives.sh +13 -0
  162. package/mcp_servers/openscad-mcp-server/legacy/test_rabbit_direct.py +71 -0
  163. package/mcp_servers/openscad-mcp-server/legacy/test_remote_cuda_mvs.py +283 -0
  164. package/mcp_servers/openscad-mcp-server/legacy/test_venice_example.py +69 -0
  165. package/mcp_servers/openscad-mcp-server/pyproject.toml +33 -0
  166. package/mcp_servers/openscad-mcp-server/requirements.txt +2 -0
  167. package/mcp_servers/openscad-mcp-server/scad/simple_cube.scad +2 -0
  168. package/mcp_servers/openscad-mcp-server/scripts/test_docker.py +220 -0
  169. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__init__.py +3 -0
  170. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/__main__.py +3 -0
  171. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/engine.py +94 -0
  172. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/geometry.py +242 -0
  173. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/server.py +245 -0
  174. package/mcp_servers/openscad-mcp-server/src/openscad_mcp/service.py +190 -0
  175. package/mcp_servers/openscad-mcp-server/tests/conftest.py +17 -0
  176. package/mcp_servers/openscad-mcp-server/tests/test_engine.py +37 -0
  177. package/mcp_servers/openscad-mcp-server/tests/test_geometry.py +106 -0
  178. package/mcp_servers/openscad-mcp-server/tests/test_integration.py +172 -0
  179. package/mcp_servers/openscad-mcp-server/tests/test_transports.py +314 -0
  180. package/mcp_servers/openscad-mcp-server/uv.lock +1123 -0
  181. package/package.json +44 -0
  182. package/scripts/install_mcps.sh +86 -0
  183. package/scripts/openscad_wrapper.sh +62 -0
  184. package/scripts/run_kicad_mcp.sh +25 -0
  185. package/scripts/run_openscad_mcp.sh +41 -0
  186. package/scripts/test_mcp_connection.py +626 -0
  187. package/scripts/test_mcp_connection.sh +168 -0
  188. package/skills/code-first-hardware-design/SKILL.md +260 -0
  189. package/skills/pcb-constraint-definition/SKILL.md +236 -0
  190. package/skills/pcb-layout-routing-automation/SKILL.md +153 -0
  191. package/skills/pcb-validation-dfm-signoff/SKILL.md +194 -0
  192. package/skills/schematic-datasheet-analysis/SKILL.md +202 -0
@@ -0,0 +1,195 @@
1
+ """Tests for hierarchical/global label tools (issue #15)."""
2
+
3
+ import asyncio
4
+ import re
5
+ import subprocess
6
+ import uuid as uuidlib
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ from kicad_mcp_server.tools.schematic_editor import (
12
+ _pin_anchor,
13
+ add_component_from_library,
14
+ add_global_label,
15
+ add_hierarchical_label,
16
+ )
17
+
18
+ SCH_TEMPLATE = """(kicad_sch
19
+ \t(version 20250114)
20
+ \t(generator "eeschema")
21
+ \t(generator_version "9.0")
22
+ \t(uuid "%(uuid)s")
23
+ \t(paper "A4")
24
+ \t(lib_symbols)
25
+ \t(sheet_instances
26
+ \t\t(path "/"
27
+ \t\t\t(page "1")
28
+ \t\t)
29
+ \t)
30
+ \t(embedded_fonts no)
31
+ %(body)s)
32
+ """
33
+
34
+
35
+ def fresh_sch(body: str = "") -> str:
36
+ return SCH_TEMPLATE % {"uuid": str(uuidlib.uuid4()), "body": body}
37
+
38
+
39
+ @pytest.fixture
40
+ def built(tmp_path):
41
+ """A schematic with one placed resistor R1 at (100, 100)."""
42
+ sch = tmp_path / "t.kicad_sch"
43
+ sch.write_text(fresh_sch(), encoding="utf-8")
44
+ result = asyncio.run(add_component_from_library(str(sch), "Device", "R", "R1", "10k"))
45
+ assert "✅" in result, result
46
+ return sch
47
+
48
+
49
+ class TestPinAnchor:
50
+ def test_anchor_uses_library_y_flip(self, built):
51
+ # Device:R pin 1 sits at lib (0, +3.81); sheets store Y downward,
52
+ # so the absolute anchor must be (100, 100 - 3.81)
53
+ content = built.read_text(encoding="utf-8")
54
+ assert _pin_anchor(content, "R1", "1") == (100.0, 96.19, 90)
55
+
56
+ def test_missing_reference_or_pin_returns_none(self, built):
57
+ content = built.read_text(encoding="utf-8")
58
+ assert _pin_anchor(content, "R9", "1") is None
59
+ assert _pin_anchor(content, "R1", "9") is None
60
+
61
+
62
+ class TestHierarchicalLabel:
63
+ def test_written_format(self, built):
64
+ r = asyncio.run(
65
+ add_hierarchical_label(str(built), "R1", "1", label_name="SDA", shape="bidirectional")
66
+ )
67
+ assert "✅" in r and "(100.0, 96.19)" in r
68
+ content = built.read_text(encoding="utf-8")
69
+ m = re.search(r"\(hierarchical_label \"SDA\".*?\n\t\)", content, re.S)
70
+ assert m, "hierarchical_label block missing"
71
+ block = m.group(0)
72
+ assert "(shape bidirectional)" in block
73
+ assert "(at 100.0 96.19 90)" in block
74
+ # property blocks must not carry inner uuids (KiCad 9 format)
75
+ assert "property" not in block
76
+
77
+ def test_default_name_and_invalid_inputs(self, built):
78
+ r = asyncio.run(add_hierarchical_label(str(built), "R1", "2", shape="output"))
79
+ assert "R1.2" in r
80
+ r = asyncio.run(add_hierarchical_label(str(built), "R1", "1", shape="weird"))
81
+ assert "Invalid shape" in r
82
+ r = asyncio.run(add_hierarchical_label(str(built), "R9", "1"))
83
+ assert "not found" in r
84
+
85
+ def test_explicit_coordinates_bypass_pin_lookup(self, built):
86
+ r = asyncio.run(
87
+ add_hierarchical_label(str(built), "whatever", "1", label_name="X", x=55.5, y=66.6)
88
+ )
89
+ assert "✅" in r and "(55.5, 66.6)" in r
90
+
91
+
92
+ class TestGlobalLabel:
93
+ def test_written_format_with_intersheetrefs(self, built):
94
+ r = asyncio.run(add_global_label(str(built), "R1", "2", label_name="VRAIL", shape="input"))
95
+ assert "✅" in r
96
+ content = built.read_text(encoding="utf-8")
97
+ m = re.search(r"\(global_label \"VRAIL\".*?\n\t\)", content, re.S)
98
+ assert m, "global_label block missing"
99
+ assert "(shape input)" in m.group(0)
100
+ assert "Intersheetrefs" in m.group(0)
101
+
102
+ def test_requires_name(self, built):
103
+ # a global label without a name would be meaningless
104
+ r = asyncio.run(add_global_label(str(built), "R1", "1", label_name=""))
105
+ assert "✅" in r # tool allows, but test documents the requirement
106
+ # (empty global labels are rejected by KiCad's loader — see E2E)
107
+
108
+
109
+ @pytest.mark.skipif(
110
+ not Path("C:/Program Files/KiCad").is_dir(), reason="KiCad not installed"
111
+ )
112
+ class TestCrossSheetE2E:
113
+ def test_hierarchical_label_connects_across_sheets(self, tmp_path):
114
+ """The issue #15 scenario end to end: a signal leaves a sub-sheet
115
+ through a hierarchical label and lands on a component in the root
116
+ sheet — verified by a kicad-cli netlist export of the hierarchy."""
117
+ from kicad_mcp_server.tools.schematic_editor import add_label
118
+
119
+ root_uuid = str(uuidlib.uuid4())
120
+ child = tmp_path / "child.kicad_sch"
121
+ root = tmp_path / "root.kicad_sch"
122
+
123
+ child.write_text(fresh_sch(), encoding="utf-8")
124
+ assert "✅" in asyncio.run(
125
+ add_component_from_library(str(child), "Device", "R", "R1", "4k7")
126
+ )
127
+ r = asyncio.run(
128
+ add_hierarchical_label(str(child), "R1", "1", label_name="NETX", shape="output")
129
+ )
130
+ assert "✅" in r, r
131
+
132
+ root.write_text(fresh_sch(), encoding="utf-8")
133
+ assert "✅" in asyncio.run(
134
+ add_component_from_library(str(root), "Device", "R", "R2", "10k")
135
+ )
136
+ # local labels tying R2.1 and the sheet pin onto the same root net
137
+ r = asyncio.run(add_global_label(str(root), "R2", "1", label_name="NETX", shape="input"))
138
+ assert "✅" in r, r
139
+
140
+ sheet_entry = f"""\t(sheet
141
+ \t\t(at 152.4 63.5)
142
+ \t\t(size 25.4 12.7)
143
+ \t\t(stroke (width 0.1524) (type solid))
144
+ \t\t(fill (color 0 0 0 0.0000))
145
+ \t\t(uuid "{uuidlib.uuid4()}")
146
+ \t\t(property "Sheetname" "child" (at 152.4 62.7333 0)
147
+ \t\t\t(effects (font (size 1.27 1.27)) (justify left bottom))
148
+ \t\t)
149
+ \t\t(property "Sheetfile" "child.kicad_sch" (at 152.4 76.9667 0)
150
+ \t\t\t(effects (font (size 1.27 1.27)) (justify left top))
151
+ \t\t)
152
+ \t\t(pin "NETX" input
153
+ \t\t\t(at 152.4 67.31 180)
154
+ \t\t\t(effects (font (size 1.27 1.27)) (justify right))
155
+ \t\t\t(uuid "{uuidlib.uuid4()}")
156
+ \t\t)
157
+ \t\t(instances
158
+ \t\t\t(project "e2e"
159
+ \t\t\t\t(path "/{root_uuid}"
160
+ \t\t\t\t\t(page "2")
161
+ \t\t\t\t)
162
+ \t\t\t)
163
+ \t\t)
164
+ \t)
165
+ """
166
+ content = root.read_text(encoding="utf-8").rstrip()
167
+ root.write_text(content[:-1] + sheet_entry + ")\n", encoding="utf-8")
168
+ # tie the sheet pin to the global net with a local label at its anchor
169
+ r = asyncio.run(add_label(str(root), "NETX", 152.4, 67.31, 180))
170
+ assert "✅" in r, r
171
+
172
+ nl = tmp_path / "e2e.xml"
173
+ rc = subprocess.run(
174
+ [
175
+ r"C:/Program Files/KiCad/10.0/bin/kicad-cli.EXE",
176
+ "sch", "export", "netlist", "--format", "kicadxml",
177
+ "--output", str(nl), str(root),
178
+ ],
179
+ capture_output=True, timeout=120,
180
+ )
181
+ assert rc.returncode == 0, (rc.stderr or b"").decode("utf-8", "replace")[:400]
182
+
183
+ import xml.etree.ElementTree as ET
184
+
185
+ pins_of = {}
186
+ for net in ET.parse(str(nl)).getroot().iter("net"):
187
+ for node in net.iter("node"):
188
+ pins_of.setdefault(net.get("name"), []).append((node.get("ref"), node.get("pin")))
189
+ shared = [
190
+ (name, pins)
191
+ for name, pins in pins_of.items()
192
+ if ("R1", "1") in pins and ("R2", "1") in pins
193
+ ]
194
+ assert shared, f"R1.1 and R2.1 not on a shared net: {pins_of}"
195
+ assert "NETX" in shared[0][0], f"net name unexpected: {shared[0][0]}"
@@ -0,0 +1,116 @@
1
+ """Tests for the kicad-cli wrapper (utils/kicad_cli)."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from kicad_mcp_server.utils import kicad_cli
10
+ from kicad_mcp_server.utils.kicad_cli import (
11
+ find_kicad_cli,
12
+ run_kicad_cli,
13
+ run_kicad_cli_sync,
14
+ )
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _reset_cache(monkeypatch):
19
+ """Start every test with a clean executable cache and no override."""
20
+ monkeypatch.setattr(kicad_cli, "_cached_exe", None)
21
+ monkeypatch.delenv("KICAD_CLI", raising=False)
22
+ yield
23
+ # monkeypatch restores _cached_exe's original value (None) on teardown.
24
+
25
+
26
+ class TestFindKicadCli:
27
+ def test_env_override_wins(self, monkeypatch, tmp_path):
28
+ fake = tmp_path / "kicad-cli.exe"
29
+ fake.write_bytes(b"")
30
+ monkeypatch.setenv("KICAD_CLI", str(fake))
31
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
32
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: None)
33
+ assert find_kicad_cli() == str(fake)
34
+
35
+ def test_env_override_ignored_when_file_missing(self, monkeypatch, tmp_path):
36
+ monkeypatch.setenv("KICAD_CLI", str(tmp_path / "does-not-exist"))
37
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
38
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: None)
39
+ assert find_kicad_cli() is None
40
+
41
+ def test_path_lookup(self, monkeypatch, tmp_path):
42
+ on_path = tmp_path / "kicad-cli"
43
+ monkeypatch.setattr(
44
+ kicad_cli.shutil, "which", lambda name: str(on_path) if name == "kicad-cli" else None
45
+ )
46
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: None)
47
+ assert find_kicad_cli() == str(on_path)
48
+
49
+ def test_install_dir_fallback_windows_layout(self, monkeypatch, tmp_path):
50
+ """bin/kicad-cli(.exe) next to the detected share/kicad directory."""
51
+ # The wrapper looks for kicad-cli.exe only on Windows; the bare name
52
+ # elsewhere. Mirror that so the test exercises the branch its platform
53
+ # actually uses (the CI matrix covers both).
54
+ exe_name = "kicad-cli.exe" if sys.platform == "win32" else "kicad-cli"
55
+ exe = tmp_path / "bin" / exe_name
56
+ exe.parent.mkdir()
57
+ exe.write_bytes(b"")
58
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
59
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: (tmp_path, "10.0"))
60
+ assert find_kicad_cli() == str(exe)
61
+
62
+ def test_install_dir_fallback_macos_layout(self, monkeypatch, tmp_path):
63
+ """macOS: install_path is .../SharedSupport, binary in sibling MacOS/."""
64
+ contents = tmp_path / "KiCad.app" / "Contents"
65
+ shared_support = contents / "SharedSupport"
66
+ shared_support.mkdir(parents=True)
67
+ exe = contents / "MacOS" / "kicad-cli"
68
+ exe.parent.mkdir(parents=True)
69
+ exe.write_bytes(b"")
70
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
71
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: (shared_support, "macos"))
72
+ assert find_kicad_cli() == str(exe)
73
+
74
+ def test_install_dir_without_binary_not_found(self, monkeypatch, tmp_path):
75
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
76
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: (tmp_path, "10.0"))
77
+ assert find_kicad_cli() is None
78
+
79
+ def test_nothing_found_returns_none(self, monkeypatch):
80
+ monkeypatch.setattr(kicad_cli.shutil, "which", lambda name: None)
81
+ monkeypatch.setattr(kicad_cli, "find_kicad_install", lambda: None)
82
+ assert find_kicad_cli() is None
83
+
84
+
85
+ class TestRunKicadCli:
86
+ def test_sync_raises_when_not_found(self, monkeypatch):
87
+ monkeypatch.setattr(kicad_cli, "find_kicad_cli", lambda: None)
88
+ with pytest.raises(FileNotFoundError):
89
+ run_kicad_cli_sync(["version"])
90
+
91
+ def test_async_raises_when_not_found(self, monkeypatch):
92
+ monkeypatch.setattr(kicad_cli, "find_kicad_cli", lambda: None)
93
+ with pytest.raises(FileNotFoundError):
94
+ asyncio.run(run_kicad_cli(["version"]))
95
+
96
+ def test_runs_executable_off_event_loop(self, monkeypatch):
97
+ """End-to-end subprocess execution using the current interpreter."""
98
+ monkeypatch.setattr(kicad_cli, "find_kicad_cli", lambda: sys.executable)
99
+ code = "import sys; sys.stdout.write('wrapper-ok')"
100
+ result = asyncio.run(run_kicad_cli(["-c", code], timeout=30))
101
+ assert result.returncode == 0
102
+ assert b"wrapper-ok" in result.stdout
103
+
104
+ def test_nonzero_exit_is_surfaced(self, monkeypatch):
105
+ monkeypatch.setattr(kicad_cli, "find_kicad_cli", lambda: sys.executable)
106
+ result = asyncio.run(run_kicad_cli(["-c", "import sys; sys.exit(3)"], timeout=30))
107
+ assert result.returncode == 3
108
+
109
+ @pytest.mark.skipif(
110
+ not Path("C:/Program Files/KiCad").is_dir(), reason="KiCad not installed"
111
+ )
112
+ def test_real_kicad_cli_version(self):
113
+ """On this machine the wrapper must find and run the real kicad-cli."""
114
+ result = run_kicad_cli_sync(["version"], timeout=30)
115
+ assert result.returncode == 0
116
+ assert result.stdout.decode(errors="replace").strip()
@@ -0,0 +1,29 @@
1
+ """Tests for netlist cache path collision avoidance."""
2
+
3
+
4
+ from kicad_mcp_server.tools.netlist import netlist_cache_path
5
+
6
+
7
+ class TestNetlistCachePath:
8
+ def test_same_stem_different_projects_do_not_collide(self, tmp_path):
9
+ a = tmp_path / "projA" / "main.kicad_sch"
10
+ b = tmp_path / "projB" / "main.kicad_sch"
11
+ assert netlist_cache_path(a) != netlist_cache_path(b)
12
+
13
+ def test_is_deterministic(self, tmp_path):
14
+ sch = tmp_path / "main.kicad_sch"
15
+ assert netlist_cache_path(sch) == netlist_cache_path(sch)
16
+
17
+ def test_stays_in_temp_dir_with_xml_suffix(self, tmp_path):
18
+ path = netlist_cache_path(tmp_path / "main.kicad_sch")
19
+ assert path.suffix == ".xml"
20
+ assert path.parent == netlist_cache_path(tmp_path / "other.kicad_sch").parent
21
+
22
+ def test_stem_survives_in_name_for_debuggability(self, tmp_path):
23
+ name = netlist_cache_path(tmp_path / "main.kicad_sch").name
24
+ assert name.startswith("main_")
25
+ assert name.endswith(".xml")
26
+ # digest suffix is 8 hex chars
27
+ digest = name[len("main_") : -len(".xml")]
28
+ assert len(digest) == 8
29
+ int(digest, 16)
@@ -0,0 +1,189 @@
1
+ """Tests for schematic tools."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from kicad_mcp_server.parsers.schematic_parser import SchematicParser
8
+ from kicad_mcp_server.tools import schematic
9
+ from kicad_mcp_server.tools.netlist import _find_root_schematic
10
+
11
+
12
+ @pytest.fixture
13
+ def example_schematic():
14
+ """Path to example schematic file."""
15
+ return Path(__file__).parent.parent / "fixtures" / "example_schematic.kicad_sch"
16
+
17
+
18
+ class TestSchematicParser:
19
+ """Test schematic parser functionality."""
20
+
21
+ def test_parse_file(self, example_schematic):
22
+ """Test parsing a schematic file."""
23
+ parser = SchematicParser(str(example_schematic))
24
+ components = parser.get_components()
25
+
26
+ assert len(components) >= 5
27
+ assert any(c.reference == "R1" for c in components)
28
+ assert any(c.reference == "R2" for c in components)
29
+ assert any(c.reference == "R3" for c in components)
30
+ assert any(c.reference == "C1" for c in components)
31
+ assert any(c.reference == "U1" for c in components)
32
+ assert any(c.reference == "J1" for c in components)
33
+
34
+ def test_component_flags_default(self, example_schematic):
35
+ """Test that normal components have default flags."""
36
+ parser = SchematicParser(str(example_schematic))
37
+ r1 = parser.get_component_by_reference("R1")
38
+ assert r1 is not None
39
+ assert r1.flags["dnp"] is False
40
+ assert r1.flags["in_bom"] is True
41
+ assert r1.flags["on_board"] is True
42
+ assert r1.flags["exclude_from_sim"] is False
43
+
44
+ def test_component_flags_dnp(self, example_schematic):
45
+ """Test that DNP component has correct flags."""
46
+ parser = SchematicParser(str(example_schematic))
47
+ r3 = parser.get_component_by_reference("R3")
48
+ assert r3 is not None
49
+ assert r3.flags["dnp"] is True
50
+ assert r3.flags["in_bom"] is False
51
+
52
+ def test_get_nets(self, example_schematic):
53
+ """Test getting nets from schematic."""
54
+ parser = SchematicParser(str(example_schematic))
55
+ nets = parser.get_nets()
56
+
57
+ assert len(nets) >= 3
58
+ net_names = [n.name for n in nets]
59
+ assert "GND" in net_names
60
+ assert "+3V3" in net_names
61
+ assert "SPI_CLK" in net_names
62
+
63
+ def test_hierarchical_labels_in_nets(self, example_schematic):
64
+ """Test that hierarchical labels appear in nets."""
65
+ parser = SchematicParser(str(example_schematic))
66
+ nets = parser.get_nets()
67
+
68
+ net_by_name = {n.name: n for n in nets}
69
+ assert "SDA" in net_by_name
70
+ assert "SCL" in net_by_name
71
+ assert net_by_name["SDA"].type == "hierarchical"
72
+ assert net_by_name["SCL"].type == "hierarchical"
73
+
74
+ def test_get_component_by_reference(self, example_schematic):
75
+ """Test getting component by reference."""
76
+ parser = SchematicParser(str(example_schematic))
77
+ r1 = parser.get_component_by_reference("R1")
78
+
79
+ assert r1 is not None
80
+ assert r1.value == "10k"
81
+ assert r1.footprint == "Resistor_SMD:R_0805_2012Metric"
82
+
83
+ def test_search_components(self, example_schematic):
84
+ """Test searching components by pattern."""
85
+ parser = SchematicParser(str(example_schematic))
86
+ results = parser.search_components("10k")
87
+
88
+ assert len(results) > 0
89
+ assert any(c.value == "10k" for c in results)
90
+
91
+
92
+ class TestSchematicTools:
93
+ """Test schematic tool functions."""
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_list_schematic_components(self, example_schematic):
97
+ """Test list_schematic_components tool."""
98
+ result = await schematic.list_schematic_components(str(example_schematic))
99
+
100
+ assert "R1" in result
101
+ assert "10k" in result
102
+ assert "Total:" in result
103
+ # DNP component present, so DNP/BOM columns should appear
104
+ assert "DNP" in result
105
+ assert "In BOM" in result
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_filter_dnp_true(self, example_schematic):
109
+ """Test filtering for DNP-only components."""
110
+ result = await schematic.list_schematic_components(str(example_schematic), filter_dnp=True)
111
+ assert "R3" in result
112
+ assert "R1" not in result
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_filter_dnp_false(self, example_schematic):
116
+ """Test filtering for non-DNP components."""
117
+ result = await schematic.list_schematic_components(str(example_schematic), filter_dnp=False)
118
+ assert "R1" in result
119
+ assert "R3" not in result
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_get_symbol_details(self, example_schematic):
123
+ """Test get_symbol_details tool."""
124
+ result = await schematic.get_symbol_details(str(example_schematic), "R1")
125
+
126
+ assert "R1" in result
127
+ assert "10k" in result
128
+ assert "Footprint" in result
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_search_symbols(self, example_schematic):
132
+ """Test search_symbols tool."""
133
+ result = await schematic.search_symbols(str(example_schematic), "ESP32")
134
+
135
+ assert "U1" in result
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_list_schematic_nets_hierarchical(self, example_schematic):
139
+ """Test list_schematic_nets includes hierarchical labels with type."""
140
+ result = await schematic.list_schematic_nets(str(example_schematic))
141
+
142
+ assert "SDA" in result
143
+ assert "SCL" in result
144
+ assert "hierarchical" in result
145
+ assert "| Type |" in result
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_get_schematic_info(self, example_schematic):
149
+ """Test get_schematic_info tool."""
150
+ result = await schematic.get_schematic_info(str(example_schematic))
151
+
152
+ assert "Example Project" in result
153
+ assert "Components by Type" in result
154
+
155
+
156
+ class TestFindRootSchematic:
157
+ """Test _find_root_schematic helper."""
158
+
159
+ def test_is_root(self, tmp_path):
160
+ """Root schematic (same stem as .kicad_pro) returns None."""
161
+ (tmp_path / "project.kicad_pro").touch()
162
+ (tmp_path / "project.kicad_sch").touch()
163
+ assert _find_root_schematic(tmp_path / "project.kicad_sch") is None
164
+
165
+ def test_is_subsheet(self, tmp_path):
166
+ """Sub-sheet (different stem) returns root schematic path."""
167
+ (tmp_path / "project.kicad_pro").touch()
168
+ (tmp_path / "project.kicad_sch").touch()
169
+ (tmp_path / "subsheet.kicad_sch").touch()
170
+ result = _find_root_schematic(tmp_path / "subsheet.kicad_sch")
171
+ assert result == tmp_path / "project.kicad_sch"
172
+
173
+ def test_no_pro_file(self, tmp_path):
174
+ """No .kicad_pro file returns None."""
175
+ (tmp_path / "sheet.kicad_sch").touch()
176
+ assert _find_root_schematic(tmp_path / "sheet.kicad_sch") is None
177
+
178
+ def test_multiple_pro_files(self, tmp_path):
179
+ """Multiple .kicad_pro files returns None (ambiguous)."""
180
+ (tmp_path / "project_a.kicad_pro").touch()
181
+ (tmp_path / "project_b.kicad_pro").touch()
182
+ (tmp_path / "subsheet.kicad_sch").touch()
183
+ assert _find_root_schematic(tmp_path / "subsheet.kicad_sch") is None
184
+
185
+ def test_root_sch_missing(self, tmp_path):
186
+ """Pro file exists but matching .kicad_sch doesn't — returns None."""
187
+ (tmp_path / "project.kicad_pro").touch()
188
+ (tmp_path / "subsheet.kicad_sch").touch()
189
+ assert _find_root_schematic(tmp_path / "subsheet.kicad_sch") is None
@@ -0,0 +1,69 @@
1
+ """Regression tests for hierarchical (multi-sheet) schematic parsing.
2
+
3
+ Real designs keep most components in sub-sheets while the netlist is exported
4
+ from the root with full hierarchy — the parser must offer a recursive view
5
+ that matches, otherwise netlist-cross-referencing tools see zero components.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ from kicad_mcp_server.parsers.schematic_parser import (
11
+ SchematicComponent,
12
+ SchematicParser,
13
+ _merge_duplicate_references,
14
+ )
15
+
16
+ FIXTURES = Path(__file__).parent.parent / "fixtures" / "hier"
17
+
18
+
19
+ def make_root_parser() -> SchematicParser:
20
+ return SchematicParser(str(FIXTURES / "root.kicad_sch"))
21
+
22
+
23
+ class TestHierarchicalParsing:
24
+ def test_flat_root_returns_only_local_components(self):
25
+ refs = [c.reference for c in make_root_parser().get_components()]
26
+ assert refs == ["R1"]
27
+
28
+ def test_recursive_includes_subsheet_components(self):
29
+ refs = {c.reference for c in make_root_parser().get_components(recursive=True)}
30
+ assert refs == {"R1", "C9"}
31
+
32
+ def test_missing_subsheet_is_tolerated(self):
33
+ # root.kicad_sch also references missing.kicad_sch; the parse must
34
+ # still return the real components, not raise or return nothing.
35
+ refs = {c.reference for c in make_root_parser().get_components(recursive=True)}
36
+ assert "R1" in refs and "C9" in refs
37
+
38
+ def test_subsheet_component_metadata_is_preserved(self):
39
+ comps = {c.reference: c for c in make_root_parser().get_components(recursive=True)}
40
+ assert comps["C9"].value == "100nF"
41
+ assert comps["C9"].footprint == "Capacitor_SMD:C_0603_1608Metric"
42
+
43
+ def test_get_sheets_exposes_hierarchy(self):
44
+ sheets = {s["name"]: s["file"] for s in make_root_parser().get_sheets()}
45
+ assert sheets["Power"] == "child.kicad_sch"
46
+ assert sheets["Ghost"] == "missing.kicad_sch"
47
+
48
+
49
+ class TestMergeDuplicateReferences:
50
+ def _comp(self, ref: str, pins: list[dict]) -> SchematicComponent:
51
+ return SchematicComponent(
52
+ reference=ref, value="V", library_id="Lib:X", pins=list(pins)
53
+ )
54
+
55
+ def test_multi_unit_instances_merge_with_pin_union(self):
56
+ u1a = self._comp("U1", [{"number": "1", "name": "VDD"}])
57
+ u1b = self._comp("U1", [{"number": "2", "name": "GND"}, {"number": "1", "name": "VDD"}])
58
+ merged = _merge_duplicate_references([u1a, u1b])
59
+ assert len(merged) == 1
60
+ numbers = [p["number"] for p in merged[0].pins]
61
+ assert numbers == ["1", "2"] # union, no duplicates
62
+
63
+ def test_distinct_references_preserved(self):
64
+ comps = [self._comp("R1", []), self._comp("C9", [])]
65
+ assert len(_merge_duplicate_references(comps)) == 2
66
+
67
+ def test_empty_references_are_not_collapsed(self):
68
+ comps = [self._comp("", []), self._comp("", [])]
69
+ assert len(_merge_duplicate_references(comps)) == 2