aether-robotics 3.7.2__tar.gz → 3.7.3__tar.gz

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 (105) hide show
  1. {aether_robotics-3.7.2/aether_robotics.egg-info → aether_robotics-3.7.3}/PKG-INFO +2 -2
  2. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/README.md +1 -1
  3. aether_robotics-3.7.3/VERSION +1 -0
  4. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/__init__.py +1 -1
  5. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/tool_discovery.py +50 -6
  6. {aether_robotics-3.7.2 → aether_robotics-3.7.3/aether_robotics.egg-info}/PKG-INFO +2 -2
  7. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/pyproject.toml +1 -1
  8. aether_robotics-3.7.3/tests/test_tool_discovery.py +133 -0
  9. aether_robotics-3.7.2/VERSION +0 -1
  10. aether_robotics-3.7.2/tests/test_tool_discovery.py +0 -46
  11. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/LICENSE +0 -0
  12. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/MANIFEST.in +0 -0
  13. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/__main__.py +0 -0
  14. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/actions/__init__.py +0 -0
  15. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/actions/abstract_actions.py +0 -0
  16. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/__init__.py +0 -0
  17. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/_generator.py +0 -0
  18. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/adeept_hat_v3.py +0 -0
  19. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/base_adapter.py +0 -0
  20. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/drone_adapter.py +0 -0
  21. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/freenove_4wd.py +0 -0
  22. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/freenove_mecanum.py +0 -0
  23. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/l298n_direct_gpio.py +0 -0
  24. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/pca9685_generic.py +0 -0
  25. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/rover_adapter.py +0 -0
  26. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/sunfounder_picarx.py +0 -0
  27. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/universal_adapter.py +0 -0
  28. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/adapters/waveshare_motor_driver.py +0 -0
  29. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/__init__.py +0 -0
  30. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/adaptation_agent.py +0 -0
  31. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/camera_agent.py +0 -0
  32. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/correction_agent.py +0 -0
  33. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/execution_agent.py +0 -0
  34. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/fault_agent.py +0 -0
  35. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/memory_agent.py +0 -0
  36. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/movement_agent.py +0 -0
  37. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/navigation_agent.py +0 -0
  38. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/perception_agent.py +0 -0
  39. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/planner_agent.py +0 -0
  40. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/power_agent.py +0 -0
  41. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/task_manager.py +0 -0
  42. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/agents/thermal_agent.py +0 -0
  43. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/app.py +0 -0
  44. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/capabilities/__init__.py +0 -0
  45. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/capabilities/capability_loader.py +0 -0
  46. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/__init__.py +0 -0
  47. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/auto_installer.py +0 -0
  48. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/auto_updater.py +0 -0
  49. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/banner.py +0 -0
  50. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/calibration.py +0 -0
  51. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/executor.py +0 -0
  52. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/feedback.py +0 -0
  53. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/genome.py +0 -0
  54. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/goal_parser.py +0 -0
  55. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/llm_planner.py +0 -0
  56. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/mapper.py +0 -0
  57. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/mavlink_integration.py +0 -0
  58. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/memory.py +0 -0
  59. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/message_bus.py +0 -0
  60. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/metrics.py +0 -0
  61. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/navigation_engine.py +0 -0
  62. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/planner.py +0 -0
  63. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/task_scheduler.py +0 -0
  64. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/tool_builder.py +0 -0
  65. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/tool_registry.py +0 -0
  66. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/core/visualizer.py +0 -0
  67. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/faults/__init__.py +0 -0
  68. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/faults/fault_detector.py +0 -0
  69. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/faults/fault_injector.py +0 -0
  70. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/simulation/__init__.py +0 -0
  71. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/simulation/environment.py +0 -0
  72. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/simulation/real_perception.py +0 -0
  73. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether/simulation/scenarios.py +0 -0
  74. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether_robotics.egg-info/SOURCES.txt +0 -0
  75. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether_robotics.egg-info/dependency_links.txt +0 -0
  76. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether_robotics.egg-info/entry_points.txt +0 -0
  77. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether_robotics.egg-info/requires.txt +0 -0
  78. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/aether_robotics.egg-info/top_level.txt +0 -0
  79. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/calibrated_camera_only_20260331_121709.json +0 -0
  80. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/calibrated_camera_only_20260331_122446.json +0 -0
  81. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/calibrated_camera_only_20260401_094544.json +0 -0
  82. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/calibrated_camera_only_20260401_094554.json +0 -0
  83. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/drone_v1.json +0 -0
  84. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/configs/rover_v1.json +0 -0
  85. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/context/aether_definitions.txt +0 -0
  86. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/install.sh +0 -0
  87. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/main.py +0 -0
  88. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/requirements.txt +0 -0
  89. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/setup.cfg +0 -0
  90. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_auto_adapter.py +0 -0
  91. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_auto_updater.py +0 -0
  92. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_calibration.py +0 -0
  93. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_calibration_unit.py +0 -0
  94. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_chaos.py +0 -0
  95. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_entrypoint.py +0 -0
  96. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_genome.py +0 -0
  97. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_mapper.py +0 -0
  98. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_mavlink_integration.py +0 -0
  99. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_navigation_engine.py +0 -0
  100. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_persistent_memory.py +0 -0
  101. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_security.py +0 -0
  102. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_task_scheduler.py +0 -0
  103. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_tool_builder.py +0 -0
  104. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/tests/test_yolo_integration.py +0 -0
  105. {aether_robotics-3.7.2 → aether_robotics-3.7.3}/weights/fault_agent.npy +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-robotics
