uro-cli 0.5.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: uro-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Uro Engine reference client — play, dry-run, and dev tools against the engine
5
5
  Project-URL: Homepage, https://github.com/cupskeee/uro
6
6
  Project-URL: Repository, https://github.com/cupskeee/uro
@@ -12,8 +12,8 @@ Requires-Python: >=3.12
12
12
  Requires-Dist: python-dotenv>=1.0
13
13
  Requires-Dist: rich>=13.9
14
14
  Requires-Dist: typer>=0.27.2
15
- Requires-Dist: uro-core[llm,postgres]==0.5.0
16
- Requires-Dist: uro-server==0.5.0
15
+ Requires-Dist: uro-core[llm,postgres]==0.7.0
16
+ Requires-Dist: uro-server==0.7.0
17
17
  Requires-Dist: websockets>=16.1
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uro-cli"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "Uro Engine reference client — play, dry-run, and dev tools against the engine"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]
@@ -13,8 +13,8 @@ requires-python = ">=3.12"
13
13
  # pair a new uro-cli with an old uro-core; `just release` bumps all three together, and the
14
14
  # `{workspace = true}` sources make a drifted pin fail `uv sync` locally.
15
15
  dependencies = [
16
- "uro-core[postgres,llm]==0.5.0",
17
- "uro-server==0.5.0",
16
+ "uro-core[postgres,llm]==0.7.0",
17
+ "uro-server==0.7.0",
18
18
  "typer>=0.27.2",
19
19
  "rich>=13.9",
20
20
  "websockets>=16.1",
@@ -0,0 +1,182 @@
1
+ from typer.testing import CliRunner
2
+ from uro_cli.main import app
3
+
4
+
5
+ def test_cli_version_runs() -> None:
6
+ result = CliRunner().invoke(app, ["version"])
7
+ assert result.exit_code == 0
8
+ assert "uro-core" in result.output
9
+
10
+
11
+ def test_provider_codex_login(monkeypatch) -> None: # type: ignore[no-untyped-def]
12
+ """The whole `uro provider codex-login` flow, with codex_auth + the store mocked."""
13
+ from uro_cli import main
14
+ from uro_core.providers import codex_auth
15
+
16
+ async def dev(**kw: object) -> dict[str, object]:
17
+ return {
18
+ "device_auth_id": "d",
19
+ "user_code": "J7DE-8NXJS",
20
+ "verification_uri": "https://auth.openai.com/codex/device",
21
+ "interval": 0, # instant poll — no real waiting in the test
22
+ "expires_in": 10,
23
+ }
24
+
25
+ async def poll(dev_id: str, code: str, **kw: object) -> dict[str, str]:
26
+ return {"authorization_code": "AC", "code_verifier": "V"}
27
+
28
+ async def exch(code: str, verifier: str, **kw: object) -> dict[str, str]:
29
+ return {"access_token": "AT", "refresh_token": "RT"}
30
+
31
+ async def disc(token: str, **kw: object) -> list[dict[str, str]]:
32
+ return [{"id": "gpt-5-codex", "modality": "chat"}]
33
+
34
+ monkeypatch.setattr(codex_auth, "request_device_code", dev)
35
+ monkeypatch.setattr(codex_auth, "poll_device_auth", poll)
36
+ monkeypatch.setattr(codex_auth, "exchange_code", exch)
37
+ monkeypatch.setattr(codex_auth, "discover_codex_models", disc)
38
+
39
+ class _Store:
40
+ async def add_credential(self, **kw: object) -> str:
41
+ assert kw["auth_mode"] == "oauth_device" and kw["provider"] == "codex"
42
+ return "cred-1"
43
+
44
+ async def add_connection(self, **kw: object) -> str:
45
+ return "conn-1"
46
+
47
+ async def set_connection_models(self, cid: str, models: object) -> bool:
48
+ return True
49
+
50
+ async def close(self) -> None:
51
+ pass
52
+
53
+ async def _noop(store: object) -> None:
54
+ return None
55
+
56
+ monkeypatch.setattr(main, "build_store", lambda: _Store())
57
+ monkeypatch.setattr(main, "connect_store", _noop)
58
+
59
+ result = CliRunner().invoke(app, ["provider", "codex-login", "--no-browser"])
60
+ assert result.exit_code == 0, result.output
61
+ assert "J7DE-8NXJS" in result.output # the displayed device code
62
+ assert "connected: conn-1" in result.output
63
+ assert "gpt-5-codex" in result.output # discovered model listed
64
+
65
+
66
+ def test_provider_bind_rejects_a_chat_model_for_the_embedder(monkeypatch) -> None: # type: ignore[no-untyped-def]
67
+ """CLI parity with the server's embedder-modality guard (review): a chat model (codex is always
68
+ chat) can't back the embedder role."""
69
+ from uro_cli import main
70
+ from uro_core.ports.model_registry import ModelConnection
71
+
72
+ class _Store:
73
+ async def get_connection(self, cid: str) -> ModelConnection:
74
+ return ModelConnection(id=cid, name="cx", provider="codex", auth_id="a1")
75
+
76
+ async def set_role_binding(self, *a: object, **kw: object) -> None:
77
+ raise AssertionError("should not bind — the guard must reject first")
78
+
79
+ async def close(self) -> None:
80
+ pass
81
+
82
+ async def _noop(store: object) -> None:
83
+ return None
84
+
85
+ monkeypatch.setattr(main, "build_store", lambda: _Store())
86
+ monkeypatch.setattr(main, "connect_store", _noop)
87
+ result = CliRunner().invoke(app, ["provider", "bind", "embedder", "conn-1", "gpt-5-codex"])
88
+ assert result.exit_code == 1
89
+ assert "embedding model" in result.output
90
+
91
+
92
+ def _credential_store(monkeypatch, linked: list[str]): # type: ignore[no-untyped-def]
93
+ """A fake registry holding credential `cred-1`, used by the connections in `linked`."""
94
+ from uro_cli import main
95
+ from uro_core.ports.model_registry import ModelConnection
96
+
97
+ deleted: list[str] = []
98
+
99
+ class _Cred:
100
+ id = "cred-1"
101
+
102
+ class _Store:
103
+ async def list_credentials(self) -> list[_Cred]:
104
+ return [_Cred()]
105
+
106
+ async def list_connections(self) -> list[ModelConnection]:
107
+ return [
108
+ ModelConnection(id=c, name=c, provider="codex", auth_id="cred-1") for c in linked
109
+ ]
110
+
111
+ async def delete_credential(self, credential_id: str) -> bool:
112
+ deleted.append(credential_id)
113
+ return True
114
+
115
+ async def close(self) -> None:
116
+ pass
117
+
118
+ async def _noop(store: object) -> None:
119
+ return None
120
+
121
+ monkeypatch.setattr(main, "build_store", lambda: _Store())
122
+ monkeypatch.setattr(main, "connect_store", _noop)
123
+ return deleted
124
+
125
+
126
+ def test_provider_credential_rm_deletes_an_unused_credential(monkeypatch) -> None: # type: ignore[no-untyped-def]
127
+ deleted = _credential_store(monkeypatch, linked=[])
128
+ result = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1"])
129
+ assert result.exit_code == 0, result.output
130
+ assert deleted == ["cred-1"] and "deleted" in result.output
131
+
132
+
133
+ def test_provider_credential_rm_refuses_one_in_use_without_force(monkeypatch) -> None: # type: ignore[no-untyped-def]
134
+ """Deleting a linked credential unlinks its connections (docs/20) — live roles would lose auth,
135
+ so the CLI makes that an explicit --force."""
136
+ deleted = _credential_store(monkeypatch, linked=["conn-1"])
137
+ refused = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1"])
138
+ assert refused.exit_code == 1 and "conn-1" in refused.output and deleted == []
139
+ forced = CliRunner().invoke(app, ["provider", "credential", "rm", "cred-1", "--force"])
140
+ assert forced.exit_code == 0, forced.output
141
+ assert deleted == ["cred-1"] and "unlinked (now unauthenticated): conn-1" in forced.output
142
+
143
+
144
+ def test_provider_credential_rm_reports_an_unknown_id(monkeypatch) -> None: # type: ignore[no-untyped-def]
145
+ deleted = _credential_store(monkeypatch, linked=[])
146
+ result = CliRunner().invoke(app, ["provider", "credential", "rm", "nope"])
147
+ assert result.exit_code == 1 and "no such credential: nope" in result.output and deleted == []
148
+
149
+
150
+ def test_token_revoke_by_participant_and_list(monkeypatch) -> None: # type: ignore[no-untyped-def]
151
+ from uro_cli import main
152
+
153
+ calls: list[tuple[str, object]] = []
154
+
155
+ def fake(server: str, path: str, token: str, body: object) -> dict[str, object]:
156
+ calls.append((path, body))
157
+ if path.endswith("/revoke"):
158
+ return {"revoked": True, "count": 2}
159
+ return {
160
+ "tokens": [
161
+ {
162
+ "token_id": "abc123def456",
163
+ "participant_id": "bob",
164
+ "revoked": False,
165
+ "created_at": "2026-10-30T00:00:00Z",
166
+ }
167
+ ]
168
+ }
169
+
170
+ monkeypatch.setattr(main, "_server_post", fake)
171
+ out = CliRunner().invoke(
172
+ app, ["token", "revoke", "c1", "--participant", "bob", "--token", "op"]
173
+ )
174
+ assert out.exit_code == 0 and "revoked 2 token(s) for bob" in out.output
175
+ assert calls[-1] == ("/campaigns/c1/tokens/revoke", {"participant": "bob"})
176
+ out = CliRunner().invoke(app, ["token", "list", "c1", "--token", "op"])
177
+ assert out.exit_code == 0 and "abc123def456 bob" in out.output and "live" in out.output
178
+ assert calls[-1] == ("/campaigns/c1/tokens", None) # a GET
179
+ both = CliRunner().invoke(
180
+ app, ["token", "revoke", "c1", "--participant", "bob", "--target", "t", "--token", "op"]
181
+ )
182
+ assert both.exit_code == 1