spooling 0.2.14__tar.gz → 0.2.15__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.
- {spooling-0.2.14/spooling.egg-info → spooling-0.2.15}/PKG-INFO +6 -3
- {spooling-0.2.14 → spooling-0.2.15}/README.md +5 -2
- {spooling-0.2.14 → spooling-0.2.15}/pyproject.toml +1 -1
- {spooling-0.2.14 → spooling-0.2.15}/spooling/cloud.py +92 -0
- {spooling-0.2.14 → spooling-0.2.15/spooling.egg-info}/PKG-INFO +6 -3
- {spooling-0.2.14 → spooling-0.2.15}/spooling.egg-info/SOURCES.txt +1 -0
- spooling-0.2.15/tests/test_cloud_teams.py +118 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_docs.py +9 -0
- {spooling-0.2.14 → spooling-0.2.15}/LICENSE +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/setup.cfg +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/__init__.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/agent.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/classifiers.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/cli.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/config.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/__init__.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/base.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/factory.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/mysql.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/connectors/types.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/db.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/embeddings.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/evals.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/experiments.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/ingest.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/insights.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/mcp_server.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/optimize.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/parser.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/pricing.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/__init__.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/antigravity.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/base.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/claude_code.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/codex.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/copilot.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/cursor.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/gemini.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/github.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/gitlab.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/kiro.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/opencode.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/session_file.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/providers/windsurf.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/redact.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/remote_otel.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/schema.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/sdk.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/search.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/server.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/sessions.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/stats.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/subscription_pricing.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/tracing.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/tunnel.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/tunnel_auth.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling/watcher.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/spooling.egg-info/top_level.txt +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_agent_spans.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_claude_code_provider.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_cloud_errors.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_insights.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_mcp_auth_middleware.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_session_ids.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_storage_optimizations.py +0 -0
- {spooling-0.2.14 → spooling-0.2.15}/tests/test_tunnel_auth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spooling
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
5
|
Author: Parsed Analytics, Inc.
|
|
6
6
|
License: MIT
|
|
@@ -420,11 +420,14 @@ spooling push --dry-run # show what would be pushed
|
|
|
420
420
|
**Which team gets the sessions** is decided by the API key you are logged in with. Each key belongs to one workspace (personal or a team), and `spooling push` sends to that workspace. Mint a key for the team in the app, then log in with it:
|
|
421
421
|
|
|
422
422
|
```bash
|
|
423
|
+
spooling cloud teams # every workspace (personal + teams) your account belongs to
|
|
424
|
+
spooling cloud whoami # the account and workspace this CLI pushes to
|
|
423
425
|
spooling cloud login --key sk_<team-key> # replaces the stored key; later pushes go to this team
|
|
424
|
-
spooling cloud status # session/cost totals of the workspace this key belongs to
|
|
425
426
|
```
|
|
426
427
|
|
|
427
|
-
|
|
428
|
+
`spooling cloud teams` marks the workspace your current key pushes to with `<- this key`, and shows your role, the number of members and the plan for each. `spooling cloud status` also prints the workspace name now. `teams` and `whoami` take `--json` for scripting.
|
|
429
|
+
|
|
430
|
+
To push to a different team, log in with that team's key. Only one key is stored at a time, so check `spooling cloud whoami` before a push if you work with several teams. These commands need a server that supports `/v1/me`; against an older one they say so and `spooling cloud status` still works.
|
|
428
431
|
|
|
429
432
|
**Which sessions go up.** With no filters, `spooling push` sends the most recent `--limit` sessions from every project on the machine. Use these to send only some of them. Filters combine with AND:
|
|
430
433
|
|
|
@@ -384,11 +384,14 @@ spooling push --dry-run # show what would be pushed
|
|
|
384
384
|
**Which team gets the sessions** is decided by the API key you are logged in with. Each key belongs to one workspace (personal or a team), and `spooling push` sends to that workspace. Mint a key for the team in the app, then log in with it:
|
|
385
385
|
|
|
386
386
|
```bash
|
|
387
|
+
spooling cloud teams # every workspace (personal + teams) your account belongs to
|
|
388
|
+
spooling cloud whoami # the account and workspace this CLI pushes to
|
|
387
389
|
spooling cloud login --key sk_<team-key> # replaces the stored key; later pushes go to this team
|
|
388
|
-
spooling cloud status # session/cost totals of the workspace this key belongs to
|
|
389
390
|
```
|
|
390
391
|
|
|
391
|
-
|
|
392
|
+
`spooling cloud teams` marks the workspace your current key pushes to with `<- this key`, and shows your role, the number of members and the plan for each. `spooling cloud status` also prints the workspace name now. `teams` and `whoami` take `--json` for scripting.
|
|
393
|
+
|
|
394
|
+
To push to a different team, log in with that team's key. Only one key is stored at a time, so check `spooling cloud whoami` before a push if you work with several teams. These commands need a server that supports `/v1/me`; against an older one they say so and `spooling cloud status` still works.
|
|
392
395
|
|
|
393
396
|
**Which sessions go up.** With no filters, `spooling push` sends the most recent `--limit` sessions from every project on the machine. Use these to send only some of them. Filters combine with AND:
|
|
394
397
|
|
|
@@ -15,6 +15,7 @@ import click
|
|
|
15
15
|
import httpx
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.markup import escape
|
|
18
|
+
from rich.table import Table
|
|
18
19
|
|
|
19
20
|
from spooling.db import get_connection
|
|
20
21
|
from spooling.redact import redact_messages, redact_traces
|
|
@@ -253,6 +254,10 @@ def cloud_status():
|
|
|
253
254
|
r.raise_for_status()
|
|
254
255
|
s = r.json()
|
|
255
256
|
console.print(f"API: [cyan]{base}[/cyan]")
|
|
257
|
+
me = _fetch_me(required=False)
|
|
258
|
+
if me and me.get("workspace"):
|
|
259
|
+
ws = me["workspace"]
|
|
260
|
+
console.print(f" workspace: [bold]{escape(ws['name'])}[/bold] [dim]({_kind_label(ws)})[/dim]")
|
|
256
261
|
console.print(f" sessions: [bold]{s.get('sessions', 0)}[/bold]")
|
|
257
262
|
console.print(f" messages: [bold]{s.get('messages', 0)}[/bold]")
|
|
258
263
|
console.print(f" providers: [bold]{s.get('providers', 0)}[/bold]")
|
|
@@ -263,6 +268,93 @@ def cloud_status():
|
|
|
263
268
|
_fail(f"{_explain_error(e, base)}")
|
|
264
269
|
|
|
265
270
|
|
|
271
|
+
def _fetch_me(*, required: bool) -> dict | None:
|
|
272
|
+
"""GET /v1/me: who this key is and which workspaces its user belongs to.
|
|
273
|
+
|
|
274
|
+
``required=False`` is for best-effort extras (returns None on any problem);
|
|
275
|
+
``required=True`` explains failures and exits non-zero.
|
|
276
|
+
"""
|
|
277
|
+
headers = _auth_headers()
|
|
278
|
+
base = _api_base()
|
|
279
|
+
try:
|
|
280
|
+
r = httpx.get(f"{base}/v1/me", headers=headers, timeout=15)
|
|
281
|
+
r.raise_for_status()
|
|
282
|
+
return r.json()
|
|
283
|
+
except httpx.HTTPStatusError as e:
|
|
284
|
+
if not required:
|
|
285
|
+
return None
|
|
286
|
+
if e.response.status_code == 404:
|
|
287
|
+
_fail(
|
|
288
|
+
"This server is older than the CLI and can't list workspaces yet.\n"
|
|
289
|
+
" Ask your admin to redeploy it. Meanwhile `spooling cloud status`\n"
|
|
290
|
+
" shows the totals of the workspace your key belongs to."
|
|
291
|
+
)
|
|
292
|
+
_fail(_explain_http(e))
|
|
293
|
+
except Exception as e: # noqa: BLE001
|
|
294
|
+
if not required:
|
|
295
|
+
return None
|
|
296
|
+
_fail(_explain_error(e, base))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _kind_label(w: dict) -> str:
|
|
300
|
+
return "Personal" if w.get("kind") == "personal" else "Team"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@cloud.command("whoami")
|
|
304
|
+
@click.option("--json", "as_json", is_flag=True, help="Print JSON instead of text")
|
|
305
|
+
def cloud_whoami(as_json: bool):
|
|
306
|
+
"""Show which account and workspace (team) this CLI pushes to.
|
|
307
|
+
|
|
308
|
+
The workspace is decided by the API key you logged in with. Use
|
|
309
|
+
`spooling cloud teams` to see every workspace you belong to.
|
|
310
|
+
"""
|
|
311
|
+
me = _fetch_me(required=True)
|
|
312
|
+
if as_json:
|
|
313
|
+
click.echo(json.dumps(me, indent=2))
|
|
314
|
+
return
|
|
315
|
+
user, ws = me["user"], me.get("workspace")
|
|
316
|
+
who = user.get("email") or user.get("name") or user.get("id")
|
|
317
|
+
console.print(f"Account: [bold]{escape(str(who))}[/bold]")
|
|
318
|
+
if ws:
|
|
319
|
+
role = f", you are {ws['role']}" if ws.get("role") else ""
|
|
320
|
+
members = f", {ws['members']} member{'s' if ws['members'] != 1 else ''}" if ws.get("members") is not None else ""
|
|
321
|
+
console.print(f"Workspace: [bold cyan]{escape(ws['name'])}[/bold cyan] [dim]({_kind_label(ws)}{role}{members}, {ws.get('plan', 'free')} plan)[/dim]")
|
|
322
|
+
console.print(f"API: [dim]{_api_base()}[/dim]")
|
|
323
|
+
console.print("[dim]`spooling push` sends sessions to this workspace. See all of yours with `spooling cloud teams`.[/dim]")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@cloud.command("teams")
|
|
327
|
+
@click.option("--json", "as_json", is_flag=True, help="Print JSON instead of a table")
|
|
328
|
+
def cloud_teams(as_json: bool):
|
|
329
|
+
"""List every workspace (personal + teams) your account belongs to.
|
|
330
|
+
|
|
331
|
+
The row marked ``<- this key`` is where `spooling push` sends sessions.
|
|
332
|
+
To push somewhere else, log in with that team's API key:
|
|
333
|
+
`spooling cloud login --key sk_<team-key>`.
|
|
334
|
+
"""
|
|
335
|
+
me = _fetch_me(required=True)
|
|
336
|
+
if as_json:
|
|
337
|
+
click.echo(json.dumps(me["workspaces"], indent=2))
|
|
338
|
+
return
|
|
339
|
+
rows = me["workspaces"]
|
|
340
|
+
table = Table(title="Your workspaces", show_lines=False)
|
|
341
|
+
table.add_column("Workspace", style="cyan")
|
|
342
|
+
table.add_column("Type")
|
|
343
|
+
table.add_column("Your role")
|
|
344
|
+
table.add_column("Members", justify="right")
|
|
345
|
+
table.add_column("Plan", style="dim")
|
|
346
|
+
table.add_column("")
|
|
347
|
+
for w in rows:
|
|
348
|
+
table.add_row(
|
|
349
|
+
escape(w["name"]), _kind_label(w), w.get("role") or "-",
|
|
350
|
+
str(w["members"]) if w.get("members") is not None else "-",
|
|
351
|
+
w.get("plan", "free"),
|
|
352
|
+
"[green]<- this key[/green]" if w.get("current") else "",
|
|
353
|
+
)
|
|
354
|
+
console.print(table if rows else "[yellow]No workspaces found for this account.[/yellow]")
|
|
355
|
+
console.print("[dim]A key pushes to one workspace. Switch with `spooling cloud login --key sk_<that team's key>`.[/dim]")
|
|
356
|
+
|
|
357
|
+
|
|
266
358
|
@cloud.command("logout")
|
|
267
359
|
def cloud_logout():
|
|
268
360
|
"""Remove the stored API key."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spooling
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Local session tracker and semantic search for AI coding assistants
|
|
5
5
|
Author: Parsed Analytics, Inc.
|
|
6
6
|
License: MIT
|
|
@@ -420,11 +420,14 @@ spooling push --dry-run # show what would be pushed
|
|
|
420
420
|
**Which team gets the sessions** is decided by the API key you are logged in with. Each key belongs to one workspace (personal or a team), and `spooling push` sends to that workspace. Mint a key for the team in the app, then log in with it:
|
|
421
421
|
|
|
422
422
|
```bash
|
|
423
|
+
spooling cloud teams # every workspace (personal + teams) your account belongs to
|
|
424
|
+
spooling cloud whoami # the account and workspace this CLI pushes to
|
|
423
425
|
spooling cloud login --key sk_<team-key> # replaces the stored key; later pushes go to this team
|
|
424
|
-
spooling cloud status # session/cost totals of the workspace this key belongs to
|
|
425
426
|
```
|
|
426
427
|
|
|
427
|
-
|
|
428
|
+
`spooling cloud teams` marks the workspace your current key pushes to with `<- this key`, and shows your role, the number of members and the plan for each. `spooling cloud status` also prints the workspace name now. `teams` and `whoami` take `--json` for scripting.
|
|
429
|
+
|
|
430
|
+
To push to a different team, log in with that team's key. Only one key is stored at a time, so check `spooling cloud whoami` before a push if you work with several teams. These commands need a server that supports `/v1/me`; against an older one they say so and `spooling cloud status` still works.
|
|
428
431
|
|
|
429
432
|
**Which sessions go up.** With no filters, `spooling push` sends the most recent `--limit` sessions from every project on the machine. Use these to send only some of them. Filters combine with AND:
|
|
430
433
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""`spooling cloud whoami` / `cloud teams` (GET /v1/me), and the workspace line in `cloud status`."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
from click.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from spooling import cloud
|
|
10
|
+
from spooling.cli import cli
|
|
11
|
+
|
|
12
|
+
ME = {
|
|
13
|
+
"user": {"id": "u1", "email": "dev@example.com", "name": "Dev"},
|
|
14
|
+
"workspace": {"id": "w1", "name": "Dev", "kind": "personal", "plan": "free", "role": "owner",
|
|
15
|
+
"members": 1, "current": True},
|
|
16
|
+
"workspaces": [
|
|
17
|
+
{"id": "w1", "name": "Dev", "kind": "personal", "plan": "free", "role": "owner", "members": 1, "current": True},
|
|
18
|
+
{"id": "w2", "name": "Toebox", "kind": "team", "plan": "team", "role": "admin", "members": 4, "current": False},
|
|
19
|
+
{"id": "w3", "name": "[red]Odd[/red] Name", "kind": "team", "plan": "free", "role": "member", "members": 2, "current": False},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
STATS = {"sessions": 12, "messages": 340, "providers": 2, "cost": 1.5}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(*args):
|
|
26
|
+
return CliRunner().invoke(cli, ["cloud", *args])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def server(monkeypatch):
|
|
31
|
+
"""Fake Spooling Cloud: path -> (status, json). Records the paths requested."""
|
|
32
|
+
routes = {"/v1/me": (200, ME), "/v1/stats": (200, STATS)}
|
|
33
|
+
seen = []
|
|
34
|
+
monkeypatch.setattr(cloud, "_auth_headers", lambda: {"Authorization": "Bearer sk_test"})
|
|
35
|
+
monkeypatch.setattr(cloud, "_api_base", lambda: "https://api.example")
|
|
36
|
+
monkeypatch.setattr(cloud, "_load_config", lambda: {"api_key": "sk_test"})
|
|
37
|
+
|
|
38
|
+
def fake_get(url, headers=None, params=None, timeout=None):
|
|
39
|
+
path = url.replace("https://api.example", "")
|
|
40
|
+
seen.append(path)
|
|
41
|
+
status, body = routes[path]
|
|
42
|
+
return httpx.Response(status, json=body, request=httpx.Request("GET", url))
|
|
43
|
+
|
|
44
|
+
monkeypatch.setattr(cloud.httpx, "get", fake_get)
|
|
45
|
+
return type("S", (), {"routes": routes, "seen": seen})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_whoami_shows_account_workspace_and_role(server):
|
|
49
|
+
r = run("whoami")
|
|
50
|
+
assert r.exit_code == 0
|
|
51
|
+
assert "dev@example.com" in r.output and "Dev" in r.output
|
|
52
|
+
assert "Personal" in r.output and "owner" in r.output and "1 member," in r.output
|
|
53
|
+
assert "spooling push" in r.output
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_whoami_json(server):
|
|
57
|
+
assert json.loads(run("whoami", "--json").output) == ME
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_teams_lists_all_workspaces_and_marks_the_current_one(server):
|
|
61
|
+
r = run("teams")
|
|
62
|
+
assert r.exit_code == 0
|
|
63
|
+
for name in ("Toebox", "Personal", "Team", "admin", "member"):
|
|
64
|
+
assert name in r.output
|
|
65
|
+
current_rows = [l for l in r.output.splitlines() if "<- this key" in l]
|
|
66
|
+
assert len(current_rows) == 1 and "Dev" in current_rows[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_teams_does_not_interpret_markup_in_names(server):
|
|
70
|
+
assert "[red]Odd[/red] Name" in run("teams").output
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_teams_json_is_the_workspace_list(server):
|
|
74
|
+
data = json.loads(run("teams", "--json").output)
|
|
75
|
+
assert [w["name"] for w in data] == ["Dev", "Toebox", "[red]Odd[/red] Name"]
|
|
76
|
+
assert [w["current"] for w in data] == [True, False, False]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_older_server_without_v1_me_explains_and_exits_nonzero(server):
|
|
80
|
+
server.routes["/v1/me"] = (404, {"detail": "Not Found"})
|
|
81
|
+
for cmd in ("whoami", "teams"):
|
|
82
|
+
r = run(cmd)
|
|
83
|
+
assert r.exit_code == 1
|
|
84
|
+
assert "older than the CLI" in r.output and "cloud status" in r.output
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_rejected_key_is_explained(server):
|
|
88
|
+
server.routes["/v1/me"] = (401, {"detail": "invalid or revoked key"})
|
|
89
|
+
r = run("whoami")
|
|
90
|
+
assert r.exit_code == 1 and "401" in r.output and "cloud login" in r.output
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_unreachable_server_exits_nonzero(server, monkeypatch):
|
|
94
|
+
def boom(*a, **k):
|
|
95
|
+
raise httpx.ConnectError("connection refused")
|
|
96
|
+
monkeypatch.setattr(cloud.httpx, "get", boom)
|
|
97
|
+
r = run("teams")
|
|
98
|
+
assert r.exit_code == 1 and "log in" not in r.output.lower()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_status_shows_workspace_name_when_server_supports_it(server):
|
|
102
|
+
r = run("status")
|
|
103
|
+
assert r.exit_code == 0
|
|
104
|
+
assert "workspace" in r.output and "Dev" in r.output and "sessions" in r.output
|
|
105
|
+
assert "/v1/me" in server.seen
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_status_still_works_against_an_older_server(server):
|
|
109
|
+
server.routes["/v1/me"] = (404, {"detail": "Not Found"})
|
|
110
|
+
r = run("status")
|
|
111
|
+
assert r.exit_code == 0
|
|
112
|
+
assert "sessions" in r.output and "workspace:" not in r.output
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_status_ignores_a_failing_v1_me(server, monkeypatch):
|
|
116
|
+
server.routes["/v1/me"] = (500, {"detail": "boom"})
|
|
117
|
+
r = run("status")
|
|
118
|
+
assert r.exit_code == 0 and "sessions" in r.output
|
|
@@ -29,3 +29,12 @@ def test_readme_documents_every_push_scoping_option():
|
|
|
29
29
|
for opt in ("--project", "--cwd", "--title", "--copy", "--with-spans", "--dry-run", "--no-redact", "cloud login --key sk_<team-key>"):
|
|
30
30
|
assert opt in readme, f"README does not mention {opt}"
|
|
31
31
|
assert "stats --by" not in readme
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_readme_documents_the_team_commands():
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
readme = (Path(__file__).resolve().parent.parent / "README.md").read_text()
|
|
37
|
+
for cmd in ("spooling cloud teams", "spooling cloud whoami"):
|
|
38
|
+
assert cmd in readme
|
|
39
|
+
out = CliRunner().invoke(cli, ["cloud", "--help"]).output
|
|
40
|
+
assert "teams" in out and "whoami" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|