agentic-comm 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.
- agentic_comm-0.1.0/.gitignore +30 -0
- agentic_comm-0.1.0/.python-version +1 -0
- agentic_comm-0.1.0/LICENSE +21 -0
- agentic_comm-0.1.0/PKG-INFO +105 -0
- agentic_comm-0.1.0/README.md +78 -0
- agentic_comm-0.1.0/pyproject.toml +50 -0
- agentic_comm-0.1.0/src/agentic_comm/__init__.py +40 -0
- agentic_comm-0.1.0/src/agentic_comm/cli_bridge.py +140 -0
- agentic_comm-0.1.0/src/agentic_comm/errors.py +49 -0
- agentic_comm-0.1.0/src/agentic_comm/models.py +51 -0
- agentic_comm-0.1.0/src/agentic_comm/store.py +145 -0
- agentic_comm-0.1.0/tests/__init__.py +0 -0
- agentic_comm-0.1.0/tests/conftest.py +29 -0
- agentic_comm-0.1.0/tests/test_comm.py +420 -0
- agentic_comm-0.1.0/tests/test_imports.py +30 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
target/
|
|
3
|
+
*.swp
|
|
4
|
+
*.swo
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.egg-info/
|
|
11
|
+
*.egg
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
.eggs/
|
|
15
|
+
*.whl
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.iml
|
|
24
|
+
.fleet/
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
*.swp
|
|
30
|
+
*~
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13.2
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Agentra Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-comm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for AgenticComm — agent-to-agent communication engine
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentralabs/agentic-comm
|
|
6
|
+
Project-URL: Documentation, https://github.com/agentralabs/agentic-comm/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/agentralabs/agentic-comm
|
|
8
|
+
Author: Agentra Labs
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,communication,messaging,pubsub
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# AgenticComm Python SDK
|
|
29
|
+
|
|
30
|
+
Python SDK for AgenticComm -- portable binary communication for AI agents. Channel-based messaging, zero dependencies.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install agentic-comm
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from agentic_comm import CommStore
|
|
42
|
+
|
|
43
|
+
store = CommStore("my_agents.acomm")
|
|
44
|
+
print(store.info())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Core Operations
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from agentic_comm import CommStore, Channel, Message
|
|
51
|
+
|
|
52
|
+
store = CommStore("my_agents.acomm")
|
|
53
|
+
|
|
54
|
+
# Channel management
|
|
55
|
+
store.create_channel("task-queue", description="Work items")
|
|
56
|
+
channels = store.list_channels()
|
|
57
|
+
|
|
58
|
+
# Send messages
|
|
59
|
+
store.send("task-queue", "Build the login page")
|
|
60
|
+
store.send("task-queue", "Review PR #42")
|
|
61
|
+
|
|
62
|
+
# Receive messages
|
|
63
|
+
messages = store.receive("task-queue")
|
|
64
|
+
for msg in messages:
|
|
65
|
+
print(f"[{msg.timestamp}] {msg.content}")
|
|
66
|
+
|
|
67
|
+
# Search
|
|
68
|
+
results = store.search("login")
|
|
69
|
+
|
|
70
|
+
# Broadcast
|
|
71
|
+
store.broadcast("System update: v1.2.0 deployed")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Subscriptions
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Subscribe an agent to a channel
|
|
78
|
+
store.subscribe("task-queue", "worker-agent-1")
|
|
79
|
+
|
|
80
|
+
# Check subscriptions
|
|
81
|
+
subs = store.subscriptions("task-queue")
|
|
82
|
+
|
|
83
|
+
# Poll all subscribed channels
|
|
84
|
+
new_messages = store.poll("worker-agent-1")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Test Coverage
|
|
88
|
+
|
|
89
|
+
Tests across import validation, model verification, and CLI bridge integration.
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python >= 3.10
|
|
94
|
+
- `acomm` binary (Rust core engine) -- install via `cargo install agentic-comm`
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
- [API Reference](../docs/public/api-reference.md)
|
|
99
|
+
- [Integration Guide](../docs/public/integration-guide.md)
|
|
100
|
+
- [Benchmarks](../docs/public/benchmarks.md)
|
|
101
|
+
- [Full README](../README.md)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# AgenticComm Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for AgenticComm -- portable binary communication for AI agents. Channel-based messaging, zero dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentic-comm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentic_comm import CommStore
|
|
15
|
+
|
|
16
|
+
store = CommStore("my_agents.acomm")
|
|
17
|
+
print(store.info())
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Core Operations
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from agentic_comm import CommStore, Channel, Message
|
|
24
|
+
|
|
25
|
+
store = CommStore("my_agents.acomm")
|
|
26
|
+
|
|
27
|
+
# Channel management
|
|
28
|
+
store.create_channel("task-queue", description="Work items")
|
|
29
|
+
channels = store.list_channels()
|
|
30
|
+
|
|
31
|
+
# Send messages
|
|
32
|
+
store.send("task-queue", "Build the login page")
|
|
33
|
+
store.send("task-queue", "Review PR #42")
|
|
34
|
+
|
|
35
|
+
# Receive messages
|
|
36
|
+
messages = store.receive("task-queue")
|
|
37
|
+
for msg in messages:
|
|
38
|
+
print(f"[{msg.timestamp}] {msg.content}")
|
|
39
|
+
|
|
40
|
+
# Search
|
|
41
|
+
results = store.search("login")
|
|
42
|
+
|
|
43
|
+
# Broadcast
|
|
44
|
+
store.broadcast("System update: v1.2.0 deployed")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Subscriptions
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# Subscribe an agent to a channel
|
|
51
|
+
store.subscribe("task-queue", "worker-agent-1")
|
|
52
|
+
|
|
53
|
+
# Check subscriptions
|
|
54
|
+
subs = store.subscriptions("task-queue")
|
|
55
|
+
|
|
56
|
+
# Poll all subscribed channels
|
|
57
|
+
new_messages = store.poll("worker-agent-1")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Test Coverage
|
|
61
|
+
|
|
62
|
+
Tests across import validation, model verification, and CLI bridge integration.
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Python >= 3.10
|
|
67
|
+
- `acomm` binary (Rust core engine) -- install via `cargo install agentic-comm`
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [API Reference](../docs/public/api-reference.md)
|
|
72
|
+
- [Integration Guide](../docs/public/integration-guide.md)
|
|
73
|
+
- [Benchmarks](../docs/public/benchmarks.md)
|
|
74
|
+
- [Full README](../README.md)
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentic-comm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python bindings for AgenticComm — agent-to-agent communication engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Agentra Labs" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agent", "communication", "messaging", "pubsub"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-cov>=5.0",
|
|
34
|
+
"mypy>=1.10",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/agentralabs/agentic-comm"
|
|
39
|
+
Documentation = "https://github.com/agentralabs/agentic-comm/tree/main/docs"
|
|
40
|
+
Repository = "https://github.com/agentralabs/agentic-comm"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/agentic_comm"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
python_version = "3.10"
|
|
50
|
+
strict = true
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""AgenticComm — Agent-to-agent communication engine.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
>>> from agentic_comm import CommStore
|
|
6
|
+
>>> store = CommStore("agents.acomm")
|
|
7
|
+
>>> store.create_channel("general", "broadcast")
|
|
8
|
+
>>> store.send_message("general", "agent-1", "Hello!")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from agentic_comm.store import CommStore
|
|
12
|
+
from agentic_comm.models import (
|
|
13
|
+
Channel,
|
|
14
|
+
Message,
|
|
15
|
+
Subscription,
|
|
16
|
+
StoreInfo,
|
|
17
|
+
)
|
|
18
|
+
from agentic_comm.errors import (
|
|
19
|
+
AcommError,
|
|
20
|
+
AcommNotFoundError,
|
|
21
|
+
CLIError,
|
|
22
|
+
StoreNotFoundError,
|
|
23
|
+
ChannelNotFoundError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CommStore",
|
|
30
|
+
"Channel",
|
|
31
|
+
"Message",
|
|
32
|
+
"Subscription",
|
|
33
|
+
"StoreInfo",
|
|
34
|
+
"AcommError",
|
|
35
|
+
"AcommNotFoundError",
|
|
36
|
+
"CLIError",
|
|
37
|
+
"StoreNotFoundError",
|
|
38
|
+
"ChannelNotFoundError",
|
|
39
|
+
"ValidationError",
|
|
40
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Low-level acomm CLI wrapper. NOT part of the public API.
|
|
2
|
+
|
|
3
|
+
This module handles subprocess management, CLI binary discovery,
|
|
4
|
+
output parsing, and error translation. The CommStore class calls this;
|
|
5
|
+
users never import it directly.
|
|
6
|
+
|
|
7
|
+
In a future version, this will be replaced by direct FFI bindings
|
|
8
|
+
to the Rust library. The public API (CommStore class) will not change.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from agentic_comm.errors import AcommNotFoundError, CLIError
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
DEFAULT_TIMEOUT = 30
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_acomm_binary(override: str | Path | None = None) -> Path:
|
|
28
|
+
"""Find the acomm CLI binary.
|
|
29
|
+
|
|
30
|
+
Search order:
|
|
31
|
+
1. Explicit override path (if provided)
|
|
32
|
+
2. ACOMM_BINARY environment variable
|
|
33
|
+
3. System PATH (shutil.which)
|
|
34
|
+
4. ~/.cargo/bin/acomm (Rust cargo install location)
|
|
35
|
+
5. /usr/local/bin/acomm
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
override: Explicit path to the binary. Checked first if provided.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Path to the acomm binary.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
AcommNotFoundError: If the binary cannot be found anywhere.
|
|
45
|
+
"""
|
|
46
|
+
searched: list[str] = []
|
|
47
|
+
|
|
48
|
+
if override is not None:
|
|
49
|
+
p = Path(override)
|
|
50
|
+
searched.append(str(p))
|
|
51
|
+
if p.is_file() and os.access(str(p), os.X_OK):
|
|
52
|
+
return p
|
|
53
|
+
raise AcommNotFoundError(searched)
|
|
54
|
+
|
|
55
|
+
env_path = os.environ.get("ACOMM_BINARY")
|
|
56
|
+
if env_path:
|
|
57
|
+
p = Path(env_path)
|
|
58
|
+
searched.append(str(p))
|
|
59
|
+
if p.is_file() and os.access(str(p), os.X_OK):
|
|
60
|
+
return p
|
|
61
|
+
|
|
62
|
+
which_result = shutil.which("acomm")
|
|
63
|
+
searched.append("PATH")
|
|
64
|
+
if which_result:
|
|
65
|
+
return Path(which_result)
|
|
66
|
+
|
|
67
|
+
cargo_bin = Path.home() / ".cargo" / "bin" / "acomm"
|
|
68
|
+
searched.append(str(cargo_bin))
|
|
69
|
+
if cargo_bin.is_file() and os.access(str(cargo_bin), os.X_OK):
|
|
70
|
+
return cargo_bin
|
|
71
|
+
|
|
72
|
+
usr_local = Path("/usr/local/bin/acomm")
|
|
73
|
+
searched.append(str(usr_local))
|
|
74
|
+
if usr_local.is_file() and os.access(str(usr_local), os.X_OK):
|
|
75
|
+
return usr_local
|
|
76
|
+
|
|
77
|
+
raise AcommNotFoundError(searched)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_cli(
|
|
81
|
+
binary: Path,
|
|
82
|
+
args: list[str],
|
|
83
|
+
*,
|
|
84
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
85
|
+
input_data: str | None = None,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Run an acomm CLI command and return stdout.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
binary: Path to the acomm binary.
|
|
91
|
+
args: Command-line arguments.
|
|
92
|
+
timeout: Subprocess timeout in seconds.
|
|
93
|
+
input_data: Optional stdin data.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
stdout as a string.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
CLIError: If the process exits with non-zero status.
|
|
100
|
+
"""
|
|
101
|
+
cmd = [str(binary)] + args
|
|
102
|
+
logger.debug("Running: %s", " ".join(cmd))
|
|
103
|
+
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
cmd,
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
input=input_data,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if result.returncode != 0:
|
|
113
|
+
raise CLIError(result.returncode, result.stderr.strip())
|
|
114
|
+
|
|
115
|
+
return result.stdout
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_cli_json(
|
|
119
|
+
binary: Path,
|
|
120
|
+
args: list[str],
|
|
121
|
+
*,
|
|
122
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
123
|
+
) -> dict | list:
|
|
124
|
+
"""Run an acomm CLI command and parse JSON output.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
binary: Path to the acomm binary.
|
|
128
|
+
args: Command-line arguments (--json is appended automatically).
|
|
129
|
+
timeout: Subprocess timeout in seconds.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Parsed JSON output.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
CLIError: If the process exits with non-zero status.
|
|
136
|
+
"""
|
|
137
|
+
if "--json" not in args:
|
|
138
|
+
args = args + ["--json"]
|
|
139
|
+
stdout = run_cli(binary, args, timeout=timeout)
|
|
140
|
+
return json.loads(stdout)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Error types for the AgenticComm Python bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AcommError(Exception):
|
|
7
|
+
"""Base exception for all AgenticComm errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AcommNotFoundError(AcommError):
|
|
11
|
+
"""Raised when the acomm CLI binary cannot be found."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, searched: list[str] | None = None) -> None:
|
|
14
|
+
locations = ", ".join(searched) if searched else "(none)"
|
|
15
|
+
super().__init__(
|
|
16
|
+
f"acomm binary not found. Searched: {locations}. "
|
|
17
|
+
"Install with: curl -sSL https://raw.githubusercontent.com/"
|
|
18
|
+
"agentralabs/agentic-comm/main/scripts/install.sh | bash"
|
|
19
|
+
)
|
|
20
|
+
self.searched = searched or []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CLIError(AcommError):
|
|
24
|
+
"""Raised when the acomm CLI returns a non-zero exit code."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, returncode: int, stderr: str) -> None:
|
|
27
|
+
super().__init__(f"acomm exited with code {returncode}: {stderr}")
|
|
28
|
+
self.returncode = returncode
|
|
29
|
+
self.stderr = stderr
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StoreNotFoundError(AcommError):
|
|
33
|
+
"""Raised when a .acomm file does not exist."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, path: str) -> None:
|
|
36
|
+
super().__init__(f"Store file not found: {path}")
|
|
37
|
+
self.path = path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ChannelNotFoundError(AcommError):
|
|
41
|
+
"""Raised when a referenced channel does not exist."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, channel_id: str) -> None:
|
|
44
|
+
super().__init__(f"Channel not found: {channel_id}")
|
|
45
|
+
self.channel_id = channel_id
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ValidationError(AcommError):
|
|
49
|
+
"""Raised when input validation fails."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Data models for AgenticComm Python bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Channel:
|
|
11
|
+
"""A communication channel."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
name: str
|
|
15
|
+
channel_type: str
|
|
16
|
+
created_at: str
|
|
17
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Message:
|
|
22
|
+
"""A message in a channel."""
|
|
23
|
+
|
|
24
|
+
id: str
|
|
25
|
+
channel_id: str
|
|
26
|
+
sender: str
|
|
27
|
+
content: str
|
|
28
|
+
timestamp: str
|
|
29
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Subscription:
|
|
34
|
+
"""A subscription to a channel or topic."""
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
channel_id: str
|
|
38
|
+
subscriber: str
|
|
39
|
+
pattern: str | None = None
|
|
40
|
+
created_at: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class StoreInfo:
|
|
45
|
+
"""Summary information about a CommStore."""
|
|
46
|
+
|
|
47
|
+
path: str
|
|
48
|
+
channels: int
|
|
49
|
+
messages: int
|
|
50
|
+
subscriptions: int
|
|
51
|
+
file_size: int = 0
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""CommStore — High-level Python API for AgenticComm.
|
|
2
|
+
|
|
3
|
+
Wraps the acomm CLI binary to provide a Pythonic interface for
|
|
4
|
+
agent-to-agent communication with channels, pub/sub, and messaging.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
>>> from agentic_comm import CommStore
|
|
9
|
+
>>> store = CommStore("agents.acomm")
|
|
10
|
+
>>> store.create_channel("general", "broadcast")
|
|
11
|
+
>>> store.send_message("general", "agent-1", "Hello!")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from agentic_comm.cli_bridge import find_acomm_binary, run_cli, run_cli_json
|
|
20
|
+
from agentic_comm.models import Channel, Message, StoreInfo
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CommStore:
|
|
26
|
+
"""Python interface to a .acomm communication store.
|
|
27
|
+
|
|
28
|
+
The store wraps the acomm CLI binary. All operations are executed
|
|
29
|
+
as subprocess calls to the Rust binary, ensuring format compatibility
|
|
30
|
+
and leveraging the same integrity checks (Blake3 / CRC32).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: Path to the .acomm file. Created if it does not exist.
|
|
34
|
+
binary: Explicit path to the acomm binary. Auto-detected if None.
|
|
35
|
+
timeout: Default subprocess timeout in seconds.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
path: str | Path,
|
|
41
|
+
*,
|
|
42
|
+
binary: str | Path | None = None,
|
|
43
|
+
timeout: int = 30,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.path = Path(path)
|
|
46
|
+
self._binary = find_acomm_binary(binary)
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
|
|
49
|
+
def _run(self, args: list[str]) -> str:
|
|
50
|
+
return run_cli(
|
|
51
|
+
self._binary,
|
|
52
|
+
["--store", str(self.path)] + args,
|
|
53
|
+
timeout=self._timeout,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _run_json(self, args: list[str]) -> dict | list:
|
|
57
|
+
return run_cli_json(
|
|
58
|
+
self._binary,
|
|
59
|
+
["--store", str(self.path)] + args,
|
|
60
|
+
timeout=self._timeout,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def info(self) -> StoreInfo:
|
|
64
|
+
"""Get summary information about this store."""
|
|
65
|
+
data = self._run_json(["info"])
|
|
66
|
+
assert isinstance(data, dict)
|
|
67
|
+
return StoreInfo(
|
|
68
|
+
path=str(self.path),
|
|
69
|
+
channels=data.get("channels", 0),
|
|
70
|
+
messages=data.get("messages", 0),
|
|
71
|
+
subscriptions=data.get("subscriptions", 0),
|
|
72
|
+
file_size=data.get("file_size", 0),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def create_channel(self, name: str, channel_type: str = "broadcast") -> str:
|
|
76
|
+
"""Create a new communication channel.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
name: Human-readable channel name.
|
|
80
|
+
channel_type: Channel type (broadcast, direct, topic).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The channel ID.
|
|
84
|
+
"""
|
|
85
|
+
data = self._run_json(["channel", "create", name, "--type", channel_type])
|
|
86
|
+
assert isinstance(data, dict)
|
|
87
|
+
return str(data.get("id", ""))
|
|
88
|
+
|
|
89
|
+
def list_channels(self) -> list[Channel]:
|
|
90
|
+
"""List all channels in the store."""
|
|
91
|
+
data = self._run_json(["channel", "list"])
|
|
92
|
+
assert isinstance(data, list)
|
|
93
|
+
return [
|
|
94
|
+
Channel(
|
|
95
|
+
id=ch["id"],
|
|
96
|
+
name=ch["name"],
|
|
97
|
+
channel_type=ch.get("channel_type", "broadcast"),
|
|
98
|
+
created_at=ch.get("created_at", ""),
|
|
99
|
+
metadata=ch.get("metadata", {}),
|
|
100
|
+
)
|
|
101
|
+
for ch in data
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def send_message(self, channel_id: str, sender: str, content: str) -> str:
|
|
105
|
+
"""Send a message to a channel.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
channel_id: Target channel ID.
|
|
109
|
+
sender: Sender identifier.
|
|
110
|
+
content: Message content.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
The message ID.
|
|
114
|
+
"""
|
|
115
|
+
data = self._run_json(
|
|
116
|
+
["message", "send", channel_id, "--sender", sender, "--content", content]
|
|
117
|
+
)
|
|
118
|
+
assert isinstance(data, dict)
|
|
119
|
+
return str(data.get("id", ""))
|
|
120
|
+
|
|
121
|
+
def search_messages(self, query: str) -> list[Message]:
|
|
122
|
+
"""Search messages by content.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
query: Search query string.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of matching messages.
|
|
129
|
+
"""
|
|
130
|
+
data = self._run_json(["message", "search", query])
|
|
131
|
+
assert isinstance(data, list)
|
|
132
|
+
return [
|
|
133
|
+
Message(
|
|
134
|
+
id=msg["id"],
|
|
135
|
+
channel_id=msg.get("channel_id", ""),
|
|
136
|
+
sender=msg.get("sender", ""),
|
|
137
|
+
content=msg.get("content", ""),
|
|
138
|
+
timestamp=msg.get("timestamp", ""),
|
|
139
|
+
metadata=msg.get("metadata", {}),
|
|
140
|
+
)
|
|
141
|
+
for msg in data
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
def __repr__(self) -> str:
|
|
145
|
+
return f"CommStore({str(self.path)!r})"
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared test fixtures for the AgenticComm SDK test suite."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import tempfile
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def tmp_dir():
|
|
11
|
+
"""Temporary directory for comm store files. Cleaned up after test."""
|
|
12
|
+
d = tempfile.mkdtemp(prefix="acomm_test_")
|
|
13
|
+
yield d
|
|
14
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def store_path(tmp_dir):
|
|
19
|
+
"""Path for a temporary comm store file."""
|
|
20
|
+
return str(Path(tmp_dir) / "test.acomm")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def store(store_path):
|
|
25
|
+
"""A CommStore instance with a temporary store file.
|
|
26
|
+
Requires the acomm CLI to be available."""
|
|
27
|
+
from agentic_comm import CommStore
|
|
28
|
+
s = CommStore(store_path)
|
|
29
|
+
return s
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Comprehensive tests for AgenticComm Python SDK.
|
|
2
|
+
|
|
3
|
+
This file tests CommStore, cli_bridge, models, and error classes.
|
|
4
|
+
Does NOT replace test_imports.py -- this is an additional test file.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
from unittest.mock import patch, MagicMock
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from agentic_comm import (
|
|
17
|
+
CommStore,
|
|
18
|
+
Channel,
|
|
19
|
+
Message,
|
|
20
|
+
Subscription,
|
|
21
|
+
StoreInfo,
|
|
22
|
+
AcommError,
|
|
23
|
+
AcommNotFoundError,
|
|
24
|
+
CLIError,
|
|
25
|
+
StoreNotFoundError,
|
|
26
|
+
ChannelNotFoundError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
__version__,
|
|
29
|
+
)
|
|
30
|
+
from agentic_comm.cli_bridge import find_acomm_binary, run_cli, run_cli_json
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# 1. Package Metadata
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestPackageMetadata:
|
|
39
|
+
def test_version_exists(self) -> None:
|
|
40
|
+
assert __version__ is not None
|
|
41
|
+
assert isinstance(__version__, str)
|
|
42
|
+
assert len(__version__) > 0
|
|
43
|
+
|
|
44
|
+
def test_version_semver(self) -> None:
|
|
45
|
+
parts = __version__.split(".")
|
|
46
|
+
assert len(parts) == 3
|
|
47
|
+
assert all(p.isdigit() for p in parts)
|
|
48
|
+
|
|
49
|
+
def test_version_is_010(self) -> None:
|
|
50
|
+
assert __version__ == "0.1.0"
|
|
51
|
+
|
|
52
|
+
def test_import_main_class(self) -> None:
|
|
53
|
+
assert CommStore is not None
|
|
54
|
+
|
|
55
|
+
def test_import_error_classes(self) -> None:
|
|
56
|
+
assert issubclass(AcommError, Exception)
|
|
57
|
+
assert issubclass(AcommNotFoundError, AcommError)
|
|
58
|
+
assert issubclass(CLIError, AcommError)
|
|
59
|
+
assert issubclass(StoreNotFoundError, AcommError)
|
|
60
|
+
assert issubclass(ChannelNotFoundError, AcommError)
|
|
61
|
+
assert issubclass(ValidationError, AcommError)
|
|
62
|
+
|
|
63
|
+
def test_main_class_has_docstring(self) -> None:
|
|
64
|
+
assert CommStore.__doc__ is not None
|
|
65
|
+
assert len(CommStore.__doc__) > 10
|
|
66
|
+
|
|
67
|
+
def test_all_exports_defined(self) -> None:
|
|
68
|
+
import agentic_comm
|
|
69
|
+
assert hasattr(agentic_comm, "__all__")
|
|
70
|
+
assert "CommStore" in agentic_comm.__all__
|
|
71
|
+
assert "AcommError" in agentic_comm.__all__
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# 2. CommStore Initialization
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestInit:
|
|
80
|
+
def test_create_with_string_path(self, tmp_path: Path) -> None:
|
|
81
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
82
|
+
obj = CommStore(str(tmp_path / "test.acomm"))
|
|
83
|
+
assert str(obj.path) == str(tmp_path / "test.acomm")
|
|
84
|
+
|
|
85
|
+
def test_create_with_path_object(self, tmp_path: Path) -> None:
|
|
86
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
87
|
+
path = tmp_path / "test.acomm"
|
|
88
|
+
obj = CommStore(path)
|
|
89
|
+
assert obj.path == path
|
|
90
|
+
|
|
91
|
+
def test_create_with_pure_posix_path(self) -> None:
|
|
92
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
93
|
+
obj = CommStore(PurePosixPath("/tmp/test.acomm"))
|
|
94
|
+
assert "test.acomm" in str(obj.path)
|
|
95
|
+
|
|
96
|
+
def test_path_converted_to_path_object(self, tmp_path: Path) -> None:
|
|
97
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
98
|
+
path = str(tmp_path / "test.acomm")
|
|
99
|
+
obj = CommStore(path)
|
|
100
|
+
assert isinstance(obj.path, Path)
|
|
101
|
+
|
|
102
|
+
def test_custom_binary_path(self, tmp_path: Path) -> None:
|
|
103
|
+
fake_bin = tmp_path / "acomm"
|
|
104
|
+
fake_bin.touch()
|
|
105
|
+
fake_bin.chmod(0o755)
|
|
106
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=fake_bin):
|
|
107
|
+
obj = CommStore(str(tmp_path / "test.acomm"), binary=fake_bin)
|
|
108
|
+
assert obj._binary == fake_bin
|
|
109
|
+
|
|
110
|
+
def test_custom_timeout(self, tmp_path: Path) -> None:
|
|
111
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
112
|
+
obj = CommStore(str(tmp_path / "test.acomm"), timeout=60)
|
|
113
|
+
assert obj._timeout == 60
|
|
114
|
+
|
|
115
|
+
def test_default_timeout_is_30(self, tmp_path: Path) -> None:
|
|
116
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
117
|
+
obj = CommStore(str(tmp_path / "test.acomm"))
|
|
118
|
+
assert obj._timeout == 30
|
|
119
|
+
|
|
120
|
+
def test_repr_does_not_crash(self, tmp_path: Path) -> None:
|
|
121
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
122
|
+
obj = CommStore(str(tmp_path / "test.acomm"))
|
|
123
|
+
r = repr(obj)
|
|
124
|
+
assert isinstance(r, str)
|
|
125
|
+
assert "CommStore" in r
|
|
126
|
+
|
|
127
|
+
def test_repr_contains_path(self, tmp_path: Path) -> None:
|
|
128
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
129
|
+
obj = CommStore(str(tmp_path / "test.acomm"))
|
|
130
|
+
assert "test.acomm" in repr(obj)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# 3. Binary Resolution (find_acomm_binary)
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestBinaryResolution:
|
|
139
|
+
def test_missing_binary_raises(self) -> None:
|
|
140
|
+
with patch("agentic_comm.cli_bridge.shutil.which", return_value=None), \
|
|
141
|
+
patch.dict(os.environ, {}, clear=True), \
|
|
142
|
+
patch("pathlib.Path.is_file", return_value=False):
|
|
143
|
+
with pytest.raises(AcommNotFoundError):
|
|
144
|
+
find_acomm_binary()
|
|
145
|
+
|
|
146
|
+
def test_error_is_acomm_subclass(self) -> None:
|
|
147
|
+
assert issubclass(AcommNotFoundError, AcommError)
|
|
148
|
+
|
|
149
|
+
def test_error_contains_install_hint(self) -> None:
|
|
150
|
+
with patch("agentic_comm.cli_bridge.shutil.which", return_value=None), \
|
|
151
|
+
patch.dict(os.environ, {}, clear=True), \
|
|
152
|
+
patch("pathlib.Path.is_file", return_value=False):
|
|
153
|
+
with pytest.raises(AcommNotFoundError, match="Install"):
|
|
154
|
+
find_acomm_binary()
|
|
155
|
+
|
|
156
|
+
def test_override_nonexistent_raises(self, tmp_path: Path) -> None:
|
|
157
|
+
fake = tmp_path / "nonexistent_binary"
|
|
158
|
+
with pytest.raises(AcommNotFoundError):
|
|
159
|
+
find_acomm_binary(fake)
|
|
160
|
+
|
|
161
|
+
def test_override_existing_executable(self, tmp_path: Path) -> None:
|
|
162
|
+
fake = tmp_path / "acomm"
|
|
163
|
+
fake.touch()
|
|
164
|
+
fake.chmod(0o755)
|
|
165
|
+
result = find_acomm_binary(fake)
|
|
166
|
+
assert result == fake
|
|
167
|
+
|
|
168
|
+
def test_which_found(self) -> None:
|
|
169
|
+
with patch("shutil.which", return_value="/usr/local/bin/acomm"), \
|
|
170
|
+
patch.dict(os.environ, {}, clear=True):
|
|
171
|
+
result = find_acomm_binary()
|
|
172
|
+
assert result == Path("/usr/local/bin/acomm")
|
|
173
|
+
|
|
174
|
+
def test_env_var_takes_priority(self, tmp_path: Path) -> None:
|
|
175
|
+
fake = tmp_path / "acomm"
|
|
176
|
+
fake.touch()
|
|
177
|
+
fake.chmod(0o755)
|
|
178
|
+
with patch.dict(os.environ, {"ACOMM_BINARY": str(fake)}):
|
|
179
|
+
result = find_acomm_binary()
|
|
180
|
+
assert result == fake
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# 4. Subprocess Execution (run_cli / run_cli_json)
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestSubprocessExecution:
|
|
189
|
+
def test_run_cli_calls_subprocess(self) -> None:
|
|
190
|
+
with patch("subprocess.run") as mock_run:
|
|
191
|
+
mock_run.return_value = MagicMock(
|
|
192
|
+
returncode=0, stdout="ok\n", stderr=""
|
|
193
|
+
)
|
|
194
|
+
result = run_cli(Path("/usr/bin/echo"), ["arg1", "arg2"])
|
|
195
|
+
assert mock_run.called
|
|
196
|
+
cmd = mock_run.call_args[0][0]
|
|
197
|
+
assert cmd[0] == "/usr/bin/echo"
|
|
198
|
+
|
|
199
|
+
def test_run_cli_includes_store_flag_in_commstore(self, tmp_path: Path) -> None:
|
|
200
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
201
|
+
obj = CommStore(str(tmp_path / "t.acomm"))
|
|
202
|
+
with patch("subprocess.run") as mock_run:
|
|
203
|
+
mock_run.return_value = MagicMock(
|
|
204
|
+
returncode=0, stdout="ok\n", stderr=""
|
|
205
|
+
)
|
|
206
|
+
obj._run(["test"])
|
|
207
|
+
cmd = mock_run.call_args[0][0]
|
|
208
|
+
assert "--store" in cmd
|
|
209
|
+
assert str(tmp_path / "t.acomm") in cmd
|
|
210
|
+
|
|
211
|
+
def test_run_cli_raises_on_nonzero_exit(self) -> None:
|
|
212
|
+
with patch("subprocess.run") as mock_run:
|
|
213
|
+
mock_run.return_value = MagicMock(
|
|
214
|
+
returncode=1, stdout="", stderr="error happened"
|
|
215
|
+
)
|
|
216
|
+
with pytest.raises(CLIError, match="error happened"):
|
|
217
|
+
run_cli(Path("/bin/false"), ["fail"])
|
|
218
|
+
|
|
219
|
+
def test_cli_error_stores_returncode(self) -> None:
|
|
220
|
+
err = CLIError(42, "bad stuff")
|
|
221
|
+
assert err.returncode == 42
|
|
222
|
+
assert err.stderr == "bad stuff"
|
|
223
|
+
|
|
224
|
+
def test_run_cli_json_parses_output(self) -> None:
|
|
225
|
+
with patch("subprocess.run") as mock_run:
|
|
226
|
+
mock_run.return_value = MagicMock(
|
|
227
|
+
returncode=0, stdout='{"key": "value"}\n', stderr=""
|
|
228
|
+
)
|
|
229
|
+
result = run_cli_json(Path("/usr/bin/echo"), ["test"])
|
|
230
|
+
assert result == {"key": "value"}
|
|
231
|
+
|
|
232
|
+
def test_run_cli_json_appends_json_flag(self) -> None:
|
|
233
|
+
with patch("subprocess.run") as mock_run:
|
|
234
|
+
mock_run.return_value = MagicMock(
|
|
235
|
+
returncode=0, stdout='{"k": 1}\n', stderr=""
|
|
236
|
+
)
|
|
237
|
+
run_cli_json(Path("/usr/bin/echo"), ["test"])
|
|
238
|
+
cmd = mock_run.call_args[0][0]
|
|
239
|
+
assert "--json" in cmd
|
|
240
|
+
|
|
241
|
+
def test_run_cli_json_does_not_duplicate_json_flag(self) -> None:
|
|
242
|
+
with patch("subprocess.run") as mock_run:
|
|
243
|
+
mock_run.return_value = MagicMock(
|
|
244
|
+
returncode=0, stdout='{"k": 1}\n', stderr=""
|
|
245
|
+
)
|
|
246
|
+
run_cli_json(Path("/usr/bin/echo"), ["test", "--json"])
|
|
247
|
+
cmd = mock_run.call_args[0][0]
|
|
248
|
+
assert cmd.count("--json") == 1
|
|
249
|
+
|
|
250
|
+
def test_run_cli_json_raises_on_invalid_json(self) -> None:
|
|
251
|
+
with patch("subprocess.run") as mock_run:
|
|
252
|
+
mock_run.return_value = MagicMock(
|
|
253
|
+
returncode=0, stdout="not json at all", stderr=""
|
|
254
|
+
)
|
|
255
|
+
with pytest.raises(json.JSONDecodeError):
|
|
256
|
+
run_cli_json(Path("/usr/bin/echo"), ["test"])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# 5. Data Models
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestModels:
|
|
265
|
+
def test_channel_creation(self) -> None:
|
|
266
|
+
ch = Channel(id="ch1", name="general", channel_type="broadcast", created_at="2026-01-01")
|
|
267
|
+
assert ch.id == "ch1"
|
|
268
|
+
assert ch.name == "general"
|
|
269
|
+
assert ch.metadata == {}
|
|
270
|
+
|
|
271
|
+
def test_message_creation(self) -> None:
|
|
272
|
+
msg = Message(id="m1", channel_id="ch1", sender="agent-1", content="hello", timestamp="now")
|
|
273
|
+
assert msg.id == "m1"
|
|
274
|
+
assert msg.content == "hello"
|
|
275
|
+
assert msg.metadata == {}
|
|
276
|
+
|
|
277
|
+
def test_subscription_creation(self) -> None:
|
|
278
|
+
sub = Subscription(id="s1", channel_id="ch1", subscriber="agent-2")
|
|
279
|
+
assert sub.id == "s1"
|
|
280
|
+
assert sub.pattern is None
|
|
281
|
+
assert sub.created_at == ""
|
|
282
|
+
|
|
283
|
+
def test_store_info_creation(self) -> None:
|
|
284
|
+
info = StoreInfo(path="/tmp/test.acomm", channels=3, messages=10, subscriptions=2)
|
|
285
|
+
assert info.channels == 3
|
|
286
|
+
assert info.file_size == 0
|
|
287
|
+
|
|
288
|
+
def test_channel_with_metadata(self) -> None:
|
|
289
|
+
ch = Channel(id="ch1", name="ops", channel_type="topic", created_at="now", metadata={"key": "val"})
|
|
290
|
+
assert ch.metadata["key"] == "val"
|
|
291
|
+
|
|
292
|
+
def test_message_with_metadata(self) -> None:
|
|
293
|
+
msg = Message(id="m1", channel_id="ch1", sender="a", content="b", timestamp="t", metadata={"x": 1})
|
|
294
|
+
assert msg.metadata["x"] == 1
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# 6. Edge Cases
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestEdgeCases:
|
|
303
|
+
def test_empty_path(self) -> None:
|
|
304
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
305
|
+
obj = CommStore("")
|
|
306
|
+
assert isinstance(obj.path, Path)
|
|
307
|
+
|
|
308
|
+
def test_path_with_spaces(self, tmp_path: Path) -> None:
|
|
309
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
310
|
+
path = tmp_path / "path with spaces" / "test.acomm"
|
|
311
|
+
obj = CommStore(str(path))
|
|
312
|
+
assert "spaces" in str(obj.path)
|
|
313
|
+
|
|
314
|
+
def test_path_with_unicode(self, tmp_path: Path) -> None:
|
|
315
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
316
|
+
path = tmp_path / "donnees" / "test.acomm"
|
|
317
|
+
obj = CommStore(str(path))
|
|
318
|
+
assert "donnees" in str(obj.path)
|
|
319
|
+
|
|
320
|
+
def test_very_long_path(self, tmp_path: Path) -> None:
|
|
321
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
322
|
+
long_name = "a" * 200
|
|
323
|
+
path = tmp_path / long_name / "test.acomm"
|
|
324
|
+
obj = CommStore(str(path))
|
|
325
|
+
assert len(str(obj.path)) > 200
|
|
326
|
+
|
|
327
|
+
def test_multiple_instances_independent(self, tmp_path: Path) -> None:
|
|
328
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
329
|
+
a = CommStore(str(tmp_path / "a.acomm"))
|
|
330
|
+
b = CommStore(str(tmp_path / "b.acomm"))
|
|
331
|
+
assert a.path != b.path
|
|
332
|
+
|
|
333
|
+
def test_dot_in_directory_name(self, tmp_path: Path) -> None:
|
|
334
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
335
|
+
path = tmp_path / "v1.0.0" / "test.acomm"
|
|
336
|
+
obj = CommStore(str(path))
|
|
337
|
+
assert "v1.0.0" in str(obj.path)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# 7. Error Handling
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class TestErrorHandling:
|
|
346
|
+
def test_acomm_error_is_exception(self) -> None:
|
|
347
|
+
assert issubclass(AcommError, Exception)
|
|
348
|
+
|
|
349
|
+
def test_acomm_error_stores_message(self) -> None:
|
|
350
|
+
err = AcommError("test message")
|
|
351
|
+
assert "test message" in str(err)
|
|
352
|
+
|
|
353
|
+
def test_acomm_not_found_stores_searched(self) -> None:
|
|
354
|
+
err = AcommNotFoundError(["/path/1", "/path/2"])
|
|
355
|
+
assert err.searched == ["/path/1", "/path/2"]
|
|
356
|
+
assert "/path/1" in str(err)
|
|
357
|
+
|
|
358
|
+
def test_acomm_not_found_empty_searched(self) -> None:
|
|
359
|
+
err = AcommNotFoundError()
|
|
360
|
+
assert err.searched == []
|
|
361
|
+
assert "(none)" in str(err)
|
|
362
|
+
|
|
363
|
+
def test_cli_error_stores_fields(self) -> None:
|
|
364
|
+
err = CLIError(1, "stderr text")
|
|
365
|
+
assert err.returncode == 1
|
|
366
|
+
assert err.stderr == "stderr text"
|
|
367
|
+
assert "1" in str(err)
|
|
368
|
+
|
|
369
|
+
def test_store_not_found_stores_path(self) -> None:
|
|
370
|
+
err = StoreNotFoundError("/missing.acomm")
|
|
371
|
+
assert err.path == "/missing.acomm"
|
|
372
|
+
|
|
373
|
+
def test_channel_not_found_stores_id(self) -> None:
|
|
374
|
+
err = ChannelNotFoundError("ch-xyz")
|
|
375
|
+
assert err.channel_id == "ch-xyz"
|
|
376
|
+
|
|
377
|
+
def test_validation_error_basic(self) -> None:
|
|
378
|
+
err = ValidationError("bad input")
|
|
379
|
+
assert "bad input" in str(err)
|
|
380
|
+
|
|
381
|
+
def test_error_hierarchy(self) -> None:
|
|
382
|
+
with pytest.raises(AcommError):
|
|
383
|
+
raise CLIError(1, "x")
|
|
384
|
+
with pytest.raises(AcommError):
|
|
385
|
+
raise AcommNotFoundError()
|
|
386
|
+
with pytest.raises(AcommError):
|
|
387
|
+
raise StoreNotFoundError("/x")
|
|
388
|
+
with pytest.raises(AcommError):
|
|
389
|
+
raise ChannelNotFoundError("x")
|
|
390
|
+
with pytest.raises(AcommError):
|
|
391
|
+
raise ValidationError("x")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ---------------------------------------------------------------------------
|
|
395
|
+
# 8. Stress Tests
|
|
396
|
+
# ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestStress:
|
|
400
|
+
def test_create_1000_instances(self, tmp_path: Path) -> None:
|
|
401
|
+
with patch("agentic_comm.cli_bridge.find_acomm_binary", return_value=Path("/fake/acomm")):
|
|
402
|
+
instances = [
|
|
403
|
+
CommStore(str(tmp_path / f"test_{i}.acomm"))
|
|
404
|
+
for i in range(1000)
|
|
405
|
+
]
|
|
406
|
+
assert len(instances) == 1000
|
|
407
|
+
assert instances[0].path != instances[999].path
|
|
408
|
+
|
|
409
|
+
def test_find_binary_1000_times_cached(self, tmp_path: Path) -> None:
|
|
410
|
+
fake = tmp_path / "acomm"
|
|
411
|
+
fake.touch()
|
|
412
|
+
fake.chmod(0o755)
|
|
413
|
+
for _ in range(1000):
|
|
414
|
+
result = find_acomm_binary(fake)
|
|
415
|
+
assert result == fake
|
|
416
|
+
|
|
417
|
+
def test_create_1000_error_instances(self) -> None:
|
|
418
|
+
errors = [CLIError(i, f"err_{i}") for i in range(1000)]
|
|
419
|
+
assert len(errors) == 1000
|
|
420
|
+
assert errors[999].returncode == 999
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Smoke tests for package imports."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_top_level_imports() -> None:
|
|
5
|
+
from agentic_comm import CommStore, Channel, Message, StoreInfo
|
|
6
|
+
assert CommStore is not None
|
|
7
|
+
assert Channel is not None
|
|
8
|
+
assert Message is not None
|
|
9
|
+
assert StoreInfo is not None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_error_imports() -> None:
|
|
13
|
+
from agentic_comm import (
|
|
14
|
+
AcommError,
|
|
15
|
+
AcommNotFoundError,
|
|
16
|
+
CLIError,
|
|
17
|
+
StoreNotFoundError,
|
|
18
|
+
ChannelNotFoundError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
assert issubclass(AcommNotFoundError, AcommError)
|
|
22
|
+
assert issubclass(CLIError, AcommError)
|
|
23
|
+
assert issubclass(StoreNotFoundError, AcommError)
|
|
24
|
+
assert issubclass(ChannelNotFoundError, AcommError)
|
|
25
|
+
assert issubclass(ValidationError, AcommError)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_version() -> None:
|
|
29
|
+
import agentic_comm
|
|
30
|
+
assert agentic_comm.__version__ == "0.1.0"
|