3
- Version: 3.7.2
3
+ Version: 3.7.3
4
4
  Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
5
5
  Author: Chahel Paatur
6
6
  License: MIT
@@ -57,7 +57,7 @@ Dynamic: license-file
57
57
  ████████████████████████████████
58
58
  ```
59
59
 
60
- # AETHER v3.7.2 — Autonomous Robotics Operating System
60
+ # AETHER v3.7.3 — Autonomous Robotics Operating System
61
61
 
62
62
  > AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
63
63
 
@@ -16,7 +16,7 @@
16
16
  ████████████████████████████████
17
17
  ```
18
18
 
19
- # AETHER v3.7.2 — Autonomous Robotics Operating System
19
+ # AETHER v3.7.3 — Autonomous Robotics Operating System
20
20
 
21
21
  > AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
22
22
 
@@ -0,0 +1 @@
1
+ 3.7.3
@@ -1,2 +1,2 @@
1
- __version__ = "3.7.2"
1
+ __version__ = "3.7.3"
2
2
  __author__ = "Chahel Paatur"
@@ -97,13 +97,49 @@ def _probe_gpio_gpiozero() -> Tuple[bool, Dict]:
97
97
 
98
98
 
99
99
  def _probe_i2c() -> Tuple[bool, Dict]:
100
- """Try board + busio for I2C (Adafruit Blinka)."""
100
+ """Enumerate I2C devices on bus 1 so adapter signature_matches can
101
+ detect HATs by address (e.g. SunFounder Robot HAT at 0x14).
102
+
103
+ smbus2 (shipped in the `pi` install extra) is the authoritative
104
+ enumerator: opening SMBus(1) proves the kernel I2C bus is enabled and
105
+ the user has permission, and read_byte() ACK-probes each address.
106
+ This replaces the old Blinka-only check, which required board+busio
107
+ and so reported "not detected" on real Pi HATs that work fine over
108
+ smbus2.
109
+
110
+ Returns (True, {"type", "available": True, "bus": 1, "devices": [...]})
111
+ on success, or (False, {"available": False, "devices": []}) on any
112
+ failure. The "devices" field is ALWAYS a list, never None. The whole
113
+ body is wrapped so a bug here can never crash boot.
114
+ """
101
115
  try:
102
- import board # noqa: F401
103
- import busio # noqa: F401
104
- return True, {"type": "blinka_i2c"}
105
- except (ImportError, RuntimeError, NotImplementedError):
106
- return False, {"detail": "board/busio not available"}
116
+ try:
117
+ import smbus2
118
+ except ImportError:
119
+ # No enumerator available — report I2C absent rather than
120
+ # crash. (Blinka was the old path; smbus2 is required for the
121
+ # actual device scan, so its absence means no enumeration.)
122
+ return False, {"available": False, "devices": []}
123
+
124
+ bus = smbus2.SMBus(1)
125
+ devices: List[int] = []
126
+ try:
127
+ for addr in range(0x03, 0x78):
128
+ try:
129
+ bus.read_byte(addr)
130
+ devices.append(addr)
131
+ except OSError:
132
+ pass # no device ACKed at this address
133
+ finally:
134
+ bus.close()
135
+
136
+ print(f" [I2C] Scanned bus 1: found {len(devices)} device(s): "
137
+ f"{[hex(d) for d in devices]}")
138
+ return True, {"type": "smbus2_i2c", "available": True,
139
+ "bus": 1, "devices": devices}
140
+ except Exception:
141
+ # No /dev/i2c-1, permission error, or any unexpected failure.
142
+ return False, {"available": False, "devices": []}
107
143
 
