Repository navigation
Expand file tree
/
Copy pathtest_cli.py
More file actions
237 lines (180 loc) · 10.3 KB
/
Copy pathtest_cli.py
File metadata and controls
237 lines (180 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import json
import subprocess
import pytest
from typer.testing import CliRunner
from shelldeck import cli
runner = CliRunner()
@pytest.fixture(autouse=True)
def home(tmp_path, monkeypatch):
monkeypatch.setenv("SHELLDECK_HOME", str(tmp_path / "home"))
monkeypatch.setenv("FC_CACHE_DIR", str(tmp_path / "fc"))
monkeypatch.setitem(cli.CFG, "port", 5499)
return tmp_path
def test_banner_loopback_has_no_network_url(capsys):
cli._banner("127.0.0.1", "started")
out = capsys.readouterr().out
assert "|___/" in out and "started" in out
assert "http://127.0.0.1:5499" in out
assert "Network: off" in out
out.encode("ascii") # piped output on Windows is cp1252; keep the banner ASCII
def test_banner_all_interfaces_shows_lan_url(capsys, monkeypatch):
monkeypatch.setattr(cli, "_lan_ip", lambda: "192.168.9.9")
cli._banner("0.0.0.0", "foreground", "https")
assert "https://192.168.9.9:5499" in capsys.readouterr().out
def test_web_starts_server_prints_banner_and_opens(monkeypatch):
calls = []
monkeypatch.setattr(cli, "_health", lambda: None)
monkeypatch.setattr(cli, "ensure_server", lambda: calls.append("server"))
monkeypatch.setattr(cli, "_banner", lambda host, status, scheme="": calls.append(status))
monkeypatch.setattr(cli, "open_window", lambda path="/": calls.append(path))
assert runner.invoke(cli.app, ["web"]).exit_code == 0
assert calls == ["server", "started", "/"]
def test_browser_by_default_app_window_on_request(monkeypatch):
opened = []
monkeypatch.setattr(cli.webbrowser, "open", lambda url: opened.append(("browser", url)))
monkeypatch.setattr(cli, "_browser", lambda: "edge.exe")
monkeypatch.setattr(cli.subprocess, "Popen", lambda argv, **kw: opened.append(("app", argv[1])))
monkeypatch.setitem(cli.CFG, "app", False)
cli.open_window("/")
monkeypatch.setitem(cli.CFG, "app", True)
cli.open_window("/")
assert opened == [("browser", "http://127.0.0.1:5499/"), ("app", "--app=http://127.0.0.1:5499/")]
def test_own_console_false_without_a_tty():
# pytest captures stdin, so this is never "a console Windows opened just for sd"
assert cli._own_console() is False
def test_serve_refuses_plain_http_off_loopback():
r = runner.invoke(cli.app, ["serve", "--host", "0.0.0.0"])
assert r.exit_code == 2 and "refusing to serve plain HTTP" in r.output
def test_serve_needs_cert_and_key_together(tmp_path):
cert = tmp_path / "c.pem"
cert.write_text("x")
r = runner.invoke(cli.app, ["serve", "--cert", str(cert)])
assert r.exit_code == 2 and "go together" in r.output
def test_render_markdown_and_code(tmp_path):
md, py = tmp_path / "a.md", tmp_path / "b.py"
md.write_text("# Title\n\nbody", encoding="utf-8")
py.write_text("def hello():\n return 1\n", encoding="utf-8")
assert "Title" in runner.invoke(cli.app, ["render", str(md)]).output
assert "hello" in runner.invoke(cli.app, ["render", str(py)]).output
@pytest.fixture
def codebase(tmp_path):
root = tmp_path / "code"
(root / "src").mkdir(parents=True)
(root / "src" / "auth.py").write_text(
"def validate_token(token):\n # check the auth token signature\n return token.startswith('ok')\n",
encoding="utf-8",
)
(root / "src" / "util.py").write_text("def add(a, b):\n return a + b\n", encoding="utf-8")
subprocess.run(["git", "init", "-q", str(root)], check=True, capture_output=True)
return root
def test_search_formats(codebase):
r = runner.invoke(cli.app, ["search", "validate token", "--root", str(codebase), "-f", "json"])
assert r.exit_code == 0
out = json.loads(r.output)
top = out["results"][0]
assert top["path"] == "src/auth.py"
assert top["snippets"][0]["start_line"] == 1
r = runner.invoke(cli.app, ["search", "validate token", "--root", str(codebase), "-f", "paths"])
assert "src/auth.py:1-" in r.output
r = runner.invoke(cli.app, ["search", "validate token", "--root", str(codebase)])
assert r.exit_code == 0 and "src/auth.py" in r.output and "Confidence" in r.output
def test_search_rejects_bad_format(codebase):
r = runner.invoke(cli.app, ["search", "x", "--root", str(codebase), "-f", "xml"])
assert r.exit_code == 1
LINK = {"url": "https://abc-def.trycloudflare.com/share/tok", "path": "/share/tok"}
def _share_api(calls, sharing=False, strong=True, terms=True):
def api(path, method="GET", payload=None, timeout=10):
calls.append((method, path))
if path == "/api/auth/status":
return {"has_password": True}
on = sharing or ("POST", "/api/share") in calls
return {"sharing": on, "link": LINK if on else None, "strong_password": strong, "cloudflared": True, "pending": [],
"terms_accepted": terms or ("POST", "/api/share/terms") in calls, "terms": ["at your own risk"]}
return api
def _interrupt(stopped):
raise KeyboardInterrupt
def test_share_starts_on_the_server_and_stops_on_ctrl_c(monkeypatch, capsys):
calls = []
monkeypatch.setattr(cli, "ensure_server", lambda: None)
monkeypatch.setattr(cli, "_health", lambda: "ok")
monkeypatch.setattr(cli, "_api", _share_api(calls))
monkeypatch.setattr(cli, "_watch_share", _interrupt)
cli.share(action=None, new_link=False)
assert LINK["url"] in capsys.readouterr().out
assert ("POST", "/api/share") in calls and calls[-1] == ("DELETE", "/api/share")
def test_share_asks_for_terms(monkeypatch):
calls = []
monkeypatch.setattr(cli, "ensure_server", lambda: None)
monkeypatch.setattr(cli, "_api", _share_api(calls, terms=False))
r = runner.invoke(cli.app, ["share"], input="n\n")
assert r.exit_code == 1 and "own risk" in r.output and ("POST", "/api/share") not in calls
monkeypatch.setattr(cli, "_watch_share", _interrupt)
monkeypatch.setattr(cli, "_health", lambda: "ok")
assert runner.invoke(cli.app, ["share"], input="y\n").exit_code == 0
assert calls.index(("POST", "/api/share/terms")) < calls.index(("POST", "/api/share"))
def test_share_attaches_without_stopping_and_stop_and_new_link(monkeypatch, capsys):
calls = []
monkeypatch.setattr(cli, "ensure_server", lambda: None)
monkeypatch.setattr(cli, "_health", lambda: "ok")
monkeypatch.setattr(cli, "_api", _share_api(calls, sharing=True))
monkeypatch.setattr(cli, "_watch_share", _interrupt)
cli.share(action=None, new_link=False) # started elsewhere (the UI): Ctrl+C leaves it running
assert ("POST", "/api/share") not in calls and ("DELETE", "/api/share") not in calls
assert runner.invoke(cli.app, ["share", "--new-link"]).exit_code == 0 and calls[-1] == ("POST", "/api/share/link")
assert runner.invoke(cli.app, ["share", "stop"]).output.strip() == "sharing stopped" and calls[-1] == ("DELETE", "/api/share")
assert runner.invoke(cli.app, ["share", "go"]).exit_code == 2
def test_share_refuses_a_short_password(monkeypatch):
monkeypatch.setattr(cli, "ensure_server", lambda: None)
monkeypatch.setattr(cli, "_api", _share_api([], strong=False))
r = runner.invoke(cli.app, ["share"])
assert r.exit_code == 1 and "12+ characters" in r.output
def test_integration_list_json(monkeypatch):
monkeypatch.setattr(cli, "_api", lambda path: {"integrations": [{"agent": "codex", "kind": "hook", "lifecycle": False,
"session_restore": True, "available": True, "notes": ""}]})
result = runner.invoke(cli.app, ["integration", "list", "--json"])
assert result.exit_code == 0
assert json.loads(result.output) == [{"agent": "codex", "kind": "hook", "lifecycle": False,
"session_restore": True, "available": True, "notes": ""}]
def test_close_only_own_sub_agents(monkeypatch):
sessions = [{"id": "me1", "nick": "Ada", "parent": None}, {"id": "kid1", "nick": "Maya", "parent": "me1"},
{"id": "user1", "nick": "Omar", "parent": None}]
calls = []
def api(path, method="GET", payload=None):
calls.append((method, path))
if path == "/api/sessions":
return {"sessions": sessions}
if path.startswith("/api/handoffs"):
return {"handoffs": [{"id": "h1"}] if "kid1" in path and calls.count(("GET", path)) == 1 else []}
return {}
monkeypatch.setattr(cli, "_api", api)
monkeypatch.setenv("SHELLDECK_SESSION_ID", "me1")
assert "not your sub-agent" in runner.invoke(cli.app, ["close", "Omar"]).output
assert "your own terminal" in runner.invoke(cli.app, ["close", "Ada"]).output
r = runner.invoke(cli.app, ["close", "Maya"]) # hand-off h1 still open
assert r.exit_code == 1 and "h1" in r.output and ("DELETE", "/api/sessions/kid1") not in calls
assert runner.invoke(cli.app, ["close", "Maya"]).output.strip() == "closed Maya"
assert ("DELETE", "/api/sessions/kid1") in calls
monkeypatch.delenv("SHELLDECK_SESSION_ID") # the user, outside shelldeck: any terminal
assert runner.invoke(cli.app, ["close", "Omar"]).output.strip() == "closed Omar"
def test_agent_explain_file(tmp_path):
from typer.testing import CliRunner
from shelldeck import cli
f = tmp_path / "screen.txt"
f.write_text("my password is hunter2\nAllow once? Yes, allow once\n")
out = CliRunner().invoke(cli.app, ["agent", "explain", "--file", str(f)]).output
assert "blocked (approval)" in out and "permission.allow-once" in out and "hunter2" not in out
f.write_text("all good\n")
assert "no question found" in CliRunner().invoke(cli.app, ["agent", "explain", "--file", str(f)]).output
def test_note_text_may_start_with_a_dash():
from shelldeck.cli import _text_args
assert _text_args(["- step one", "--title", "T"]) == ["--title", "T", "--", "- step one"]
assert _text_args(["abc1", "-- x"]) == ["--", "abc1", "-- x"]
assert _text_args(["--", "- a", "-t", "T"]) == ["-t", "T", "--", "- a"]
assert _text_args(["--help"]) == ["--help"]
def test_shorthand_routes_folders_and_plugin_commands(tmp_path, monkeypatch):
seen = []
monkeypatch.setattr(cli, "app", lambda: seen.append(cli.sys.argv[1:]))
for argv in (["sd", str(tmp_path)], ["sd", "nofolder"], ["sd", "docker", "ps", "-a"], ["sd", "agents"]):
monkeypatch.setattr(cli.sys, "argv", argv)
cli._main()
assert seen == [["open", str(tmp_path)], ["open", "nofolder"], ["plugin", "run", "--", "docker", "ps", "-a"], ["agents"]]