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