agentsnap 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.
- agentsnap-0.1.0/.gitignore +20 -0
- agentsnap-0.1.0/.python-version +1 -0
- agentsnap-0.1.0/.superpowers/sdd/.gitignore +1 -0
- agentsnap-0.1.0/.superpowers/sdd/progress.md +10 -0
- agentsnap-0.1.0/.superpowers/sdd/review-0835bb9..4ab6190.diff +192 -0
- agentsnap-0.1.0/.superpowers/sdd/review-0af0c0f..92532e8.diff +310 -0
- agentsnap-0.1.0/.superpowers/sdd/review-3bf5ddf..0835bb9.diff +197 -0
- agentsnap-0.1.0/.superpowers/sdd/review-3bf5ddf..8b5232f.diff +1126 -0
- agentsnap-0.1.0/.superpowers/sdd/review-4ab6190..cfce939.diff +220 -0
- agentsnap-0.1.0/.superpowers/sdd/review-6128d5a..6128d5a.diff +7 -0
- agentsnap-0.1.0/.superpowers/sdd/review-67b1b92..8b5232f.diff +270 -0
- agentsnap-0.1.0/.superpowers/sdd/review-6fa62b9..4551594.diff +326 -0
- agentsnap-0.1.0/.superpowers/sdd/review-6fa62b9..703231a.diff +352 -0
- agentsnap-0.1.0/.superpowers/sdd/review-77a5e79..a7be5f4.diff +218 -0
- agentsnap-0.1.0/.superpowers/sdd/review-8b5232f..e4dd96e.diff +102 -0
- agentsnap-0.1.0/.superpowers/sdd/review-a7be5f4..a7be5f4.diff +7 -0
- agentsnap-0.1.0/.superpowers/sdd/review-b76ce6c..6128d5a.diff +1126 -0
- agentsnap-0.1.0/.superpowers/sdd/review-b76ce6c..b920f99.diff +152 -0
- agentsnap-0.1.0/.superpowers/sdd/review-b920f99..0af0c0f.diff +454 -0
- agentsnap-0.1.0/.superpowers/sdd/review-cfce939..67b1b92.diff +279 -0
- agentsnap-0.1.0/.superpowers/sdd/review-e4dd96e..6fa62b9.diff +427 -0
- agentsnap-0.1.0/.superpowers/sdd/review-e4dd96e..a7be5f4.diff +939 -0
- agentsnap-0.1.0/.superpowers/sdd/task-1-brief.md +176 -0
- agentsnap-0.1.0/.superpowers/sdd/task-1-report.md +32 -0
- agentsnap-0.1.0/.superpowers/sdd/task-2-brief.md +458 -0
- agentsnap-0.1.0/.superpowers/sdd/task-2-report.md +49 -0
- agentsnap-0.1.0/.superpowers/sdd/task-3-brief.md +344 -0
- agentsnap-0.1.0/.superpowers/sdd/task-3-report.md +36 -0
- agentsnap-0.1.0/.superpowers/sdd/task-4-brief.md +151 -0
- agentsnap-0.1.0/.superpowers/sdd/task-4-report.md +41 -0
- agentsnap-0.1.0/.superpowers/sdd/task-5-brief.md +228 -0
- agentsnap-0.1.0/.superpowers/sdd/task-5-report.md +45 -0
- agentsnap-0.1.0/CLAUDE.md +119 -0
- agentsnap-0.1.0/PKG-INFO +456 -0
- agentsnap-0.1.0/README.md +419 -0
- agentsnap-0.1.0/USAGE.md +513 -0
- agentsnap-0.1.0/agentsnap/__init__.py +23 -0
- agentsnap-0.1.0/agentsnap/adapters/__init__.py +0 -0
- agentsnap-0.1.0/agentsnap/adapters/anthropic.py +51 -0
- agentsnap-0.1.0/agentsnap/adapters/cohere.py +59 -0
- agentsnap-0.1.0/agentsnap/adapters/google.py +64 -0
- agentsnap-0.1.0/agentsnap/adapters/groq.py +19 -0
- agentsnap-0.1.0/agentsnap/adapters/langgraph.py +94 -0
- agentsnap-0.1.0/agentsnap/adapters/mistral.py +65 -0
- agentsnap-0.1.0/agentsnap/adapters/openai.py +60 -0
- agentsnap-0.1.0/agentsnap/adapters/openrouter.py +28 -0
- agentsnap-0.1.0/agentsnap/adapters/tool.py +36 -0
- agentsnap-0.1.0/agentsnap/cli.py +225 -0
- agentsnap-0.1.0/agentsnap/config.py +158 -0
- agentsnap-0.1.0/agentsnap/core/__init__.py +0 -0
- agentsnap-0.1.0/agentsnap/core/asserter.py +107 -0
- agentsnap-0.1.0/agentsnap/core/diff.py +345 -0
- agentsnap-0.1.0/agentsnap/core/recorder.py +79 -0
- agentsnap-0.1.0/agentsnap/core/snapshot.py +77 -0
- agentsnap-0.1.0/agentsnap/exceptions.py +48 -0
- agentsnap-0.1.0/agentsnap/patches.py +236 -0
- agentsnap-0.1.0/agentsnap/pytest_plugin.py +257 -0
- agentsnap-0.1.0/agentsnap/setup_wizard.py +230 -0
- agentsnap-0.1.0/agentsnap/wrap.py +105 -0
- agentsnap-0.1.0/conftest.py +6 -0
- agentsnap-0.1.0/examples/demo_mock.py +388 -0
- agentsnap-0.1.0/examples/demo_real.py +392 -0
- agentsnap-0.1.0/main.py +6 -0
- agentsnap-0.1.0/pyproject.toml +65 -0
- agentsnap-0.1.0/tests/__init__.py +0 -0
- agentsnap-0.1.0/tests/fixtures/__init__.py +0 -0
- agentsnap-0.1.0/tests/fixtures/mock_agents.py +102 -0
- agentsnap-0.1.0/tests/integration/__init__.py +0 -0
- agentsnap-0.1.0/tests/integration/test_cli_init.py +200 -0
- agentsnap-0.1.0/tests/integration/test_cli_update.py +70 -0
- agentsnap-0.1.0/tests/integration/test_examples.py +63 -0
- agentsnap-0.1.0/tests/integration/test_langgraph_callback.py +155 -0
- agentsnap-0.1.0/tests/integration/test_pytest_plugin.py +101 -0
- agentsnap-0.1.0/tests/integration/test_record_assert.py +258 -0
- agentsnap-0.1.0/tests/integration/test_zero_instrument.py +173 -0
- agentsnap-0.1.0/tests/unit/__init__.py +0 -0
- agentsnap-0.1.0/tests/unit/test_diff.py +241 -0
- agentsnap-0.1.0/tests/unit/test_patches.py +191 -0
- agentsnap-0.1.0/tests/unit/test_serialization.py +68 -0
- agentsnap-0.1.0/tests/unit/test_setup_wizard.py +275 -0
- agentsnap-0.1.0/uv.lock +3319 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# SDD Progress Ledger
|
|
2
|
+
# Plan: docs/superpowers/plans/2026-06-27-setup-wizard.md
|
|
3
|
+
# Branch base: b76ce6c3517bf78d441e76c4a40d09fda812ce53
|
|
4
|
+
|
|
5
|
+
## Tasks
|
|
6
|
+
- [x] Task 1: config.py write_config() + tomlkit dep (commit b76ce6c..b920f99, review clean)
|
|
7
|
+
- [x] Task 2: setup_wizard.py — WizardResult, run_wizard, apply_result (commit b920f99..0af0c0f, review clean)
|
|
8
|
+
Minor findings: mid-file imports in test (inherited from brief); mock patches openai.OpenAI globally (works but subtle)
|
|
9
|
+
- [x] Task 3: CLI commands — agentsnap init and agentsnap check (commits 0af0c0f..6128d5a, review clean; Important findings fixed: SystemExit consistency, config.get defaults, unused imports)
|
|
10
|
+
- [x] Task 4: README and USAGE update (commit 8e0cac5, review pending)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Review package: 0835bb9..HEAD
|
|
2
|
+
|
|
3
|
+
## Commits
|
|
4
|
+
4ab6190 feat: agentsnap update shows diff and prompts for confirmation
|
|
5
|
+
|
|
6
|
+
## Files changed
|
|
7
|
+
agentsnap/cli.py | 49 +++++++++++++++++++++++--
|
|
8
|
+
tests/integration/test_cli_update.py | 70 ++++++++++++++++++++++++++++++++++++
|
|
9
|
+
2 files changed, 116 insertions(+), 3 deletions(-)
|
|
10
|
+
|
|
11
|
+
## Diff
|
|
12
|
+
diff --git a/agentsnap/cli.py b/agentsnap/cli.py
|
|
13
|
+
index a645b5f..3a9011d 100644
|
|
14
|
+
--- a/agentsnap/cli.py
|
|
15
|
+
+++ b/agentsnap/cli.py
|
|
16
|
+
@@ -1,21 +1,20 @@
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
|
|
26
|
+
from agentsnap.core.snapshot import list_snapshots, last_run_path, snapshot_path
|
|
27
|
+
-from agentsnap.exceptions import SnapshotNotFoundError
|
|
28
|
+
|
|
29
|
+
DEFAULT_SNAPSHOT_DIR = "__agent_snapshots__"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
def cli() -> None:
|
|
34
|
+
"""agentsnap — deterministic snapshot testing for AI agents."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cli.command("record")
|
|
38
|
+
@@ -49,32 +48,76 @@ def run_cmd(test_file: str, snapshot_dir: str) -> None:
|
|
39
|
+
def diff_cmd(snapshot_file: str) -> None:
|
|
40
|
+
"""Pretty-print snapshot contents."""
|
|
41
|
+
path = Path(snapshot_file)
|
|
42
|
+
if not path.exists():
|
|
43
|
+
click.echo(f"Snapshot not found: {snapshot_file}", err=True)
|
|
44
|
+
raise SystemExit(1)
|
|
45
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
46
|
+
click.echo(json.dumps(data, indent=2, sort_keys=True))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
+def _print_update_diff(old: dict, new: dict) -> None:
|
|
50
|
+
+ """Print a human-readable diff between golden and last-run snapshots."""
|
|
51
|
+
+ click.echo("\nChanges to approve:")
|
|
52
|
+
+
|
|
53
|
+
+ old_output = old.get("output", "")
|
|
54
|
+
+ new_output = new.get("output", "")
|
|
55
|
+
+ if old_output != new_output:
|
|
56
|
+
+ click.echo(f" output:\n old: {old_output!r}\n new: {new_output!r}")
|
|
57
|
+
+ else:
|
|
58
|
+
+ click.echo(f" output: unchanged ({old_output!r})")
|
|
59
|
+
+
|
|
60
|
+
+ old_tools = [s["name"] for s in old.get("trace", []) if s.get("type") == "tool_call"]
|
|
61
|
+
+ new_tools = [s["name"] for s in new.get("trace", []) if s.get("type") == "tool_call"]
|
|
62
|
+
+ if old_tools != new_tools:
|
|
63
|
+
+ click.echo(f" tool sequence:\n old: {old_tools}\n new: {new_tools}")
|
|
64
|
+
+ else:
|
|
65
|
+
+ click.echo(f" tool sequence: unchanged {old_tools}")
|
|
66
|
+
+
|
|
67
|
+
+ old_steps = len(old.get("trace", []))
|
|
68
|
+
+ new_steps = len(new.get("trace", []))
|
|
69
|
+
+ if old_steps != new_steps:
|
|
70
|
+
+ click.echo(f" trace steps: {old_steps} → {new_steps}")
|
|
71
|
+
+
|
|
72
|
+
+ old_model = old.get("model", "unknown")
|
|
73
|
+
+ new_model = new.get("model", "unknown")
|
|
74
|
+
+ if old_model != new_model:
|
|
75
|
+
+ click.echo(f" model: {old_model!r} → {new_model!r}")
|
|
76
|
+
+
|
|
77
|
+
+
|
|
78
|
+
@cli.command("update")
|
|
79
|
+
@click.argument("test_name")
|
|
80
|
+
@click.option("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR, show_default=True)
|
|
81
|
+
-def update_cmd(test_name: str, snapshot_dir: str) -> None:
|
|
82
|
+
- """Copy the last run trace over the snapshot (approve a regression)."""
|
|
83
|
+
+@click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompt.")
|
|
84
|
+
+def update_cmd(test_name: str, snapshot_dir: str, yes: bool) -> None:
|
|
85
|
+
+ """Show what changed and promote the last run to the golden snapshot."""
|
|
86
|
+
src = last_run_path(test_name, snapshot_dir)
|
|
87
|
+
dst = snapshot_path(test_name, snapshot_dir)
|
|
88
|
+
+
|
|
89
|
+
if not src.exists():
|
|
90
|
+
click.echo(
|
|
91
|
+
f"No last run found for '{test_name}'. Run 'agentsnap run' first.", err=True
|
|
92
|
+
)
|
|
93
|
+
raise SystemExit(1)
|
|
94
|
+
+
|
|
95
|
+
+ if dst.exists():
|
|
96
|
+
+ old = json.loads(dst.read_text(encoding="utf-8"))
|
|
97
|
+
+ new = json.loads(src.read_text(encoding="utf-8"))
|
|
98
|
+
+ _print_update_diff(old, new)
|
|
99
|
+
+ else:
|
|
100
|
+
+ click.echo("No existing snapshot — will create a new golden.")
|
|
101
|
+
+
|
|
102
|
+
+ if not yes:
|
|
103
|
+
+ if not click.confirm("\nApprove and update snapshot?"):
|
|
104
|
+
+ click.echo("Aborted.")
|
|
105
|
+
+ raise SystemExit(1)
|
|
106
|
+
+
|
|
107
|
+
shutil.copy2(src, dst)
|
|
108
|
+
click.echo(f"Updated snapshot: {dst}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@cli.command("list")
|
|
112
|
+
@click.option("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR, show_default=True)
|
|
113
|
+
def list_cmd(snapshot_dir: str) -> None:
|
|
114
|
+
"""List all snapshots in the snapshot directory."""
|
|
115
|
+
snapshots = list_snapshots(snapshot_dir)
|
|
116
|
+
if not snapshots:
|
|
117
|
+
diff --git a/tests/integration/test_cli_update.py b/tests/integration/test_cli_update.py
|
|
118
|
+
new file mode 100644
|
|
119
|
+
index 0000000..a6efe57
|
|
120
|
+
--- /dev/null
|
|
121
|
+
+++ b/tests/integration/test_cli_update.py
|
|
122
|
+
@@ -0,0 +1,70 @@
|
|
123
|
+
+from __future__ import annotations
|
|
124
|
+
+
|
|
125
|
+
+import json
|
|
126
|
+
+from pathlib import Path
|
|
127
|
+
+
|
|
128
|
+
+import pytest
|
|
129
|
+
+from click.testing import CliRunner
|
|
130
|
+
+
|
|
131
|
+
+from agentsnap.cli import cli
|
|
132
|
+
+from agentsnap.core.snapshot import last_run_path, snapshot_path
|
|
133
|
+
+
|
|
134
|
+
+
|
|
135
|
+
+def _write_snap(path: Path, output: str, tools: list[str]) -> None:
|
|
136
|
+
+ path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
+ trace = [{"type": "tool_call", "name": t, "args": {}, "result": "r", "step": i}
|
|
138
|
+
+ for i, t in enumerate(tools)]
|
|
139
|
+
+ data = {"output": output, "trace": trace, "model": "test", "version": "1.0",
|
|
140
|
+
+ "recorded_at": "2026-01-01T00:00:00+00:00", "input": None}
|
|
141
|
+
+ path.write_text(json.dumps(data), encoding="utf-8")
|
|
142
|
+
+
|
|
143
|
+
+
|
|
144
|
+
+def test_update_shows_diff_and_confirms(tmp_path):
|
|
145
|
+
+ runner = CliRunner()
|
|
146
|
+
+ snap_dir = str(tmp_path / "snaps")
|
|
147
|
+
+ name = "my_test"
|
|
148
|
+
+
|
|
149
|
+
+ _write_snap(snapshot_path(name, snap_dir), output="old output", tools=["search"])
|
|
150
|
+
+ _write_snap(last_run_path(name, snap_dir), output="new output", tools=["fetch"])
|
|
151
|
+
+
|
|
152
|
+
+ result = runner.invoke(cli, ["update", name, f"--snapshot-dir={snap_dir}", "--yes"])
|
|
153
|
+
+
|
|
154
|
+
+ assert result.exit_code == 0, result.output
|
|
155
|
+
+ assert "old output" in result.output
|
|
156
|
+
+ assert "new output" in result.output
|
|
157
|
+
+ assert "fetch" in result.output
|
|
158
|
+
+
|
|
159
|
+
+
|
|
160
|
+
+def test_update_aborts_without_yes(tmp_path):
|
|
161
|
+
+ runner = CliRunner()
|
|
162
|
+
+ snap_dir = str(tmp_path / "snaps")
|
|
163
|
+
+ name = "abort_test"
|
|
164
|
+
+
|
|
165
|
+
+ _write_snap(snapshot_path(name, snap_dir), output="old", tools=[])
|
|
166
|
+
+ _write_snap(last_run_path(name, snap_dir), output="new", tools=[])
|
|
167
|
+
+
|
|
168
|
+
+ # Simulate user typing 'n'
|
|
169
|
+
+ result = runner.invoke(cli, ["update", name, f"--snapshot-dir={snap_dir}"], input="n\n")
|
|
170
|
+
+
|
|
171
|
+
+ assert result.exit_code != 0
|
|
172
|
+
+ # Golden should be unchanged
|
|
173
|
+
+ data = json.loads(snapshot_path(name, snap_dir).read_text())
|
|
174
|
+
+ assert data["output"] == "old"
|
|
175
|
+
+
|
|
176
|
+
+
|
|
177
|
+
+def test_update_no_last_run_exits_nonzero(tmp_path):
|
|
178
|
+
+ runner = CliRunner()
|
|
179
|
+
+ result = runner.invoke(cli, ["update", "missing", f"--snapshot-dir={tmp_path}"])
|
|
180
|
+
+ assert result.exit_code != 0
|
|
181
|
+
+
|
|
182
|
+
+
|
|
183
|
+
+def test_update_no_existing_golden_creates_new(tmp_path):
|
|
184
|
+
+ runner = CliRunner()
|
|
185
|
+
+ snap_dir = str(tmp_path / "snaps")
|
|
186
|
+
+ name = "new_golden"
|
|
187
|
+
+
|
|
188
|
+
+ _write_snap(last_run_path(name, snap_dir), output="first output", tools=["lookup"])
|
|
189
|
+
+
|
|
190
|
+
+ result = runner.invoke(cli, ["update", name, f"--snapshot-dir={snap_dir}", "--yes"])
|
|
191
|
+
+ assert result.exit_code == 0
|
|
192
|
+
+ assert snapshot_path(name, snap_dir).exists()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Review package: 0af0c0f3bd2c6900446a8b78ff9a46c4a6d99934..HEAD
|
|
2
|
+
|
|
3
|
+
## Commits
|
|
4
|
+
92532e8 feat: add agentsnap init and agentsnap check CLI commands
|
|
5
|
+
|
|
6
|
+
## Files changed
|
|
7
|
+
agentsnap/cli.py | 86 ++++++++++++++++++
|
|
8
|
+
tests/integration/test_cli_init.py | 182 +++++++++++++++++++++++++++++++++++++
|
|
9
|
+
2 files changed, 268 insertions(+)
|
|
10
|
+
|
|
11
|
+
## Diff
|
|
12
|
+
diff --git a/agentsnap/cli.py b/agentsnap/cli.py
|
|
13
|
+
index 3a9011d..ce499f8 100644
|
|
14
|
+
--- a/agentsnap/cli.py
|
|
15
|
+
+++ b/agentsnap/cli.py
|
|
16
|
+
@@ -108,20 +108,106 @@ def update_cmd(test_name: str, snapshot_dir: str, yes: bool) -> None:
|
|
17
|
+
|
|
18
|
+
if not yes:
|
|
19
|
+
if not click.confirm("\nApprove and update snapshot?"):
|
|
20
|
+
click.echo("Aborted.")
|
|
21
|
+
raise SystemExit(1)
|
|
22
|
+
|
|
23
|
+
shutil.copy2(src, dst)
|
|
24
|
+
click.echo(f"Updated snapshot: {dst}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
+@cli.command("init")
|
|
28
|
+
+def init_cmd() -> None:
|
|
29
|
+
+ """Interactive setup wizard — choose LLM judge or offline embeddings."""
|
|
30
|
+
+ from agentsnap.setup_wizard import (
|
|
31
|
+
+ _download_model,
|
|
32
|
+
+ apply_result,
|
|
33
|
+
+ run_wizard,
|
|
34
|
+
+ test_judge_connection,
|
|
35
|
+
+ )
|
|
36
|
+
+
|
|
37
|
+
+ result = run_wizard()
|
|
38
|
+
+ project_dir = Path.cwd()
|
|
39
|
+
+ apply_result(result, project_dir)
|
|
40
|
+
+
|
|
41
|
+
+ if result.backend == "offline":
|
|
42
|
+
+ if result.pre_download_model:
|
|
43
|
+
+ click.echo("\nDownloading all-MiniLM-L6-v2...")
|
|
44
|
+
+ _download_model()
|
|
45
|
+
+ click.echo(" Model cached.")
|
|
46
|
+
+ else:
|
|
47
|
+
+ click.echo(
|
|
48
|
+
+ "\nModel will download automatically on first test run (~22 MB)."
|
|
49
|
+
+ )
|
|
50
|
+
+ click.echo("\nOffline embeddings configured.")
|
|
51
|
+
+ else:
|
|
52
|
+
+ click.echo("\nTesting connection...")
|
|
53
|
+
+ try:
|
|
54
|
+
+ latency = test_judge_connection(
|
|
55
|
+
+ base_url=result.judge_base_url,
|
|
56
|
+
+ model=result.judge_model,
|
|
57
|
+
+ api_key=result.api_key,
|
|
58
|
+
+ )
|
|
59
|
+
+ click.echo(f" Connection ok ({latency:.1f}s)")
|
|
60
|
+
+ except RuntimeError as exc:
|
|
61
|
+
+ click.echo(f" Warning: {exc}", err=True)
|
|
62
|
+
+ click.echo(
|
|
63
|
+
+ " Setup saved anyway — fix the key and re-run `agentsnap check`."
|
|
64
|
+
+ )
|
|
65
|
+
+
|
|
66
|
+
+ if result.save_key_to_env:
|
|
67
|
+
+ click.echo(f" API key written to .env ({result.api_key_env_var})")
|
|
68
|
+
+ click.echo("\nLLM judge configured.")
|
|
69
|
+
+
|
|
70
|
+
+ click.echo("Configuration written to pyproject.toml.")
|
|
71
|
+
+ click.echo("\nRun `pytest` to verify everything works.")
|
|
72
|
+
+
|
|
73
|
+
+
|
|
74
|
+
+@cli.command("check")
|
|
75
|
+
+def check_cmd() -> None:
|
|
76
|
+
+ """Verify current agentsnap setup and backend connectivity."""
|
|
77
|
+
+ import sys
|
|
78
|
+
+
|
|
79
|
+
+ from agentsnap import config
|
|
80
|
+
+ from agentsnap.setup_wizard import check_offline_model, test_judge_connection
|
|
81
|
+
+
|
|
82
|
+
+ cfg = config.load(Path.cwd())
|
|
83
|
+
+ api_key = cfg.get("judge_api_key")
|
|
84
|
+
+
|
|
85
|
+
+ if api_key:
|
|
86
|
+
+ click.echo("Backend : LLM judge")
|
|
87
|
+
+ click.echo(f"Provider: {cfg['judge_base_url']}")
|
|
88
|
+
+ click.echo(f"Model : {cfg['judge_model']}")
|
|
89
|
+
+ click.echo("API key : found")
|
|
90
|
+
+ try:
|
|
91
|
+
+ latency = test_judge_connection(
|
|
92
|
+
+ base_url=cfg["judge_base_url"],
|
|
93
|
+
+ model=cfg["judge_model"],
|
|
94
|
+
+ api_key=api_key,
|
|
95
|
+
+ )
|
|
96
|
+
+ click.echo(f"Status : ok ({latency:.2f}s)")
|
|
97
|
+
+ except RuntimeError as exc:
|
|
98
|
+
+ click.echo(f"Status : error — {exc}", err=True)
|
|
99
|
+
+ sys.exit(1)
|
|
100
|
+
+ else:
|
|
101
|
+
+ cached = check_offline_model()
|
|
102
|
+
+ click.echo("Backend : offline embeddings (all-MiniLM-L6-v2)")
|
|
103
|
+
+ if cached:
|
|
104
|
+
+ click.echo(f"Model : cached at {cached}")
|
|
105
|
+
+ click.echo("Status : ok")
|
|
106
|
+
+ else:
|
|
107
|
+
+ click.echo(
|
|
108
|
+
+ "Model : not cached — will download (~22 MB) on first test run"
|
|
109
|
+
+ )
|
|
110
|
+
+ click.echo("Status : ok (will download on first run)")
|
|
111
|
+
+
|
|
112
|
+
+
|
|
113
|
+
@cli.command("list")
|
|
114
|
+
@click.option("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR, show_default=True)
|
|
115
|
+
def list_cmd(snapshot_dir: str) -> None:
|
|
116
|
+
"""List all snapshots in the snapshot directory."""
|
|
117
|
+
snapshots = list_snapshots(snapshot_dir)
|
|
118
|
+
if not snapshots:
|
|
119
|
+
click.echo(f"No snapshots found in '{snapshot_dir}'.")
|
|
120
|
+
return
|
|
121
|
+
click.echo(f"Snapshots in '{snapshot_dir}':")
|
|
122
|
+
for p in snapshots:
|
|
123
|
+
diff --git a/tests/integration/test_cli_init.py b/tests/integration/test_cli_init.py
|
|
124
|
+
new file mode 100644
|
|
125
|
+
index 0000000..2728edd
|
|
126
|
+
--- /dev/null
|
|
127
|
+
+++ b/tests/integration/test_cli_init.py
|
|
128
|
+
@@ -0,0 +1,182 @@
|
|
129
|
+
+from __future__ import annotations
|
|
130
|
+
+
|
|
131
|
+
+import unittest.mock as mock
|
|
132
|
+
+from pathlib import Path
|
|
133
|
+
+
|
|
134
|
+
+import pytest
|
|
135
|
+
+from click.testing import CliRunner
|
|
136
|
+
+
|
|
137
|
+
+from agentsnap.cli import cli
|
|
138
|
+
+
|
|
139
|
+
+
|
|
140
|
+
+# ── agentsnap init — offline path ─────────────────────────────────────────────
|
|
141
|
+
+
|
|
142
|
+
+def test_init_offline_no_predownload(tmp_path):
|
|
143
|
+
+ """User picks [2] offline, declines pre-download."""
|
|
144
|
+
+ runner = CliRunner()
|
|
145
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
146
|
+
+ result = runner.invoke(
|
|
147
|
+
+ cli,
|
|
148
|
+
+ ["init"],
|
|
149
|
+
+ input="2\nn\n", # [2] offline, [n] no pre-download
|
|
150
|
+
+ catch_exceptions=False,
|
|
151
|
+
+ )
|
|
152
|
+
+ assert result.exit_code == 0, result.output
|
|
153
|
+
+ assert "offline" in result.output.lower() or "embedding" in result.output.lower()
|
|
154
|
+
+
|
|
155
|
+
+
|
|
156
|
+
+def test_init_offline_with_predownload(tmp_path):
|
|
157
|
+
+ """User picks [2] offline and accepts pre-download; download is mocked."""
|
|
158
|
+
+ runner = CliRunner()
|
|
159
|
+
+ with mock.patch("agentsnap.setup_wizard._download_model") as mock_dl:
|
|
160
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
161
|
+
+ result = runner.invoke(
|
|
162
|
+
+ cli,
|
|
163
|
+
+ ["init"],
|
|
164
|
+
+ input="2\ny\n",
|
|
165
|
+
+ catch_exceptions=False,
|
|
166
|
+
+ )
|
|
167
|
+
+ assert result.exit_code == 0, result.output
|
|
168
|
+
+ mock_dl.assert_called_once()
|
|
169
|
+
+
|
|
170
|
+
+
|
|
171
|
+
+def test_init_writes_pyproject_toml(tmp_path):
|
|
172
|
+
+ """init must create/update pyproject.toml in the working directory."""
|
|
173
|
+
+ runner = CliRunner()
|
|
174
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
175
|
+
+ runner.invoke(cli, ["init"], input="2\nn\n", catch_exceptions=False)
|
|
176
|
+
+ assert Path("pyproject.toml").exists()
|
|
177
|
+
+
|
|
178
|
+
+
|
|
179
|
+
+def test_init_menu_shows_coming_soon(tmp_path):
|
|
180
|
+
+ """The wizard output must mention coming soon for local LLM option."""
|
|
181
|
+
+ runner = CliRunner()
|
|
182
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
183
|
+
+ result = runner.invoke(
|
|
184
|
+
+ cli,
|
|
185
|
+
+ ["init"],
|
|
186
|
+
+ input="2\nn\n",
|
|
187
|
+
+ catch_exceptions=False,
|
|
188
|
+
+ )
|
|
189
|
+
+ assert "coming soon" in result.output.lower()
|
|
190
|
+
+
|
|
191
|
+
+
|
|
192
|
+
+# ── agentsnap init — judge path ───────────────────────────────────────────────
|
|
193
|
+
+
|
|
194
|
+
+def test_init_judge_openrouter(tmp_path):
|
|
195
|
+
+ """User picks [1] judge with OpenRouter; connectivity test is mocked."""
|
|
196
|
+
+ runner = CliRunner()
|
|
197
|
+
+ with mock.patch("agentsnap.setup_wizard.test_judge_connection", return_value=0.5):
|
|
198
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
199
|
+
+ result = runner.invoke(
|
|
200
|
+
+ cli,
|
|
201
|
+
+ ["init"],
|
|
202
|
+
+ # [1] judge, [1] openrouter, accept default model, api key, [y] save to .env
|
|
203
|
+
+ input="1\n1\n\nsk-or-test\ny\n",
|
|
204
|
+
+ catch_exceptions=False,
|
|
205
|
+
+ )
|
|
206
|
+
+ assert result.exit_code == 0, result.output
|
|
207
|
+
+ assert "judge" in result.output.lower() or "connection" in result.output.lower()
|
|
208
|
+
+
|
|
209
|
+
+
|
|
210
|
+
+def test_init_judge_saves_key_to_env_not_pyproject(tmp_path):
|
|
211
|
+
+ """API key must be written to .env, never to pyproject.toml."""
|
|
212
|
+
+ runner = CliRunner()
|
|
213
|
+
+ with mock.patch("agentsnap.setup_wizard.test_judge_connection", return_value=0.3):
|
|
214
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
215
|
+
+ runner.invoke(
|
|
216
|
+
+ cli,
|
|
217
|
+
+ ["init"],
|
|
218
|
+
+ input="1\n1\n\nsk-or-testkey\ny\n",
|
|
219
|
+
+ catch_exceptions=False,
|
|
220
|
+
+ )
|
|
221
|
+
+ assert Path(".env").exists()
|
|
222
|
+
+ assert "sk-or-testkey" in Path(".env").read_text()
|
|
223
|
+
+ assert "sk-or-testkey" not in Path("pyproject.toml").read_text()
|
|
224
|
+
+
|
|
225
|
+
+
|
|
226
|
+
+def test_init_judge_connection_failure_shows_warning(tmp_path):
|
|
227
|
+
+ """If connectivity test fails, init still completes and shows a warning."""
|
|
228
|
+
+ runner = CliRunner()
|
|
229
|
+
+ with mock.patch(
|
|
230
|
+
+ "agentsnap.setup_wizard.test_judge_connection",
|
|
231
|
+
+ side_effect=RuntimeError("auth failed"),
|
|
232
|
+
+ ):
|
|
233
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
234
|
+
+ result = runner.invoke(
|
|
235
|
+
+ cli,
|
|
236
|
+
+ ["init"],
|
|
237
|
+
+ input="1\n1\n\nsk-bad\ny\n",
|
|
238
|
+
+ catch_exceptions=False,
|
|
239
|
+
+ )
|
|
240
|
+
+ assert result.exit_code == 0, result.output
|
|
241
|
+
+ assert "warning" in result.output.lower() or "failed" in result.output.lower()
|
|
242
|
+
+
|
|
243
|
+
+
|
|
244
|
+
+# ── agentsnap check ───────────────────────────────────────────────────────────
|
|
245
|
+
+
|
|
246
|
+
+def test_check_offline_cached(tmp_path, monkeypatch):
|
|
247
|
+
+ """check exits 0 and reports model cached when offline model is present."""
|
|
248
|
+
+ monkeypatch.setattr(
|
|
249
|
+
+ "agentsnap.setup_wizard.check_offline_model",
|
|
250
|
+
+ lambda: "/fake/cache/models--sentence-transformers--all-MiniLM-L6-v2",
|
|
251
|
+
+ )
|
|
252
|
+
+ monkeypatch.delenv("AGENTSNAP_JUDGE_API_KEY", raising=False)
|
|
253
|
+
+ monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
254
|
+
+
|
|
255
|
+
+ runner = CliRunner()
|
|
256
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
257
|
+
+ result = runner.invoke(cli, ["check"], catch_exceptions=False)
|
|
258
|
+
+ assert result.exit_code == 0, result.output
|
|
259
|
+
+ assert "offline" in result.output.lower() or "embedding" in result.output.lower()
|
|
260
|
+
+
|
|
261
|
+
+
|
|
262
|
+
+def test_check_offline_not_cached(tmp_path, monkeypatch):
|
|
263
|
+
+ """check exits 0 but notes model will download on first test run."""
|
|
264
|
+
+ monkeypatch.setattr(
|
|
265
|
+
+ "agentsnap.setup_wizard.check_offline_model",
|
|
266
|
+
+ lambda: None,
|
|
267
|
+
+ )
|
|
268
|
+
+ monkeypatch.delenv("AGENTSNAP_JUDGE_API_KEY", raising=False)
|
|
269
|
+
+ monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
270
|
+
+
|
|
271
|
+
+ runner = CliRunner()
|
|
272
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
273
|
+
+ result = runner.invoke(cli, ["check"], catch_exceptions=False)
|
|
274
|
+
+ assert result.exit_code == 0
|
|
275
|
+
+ assert (
|
|
276
|
+
+ "download" in result.output.lower()
|
|
277
|
+
+ or "not cached" in result.output.lower()
|
|
278
|
+
+ or "first" in result.output.lower()
|
|
279
|
+
+ )
|
|
280
|
+
+
|
|
281
|
+
+
|
|
282
|
+
+def test_check_judge_connected(tmp_path, monkeypatch):
|
|
283
|
+
+ """check exits 0 and reports latency when judge connectivity passes."""
|
|
284
|
+
+ monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
|
285
|
+
+
|
|
286
|
+
+ with mock.patch("agentsnap.setup_wizard.test_judge_connection", return_value=0.4):
|
|
287
|
+
+ runner = CliRunner()
|
|
288
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
289
|
+
+ result = runner.invoke(cli, ["check"], catch_exceptions=False)
|
|
290
|
+
+ assert result.exit_code == 0, result.output
|
|
291
|
+
+ assert (
|
|
292
|
+
+ "ok" in result.output.lower()
|
|
293
|
+
+ or "connected" in result.output.lower()
|
|
294
|
+
+ or "0." in result.output
|
|
295
|
+
+ )
|
|
296
|
+
+
|
|
297
|
+
+
|
|
298
|
+
+def test_check_judge_unreachable(tmp_path, monkeypatch):
|
|
299
|
+
+ """check exits 1 when judge API call fails."""
|
|
300
|
+
+ monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-bad")
|
|
301
|
+
+
|
|
302
|
+
+ with mock.patch(
|
|
303
|
+
+ "agentsnap.setup_wizard.test_judge_connection",
|
|
304
|
+
+ side_effect=RuntimeError("auth error"),
|
|
305
|
+
+ ):
|
|
306
|
+
+ runner = CliRunner()
|
|
307
|
+
+ with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
308
|
+
+ result = runner.invoke(cli, ["check"])
|
|
309
|
+
+ assert result.exit_code == 1
|
|
310
|
+
+ assert "error" in result.output.lower() or "failed" in result.output.lower()
|