108
144
 
109
145
  def _probe_mavlink_serial() -> Tuple[bool, Dict]:
@@ -1203,6 +1239,14 @@ class ToolDiscovery:
1203
1239
  if key == "oled":
1204
1240
  iface = info.get("interface", "spi").upper()
1205
1241
  detail = f"{hw_type} {resolution} {iface}"
1242
+ if key == "i2c":
1243
+ devs = info.get("devices", [])
1244
+ bus = info.get("bus", 1)
1245
+ if devs:
1246
+ addrs = ", ".join(hex(d) for d in devs)
1247
+ detail = f"bus {bus}, {len(devs)} device(s) at {addrs}"
1248
+ else:
1249
+ detail = f"bus {bus}, no devices found"
1206
1250
  else:
1207
1251
  if key == "oled":
1208
1252
  if info.get("spi_not_enabled"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aether-robotics
3
- Version: 3.7.2
3
+ Version: 3.7.3
4
4
  Summary: Autonomous multi-agent robotics system with DRL-First Hybrid FDIR
5
5
  Author: Chahel Paatur
6
6
  License: MIT
@@ -57,7 +57,7 @@ Dynamic: license-file
57
57
  ████████████████████████████████
58
58
  ```
59
59
 
60
- # AETHER v3.7.2 — Autonomous Robotics Operating System
60
+ # AETHER v3.7.3 — Autonomous Robotics Operating System
61
61
 
62
62
  > AETHER is the autonomous operating system for robots. Plug in and talk to your robot in plain English and ask it to do anything you want.
63
63
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aether-robotics"
7
- version = "3.7.2"
7
+ version = "3.7.3"
8
8
  description = "Autonomous multi-agent robotics system with DRL-First Hybrid FDIR"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,133 @@
1
+ """Tests for ToolDiscovery: platform detection, manifest structure."""
2
+ import os
3
+ import sys
4
+ from types import ModuleType
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
10
+
11
+ from aether.core.tool_discovery import ToolDiscovery, _probe_i2c
12
+
13
+
14
+ @pytest.fixture(scope="module")
15
+ def manifest():
16
+ d = ToolDiscovery()
17
+ d.discover()
18
+ return d.manifest
19
+
20
+
21
+ class TestToolDiscovery:
22
+ def test_platform_detected(self, manifest):
23
+ assert "platform" in manifest
24
+ assert manifest["platform"] != ""
25
+
26
+ def test_camera_found_or_missing(self, manifest):
27
+ hw = manifest.get("hardware", {})
28
+ cam = hw.get("camera", {})
29
+ # camera key must exist with an 'available' field
30
+ assert "available" in cam
31
+
32
+ def test_manifest_has_capability_score(self, manifest):
33
+ assert "capability_score" in manifest
34
+ score = manifest["capability_score"]
35
+ assert isinstance(score, (int, float))
36
+ assert 0 <= score <= 100
37
+
38
+ def test_available_tools_non_empty(self, manifest):
39
+ tools = manifest.get("available_tools", [])
40
+ assert isinstance(tools, list)
41
+ assert len(tools) > 0
42
+
43
+ def test_missing_capabilities_is_list(self, manifest):
44
+ missing = manifest.get("missing_capabilities", [])
45
+ assert isinstance(missing, list)
46
+ # On a dev machine at least some capabilities will be missing
47
+ # (e.g., GPIO on macOS), so the list should be non-empty
48
+ assert len(missing) > 0
49
+
50
+
51
+ # ── I2C device enumeration (v3.7.3) ────────────────────────────────────
52
+
53
+
54
+ def _fake_smbus2(ack_addrs=None, instantiation_error=None):
55
+ """Build a fake smbus2 module whose SMBus(1).read_byte ACKs (returns)
56
+ only at ack_addrs and raises OSError elsewhere. If instantiation_error
57
+ is set, SMBus(bus) raises it (simulates a missing /dev/i2c-N)."""
58
+ ack = set(ack_addrs or [])
59
+ mod = ModuleType("smbus2")
60
+
61
+ class _SMBus:
62
+ def __init__(self, bus):
63
+ if instantiation_error is not None:
64
+ raise instantiation_error
65
+ self.bus = bus
66
+
67
+ def read_byte(self, addr):
68
+ if addr in ack:
69
+ return 0
70
+ raise OSError("no device ACKed")
71
+
72
+ def close(self):
73
+ pass
74
+
75
+ mod.SMBus = _SMBus
76
+ return mod
77
+
78
+
79
+ def test_probe_i2c_returns_devices_list_on_success():
80
+ fake = _fake_smbus2(ack_addrs=[0x14, 0x40])
81
+ with patch.dict(sys.modules, {"smbus2": fake}):
82
+ ok, info = _probe_i2c()
83
+ assert ok is True
84
+ assert info["available"] is True
85
+ assert info["bus"] == 1
86
+ assert info["type"] == "smbus2_i2c"
87
+ assert info["devices"] == [0x14, 0x40] # ascending scan order
88
+
89
+
90
+ def test_probe_i2c_returns_empty_list_on_no_devices():
91
+ fake = _fake_smbus2(ack_addrs=[]) # nothing ACKs
92
+ with patch.dict(sys.modules, {"smbus2": fake}):
93
+ ok, info = _probe_i2c()
94
+ assert ok is True # bus opened fine, just no peripherals
95
+ assert info["devices"] == []
96
+
97
+
98
+ def test_probe_i2c_returns_false_on_no_bus():
99
+ # SMBus(1) raises FileNotFoundError — /dev/i2c-1 absent / I2C disabled
100
+ fake = _fake_smbus2(instantiation_error=FileNotFoundError("/dev/i2c-1"))
101
+ with patch.dict(sys.modules, {"smbus2": fake}):
102
+ ok, info = _probe_i2c()
103
+ assert ok is False
104
+ assert info["available"] is False
105
+ assert info["devices"] == []
106
+
107
+
108
+ def test_probe_i2c_never_crashes_boot():
109
+ # smbus2 import raises ImportError — must return False cleanly, no crash
110
+ with patch.dict(sys.modules, {"smbus2": None}):
111
+ ok, info = _probe_i2c()
112
+ assert ok is False
113
+ assert info["available"] is False
114
+ assert info["devices"] == []
115
+
116
+
117
+ def test_capability_discovery_printer_shows_i2c_with_devices(capsys):
118
+ """With a device enumerated, the summary's i2c line must read OK with
119
+ the address — never the old '--- not detected'."""
120
+ with patch("aether.core.tool_discovery._probe_i2c",
121
+ return_value=(True, {"type": "smbus2_i2c", "available": True,
122
+ "bus": 1, "devices": [0x14]})):
123
+ d = ToolDiscovery()
124
+ d.discover()
125
+ d.print_summary()
126
+
127
+ out = capsys.readouterr().out
128
+ i2c_line = next(ln for ln in out.splitlines()
129
+ if ln.strip().startswith(("[+] i2c", "[-] i2c")))
130
+ assert "[+]" in i2c_line
131
+ assert "OK" in i2c_line
132
+ assert "0x14" in i2c_line
133
+ assert "not detected" not in i2c_line
@@ -1 +0,0 @@
1
- 3.7.2
@@ -1,46 +0,0 @@
1
- """Tests for ToolDiscovery: platform detection, manifest structure."""
2
- import os
3
- import sys
4
-
5
- import pytest
6
-
7
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8
-
9
- from aether.core.tool_discovery import ToolDiscovery
10
-
11
-
12
- @pytest.fixture(scope="module")
13
- def manifest():
14
- d = ToolDiscovery()
15
- d.discover()
16
- return d.manifest
17
-
18
-
19
- class TestToolDiscovery:
20
- def test_platform_detected(self, manifest):
21
- assert "platform" in manifest
22
- assert manifest["platform"] != ""
23
-
24
- def test_camera_found_or_missing(self, manifest):
25
- hw = manifest.get("hardware", {})
26
- cam = hw.get("camera", {})
27
- # camera key must exist with an 'available' field
28
- assert "available" in cam
29
-
30
- def test_manifest_has_capability_score(self, manifest):
31
- assert "capability_score" in manifest
32
- score = manifest["capability_score"]
33
- assert isinstance(score, (int, float))
34
- assert 0 <= score <= 100
35
-
36
- def test_available_tools_non_empty(self, manifest):
37
- tools = manifest.get("available_tools", [])
38
- assert isinstance(tools, list)
39
- assert len(tools) > 0
40
-
41
- def test_missing_capabilities_is_list(self, manifest):
42
- missing = manifest.get("missing_capabilities", [])
43
- assert isinstance(missing, list)
44
- # On a dev machine at least some capabilities will be missing
45
- # (e.g., GPIO on macOS), so the list should be non-empty
46
- assert len(missing) > 0
File without changes
File without changes