agentic-evolve 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_evolve-0.1.0/.gitignore +93 -0
- agentic_evolve-0.1.0/PKG-INFO +50 -0
- agentic_evolve-0.1.0/README.md +26 -0
- agentic_evolve-0.1.0/pyproject.toml +44 -0
- agentic_evolve-0.1.0/src/agentic_evolve/__init__.py +141 -0
- agentic_evolve-0.1.0/tests/__init__.py +0 -0
- agentic_evolve-0.1.0/tests/test_evolve.py +310 -0
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
*~
|
|
31
|
+
|
|
32
|
+
# Testing
|
|
33
|
+
.pytest_cache/
|
|
34
|
+
.coverage
|
|
35
|
+
htmlcov/
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.tox/
|
|
38
|
+
|
|
39
|
+
# LaTeX build artifacts
|
|
40
|
+
*.aux
|
|
41
|
+
*.bbl
|
|
42
|
+
*.blg
|
|
43
|
+
*.log
|
|
44
|
+
*.out
|
|
45
|
+
*.toc
|
|
46
|
+
*.fls
|
|
47
|
+
*.fdb_latexmk
|
|
48
|
+
*.synctex.gz
|
|
49
|
+
|
|
50
|
+
# Planning docs / AI prompts (internal only)
|
|
51
|
+
planning-docs/
|
|
52
|
+
CLAUDE-CODE-INSTRUCTIONS*.md
|
|
53
|
+
SPEC-*.md
|
|
54
|
+
|
|
55
|
+
# Internal vision / roadmap documents
|
|
56
|
+
docs/VISION-*.md
|
|
57
|
+
|
|
58
|
+
# MCP crate internal docs / specs / AI prompts
|
|
59
|
+
crates/agentic-evolve-mcp/docs/
|
|
60
|
+
crates/agentic-evolve-mcp/scripts/
|
|
61
|
+
|
|
62
|
+
# Internal / not for public repo
|
|
63
|
+
CLAUDE.md
|
|
64
|
+
sister.manifest.json
|
|
65
|
+
|
|
66
|
+
# Claude Code
|
|
67
|
+
.claude/
|
|
68
|
+
.agentra/
|
|
69
|
+
docs/REPO_HYGIENE.md
|
|
70
|
+
|
|
71
|
+
# Root-level paper files (canonical copies are in paper/)
|
|
72
|
+
/agenticevolve-paper.*
|
|
73
|
+
/references.bib
|
|
74
|
+
|
|
75
|
+
# FFI (build artifacts)
|
|
76
|
+
/ffi/
|
|
77
|
+
|
|
78
|
+
# Scripts (internal tooling)
|
|
79
|
+
/agent/scripts/
|
|
80
|
+
/installer/scripts/
|
|
81
|
+
|
|
82
|
+
# Internal docs (not for public repo)
|
|
83
|
+
docs/internal/
|
|
84
|
+
ECOSYSTEM-CONVENTIONS.md
|
|
85
|
+
|
|
86
|
+
# Local goals (internal only)
|
|
87
|
+
goals/
|
|
88
|
+
|
|
89
|
+
# Environment
|
|
90
|
+
.env
|
|
91
|
+
.env.local
|
|
92
|
+
*.pem
|
|
93
|
+
*.key
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-evolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pattern library for AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentralabs/agentic-evolve
|
|
6
|
+
Project-URL: Documentation, https://github.com/agentralabs/agentic-evolve/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/agentralabs/agentic-evolve
|
|
8
|
+
Author: Agentra Labs
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,evolve,mcp,pattern
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# AgenticEvolve Python SDK
|
|
26
|
+
|
|
27
|
+
Pattern library for AI agents -- store, match, and crystallize verified code patterns for reuse.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install agentic-evolve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from agentic_evolve import EvolveStore
|
|
39
|
+
|
|
40
|
+
es = EvolveStore("project.evolve")
|
|
41
|
+
es.store_pattern("error-handler", language="python", body="try/except with logging")
|
|
42
|
+
es.match_pattern("error handling")
|
|
43
|
+
es.crystallize()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires the `evolve` CLI binary on PATH. Install via:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -fsSL https://agentralabs.tech/install/evolve | bash
|
|
50
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# AgenticEvolve Python SDK
|
|
2
|
+
|
|
3
|
+
Pattern library for AI agents -- store, match, and crystallize verified code patterns for reuse.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentic-evolve
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from agentic_evolve import EvolveStore
|
|
15
|
+
|
|
16
|
+
es = EvolveStore("project.evolve")
|
|
17
|
+
es.store_pattern("error-handler", language="python", body="try/except with logging")
|
|
18
|
+
es.match_pattern("error handling")
|
|
19
|
+
es.crystallize()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires the `evolve` CLI binary on PATH. Install via:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -fsSL https://agentralabs.tech/install/evolve | bash
|
|
26
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentic-evolve"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pattern library for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Agentra Labs" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "agents", "evolve", "pattern", "mcp"]
|
|
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
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-cov>=5.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/agentralabs/agentic-evolve"
|
|
37
|
+
Documentation = "https://github.com/agentralabs/agentic-evolve/tree/main/docs"
|
|
38
|
+
Repository = "https://github.com/agentralabs/agentic-evolve"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/agentic_evolve"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""AgenticEvolve — Pattern library for AI agents.
|
|
2
|
+
|
|
3
|
+
Pure-Python SDK that wraps the ``evolve`` CLI binary via subprocess.
|
|
4
|
+
Zero required dependencies; only stdlib: subprocess, json, pathlib, dataclasses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EvolveError(Exception):
|
|
22
|
+
"""Raised when an evolve CLI command fails."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class EvolveStore:
|
|
27
|
+
"""Interface to an ``.evolve`` pattern library file.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
path : str | Path
|
|
32
|
+
Path to the ``.evolve`` file. Created automatically on first write
|
|
33
|
+
if it does not exist.
|
|
34
|
+
binary : str
|
|
35
|
+
Name or path of the ``evolve`` CLI binary.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
path: str | Path
|
|
39
|
+
binary: str = "evolve"
|
|
40
|
+
_resolved_binary: Optional[str] = field(default=None, repr=False, init=False)
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
self.path = Path(self.path)
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Internal helpers
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _find_binary(self) -> str:
|
|
50
|
+
if self._resolved_binary is not None:
|
|
51
|
+
return self._resolved_binary
|
|
52
|
+
|
|
53
|
+
import shutil
|
|
54
|
+
|
|
55
|
+
found = shutil.which(self.binary)
|
|
56
|
+
if found is None:
|
|
57
|
+
raise EvolveError(
|
|
58
|
+
f"Cannot find '{self.binary}' on PATH. "
|
|
59
|
+
"Install AgenticEvolve: curl -fsSL https://agentralabs.tech/install/evolve | bash"
|
|
60
|
+
)
|
|
61
|
+
self._resolved_binary = found
|
|
62
|
+
return found
|
|
63
|
+
|
|
64
|
+
def _run(self, *args: str, check: bool = True) -> str:
|
|
65
|
+
"""Execute an evolve CLI command and return stdout."""
|
|
66
|
+
cmd = [self._find_binary(), "--file", str(self.path), *args]
|
|
67
|
+
logger.debug("Running: %s", " ".join(cmd))
|
|
68
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
69
|
+
if check and result.returncode != 0:
|
|
70
|
+
raise EvolveError(
|
|
71
|
+
f"evolve command failed (exit {result.returncode}): {result.stderr.strip()}"
|
|
72
|
+
)
|
|
73
|
+
return result.stdout.strip()
|
|
74
|
+
|
|
75
|
+
def _run_json(self, *args: str) -> Any:
|
|
76
|
+
"""Execute a command and parse JSON output."""
|
|
77
|
+
raw = self._run(*args, "--format", "json")
|
|
78
|
+
return json.loads(raw) if raw else {}
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Pattern operations
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def store_pattern(
|
|
85
|
+
self,
|
|
86
|
+
name: str,
|
|
87
|
+
*,
|
|
88
|
+
language: Optional[str] = None,
|
|
89
|
+
body: Optional[str] = None,
|
|
90
|
+
) -> str:
|
|
91
|
+
"""Store a code pattern. Returns the pattern ID."""
|
|
92
|
+
args = ["pattern", "store", name]
|
|
93
|
+
if language:
|
|
94
|
+
args.extend(["--language", language])
|
|
95
|
+
if body:
|
|
96
|
+
args.extend(["--body", body])
|
|
97
|
+
return self._run(*args)
|
|
98
|
+
|
|
99
|
+
def match_pattern(
|
|
100
|
+
self,
|
|
101
|
+
query: str,
|
|
102
|
+
*,
|
|
103
|
+
language: Optional[str] = None,
|
|
104
|
+
) -> list[dict[str, Any]]:
|
|
105
|
+
"""Match patterns by query. Returns matching patterns as JSON."""
|
|
106
|
+
args = ["match", "signature", query, "--format", "json"]
|
|
107
|
+
if language:
|
|
108
|
+
args.extend(["--language", language])
|
|
109
|
+
raw = self._run(*args)
|
|
110
|
+
return json.loads(raw) if raw else []
|
|
111
|
+
|
|
112
|
+
def list_patterns(self) -> list[dict[str, Any]]:
|
|
113
|
+
"""List all stored patterns."""
|
|
114
|
+
raw = self._run("pattern", "list", "--format", "json")
|
|
115
|
+
return json.loads(raw) if raw else []
|
|
116
|
+
|
|
117
|
+
def crystallize(self) -> str:
|
|
118
|
+
"""Crystallize verified patterns for reuse. Returns summary."""
|
|
119
|
+
return self._run("crystallize")
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
# Stats
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def stats(self) -> dict[str, Any]:
|
|
126
|
+
"""Get evolve store statistics."""
|
|
127
|
+
raw = self._run("stats", "--format", "json")
|
|
128
|
+
return json.loads(raw) if raw else {}
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# File operations
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def save(self) -> None:
|
|
135
|
+
"""Explicit save (most operations auto-save)."""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def exists(self) -> bool:
|
|
140
|
+
"""Whether the .evolve file exists on disk."""
|
|
141
|
+
return self.path.exists()
|
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Comprehensive tests for AgenticEvolve Python SDK."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
from unittest.mock import patch, MagicMock
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from agentic_evolve import EvolveStore, EvolveError, __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# 1. Package Metadata
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestPackageMetadata:
|
|
20
|
+
def test_version_exists(self) -> None:
|
|
21
|
+
assert __version__ is not None
|
|
22
|
+
assert isinstance(__version__, str)
|
|
23
|
+
assert len(__version__) > 0
|
|
24
|
+
|
|
25
|
+
def test_version_semver(self) -> None:
|
|
26
|
+
parts = __version__.split(".")
|
|
27
|
+
assert len(parts) == 3
|
|
28
|
+
assert all(p.isdigit() for p in parts)
|
|
29
|
+
|
|
30
|
+
def test_version_is_010(self) -> None:
|
|
31
|
+
assert __version__ == "0.1.0"
|
|
32
|
+
|
|
33
|
+
def test_import_main_class(self) -> None:
|
|
34
|
+
assert EvolveStore is not None
|
|
35
|
+
|
|
36
|
+
def test_import_error_class(self) -> None:
|
|
37
|
+
assert EvolveError is not None
|
|
38
|
+
assert issubclass(EvolveError, Exception)
|
|
39
|
+
|
|
40
|
+
def test_main_class_has_docstring(self) -> None:
|
|
41
|
+
assert EvolveStore.__doc__ is not None
|
|
42
|
+
assert len(EvolveStore.__doc__) > 10
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# 2. Initialization
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestInit:
|
|
51
|
+
def test_create_with_string_path(self, tmp_path: Path) -> None:
|
|
52
|
+
path = str(tmp_path / "test.evolve")
|
|
53
|
+
obj = EvolveStore(path)
|
|
54
|
+
assert str(obj.path) == path
|
|
55
|
+
|
|
56
|
+
def test_create_with_path_object(self, tmp_path: Path) -> None:
|
|
57
|
+
path = tmp_path / "test.evolve"
|
|
58
|
+
obj = EvolveStore(path)
|
|
59
|
+
assert obj.path == path
|
|
60
|
+
|
|
61
|
+
def test_create_with_pure_posix_path(self) -> None:
|
|
62
|
+
obj = EvolveStore(PurePosixPath("/tmp/test.evolve"))
|
|
63
|
+
assert "test.evolve" in str(obj.path)
|
|
64
|
+
|
|
65
|
+
def test_path_converted_to_path_object(self, tmp_path: Path) -> None:
|
|
66
|
+
path = str(tmp_path / "test.evolve")
|
|
67
|
+
obj = EvolveStore(path)
|
|
68
|
+
assert isinstance(obj.path, Path)
|
|
69
|
+
|
|
70
|
+
def test_custom_binary_name(self, tmp_path: Path) -> None:
|
|
71
|
+
obj = EvolveStore(str(tmp_path / "test.evolve"), binary="custom-bin")
|
|
72
|
+
assert obj.binary == "custom-bin"
|
|
73
|
+
|
|
74
|
+
def test_default_binary_name(self, tmp_path: Path) -> None:
|
|
75
|
+
obj = EvolveStore(str(tmp_path / "test.evolve"))
|
|
76
|
+
assert obj.binary == "evolve"
|
|
77
|
+
|
|
78
|
+
def test_exists_false_for_new(self, tmp_path: Path) -> None:
|
|
79
|
+
obj = EvolveStore(str(tmp_path / "nonexistent.evolve"))
|
|
80
|
+
assert not obj.exists
|
|
81
|
+
|
|
82
|
+
def test_exists_true_when_file_present(self, tmp_path: Path) -> None:
|
|
83
|
+
path = tmp_path / "exists.evolve"
|
|
84
|
+
path.touch()
|
|
85
|
+
obj = EvolveStore(str(path))
|
|
86
|
+
assert obj.exists
|
|
87
|
+
|
|
88
|
+
def test_save_is_noop(self, tmp_path: Path) -> None:
|
|
89
|
+
obj = EvolveStore(str(tmp_path / "test.evolve"))
|
|
90
|
+
obj.save()
|
|
91
|
+
|
|
92
|
+
def test_repr_does_not_crash(self, tmp_path: Path) -> None:
|
|
93
|
+
obj = EvolveStore(str(tmp_path / "test.evolve"))
|
|
94
|
+
r = repr(obj)
|
|
95
|
+
assert isinstance(r, str)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# 3. Binary Resolution
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestBinaryResolution:
|
|
104
|
+
def test_missing_binary_raises(self, tmp_path: Path) -> None:
|
|
105
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"), binary="nonexistent-xyz-999")
|
|
106
|
+
with pytest.raises(EvolveError):
|
|
107
|
+
obj._find_binary()
|
|
108
|
+
|
|
109
|
+
def test_error_contains_binary_name(self, tmp_path: Path) -> None:
|
|
110
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"), binary="nonexistent-xyz-999")
|
|
111
|
+
with pytest.raises(EvolveError, match="nonexistent-xyz-999"):
|
|
112
|
+
obj._find_binary()
|
|
113
|
+
|
|
114
|
+
def test_error_contains_install_hint(self, tmp_path: Path) -> None:
|
|
115
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"), binary="nonexistent-xyz-999")
|
|
116
|
+
with pytest.raises(EvolveError, match="Install"):
|
|
117
|
+
obj._find_binary()
|
|
118
|
+
|
|
119
|
+
def test_caches_result(self, tmp_path: Path) -> None:
|
|
120
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
121
|
+
obj._resolved_binary = "/fake/path/evolve"
|
|
122
|
+
assert obj._find_binary() == "/fake/path/evolve"
|
|
123
|
+
|
|
124
|
+
def test_cache_persists_across_calls(self, tmp_path: Path) -> None:
|
|
125
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
126
|
+
obj._resolved_binary = "/cached/bin"
|
|
127
|
+
assert obj._find_binary() == "/cached/bin"
|
|
128
|
+
assert obj._find_binary() == "/cached/bin"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# 4. Subprocess Execution
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestSubprocessExecution:
|
|
137
|
+
def test_run_calls_subprocess(self, tmp_path: Path) -> None:
|
|
138
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
139
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
140
|
+
with patch("subprocess.run") as mock_run:
|
|
141
|
+
mock_run.return_value = MagicMock(
|
|
142
|
+
returncode=0, stdout="ok\n", stderr=""
|
|
143
|
+
)
|
|
144
|
+
result = obj._run("arg1", "arg2")
|
|
145
|
+
assert mock_run.called
|
|
146
|
+
cmd = mock_run.call_args[0][0]
|
|
147
|
+
assert cmd[0] == "/usr/bin/echo"
|
|
148
|
+
assert "arg1" in cmd
|
|
149
|
+
assert "arg2" in cmd
|
|
150
|
+
|
|
151
|
+
def test_run_includes_file_flag(self, tmp_path: Path) -> None:
|
|
152
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
153
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
154
|
+
with patch("subprocess.run") as mock_run:
|
|
155
|
+
mock_run.return_value = MagicMock(
|
|
156
|
+
returncode=0, stdout="ok\n", stderr=""
|
|
157
|
+
)
|
|
158
|
+
obj._run("test")
|
|
159
|
+
cmd = mock_run.call_args[0][0]
|
|
160
|
+
assert "--file" in cmd
|
|
161
|
+
assert str(tmp_path / "t.evolve") in cmd
|
|
162
|
+
|
|
163
|
+
def test_run_raises_on_nonzero_exit(self, tmp_path: Path) -> None:
|
|
164
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
165
|
+
obj._resolved_binary = "/bin/false"
|
|
166
|
+
with patch("subprocess.run") as mock_run:
|
|
167
|
+
mock_run.return_value = MagicMock(
|
|
168
|
+
returncode=1, stdout="", stderr="error happened"
|
|
169
|
+
)
|
|
170
|
+
with pytest.raises(EvolveError, match="error happened"):
|
|
171
|
+
obj._run("fail")
|
|
172
|
+
|
|
173
|
+
def test_run_returns_stripped_stdout(self, tmp_path: Path) -> None:
|
|
174
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
175
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
176
|
+
with patch("subprocess.run") as mock_run:
|
|
177
|
+
mock_run.return_value = MagicMock(
|
|
178
|
+
returncode=0, stdout=" hello world \n", stderr=""
|
|
179
|
+
)
|
|
180
|
+
result = obj._run("test")
|
|
181
|
+
assert result == "hello world"
|
|
182
|
+
|
|
183
|
+
def test_run_json_parses_output(self, tmp_path: Path) -> None:
|
|
184
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
185
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
186
|
+
with patch("subprocess.run") as mock_run:
|
|
187
|
+
mock_run.return_value = MagicMock(
|
|
188
|
+
returncode=0, stdout='{"key": "value"}\n', stderr=""
|
|
189
|
+
)
|
|
190
|
+
result = obj._run_json("test")
|
|
191
|
+
assert result == {"key": "value"}
|
|
192
|
+
|
|
193
|
+
def test_run_json_raises_on_invalid_json(self, tmp_path: Path) -> None:
|
|
194
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
195
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
196
|
+
with patch("subprocess.run") as mock_run:
|
|
197
|
+
mock_run.return_value = MagicMock(
|
|
198
|
+
returncode=0, stdout="not json at all", stderr=""
|
|
199
|
+
)
|
|
200
|
+
with pytest.raises((json.JSONDecodeError, EvolveError)):
|
|
201
|
+
obj._run_json("test")
|
|
202
|
+
|
|
203
|
+
def test_run_json_returns_empty_dict_on_empty_output(self, tmp_path: Path) -> None:
|
|
204
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
205
|
+
obj._resolved_binary = "/usr/bin/echo"
|
|
206
|
+
with patch("subprocess.run") as mock_run:
|
|
207
|
+
mock_run.return_value = MagicMock(
|
|
208
|
+
returncode=0, stdout="", stderr=""
|
|
209
|
+
)
|
|
210
|
+
result = obj._run_json("test")
|
|
211
|
+
assert result == {}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# 5. Edge Cases
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestEdgeCases:
|
|
220
|
+
def test_empty_path(self) -> None:
|
|
221
|
+
obj = EvolveStore("")
|
|
222
|
+
assert isinstance(obj.path, Path)
|
|
223
|
+
|
|
224
|
+
def test_path_with_spaces(self, tmp_path: Path) -> None:
|
|
225
|
+
path = tmp_path / "path with spaces" / "test.evolve"
|
|
226
|
+
obj = EvolveStore(str(path))
|
|
227
|
+
assert "spaces" in str(obj.path)
|
|
228
|
+
|
|
229
|
+
def test_path_with_unicode(self, tmp_path: Path) -> None:
|
|
230
|
+
path = tmp_path / "donnees" / "test.evolve"
|
|
231
|
+
obj = EvolveStore(str(path))
|
|
232
|
+
assert "donnees" in str(obj.path)
|
|
233
|
+
|
|
234
|
+
def test_very_long_path(self, tmp_path: Path) -> None:
|
|
235
|
+
long_name = "a" * 200
|
|
236
|
+
path = tmp_path / long_name / "test.evolve"
|
|
237
|
+
obj = EvolveStore(str(path))
|
|
238
|
+
assert len(str(obj.path)) > 200
|
|
239
|
+
|
|
240
|
+
def test_save_idempotent(self, tmp_path: Path) -> None:
|
|
241
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
242
|
+
obj.save()
|
|
243
|
+
obj.save()
|
|
244
|
+
obj.save()
|
|
245
|
+
|
|
246
|
+
def test_multiple_instances_independent(self, tmp_path: Path) -> None:
|
|
247
|
+
a = EvolveStore(str(tmp_path / "a.evolve"))
|
|
248
|
+
b = EvolveStore(str(tmp_path / "b.evolve"))
|
|
249
|
+
assert a.path != b.path
|
|
250
|
+
a._resolved_binary = "/path/a"
|
|
251
|
+
assert b._resolved_binary is None
|
|
252
|
+
|
|
253
|
+
def test_dot_in_directory_name(self, tmp_path: Path) -> None:
|
|
254
|
+
path = tmp_path / "v1.0.0" / "test.evolve"
|
|
255
|
+
obj = EvolveStore(str(path))
|
|
256
|
+
assert "v1.0.0" in str(obj.path)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# 6. Error Handling
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestErrorHandling:
|
|
265
|
+
def test_error_is_exception(self) -> None:
|
|
266
|
+
assert issubclass(EvolveError, Exception)
|
|
267
|
+
|
|
268
|
+
def test_error_stores_message(self) -> None:
|
|
269
|
+
err = EvolveError("test message")
|
|
270
|
+
assert "test message" in str(err)
|
|
271
|
+
|
|
272
|
+
def test_error_caught_as_exception(self) -> None:
|
|
273
|
+
with pytest.raises(Exception):
|
|
274
|
+
raise EvolveError("boom")
|
|
275
|
+
|
|
276
|
+
def test_error_caught_specifically(self) -> None:
|
|
277
|
+
try:
|
|
278
|
+
raise EvolveError("specific")
|
|
279
|
+
except EvolveError as e:
|
|
280
|
+
assert "specific" in str(e)
|
|
281
|
+
|
|
282
|
+
def test_error_repr(self) -> None:
|
|
283
|
+
err = EvolveError("repr test")
|
|
284
|
+
assert repr(err) is not None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# 7. Stress Tests
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class TestStress:
|
|
293
|
+
def test_create_1000_instances(self, tmp_path: Path) -> None:
|
|
294
|
+
instances = [
|
|
295
|
+
EvolveStore(str(tmp_path / f"test_{i}.evolve"))
|
|
296
|
+
for i in range(1000)
|
|
297
|
+
]
|
|
298
|
+
assert len(instances) == 1000
|
|
299
|
+
assert instances[0].path != instances[999].path
|
|
300
|
+
|
|
301
|
+
def test_find_binary_1000_cached(self, tmp_path: Path) -> None:
|
|
302
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
303
|
+
obj._resolved_binary = "/cached/bin"
|
|
304
|
+
for _ in range(1000):
|
|
305
|
+
assert obj._find_binary() == "/cached/bin"
|
|
306
|
+
|
|
307
|
+
def test_save_100_times(self, tmp_path: Path) -> None:
|
|
308
|
+
obj = EvolveStore(str(tmp_path / "t.evolve"))
|
|
309
|
+
for _ in range(100):
|
|
310
|
+
obj.save()
|