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.
Files changed (81) hide show
  1. agentsnap-0.1.0/.gitignore +20 -0
  2. agentsnap-0.1.0/.python-version +1 -0
  3. agentsnap-0.1.0/.superpowers/sdd/.gitignore +1 -0
  4. agentsnap-0.1.0/.superpowers/sdd/progress.md +10 -0
  5. agentsnap-0.1.0/.superpowers/sdd/review-0835bb9..4ab6190.diff +192 -0
  6. agentsnap-0.1.0/.superpowers/sdd/review-0af0c0f..92532e8.diff +310 -0
  7. agentsnap-0.1.0/.superpowers/sdd/review-3bf5ddf..0835bb9.diff +197 -0
  8. agentsnap-0.1.0/.superpowers/sdd/review-3bf5ddf..8b5232f.diff +1126 -0
  9. agentsnap-0.1.0/.superpowers/sdd/review-4ab6190..cfce939.diff +220 -0
  10. agentsnap-0.1.0/.superpowers/sdd/review-6128d5a..6128d5a.diff +7 -0
  11. agentsnap-0.1.0/.superpowers/sdd/review-67b1b92..8b5232f.diff +270 -0
  12. agentsnap-0.1.0/.superpowers/sdd/review-6fa62b9..4551594.diff +326 -0
  13. agentsnap-0.1.0/.superpowers/sdd/review-6fa62b9..703231a.diff +352 -0
  14. agentsnap-0.1.0/.superpowers/sdd/review-77a5e79..a7be5f4.diff +218 -0
  15. agentsnap-0.1.0/.superpowers/sdd/review-8b5232f..e4dd96e.diff +102 -0
  16. agentsnap-0.1.0/.superpowers/sdd/review-a7be5f4..a7be5f4.diff +7 -0
  17. agentsnap-0.1.0/.superpowers/sdd/review-b76ce6c..6128d5a.diff +1126 -0
  18. agentsnap-0.1.0/.superpowers/sdd/review-b76ce6c..b920f99.diff +152 -0
  19. agentsnap-0.1.0/.superpowers/sdd/review-b920f99..0af0c0f.diff +454 -0
  20. agentsnap-0.1.0/.superpowers/sdd/review-cfce939..67b1b92.diff +279 -0
  21. agentsnap-0.1.0/.superpowers/sdd/review-e4dd96e..6fa62b9.diff +427 -0
  22. agentsnap-0.1.0/.superpowers/sdd/review-e4dd96e..a7be5f4.diff +939 -0
  23. agentsnap-0.1.0/.superpowers/sdd/task-1-brief.md +176 -0
  24. agentsnap-0.1.0/.superpowers/sdd/task-1-report.md +32 -0
  25. agentsnap-0.1.0/.superpowers/sdd/task-2-brief.md +458 -0
  26. agentsnap-0.1.0/.superpowers/sdd/task-2-report.md +49 -0
  27. agentsnap-0.1.0/.superpowers/sdd/task-3-brief.md +344 -0
  28. agentsnap-0.1.0/.superpowers/sdd/task-3-report.md +36 -0
  29. agentsnap-0.1.0/.superpowers/sdd/task-4-brief.md +151 -0
  30. agentsnap-0.1.0/.superpowers/sdd/task-4-report.md +41 -0
  31. agentsnap-0.1.0/.superpowers/sdd/task-5-brief.md +228 -0
  32. agentsnap-0.1.0/.superpowers/sdd/task-5-report.md +45 -0
  33. agentsnap-0.1.0/CLAUDE.md +119 -0
  34. agentsnap-0.1.0/PKG-INFO +456 -0
  35. agentsnap-0.1.0/README.md +419 -0
  36. agentsnap-0.1.0/USAGE.md +513 -0
  37. agentsnap-0.1.0/agentsnap/__init__.py +23 -0
  38. agentsnap-0.1.0/agentsnap/adapters/__init__.py +0 -0
  39. agentsnap-0.1.0/agentsnap/adapters/anthropic.py +51 -0
  40. agentsnap-0.1.0/agentsnap/adapters/cohere.py +59 -0
  41. agentsnap-0.1.0/agentsnap/adapters/google.py +64 -0
  42. agentsnap-0.1.0/agentsnap/adapters/groq.py +19 -0
  43. agentsnap-0.1.0/agentsnap/adapters/langgraph.py +94 -0
  44. agentsnap-0.1.0/agentsnap/adapters/mistral.py +65 -0
  45. agentsnap-0.1.0/agentsnap/adapters/openai.py +60 -0
  46. agentsnap-0.1.0/agentsnap/adapters/openrouter.py +28 -0
  47. agentsnap-0.1.0/agentsnap/adapters/tool.py +36 -0
  48. agentsnap-0.1.0/agentsnap/cli.py +225 -0
  49. agentsnap-0.1.0/agentsnap/config.py +158 -0
  50. agentsnap-0.1.0/agentsnap/core/__init__.py +0 -0
  51. agentsnap-0.1.0/agentsnap/core/asserter.py +107 -0
  52. agentsnap-0.1.0/agentsnap/core/diff.py +345 -0
  53. agentsnap-0.1.0/agentsnap/core/recorder.py +79 -0
  54. agentsnap-0.1.0/agentsnap/core/snapshot.py +77 -0
  55. agentsnap-0.1.0/agentsnap/exceptions.py +48 -0
  56. agentsnap-0.1.0/agentsnap/patches.py +236 -0
  57. agentsnap-0.1.0/agentsnap/pytest_plugin.py +257 -0
  58. agentsnap-0.1.0/agentsnap/setup_wizard.py +230 -0
  59. agentsnap-0.1.0/agentsnap/wrap.py +105 -0
  60. agentsnap-0.1.0/conftest.py +6 -0
  61. agentsnap-0.1.0/examples/demo_mock.py +388 -0
  62. agentsnap-0.1.0/examples/demo_real.py +392 -0
  63. agentsnap-0.1.0/main.py +6 -0
  64. agentsnap-0.1.0/pyproject.toml +65 -0
  65. agentsnap-0.1.0/tests/__init__.py +0 -0
  66. agentsnap-0.1.0/tests/fixtures/__init__.py +0 -0
  67. agentsnap-0.1.0/tests/fixtures/mock_agents.py +102 -0
  68. agentsnap-0.1.0/tests/integration/__init__.py +0 -0
  69. agentsnap-0.1.0/tests/integration/test_cli_init.py +200 -0
  70. agentsnap-0.1.0/tests/integration/test_cli_update.py +70 -0
  71. agentsnap-0.1.0/tests/integration/test_examples.py +63 -0
  72. agentsnap-0.1.0/tests/integration/test_langgraph_callback.py +155 -0
  73. agentsnap-0.1.0/tests/integration/test_pytest_plugin.py +101 -0
  74. agentsnap-0.1.0/tests/integration/test_record_assert.py +258 -0
  75. agentsnap-0.1.0/tests/integration/test_zero_instrument.py +173 -0
  76. agentsnap-0.1.0/tests/unit/__init__.py +0 -0
  77. agentsnap-0.1.0/tests/unit/test_diff.py +241 -0
  78. agentsnap-0.1.0/tests/unit/test_patches.py +191 -0
  79. agentsnap-0.1.0/tests/unit/test_serialization.py +68 -0
  80. agentsnap-0.1.0/tests/unit/test_setup_wizard.py +275 -0
  81. agentsnap-0.1.0/uv.lock +3319 -0
@@ -0,0 +1,20 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .claude/
13
+
14
+ .pytest_cache/
15
+
16
+ __agent_snapshots__/
17
+
18
+ .env
19
+
20
+ docs/
@@ -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()