hivescope 0.1.0__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.
- hivescope-0.1.0/PKG-INFO +97 -0
- hivescope-0.1.0/README.md +72 -0
- hivescope-0.1.0/hivescope/__init__.py +67 -0
- hivescope-0.1.0/hivescope/assertions.py +225 -0
- hivescope-0.1.0/hivescope/database.py +113 -0
- hivescope-0.1.0/hivescope/node.py +465 -0
- hivescope-0.1.0/hivescope/plugins/__init__.py +12 -0
- hivescope-0.1.0/hivescope/plugins/agent.py +159 -0
- hivescope-0.1.0/hivescope/plugins/binary.py +82 -0
- hivescope-0.1.0/hivescope/plugins/loopback.py +326 -0
- hivescope-0.1.0/hivescope/plugins/network.py +79 -0
- hivescope-0.1.0/hivescope/plugins/ovoscope_agent.py +251 -0
- hivescope-0.1.0/hivescope/pytest_fixtures.py +181 -0
- hivescope-0.1.0/hivescope/recorder.py +111 -0
- hivescope-0.1.0/hivescope/scenarios.py +240 -0
- hivescope-0.1.0/hivescope/topology.py +251 -0
- hivescope-0.1.0/hivescope/topology_plot.py +359 -0
- hivescope-0.1.0/hivescope/utils.py +65 -0
- hivescope-0.1.0/hivescope/version.py +8 -0
- hivescope-0.1.0/hivescope.egg-info/PKG-INFO +97 -0
- hivescope-0.1.0/hivescope.egg-info/SOURCES.txt +24 -0
- hivescope-0.1.0/hivescope.egg-info/dependency_links.txt +1 -0
- hivescope-0.1.0/hivescope.egg-info/requires.txt +9 -0
- hivescope-0.1.0/hivescope.egg-info/top_level.txt +1 -0
- hivescope-0.1.0/pyproject.toml +47 -0
- hivescope-0.1.0/setup.cfg +4 -0
hivescope-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivescope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hivescope: E2E testing library for HiveMind protocol
|
|
5
|
+
Author-email: JarbasAi <jarbasai@mailfence.com>
|
|
6
|
+
License: AGPL-3.0
|
|
7
|
+
Keywords: hivemind,protocol,testing,e2e,pytest
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: hivemind-bus-client>=0.7.0a2
|
|
18
|
+
Requires-Dist: hivemind-core>=4.0
|
|
19
|
+
Requires-Dist: ovos-utils>=0.0.40
|
|
20
|
+
Requires-Dist: pytest>=7.4
|
|
21
|
+
Requires-Dist: websockets>=11
|
|
22
|
+
Provides-Extra: ovos
|
|
23
|
+
Requires-Dist: ovoscope>=0.13; extra == "ovos"
|
|
24
|
+
Requires-Dist: ovos-core>=0.0.30; extra == "ovos"
|
|
25
|
+
|
|
26
|
+
# Hivescope
|
|
27
|
+
|
|
28
|
+
A self-contained pytest-based E2E testing library for HiveMind protocol implementations.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install "hivescope @ git+https://github.com/JarbasHiveMind/hivescope@dev"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With OVOS skill-level testing support:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install "hivescope[ovos] @ git+https://github.com/JarbasHiveMind/hivescope@dev"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from hivescope.scenarios import single_satellite
|
|
46
|
+
from hivescope.assertions import assert_handshake_complete, assert_encryption_match
|
|
47
|
+
|
|
48
|
+
def test_handshake():
|
|
49
|
+
builder = single_satellite()
|
|
50
|
+
builder.start_all()
|
|
51
|
+
try:
|
|
52
|
+
master = builder.get_master("M0")
|
|
53
|
+
satellite = builder.get_satellite("S0")
|
|
54
|
+
assert_handshake_complete(master, satellite)
|
|
55
|
+
assert_encryption_match(master, satellite)
|
|
56
|
+
finally:
|
|
57
|
+
builder.stop_all()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Using pytest fixtures (add to `tests/conftest.py`):
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
pytest_plugins = ['hivescope.pytest_fixtures']
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def test_message_forwarded(master_node, satellite_node):
|
|
68
|
+
from ovos_bus_client.message import Message
|
|
69
|
+
satellite_node.send(Message("test:ping", {}))
|
|
70
|
+
master_node.recorder.assert_received("BUS", count=1)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Templates
|
|
74
|
+
|
|
75
|
+
Copy-paste test templates from `templates/` into your repo's `tests/e2e/`:
|
|
76
|
+
|
|
77
|
+
| Template | Covers |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `test_template_handshake.py` | Cipher/encoding agreement, handshake completion |
|
|
80
|
+
| `test_template_routing.py` | Message routing through master |
|
|
81
|
+
| `test_template_acl.py` | ACL enforcement for restricted satellites |
|
|
82
|
+
| `test_template_binary.py` | Binary protocol message handling |
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
| Key | Default | Purpose |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `use_loopback` | `False` | Pass `True` to `add_master()` to use loopback network protocol instead of in-process |
|
|
89
|
+
| `[ovos]` extra | not installed | Enables `OvoscopeAgentProtocol` backed by a live MiniCroft |
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
See [docs/index.md](docs/index.md) for the full public API.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
AGPL-3.0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Hivescope
|
|
2
|
+
|
|
3
|
+
A self-contained pytest-based E2E testing library for HiveMind protocol implementations.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install "hivescope @ git+https://github.com/JarbasHiveMind/hivescope@dev"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With OVOS skill-level testing support:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "hivescope[ovos] @ git+https://github.com/JarbasHiveMind/hivescope@dev"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from hivescope.scenarios import single_satellite
|
|
21
|
+
from hivescope.assertions import assert_handshake_complete, assert_encryption_match
|
|
22
|
+
|
|
23
|
+
def test_handshake():
|
|
24
|
+
builder = single_satellite()
|
|
25
|
+
builder.start_all()
|
|
26
|
+
try:
|
|
27
|
+
master = builder.get_master("M0")
|
|
28
|
+
satellite = builder.get_satellite("S0")
|
|
29
|
+
assert_handshake_complete(master, satellite)
|
|
30
|
+
assert_encryption_match(master, satellite)
|
|
31
|
+
finally:
|
|
32
|
+
builder.stop_all()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Using pytest fixtures (add to `tests/conftest.py`):
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
pytest_plugins = ['hivescope.pytest_fixtures']
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
def test_message_forwarded(master_node, satellite_node):
|
|
43
|
+
from ovos_bus_client.message import Message
|
|
44
|
+
satellite_node.send(Message("test:ping", {}))
|
|
45
|
+
master_node.recorder.assert_received("BUS", count=1)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Templates
|
|
49
|
+
|
|
50
|
+
Copy-paste test templates from `templates/` into your repo's `tests/e2e/`:
|
|
51
|
+
|
|
52
|
+
| Template | Covers |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `test_template_handshake.py` | Cipher/encoding agreement, handshake completion |
|
|
55
|
+
| `test_template_routing.py` | Message routing through master |
|
|
56
|
+
| `test_template_acl.py` | ACL enforcement for restricted satellites |
|
|
57
|
+
| `test_template_binary.py` | Binary protocol message handling |
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
| Key | Default | Purpose |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `use_loopback` | `False` | Pass `True` to `add_master()` to use loopback network protocol instead of in-process |
|
|
64
|
+
| `[ovos]` extra | not installed | Enables `OvoscopeAgentProtocol` backed by a live MiniCroft |
|
|
65
|
+
|
|
66
|
+
## API Reference
|
|
67
|
+
|
|
68
|
+
See [docs/index.md](docs/index.md) for the full public API.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
AGPL-3.0
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hivescope: E2E Testing Library for HiveMind
|
|
3
|
+
|
|
4
|
+
A reusable pytest-based framework for writing end-to-end tests of HiveMind
|
|
5
|
+
protocol implementations. Provides stable APIs for topology simulation,
|
|
6
|
+
message routing verification, and protocol-level assertions.
|
|
7
|
+
|
|
8
|
+
Public API — stable across versions:
|
|
9
|
+
- TopologyBuilder: Builder for test network topologies
|
|
10
|
+
- MasterNode, SatelliteNode, RelayNode: Network node types
|
|
11
|
+
- TestAgentProtocol: Agent protocol backed by FakeBus (fast, deterministic)
|
|
12
|
+
- OvoscopeAgentProtocol: Agent protocol backed by MiniCroft (realistic, slow)
|
|
13
|
+
- TestBinaryProtocol: Binary data handler stub for testing
|
|
14
|
+
- TestNetworkProtocol: Network protocol stub for in-process testing
|
|
15
|
+
- MessageRecorder, RecordedMessage: Message recording and inspection
|
|
16
|
+
- InMemoryClientDatabase: In-memory credential store for testing
|
|
17
|
+
|
|
18
|
+
Fixtures & Helpers (use via conftest.py):
|
|
19
|
+
- pytest fixtures: topology, master_node, satellite_node, etc.
|
|
20
|
+
- Assertion helpers: assert_handshake_complete(), assert_message_routed(), etc.
|
|
21
|
+
- Preset topologies: single_satellite(), three_satellites(), with_relay(), etc.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
from hivescope import TopologyBuilder
|
|
25
|
+
from hivescope.scenarios import single_satellite
|
|
26
|
+
|
|
27
|
+
# Build and run a simple test topology
|
|
28
|
+
b = single_satellite()
|
|
29
|
+
b.start_all()
|
|
30
|
+
# ... test assertions ...
|
|
31
|
+
b.stop_all()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from hivescope.topology import TopologyBuilder
|
|
35
|
+
from hivescope.node import MasterNode, SatelliteNode
|
|
36
|
+
from hivescope.topology import RelayNode
|
|
37
|
+
from hivescope.recorder import MessageRecorder, RecordedMessage
|
|
38
|
+
from hivescope.database import InMemoryClientDatabase
|
|
39
|
+
from hivescope.plugins.agent import TestAgentProtocol
|
|
40
|
+
from hivescope.plugins.binary import TestBinaryProtocol
|
|
41
|
+
from hivescope.plugins.network import TestNetworkProtocol
|
|
42
|
+
|
|
43
|
+
# Optional: OvoscopeAgentProtocol requires ovoscope+ovos-core
|
|
44
|
+
try:
|
|
45
|
+
from hivescope.plugins.ovoscope_agent import OvoscopeAgentProtocol
|
|
46
|
+
except ImportError:
|
|
47
|
+
OvoscopeAgentProtocol = None
|
|
48
|
+
|
|
49
|
+
from hivescope.version import __version__
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Core topology classes (stable)
|
|
53
|
+
"TopologyBuilder",
|
|
54
|
+
"MasterNode",
|
|
55
|
+
"SatelliteNode",
|
|
56
|
+
"RelayNode",
|
|
57
|
+
# Protocol plugins (stable)
|
|
58
|
+
"TestAgentProtocol",
|
|
59
|
+
"TestBinaryProtocol",
|
|
60
|
+
"TestNetworkProtocol",
|
|
61
|
+
"OvoscopeAgentProtocol",
|
|
62
|
+
# Message recording (stable)
|
|
63
|
+
"MessageRecorder",
|
|
64
|
+
"RecordedMessage",
|
|
65
|
+
# Database (stable)
|
|
66
|
+
"InMemoryClientDatabase",
|
|
67
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common assertion helpers for hivescope e2e tests.
|
|
3
|
+
|
|
4
|
+
Simplify protocol-level assertions with helpers like:
|
|
5
|
+
- assert_handshake_complete(master, satellite)
|
|
6
|
+
- assert_message_routed(master, msg_type, count)
|
|
7
|
+
- assert_acl_enforced(master, satellite, msg_type)
|
|
8
|
+
|
|
9
|
+
Each helper performs a specific protocol check and raises AssertionError
|
|
10
|
+
with detailed failure messages if the check fails.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
|
|
14
|
+
from hivescope.assertions import (
|
|
15
|
+
assert_handshake_complete,
|
|
16
|
+
assert_message_routed,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def test_handshake(master_node, satellite_node):
|
|
20
|
+
satellite_node.connect(master_node)
|
|
21
|
+
satellite_node.wait_for_handshake(timeout=5)
|
|
22
|
+
|
|
23
|
+
assert_handshake_complete(master_node, satellite_node)
|
|
24
|
+
assert_message_routed(master_node, "HELLO", count=1)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import Optional, Any
|
|
28
|
+
from hivescope.node import MasterNode, SatelliteNode
|
|
29
|
+
from hivemind_bus_client.message import HiveMessageType
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def assert_handshake_complete(
|
|
33
|
+
master: MasterNode,
|
|
34
|
+
satellite: SatelliteNode,
|
|
35
|
+
timeout: float = 5.0
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Assert that satellite has completed handshake with master.
|
|
39
|
+
|
|
40
|
+
Checks:
|
|
41
|
+
- satellite.crypto_key is not None
|
|
42
|
+
- satellite.handshake_event is set
|
|
43
|
+
- master has registered the satellite peer
|
|
44
|
+
|
|
45
|
+
Raises AssertionError with diagnostic details if any check fails.
|
|
46
|
+
"""
|
|
47
|
+
errors = []
|
|
48
|
+
|
|
49
|
+
if satellite.shim.crypto_key is None:
|
|
50
|
+
errors.append("satellite.shim.crypto_key is None (no crypto negotiated)")
|
|
51
|
+
|
|
52
|
+
if not satellite.shim.handshake_event.is_set():
|
|
53
|
+
errors.append("satellite.shim.handshake_event not set (handshake not complete)")
|
|
54
|
+
|
|
55
|
+
connected_peers = master.connected_peers()
|
|
56
|
+
if satellite.peer not in connected_peers:
|
|
57
|
+
errors.append(
|
|
58
|
+
f"satellite peer '{satellite.peer}' not in master's connected_peers: {connected_peers}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if errors:
|
|
62
|
+
raise AssertionError(
|
|
63
|
+
f"Handshake not complete:\n " + "\n ".join(errors)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def assert_message_routed(
|
|
68
|
+
node,
|
|
69
|
+
msg_type: str,
|
|
70
|
+
count: int = 1,
|
|
71
|
+
direction: Optional[str] = None,
|
|
72
|
+
timeout: float = 2.0
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Assert that a specific message type was routed through a node.
|
|
76
|
+
|
|
77
|
+
Checks:
|
|
78
|
+
- message was recorded by node.recorder
|
|
79
|
+
- count matches expected number
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
node: MasterNode or SatelliteNode with MessageRecorder
|
|
83
|
+
msg_type: HiveMessageType name (e.g., "HELLO", "BUS", "BROADCAST")
|
|
84
|
+
count: Expected number of messages
|
|
85
|
+
direction: Optional "inbound" or "outbound" filter
|
|
86
|
+
timeout: Wait time for message to appear (not implemented yet)
|
|
87
|
+
|
|
88
|
+
Raises AssertionError if count doesn't match.
|
|
89
|
+
"""
|
|
90
|
+
messages = node.recorder.messages
|
|
91
|
+
|
|
92
|
+
if direction:
|
|
93
|
+
messages = [m for m in messages if m.direction == direction]
|
|
94
|
+
|
|
95
|
+
matching = [m for m in messages if m.msg_type == msg_type]
|
|
96
|
+
actual_count = len(matching)
|
|
97
|
+
|
|
98
|
+
if actual_count != count:
|
|
99
|
+
raise AssertionError(
|
|
100
|
+
f"Expected {count} '{msg_type}' messages, got {actual_count}.\n"
|
|
101
|
+
f"All messages: {[m.msg_type for m in node.recorder.messages]}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def assert_acl_enforced(
|
|
106
|
+
master: MasterNode,
|
|
107
|
+
satellite: SatelliteNode,
|
|
108
|
+
msg_type: str,
|
|
109
|
+
allowed: bool = False
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Assert that ACL is enforced for a message type on a satellite.
|
|
113
|
+
|
|
114
|
+
If allowed=False, verify that sending msg_type to satellite is blocked.
|
|
115
|
+
If allowed=True, verify that msg_type is allowed through.
|
|
116
|
+
|
|
117
|
+
This is a placeholder for more complex ACL assertions.
|
|
118
|
+
"""
|
|
119
|
+
# TODO: Implement after ACL enforcement logic is fully understood
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def assert_encryption_match(
|
|
124
|
+
master: MasterNode,
|
|
125
|
+
satellite: SatelliteNode
|
|
126
|
+
) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Assert that the master-side connection and satellite shim have matching
|
|
129
|
+
encryption settings after handshake.
|
|
130
|
+
|
|
131
|
+
Checks:
|
|
132
|
+
- cipher type matches
|
|
133
|
+
- json_encoding matches
|
|
134
|
+
|
|
135
|
+
Raises AssertionError if settings don't match.
|
|
136
|
+
"""
|
|
137
|
+
errors = []
|
|
138
|
+
|
|
139
|
+
master_conn = next(
|
|
140
|
+
(c for c in master.hm_protocol.clients.values()
|
|
141
|
+
if c.peer == satellite.peer),
|
|
142
|
+
None,
|
|
143
|
+
)
|
|
144
|
+
if master_conn is None:
|
|
145
|
+
raise AssertionError(
|
|
146
|
+
f"satellite peer '{satellite.peer}' not registered at master; "
|
|
147
|
+
"cannot compare encryption settings"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if master_conn.cipher != satellite.shim.cipher:
|
|
151
|
+
errors.append(
|
|
152
|
+
f"cipher mismatch: master={master_conn.cipher}, "
|
|
153
|
+
f"satellite={satellite.shim.cipher}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if master_conn.json_encoding != satellite.shim.json_encoding:
|
|
157
|
+
errors.append(
|
|
158
|
+
f"json_encoding mismatch: master={master_conn.json_encoding}, "
|
|
159
|
+
f"satellite={satellite.shim.json_encoding}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if errors:
|
|
163
|
+
raise AssertionError(
|
|
164
|
+
f"Encryption settings don't match:\n " + "\n ".join(errors)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def assert_client_registered(
|
|
169
|
+
master: MasterNode,
|
|
170
|
+
peer: str
|
|
171
|
+
) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Assert that a client is registered in master's connected_peers.
|
|
174
|
+
|
|
175
|
+
Raises AssertionError if peer is not registered.
|
|
176
|
+
"""
|
|
177
|
+
connected = master.connected_peers()
|
|
178
|
+
if peer not in connected:
|
|
179
|
+
raise AssertionError(
|
|
180
|
+
f"Peer '{peer}' not registered in master. "
|
|
181
|
+
f"Connected peers: {connected}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def assert_client_not_registered(
|
|
186
|
+
master: MasterNode,
|
|
187
|
+
peer: str
|
|
188
|
+
) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Assert that a client is NOT registered in master's connected_peers.
|
|
191
|
+
|
|
192
|
+
Raises AssertionError if peer is registered.
|
|
193
|
+
"""
|
|
194
|
+
connected = master.connected_peers()
|
|
195
|
+
if peer in connected:
|
|
196
|
+
raise AssertionError(
|
|
197
|
+
f"Peer '{peer}' is registered in master. "
|
|
198
|
+
f"Connected peers: {connected}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def assert_message_received_by(
|
|
203
|
+
node,
|
|
204
|
+
msg_type: str,
|
|
205
|
+
count: int = 1
|
|
206
|
+
) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Assert that node's recorder has received a message type.
|
|
209
|
+
|
|
210
|
+
Convenience wrapper for assert_message_routed with direction='inbound'.
|
|
211
|
+
"""
|
|
212
|
+
assert_message_routed(node, msg_type, count=count, direction="inbound")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def assert_message_sent_by(
|
|
216
|
+
node,
|
|
217
|
+
msg_type: str,
|
|
218
|
+
count: int = 1
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Assert that node's recorder has sent a message type.
|
|
222
|
+
|
|
223
|
+
Convenience wrapper for assert_message_routed with direction='outbound'.
|
|
224
|
+
"""
|
|
225
|
+
assert_message_routed(node, msg_type, count=count, direction="outbound")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-memory ClientDatabase that avoids any plugin loading or disk I/O.
|
|
3
|
+
Implements the same public interface as hivemind_core.database.ClientDatabase
|
|
4
|
+
so it can be passed directly to HiveMindListenerProtocol.
|
|
5
|
+
"""
|
|
6
|
+
from typing import List, Optional, Iterable
|
|
7
|
+
|
|
8
|
+
from hivemind_plugin_manager.database import Client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InMemoryClientDatabase:
|
|
12
|
+
"""Drop-in replacement for ClientDatabase backed by a plain dict."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._clients: dict[str, Client] = {} # keyed by api_key
|
|
16
|
+
|
|
17
|
+
# --- write API ---
|
|
18
|
+
|
|
19
|
+
def add_client(self,
|
|
20
|
+
name: str,
|
|
21
|
+
key: str = "",
|
|
22
|
+
admin: bool = False,
|
|
23
|
+
intent_blacklist: Optional[List[str]] = None,
|
|
24
|
+
skill_blacklist: Optional[List[str]] = None,
|
|
25
|
+
message_blacklist: Optional[List[str]] = None,
|
|
26
|
+
allowed_types: Optional[List[str]] = None,
|
|
27
|
+
crypto_key: Optional[str] = None,
|
|
28
|
+
password: Optional[str] = None,
|
|
29
|
+
can_escalate: bool = True,
|
|
30
|
+
can_propagate: bool = True,
|
|
31
|
+
can_broadcast: bool = True) -> bool:
|
|
32
|
+
if crypto_key is not None:
|
|
33
|
+
crypto_key = crypto_key[:16]
|
|
34
|
+
existing = self.get_client_by_api_key(key)
|
|
35
|
+
if existing:
|
|
36
|
+
if name:
|
|
37
|
+
existing.name = name
|
|
38
|
+
if intent_blacklist is not None:
|
|
39
|
+
existing.intent_blacklist = intent_blacklist
|
|
40
|
+
if skill_blacklist is not None:
|
|
41
|
+
existing.skill_blacklist = skill_blacklist
|
|
42
|
+
if message_blacklist is not None:
|
|
43
|
+
existing.message_blacklist = message_blacklist
|
|
44
|
+
if allowed_types is not None:
|
|
45
|
+
existing.allowed_types = allowed_types
|
|
46
|
+
existing.is_admin = admin
|
|
47
|
+
if crypto_key:
|
|
48
|
+
existing.crypto_key = crypto_key
|
|
49
|
+
if password:
|
|
50
|
+
existing.password = password
|
|
51
|
+
existing.can_escalate = can_escalate
|
|
52
|
+
existing.can_propagate = can_propagate
|
|
53
|
+
existing.can_broadcast = can_broadcast
|
|
54
|
+
self._clients[key] = existing
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
client = Client(
|
|
58
|
+
api_key=key,
|
|
59
|
+
name=name,
|
|
60
|
+
client_id=self.total_clients() + 1,
|
|
61
|
+
is_admin=admin,
|
|
62
|
+
intent_blacklist=intent_blacklist,
|
|
63
|
+
skill_blacklist=skill_blacklist,
|
|
64
|
+
message_blacklist=message_blacklist,
|
|
65
|
+
allowed_types=allowed_types,
|
|
66
|
+
crypto_key=crypto_key,
|
|
67
|
+
password=password,
|
|
68
|
+
can_escalate=can_escalate,
|
|
69
|
+
can_propagate=can_propagate,
|
|
70
|
+
can_broadcast=can_broadcast,
|
|
71
|
+
)
|
|
72
|
+
self._clients[key] = client
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
def update_item(self, client: Client) -> bool:
|
|
76
|
+
self._clients[client.api_key] = client
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
def delete_client(self, key: str) -> bool:
|
|
80
|
+
if key in self._clients:
|
|
81
|
+
# mark revoked (don't reuse client_id)
|
|
82
|
+
c = self._clients[key]
|
|
83
|
+
self._clients[key] = Client(client_id=c.client_id, api_key="revoked")
|
|
84
|
+
return True
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# --- read API ---
|
|
88
|
+
|
|
89
|
+
def get_client_by_api_key(self, api_key: str) -> Optional[Client]:
|
|
90
|
+
return self._clients.get(api_key)
|
|
91
|
+
|
|
92
|
+
def get_clients_by_name(self, name: str) -> List[Client]:
|
|
93
|
+
return [c for c in self._clients.values() if c.name == name]
|
|
94
|
+
|
|
95
|
+
def total_clients(self) -> int:
|
|
96
|
+
return len(self._clients)
|
|
97
|
+
|
|
98
|
+
# --- lifecycle ---
|
|
99
|
+
|
|
100
|
+
def sync(self):
|
|
101
|
+
pass # nothing to reload from disk
|
|
102
|
+
|
|
103
|
+
def __enter__(self):
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __exit__(self, _type, value, traceback):
|
|
107
|
+
pass # nothing to commit
|
|
108
|
+
|
|
109
|
+
def __iter__(self) -> Iterable[Client]:
|
|
110
|
+
return iter(list(self._clients.values()))
|
|
111
|
+
|
|
112
|
+
def __len__(self) -> int:
|
|
113
|
+
return len(self._clients)
|