Files
Jason ZhouandClaude Opus 5.5 2de39612db fix(call): replays report zero cost; stored answers readable by call id (#703)
* fix(call): replays report zero cost; stored answers readable by call id

An idempotent replay echoed the first call's charge in X-Treg-Cost-Micro,
so a client summing that header counted one call twice (our own MCP tool
said "nothing was charged" beside a non-zero cost_usd). A replay now sends
X-Treg-Cost-Micro: 0 and the original charge in X-Treg-Original-Cost-Micro;
the CLI charge line reads the new header and falls back for older servers.

GET /calls/{id}/result took only the numeric audit row id, which a caller
holding the X-Treg-Call-Id it was given could not use without a second
lookup. It now accepts either. The endpoint was also absent from every
agent-facing doc, so a team that paid for answers its own code dropped had
no way to learn they could fetch them back; llms.txt, skill.md and
integrate.md now say so. Fragment updated: interface/api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(context): replay cost header and call-id result lookup in money + archive

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 14:14:52 +10:00

1101 lines
51 KiB
Python

"""The CLI is a thin client — unit-test parsing + the identity-config round-trip (no network)."""
from __future__ import annotations
import json
import stat
import pytest
from treg import cli
@pytest.fixture(autouse=True)
def _isolate_cli_config(tmp_path, monkeypatch):
"""Never let a CLI test touch the real ~/.treg/config.json. Some commands persist config
(e.g. _clear_active_if_targeted -> _save_config), so an un-isolated test would wipe the
developer's own login mid-suite. Redirect CONFIG_PATH to a tmp file for every test here."""
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
def test_resources_list_uses_the_unified_server_endpoint(monkeypatch, capsys):
calls = []
class Response:
status_code = 200
headers = {}
text = ""
def __init__(self, body): self.body = body
def json(self): return self.body
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def get(self, path, params=None):
calls.append((path, params))
if path == "/orgs":
return Response([{"org_id": 7, "slug": "team", "active": True}])
return Response([{"provider": "fishaudio", "kind": "voice",
"upstream_id": "voice-1", "display_name": "Narrator"}])
monkeypatch.setattr(cli, "_client", lambda cfg: Client())
args = type("Args", (), {
"provider": "fishaudio", "kind": "voice", "include_deleted": False,
})()
cli.cmd_resources_list(args, {"base_url": "http://registry", "active_org": "team"})
assert calls == [
("/orgs", None),
("/orgs/7/provider-resources", {"provider": "fishaudio", "kind": "voice"}),
]
assert '"upstream_id": "voice-1"' in capsys.readouterr().out
def test_call_named_and_single_url():
p = cli.build_parser()
a = p.parse_args(["call", "echo", "v1/x", "--method", "POST"])
assert a.target == "echo" and a.path == "v1/x" and a.method == "POST"
b = p.parse_args(["call", "https://api.intercom.io/me"])
assert b.target == "https://api.intercom.io/me" and b.path == ""
def test_host_subcommand_is_registered_not_mistaken_for_system_binary():
"""Issue #557: `treg host` must parse as the host subcommand, not fall through to /usr/bin/host.
When `host` was missing from an older release, `_looks_like_a_program` matched it to the system
`host` binary (DNS lookup tool), causing `treg host face.jpg` to run `/usr/bin/host face.jpg`
via `treg with` — banner, then a confusing DNS error. The fix: keep `host` as a registered
subcommand so the bare-word fallback never applies."""
p = cli.build_parser()
subcommands = cli._subcommands(p)
assert "host" in subcommands, "`host` must be a registered subcommand"
assert p.parse_args(["host", "face.jpg"]).fn is cli.cmd_host
# The bare-word fallback (`treg claude` → `treg with claude`) must NOT treat `host` as a program
# even though /usr/bin/host exists on most Unix systems.
assert cli._looks_like_a_program(["host", "face.jpg"], subcommands) is False
def test_legacy_multiorg_config_migrates(tmp_path, monkeypatch):
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
(tmp_path / "config.json").write_text(json.dumps({
"base_url": "https://treg.to", "active_org": "team-a",
"orgs": {"team-a": {"token": "OLD", "org_id": 3}}}))
cfg = cli._load_config()
assert cfg["token"] == "OLD" and cfg["active_org"] == "team-a" and cfg["identity"] is False
def test_load_config_defaults_to_production_not_localhost(tmp_path, monkeypatch):
"""A fresh CLI (no ~/.treg/config.json) must default to https://treg.to, NOT localhost.
The first-run bug was: install.sh's `treg config --base-url` failed silently, so login
opened Chrome to http://localhost:18790/login — nothing listening, connection refused."""
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "missing" / "config.json")
cfg = cli._load_config()
assert cfg["base_url"] == cli.PRODUCTION_BASE_URL
assert "localhost" not in cfg["base_url"]
assert "127.0.0.1" not in cfg["base_url"]
def test_is_loopback_url_detects_localhost_variants():
"""_is_loopback_url recognises localhost, 127.x.x.x, and [::1] so the login flow can warn
when pointed at a local server that isn't running."""
assert cli._is_loopback_url("http://localhost:18790") is True
assert cli._is_loopback_url("http://127.0.0.1:8000") is True
assert cli._is_loopback_url("http://127.0.0.42/path") is True
assert cli._is_loopback_url("https://[::1]/") is True # IPv6 needs brackets in URLs
assert cli._is_loopback_url("https://[::1]:8000/") is True
assert cli._is_loopback_url("https://treg.to") is False
assert cli._is_loopback_url("https://api.example.com:443/v1") is False
assert cli._is_loopback_url("") is False # empty URL shouldn't crash
def test_login_exits_with_helpful_message_when_localhost_unreachable(monkeypatch):
"""When base_url is localhost and the server can't be reached, login must fail early with
a message that tells the user how to fix it (point at production), not silently open a
dead browser page."""
import httpx
def fake_post(url, **kw):
raise httpx.ConnectError("Connection refused")
monkeypatch.setattr(httpx, "post", fake_post)
args = type("A", (), {"token": None, "email": None})()
cfg = {"base_url": "http://localhost:18790", "token": None}
with pytest.raises(SystemExit) as exc_info:
cli.cmd_login(args, cfg)
error_msg = str(exc_info.value)
assert "localhost" in error_msg.lower() or "Cannot reach" in error_msg
assert "treg config --base-url" in error_msg
assert cli.PRODUCTION_BASE_URL in error_msg
def test_token_org_claim_reads_a_team_pinned_token():
"""`_token_org_claim` decodes the org slug a team-pinned identity token carries — without the
signature check (the server still authorizes), and returning None on anything else."""
import base64
payload = base64.urlsafe_b64encode(
json.dumps({"uid": 7, "exp": 9999999999, "tv": 0, "org": "team-b"}).encode()
).decode().rstrip("=")
assert cli._token_org_claim(f"{payload}.not-a-real-signature") == "team-b"
assert cli._token_org_claim("tok-opaque-per-org") is None # opaque membership token
assert cli._token_org_claim(None) is None
assert cli._token_org_claim("") is None
def test_login_token_keeps_a_typed_default_as_a_human_identity(monkeypatch):
import base64
payload = base64.urlsafe_b64encode(json.dumps({"scp": "team"}).encode()).decode().rstrip("=")
class Response:
status_code = 200
def json(self): return {"email": "me@example.com"}
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def get(self, path): return Response()
monkeypatch.setattr(cli, "_client", lambda cfg: Client())
monkeypatch.setattr(cli, "_pick_active_org", lambda cfg: None)
cfg = {"base_url": "http://x"}
cli.cmd_login(type("A", (), {"token": f"{payload}.sig", "email": None})(), cfg)
assert cfg["identity"] is True
def test_pick_active_org_prefers_the_tokens_baked_org(monkeypatch):
"""Against an older server that marks nothing active for a team-pinned token, `_pick_active_org`
must land on the token's own org — not the first membership, which for a multi-team user is an
arbitrary other team (`treg login --token <superdesign key>` used to activate `oauth-test`)."""
import base64
class _Resp:
status_code = 200
def json(self):
return [{"slug": "first-team", "active": False}, {"slug": "second-team", "active": False}]
class _Client:
def __enter__(self): return self
def __exit__(self, *a): return False
def get(self, path): return _Resp()
monkeypatch.setattr(cli, "_client", lambda cfg: _Client())
payload = base64.urlsafe_b64encode(
json.dumps({"uid": 7, "exp": 9999999999, "tv": 0, "org": "second-team"}).encode()
).decode().rstrip("=")
cfg = {"base_url": "http://x", "token": f"{payload}.sig", "active_org": None}
cli._pick_active_org(cfg)
assert cfg["active_org"] == "second-team"
# an opaque token (no claim) still falls back to the first membership
cfg = {"base_url": "http://x", "token": "tok-opaque", "active_org": None}
cli._pick_active_org(cfg)
assert cfg["active_org"] == "first-team"
def test_client_sends_token_and_active_org(monkeypatch):
monkeypatch.setattr(cli, "_ORG_OVERRIDE", None)
cfg = {"base_url": "http://x", "token": "TK", "active_org": "team-a"}
with cli._client(cfg) as c:
assert c.headers["X-Treg-Token"] == "TK" and c.headers["X-Treg-Org"] == "team-a"
def test_org_override_beats_active(monkeypatch):
monkeypatch.setattr(cli, "_ORG_OVERRIDE", "team-b")
cfg = {"base_url": "http://x", "token": "TK", "active_org": "team-a"}
assert cli._effective_org(cfg) == "team-b"
with cli._client(cfg) as c:
assert c.headers["X-Treg-Org"] == "team-b"
def test_org_use_saves_team_and_active_default_together(monkeypatch):
class Response:
status_code = 200
def __init__(self, body):
self.body = body
def json(self):
return self.body
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def get(self, path, **kwargs):
if path == "/orgs":
return Response([
{"slug": "team-one", "active": True},
{"slug": "team-two", "active": False},
])
assert path == "/auth/cli-token"
assert kwargs["headers"] == {"X-Treg-Org": "team-two"}
return Response({
"org": "team-two", "token": "team-two-default",
"default_key_state": "active",
})
monkeypatch.setattr(cli, "_client", lambda cfg, auth=True: Client())
cfg = {"base_url": "http://x", "token": "team-one-default",
"active_org": "team-one", "identity": True}
cli.cmd_org_use(type("A", (), {"slug": "team-two"})(), cfg)
assert cfg["active_org"] == "team-two" and cfg["token"] == "team-two-default"
saved = cli._load_config()
assert saved["active_org"] == "team-two" and saved["token"] == "team-two-default"
def test_org_use_keeps_previous_team_when_default_is_disabled(monkeypatch):
class Response:
status_code = 200
def __init__(self, body):
self.body = body
def json(self):
return self.body
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def get(self, path, **kwargs):
if path == "/orgs":
return Response([
{"slug": "team-one", "active": True},
{"slug": "team-two", "active": False},
])
return Response({
"org": "team-two", "token": "disabled-default",
"default_key_state": "disabled",
})
monkeypatch.setattr(cli, "_client", lambda cfg, auth=True: Client())
cfg = {"base_url": "http://x", "token": "team-one-default",
"active_org": "team-one", "identity": True}
cli._save_config(cfg)
before = dict(cfg)
with pytest.raises(SystemExit, match="Default key is disabled.*Active team unchanged"):
cli.cmd_org_use(type("A", (), {"slug": "team-two"})(), cfg)
assert cfg == before
saved = cli._load_config()
assert saved["active_org"] == before["active_org"]
assert saved["token"] == before["token"]
def test_org_use_does_not_move_an_opaque_key_to_another_team(monkeypatch):
class Response:
status_code = 200
def json(self):
return [{"slug": "team-one"}, {"slug": "team-two"}]
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def get(self, path, **kwargs): return Response()
monkeypatch.setattr(cli, "_client", lambda cfg, auth=True: Client())
cfg = {"base_url": "http://x", "token": "opaque-agent-key",
"active_org": "team-one", "identity": False}
cli._save_config(cfg)
with pytest.raises(SystemExit, match="cannot switch teams.*Active team unchanged"):
cli.cmd_org_use(type("A", (), {"slug": "team-two"})(), cfg)
saved = cli._load_config()
assert saved["active_org"] == "team-one"
assert saved["token"] == "opaque-agent-key"
def test_pop_org_flag():
a = ["tool", "ls", "--org", "team-b"]
assert cli._pop_org_flag(a) == "team-b" and a == ["tool", "ls"]
b = ["tool", "ls", "--org=team-c"]
assert cli._pop_org_flag(b) == "team-c" and b == ["tool", "ls"]
assert cli._pop_org_flag(["x"]) is None
def test_admin_client_prefers_admin_token():
cfg = {"base_url": "http://x", "admin_token": "ENV", "token": "USER"}
with cli._admin_client(cfg) as c:
assert c.headers["X-Treg-Token"] == "ENV"
del cfg["admin_token"]
with cli._admin_client(cfg) as c:
assert c.headers["X-Treg-Token"] == "USER"
def test_org_delete_requires_matching_slug():
cfg = {"base_url": "http://x", "token": "T", "active_org": "team-a"}
args = type("A", (), {"slug": "wrong"})()
with pytest.raises(SystemExit):
cli.cmd_org_delete(args, cfg)
# ---- bug-hunt regressions -----------------------------------------------------------------
def test_corrupt_config_does_not_brick_the_cli(tmp_path, monkeypatch):
"""A half-written / hand-broken config must load as empty, not JSONDecodeError on every run."""
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
(tmp_path / "config.json").write_text("{ this is not valid json")
cfg = cli._load_config() # must not raise
assert cfg["token"] is None and cfg["base_url"].startswith("http")
def test_save_config_is_atomic(tmp_path, monkeypatch):
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
cli._save_config({"base_url": "http://x", "token": "T"})
assert not (tmp_path / "config.json.tmp").exists() # temp renamed away, no litter
assert cli._load_config()["token"] == "T"
assert stat.S_IMODE((tmp_path / "config.json").stat().st_mode) == 0o600
def test_bootstrap_token_is_never_installed_into_mcp():
import base64
payload = base64.urlsafe_b64encode(json.dumps({"scp": "bootstrap"}).encode()).decode().rstrip("=")
with pytest.raises(SystemExit, match="temporary login token"):
cli.cmd_mcp_install(object(), {"token": f"{payload}.sig"})
def test_team_create_and_join_replace_the_saved_bootstrap(monkeypatch):
class Response:
status_code = 200
text = "{}"
def __init__(self, body): self.body = body
def json(self): return self.body
class Client:
def __init__(self, response): self.response = response
def __enter__(self): return self
def __exit__(self, *args): return False
def post(self, path, json): return self.response
monkeypatch.setattr(cli, "_show", lambda response: None)
created = Response({"token": "team-default", "org": "acme"})
monkeypatch.setattr(cli, "_client", lambda cfg: Client(created))
cfg = {"token": "bootstrap", "identity": True}
cli.cmd_org_create(type("A", (), {"name": "Acme"})(), cfg)
assert cfg == {"token": "team-default", "identity": True, "active_org": "acme"}
joined = Response({"token": "joined-default", "org": "joined"})
monkeypatch.setattr(cli, "_client", lambda cfg, auth=False: Client(joined))
cli.cmd_org_join(type("A", (), {"code": "inv", "email": "me@example.com"})(), cfg)
assert cfg["token"] == "joined-default" and cfg["active_org"] == "joined"
class _FakeResp:
status_code = 200
def json(self): return {}
text = "{}"
class _FakeClient:
def __init__(self): self.calls = []
def __enter__(self): return self
def __exit__(self, *a): return False
def request(self, method, url, params=None, content=None, headers=None):
self.calls.append((method, url, params, content, headers))
return _FakeResp()
def test_call_preserves_duplicate_query_keys(monkeypatch):
fake = _FakeClient()
monkeypatch.setattr(cli, "_client", lambda cfg: fake)
monkeypatch.setattr(cli, "_show", lambda r: None)
args = cli.build_parser().parse_args(["call", "echo", "--query", "tag=a", "--query", "tag=b"])
cli.cmd_call(args, {"base_url": "http://x"})
_, _, params, _, _ = fake.calls[0]
assert params == [("tag", "a"), ("tag", "b")] # both survive; a dict would drop tag=a
def test_call_sends_explicit_authorization_method_as_treg_control_header(monkeypatch):
fake = _FakeClient()
monkeypatch.setattr(cli, "_client", lambda cfg: fake)
monkeypatch.setattr(cli, "_show", lambda r: None)
args = cli.build_parser().parse_args([
"call", "future-provider.endpoint",
"--authorization-method", "delegated-admin",
"--query", "page_id=PAGE-1",
])
cli.cmd_call(args, {"base_url": "http://x"})
assert fake.calls[0][4]["X-Treg-Authorization-Method"] == "delegated-admin"
def _args(**kw):
return type("A", (), kw)()
# Bad local input is a clean exit with a message, never a traceback.
@pytest.mark.parametrize("run", [
pytest.param(lambda: cli.cmd_call(cli.build_parser().parse_args(["call", "echo", "--query", "flag"]),
{"base_url": "http://x"}), id="call-query-without-equals"),
pytest.param(lambda: cli._parse_bind("secret=abc"), id="bind-non-int-secret"),
pytest.param(lambda: cli._load_json_arg("{bad", "binding"), id="bad-json-arg"),
pytest.param(lambda: cli._pop_org_flag(["tool", "ls", "--org"]), id="org-flag-missing-value"),
pytest.param(lambda: cli.cmd_oauth_connect(
_args(client_secret="/nonexistent/x.json", name="g", scopes=[], provider=None, capability=None),
{"base_url": "http://x"}), id="oauth-connect-missing-file"),
# Registry mode needs --provider; BYO needs --client-secret. Neither is a usage error.
pytest.param(lambda: cli.cmd_oauth_connect(
_args(client_secret=None, name=None, scopes=[], provider=None, capability=None),
{"base_url": "http://x"}), id="oauth-connect-without-provider-or-client-secret"),
pytest.param(lambda: cli.cmd_skill_push(_args(file="/nonexistent/skill.json"), {"base_url": "http://x"}),
id="skill-push-missing-file"),
])
def test_bad_local_input_exits_cleanly(monkeypatch, run):
monkeypatch.setattr(cli, "_client", lambda cfg: _FakeClient())
with pytest.raises(SystemExit):
run()
def test_oauth_connect_prints_provider_guidance_from_the_api(monkeypatch, capsys):
class Response:
status_code = 200
def json(self):
return {
"state": "STATE",
"redirect_uri": "https://registry.example/oauth/callback",
"consent_url": "https://provider.example/authorize",
"connect_guidance": "Use the linked workspace administrator grant.",
}
class StatusResponse:
def json(self):
return {"status": "done", "secret_id": 7, "name": "future-provider"}
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def post(self, path, json): return Response()
def get(self, path): return StatusResponse()
monkeypatch.setattr(cli, "_client", lambda cfg: Client())
monkeypatch.setattr(cli.time, "sleep", lambda seconds: None)
args = cli.build_parser().parse_args([
"connections", "connect", "--provider", "future-provider",
])
cli.cmd_oauth_connect(args, {"base_url": "http://x"})
output = capsys.readouterr().out
assert "Use the linked workspace administrator grant." in output
def test_clear_active_only_when_targeted(monkeypatch):
# a one-shot --org override on a DIFFERENT org must not wipe the stored active org
monkeypatch.setattr(cli, "_ORG_OVERRIDE", "beta")
cfg = {"active_org": "alpha"}
cli._clear_active_if_targeted(cfg)
assert cfg["active_org"] == "alpha" # untouched
# acting on the stored active org clears it
monkeypatch.setattr(cli, "_ORG_OVERRIDE", None)
cli._clear_active_if_targeted(cfg)
assert cfg["active_org"] is None
def test_find_env_upwards_locates_project_env(tmp_path):
"""A skills dir sits UNDER a project whose .env is at the root — the walk-up must find it so
env-credentialed skills (render/vercel) aren't gapped 'needs env var … not found'."""
(tmp_path / ".env").write_text("RENDER_API_KEY=x\n")
skills = tmp_path / ".claude" / "skills"
skills.mkdir(parents=True)
found = cli._find_env_upwards(str(skills))
assert found == str(tmp_path / ".env")
# a path nested deeper under the project still resolves to the same root .env
assert cli._find_env_upwards(str(skills / "render")) == str(tmp_path / ".env")
def test_secret_add_env_var_parses_and_strips_quotes(tmp_path, monkeypatch):
"""`secret add --env-var` reads ONE var from an .env via treg's parser — a quoted value
(AGENTMAIL_API_KEY="am_…") is stored WITHOUT the quotes (the bug agents hit hand-extracting)."""
(tmp_path / ".env").write_text('AGENTMAIL_API_KEY="am_us_pod_QUOTED"\nOTHER=nope\n')
posted = {}
class _FakeResp:
status_code = 200
def json(self): return {"id": 1}
class _FakeClient:
def __enter__(self): return self
def __exit__(self, *a): return False
def post(self, path, json):
posted.update(json)
return _FakeResp()
monkeypatch.setattr(cli, "_client", lambda cfg: _FakeClient())
args = type("A", (), {"name": "agentmail-key", "env_var": "AGENTMAIL_API_KEY",
"env_file": str(tmp_path / ".env"), "dir": None, "file": None,
"value": None, "kind": "env"})()
cli.cmd_secret_add(args, {"base_url": "http://x", "token": "t"})
assert posted["value"] == "am_us_pod_QUOTED" # no surrounding quotes
assert posted["name"] == "agentmail-key" and posted["kind"] == "env"
def test_onboard_setup_import_args_are_complete():
"""`_run_setup` builds upload args from build_parser() so it can't drift out of sync with new
upload flags (regression: a hand-built Namespace missing `no_oauth` crashed onboarding Set up)."""
a = cli.build_parser().parse_args(["upload"])
for attr in ("no_oauth", "llm", "llm_token", "llm_model", "llm_base_url",
"dry_run", "all", "select", "replace", "env_file", "skills_dir", "mode"):
assert hasattr(a, attr), f"upload args missing {attr}"
def test_scan_upload_import_verbs():
"""`treg scan` is the read-only preview (forced dry_run, no prompts); `treg upload` is the real
thing; `treg import` stays a working alias of upload (old docs/scripts must not break)."""
p = cli.build_parser()
s = p.parse_args(["scan"])
assert s.dry_run and s.as_scan and s.all and s.no_oauth and s.fn is cli.cmd_import
u = p.parse_args(["upload"])
assert not u.dry_run and not u.as_scan and u.fn is cli.cmd_import
i = p.parse_args(["import", "--dry-run"]) # alias keeps full flag surface
assert i.dry_run and not i.as_scan and i.cmd == "import" and i.fn is cli.cmd_import
def test_onboard_setup_source_picks_scan_dirs(tmp_path, monkeypatch):
"""`treg onboard --path setup --source global` scans the machine-wide agent skill folders
(~/.claude/skills, …); `--source local` keeps the project-only scan; `--source both` unions them."""
gdir = tmp_path / "home" / ".claude" / "skills"
(gdir / "globskill").mkdir(parents=True)
(gdir / "globskill" / "SKILL.md").write_text("---\nname: globskill\n---\nhi")
proj = tmp_path / "proj"
local = proj / ".claude" / "skills" / "localskill"
local.mkdir(parents=True)
(local / "SKILL.md").write_text("---\nname: localskill\n---\nhi")
monkeypatch.chdir(proj)
from treg import agents as ag
monkeypatch.setattr(ag, "detect_installed", lambda: ["claude-code"])
monkeypatch.setattr(ag, "global_dir", lambda a: gdir)
monkeypatch.setattr(cli, "_onboard_active_org", lambda cfg: {"name": "T", "slug": "t", "role": "admin"})
scanned: list[list[str]] = []
monkeypatch.setattr(cli, "_import_skills", lambda args, cfg, dirs, env: scanned.append([str(d) for d in dirs]))
monkeypatch.setattr(cli, "_client", lambda cfg: (_ for _ in ()).throw(RuntimeError("no server")))
def run(source):
scanned.clear()
args = cli.build_parser().parse_args(["onboard", "--path", "setup", "--source", source])
cli._run_setup({"base_url": "http://x", "token": "t"}, args)
return scanned[0] if scanned else []
assert run("global") == [str(gdir)]
got_local = run("local")
assert str(gdir) not in got_local and any(d.endswith("skills") for d in got_local)
got_both = run("both")
assert str(gdir) in got_both and got_both != [str(gdir)]
def test_only_resolvable_gaps():
def mk(gaps):
return type("D", (), {"gaps": gaps})()
assert cli._only_resolvable_gaps(mk([])) # no gaps → checkable
assert cli._only_resolvable_gaps(mk(["needs env var STRIPE_KEY — not found in the env"])) # env-var → fixable
assert not cli._only_resolvable_gaps(mk(["treg.json secret file missing: token.json"])) # file gap → not
def test_prompt_missing_skill_creds_fills_values_and_clears_gaps(monkeypatch):
"""A skill needing an env var absent from .env prompts for it; on a value, the var lands in
`values` and the gap clears so the skill registers. Blank input leaves it skipped."""
answers = iter(["dev-token-123", ""]) # first var answered, second left blank
monkeypatch.setattr(cli.getpass, "getpass", lambda prompt="": next(answers))
good = type("D", (), {"gaps": ["needs env var GOOGLE_ADS_DEVELOPER_TOKEN — not found in the env"]})()
skip = type("D", (), {"gaps": ["needs env var INTERCOM_TOKEN — not found in the env"]})()
values = {}
cli._prompt_missing_skill_creds([good, skip], values)
assert values == {"GOOGLE_ADS_DEVELOPER_TOKEN": "dev-token-123"} # only the answered one
assert good.gaps == [] # resolved → will register
assert skip.gaps and "INTERCOM_TOKEN" in skip.gaps[0] # blank → still gapped, still skipped
def test_load_catalog_prefers_newer_bundled_over_older_server(tmp_path, monkeypatch):
"""A CLI updated ahead of its server must NOT regress to the server's older catalog (which lacks new
CLIs + auth_mechanism/detect). _load_catalog uses whichever is newer by CATALOG_VERSION."""
from treg import providers as prov
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
class _Resp:
status_code = 200
text = '{"version": 1, "providers": [{"provider": "OldOnly"}]}'
def json(self): return {"version": 1, "providers": [{"provider": "OldOnly"}]}
class _C:
def __enter__(self): return self
def __exit__(self, *a): return False
def get(self, path): return _Resp()
monkeypatch.setattr(cli, "_client", lambda cfg, auth=False: _C())
cat = cli._load_catalog({"base_url": "http://old-server"})
assert cat is prov.CATALOG # bundled (v8) wins over the server's v1
# …but a server that's newer/equal wins (it can grow without a CLI release)
class _RespNew(_Resp):
text = '{"version": 999, "providers": [{"provider": "NewServer"}]}'
def json(self): return {"version": 999, "providers": [{"provider": "NewServer"}]}
monkeypatch.setattr(cli, "_client", lambda cfg, auth=False: type("X", (_C,), {"get": lambda s, p: _RespNew()})())
cat2 = cli._load_catalog({"base_url": "http://new-server"})
assert cat2 == [{"provider": "NewServer"}]
# ---- shared-key output redaction (the streaming scrubber) ---------------------------------
@pytest.mark.parametrize("secrets,chunks,expected", [
([b"SEKRET"], [b"before SEK", b"RET after"], b"before *** after"), # across a chunk boundary
([b"KEY"], [b"hello world"], b"hello world"), # no secret in the stream
([], [b"anything at all"], b"anything at all"), # no secrets at all
])
def test_stream_redactor(secrets, chunks, expected):
r = cli._StreamRedactor(secrets)
out = b"".join(r.feed(c) for c in chunks) + r.flush()
assert out == expected
for secret in secrets:
assert secret not in out
def test_traversable_by_others_root_yes_missing_no():
# world-traversable → True; a missing path (OSError) → False (conservative)
assert cli._traversable_by_others("/") is True
assert cli._traversable_by_others("/no/such/path/zzz") is False
def test_call_content_type_flag_and_json_sniff(monkeypatch):
"""`call` sends a Content-Type: explicit --content-type wins; else a JSON body sniffs to
application/json (npm publish et al. reject an untyped body); a non-JSON body sends none."""
p = cli.build_parser()
assert p.parse_args(["call", "t", "p", "--content-type", "text/plain"]).content_type == "text/plain"
fake = _FakeClient()
monkeypatch.setattr(cli, "_client", lambda cfg: fake)
monkeypatch.setattr(cli, "_show", lambda r: None)
cfg = {"base_url": "http://x", "token": "T"}
def sent_headers(*extra) -> dict:
cli.cmd_call(p.parse_args(["call", "t", "p", "--method", "PUT", *extra]), cfg)
return fake.calls[-1][4]
assert sent_headers("--data", '{"ok":1}') == {"content-type": "application/json"} # sniffed from JSON body
assert sent_headers("--data", "plain text") == {} # non-JSON body: no guess
assert sent_headers("--data", "plain", "--content-type", "text/csv") == {"content-type": "text/csv"} # flag wins
def test_hidden_aliases_still_parse_and_route():
"""Every removed/renamed top-level command keeps working for existing scripts + agent
instructions — it is only absent from --help."""
p = cli.build_parser()
for argv, fn in [
(["add", "stripe", "--base-url", "https://api.stripe.com", "--secret", "STRIPE_KEY"], cli.cmd_add),
(["oauth", "providers"], cli.cmd_oauth_providers),
(["oauth", "connect", "--provider", "gsc"], cli.cmd_oauth_connect),
(["run", "stripe", "--", "get", "/v1/balance"], cli.cmd_run),
(["runs"], cli.cmd_runs),
(["calls"], cli.cmd_calls),
(["shell", "start"], cli.cmd_shell_start),
(["shell", "stop"], cli.cmd_shell_stop),
(["setup-local-run"], cli.cmd_setup_local_run),
(["import"], cli.cmd_import),
]:
assert p.parse_args(argv).fn is fn, argv
def test_alias_flags_match_their_canonical_command():
"""An alias is the SAME parser, not a stub: its flags must still parse."""
p = cli.build_parser()
assert p.parse_args(["run", "--server", "sk", "--", "x"]).server is True
assert p.parse_args(["cli", "run", "--server", "sk", "--", "x"]).server is True
assert p.parse_args(["shell", "start", "--ttl", "60"]).ttl == 60
assert p.parse_args(["cli", "shell", "start", "--ttl", "60"]).ttl == 60
assert p.parse_args(["setup-local-run", "--run-proof", "P"]).run_proof == "P"
assert p.parse_args(["cli", "setup", "--run-proof", "P"]).run_proof == "P"
assert p.parse_args(["runs", "--limit", "5"]).limit == 5
assert p.parse_args(["calls", "--limit", "5"]).limit == 5
def test_audit_merges_calls_and_runs(monkeypatch, capsys):
"""The default `audit` view interleaves both logs by time, and drops the `local_run`
CallRecords that /runs already reports as its local rows (else each shows up twice)."""
class _Resp:
status_code = 200
def __init__(self, body): self._b = body
def json(self): return self._b
bodies = {
"/calls": [
{"id": 2, "user_email": "a@x.dev", "tool_name": "stripe", "method": "GET",
"path": "v1/charges", "status_code": 200, "kind": "call", "created_at": "2026-07-28T10:00:00"},
{"id": 1, "user_email": "a@x.dev", "tool_name": "gh", "method": "GRANT",
"path": "pr list", "status_code": 200, "kind": "local_run", "created_at": "2026-07-28T09:00:00"},
],
"/runs": [
{"id": "l1", "user_email": "a@x.dev", "tool": "gh", "argv": ["pr", "list"],
"exit_code": None, "where": "local", "created_at": "2026-07-28T09:00:00"},
],
}
class _C:
def __enter__(self): return self
def __exit__(self, *a): return False
def get(self, url, params=None): return _Resp(bodies[url])
monkeypatch.setattr(cli, "_client", lambda cfg: _C())
args = cli.build_parser().parse_args(["audit"])
cli.cmd_audit(args, {"base_url": "http://x"})
rows = json.loads(capsys.readouterr().out)
assert [r["kind"] for r in rows] == ["call", "run"] # newest first
assert rows[0]["detail"] == "GET v1/charges" and rows[0]["where"] == "proxy"
assert rows[1]["detail"] == "pr list" and rows[1]["where"] == "local"
def test_audit_filters_delegate_to_the_single_source_views(monkeypatch):
seen = []
monkeypatch.setattr(cli, "cmd_calls", lambda a, c: seen.append("calls"))
monkeypatch.setattr(cli, "cmd_runs", lambda a, c: seen.append("runs"))
p = cli.build_parser()
cli.cmd_audit(p.parse_args(["audit", "--calls"]), {})
cli.cmd_audit(p.parse_args(["audit", "--runs"]), {})
assert seen == ["calls", "runs"]
with pytest.raises(SystemExit): # the two filters are mutually exclusive
p.parse_args(["audit", "--calls", "--runs"])
# ---- catalog search / get (the discover -> inspect pair) ----------------------------------
def test_catalog_verbs_parse_without_displacing_a_platform_slug():
"""`search`/`get` are positional verbs, not subparsers — so `treg catalog tiktok` still browses
a shelf, and the multi-word query needs no quoting."""
p = cli.build_parser()
a = p.parse_args(["catalog", "search", "tiktok", "comments", "--limit", "5"])
assert a.fn is cli.cmd_catalog and a.platform == "search" and a.rest == ["tiktok", "comments"] and a.limit == 5
g = p.parse_args(["catalog", "get", "tikhub.tiktok.video.comments"])
assert g.platform == "get" and g.rest == ["tikhub.tiktok.video.comments"]
assert p.parse_args(["catalog", "tiktok"]).rest == []
assert p.parse_args(["catalog"]).platform is None
class _CatalogResp:
def __init__(self, body, status=200): self._b, self.status_code = body, status
def json(self): return self._b
def _stub_catalog_client(monkeypatch, routes: dict):
class _C:
def __enter__(self): return self
def __exit__(self, *a): return False
def get(self, url, params=None): return routes[url]
monkeypatch.setattr(cli, "_client", lambda cfg, auth=True: _C())
def test_catalog_get_renders_params_siblings_and_the_command(monkeypatch, capsys):
body = {
"endpoint": {
"id": "justoneapi.tiktok.video.comments", "provider": "justoneapi", "provider_display": "JustOneAPI",
"summary": "A post's comments", "method": "GET", "path": "/api/tiktok/get-post-comment/v1",
"scope": "any_account", "tier": "core",
"cost": {"type": "per_success", "value": 0.1, "currency": "CNY", "usd": 0.014, "note": "billed on success"},
"verified": "2026-07-28", "docs_url": "https://docs.example/comments", "has_example": True,
"input": {"queryParams": {
"awemeId": {"type": "string", "required": True, "note": "the post id", "example": "76662"},
"cursor": {"type": "string", "required": False, "note": "'0' for the first page"}}},
"capability": "tiktok.video.comments", "capability_description": "List a video's comments",
"platform": "tiktok", "platform_label": "TikTok"},
"provider": {"service": "justoneapi", "display_name": "JustOneAPI",
"pricing_url": "https://justoneapi.com/", "limits": "60 req/min"},
"siblings": [{"id": "tikhub.tiktok.video.comments", "provider": "tikhub", "provider_display": "TikHub",
"summary": "s", "method": "GET", "path": "/p", "scope": "any_account", "tier": "core",
"cost": {"type": "per_call", "value": 0.001, "currency": "USD", "usd": 0.001},
"verified": "2026-07-28", "docs_url": "", "has_example": True, "input": None}],
"call_template": "treg call justoneapi /api/tiktok/get-post-comment/v1 --query awemeId=76662",
"example_response": {"code": 0, "data": {"comments": [{"text": "hi"}]}},
"hints": [],
}
_stub_catalog_client(monkeypatch, {"/catalog/endpoints/justoneapi.tiktok.video.comments": _CatalogResp(body)})
cli.cmd_catalog(cli.build_parser().parse_args(
["catalog", "get", "justoneapi.tiktok.video.comments"]), {"base_url": "http://x"})
out = capsys.readouterr().out
assert "$0.014/success" in out and "(CNY 0.1)" in out # usd to compare, original to verify
provider = body["provider"]
assert provider["limits"] in out and provider["pricing_url"] in out
assert "tikhub.tiktok.video.comments" in out # the sibling, for comparison
assert "awemeId" in out and "the post id" in out and "e.g. 76662" in out
assert "treg call justoneapi /api/tiktok/get-post-comment/v1 --query awemeId=76662" in out
assert '"comments"' in out # the example response, inline
def test_a_credit_price_reads_as_dollars_with_the_credits_behind_it():
"""A bare credit count is not a price — a reader cannot compare "1 credit" to "$0.001". When
the server priced the credit, dollars lead; only an unpriced credit shows alone."""
priced = {"type": "per_call", "value": 1, "currency": "credit", "usd": 0.00188}
assert cli._cost_label(priced) == "$0.00188/call (1 credit)"
assert cli._cost_usd(priced) == "$0.00188/call" # narrow comparison column
unpriced = {"type": "per_success", "value": 3, "currency": "credit", "usd": None}
assert cli._cost_label(unpriced) == "3 credits/success" # native, labelled as credits
# a duration-priced video table is quoted per second, not as a whole-call total range
per_second = {"type": "per_success", "usd": 13.87, "usd_min": 0.4744,
"rate_usd_min": 0.1186, "rate_usd": 0.462, "rate_unit": "s"}
assert cli._cost_usd(per_second) == "$0.119-$0.462/s"
assert cli._cost_usd(unpriced) == "3 credits/success"
assert "$" not in cli._cost_usd(unpriced), "no rate, no invented dollar figure"
def test_catalog_get_needs_an_id_and_404s_helpfully(monkeypatch, capsys):
p = cli.build_parser()
with pytest.raises(SystemExit):
cli.cmd_catalog(p.parse_args(["catalog", "get"]), {"base_url": "http://x"})
_stub_catalog_client(monkeypatch, {"/catalog/endpoints/nope.x": _CatalogResp({}, status=404)})
with pytest.raises(SystemExit):
cli.cmd_catalog(p.parse_args(["catalog", "get", "nope.x"]), {"base_url": "http://x"})
assert "find one with: treg catalog search" in capsys.readouterr().out
# ---- onboarding path 1: the catalog ---------------------------------------------------------
class _CatResp:
def __init__(self, payload, status=200, text=None):
self._payload, self.status_code = payload, status
self.text = text if text is not None else json.dumps(payload)
def json(self):
return self._payload
class _CatClient:
"""Stands in for `_client(cfg)`: answers the three endpoints the catalog path uses."""
def __init__(self, access: dict, call_status: int = 200):
self.access, self.call_status = access, call_status
self.seen: list[str] = []
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get(self, path, **kw):
self.seen.append(path)
if path.startswith("/catalog/search"):
return _CatResp({"results": [{"id": "tikhub.tiktok.user.profile", "provider": "tikhub",
"summary": "Public TikTok profile"}]})
if path.endswith("/access"):
return _CatResp(self.access)
if path.endswith("/balance"):
return _CatResp({"balance_micro": 999_000, "blocks": [], "entries": []})
return _CatResp({}, 404)
def request(self, method, path, **kw):
self.seen.append(f"{method} {path}")
return _CatResp({"ok": True}, self.call_status, text='{"ok": true}')
def _onboard_catalog(monkeypatch, access, call_status=200):
client = _CatClient(access, call_status)
monkeypatch.setattr(cli, "_client", lambda cfg: client)
monkeypatch.setattr(cli, "_active_org_id", lambda cfg, c: 1)
args = cli.build_parser().parse_args(["onboard", "--path", "catalog", "--yes"])
cli._run_catalog({"base_url": "http://x", "token": "t"}, args)
return client
def test_onboard_catalog_calls_when_treg_serves_it(monkeypatch, capsys):
"""The happy path: treg's own key covers it, so the price is stated and the call is made."""
c = _onboard_catalog(monkeypatch, {"tier": "platform", "estimated_cost_usd": 0.001})
out = capsys.readouterr().out
assert "on treg's key" in out and "$0.001" in out
assert "GET /call/tikhub.tiktok.user.profile" in c.seen # it really called
assert "treg balance" in out # and showed what it cost
def test_onboard_catalog_says_not_metered_for_your_own_key(monkeypatch, capsys):
c = _onboard_catalog(monkeypatch, {"tier": "credential"})
out = capsys.readouterr().out
assert "OWN credential" in out and "not metered" in out
assert any(s.startswith("GET /call/") for s in c.seen)
assert "treg balance" not in out # nothing was spent, so don't show a balance
def test_onboard_catalog_dead_end_names_the_one_command_that_fixes_it(monkeypatch, capsys):
"""Before platform providers are switched on (TREG_PLATFORM_PROVIDERS empty) this is what every
new user sees — so it must not read like a failure, and must never pretend to call."""
c = _onboard_catalog(monkeypatch, {"tier": "none", "detail": "no tikhub credential in this org yet"})
out = capsys.readouterr().out
assert "treg connections connect --provider tikhub" in out
assert not any(s.startswith("GET /call/") for s in c.seen) # nothing was called
def test_show_prints_failure_diagnostics_on_stderr_and_keeps_stdout_the_body(capsys):
"""A failed call must be filable: stdout stays the exact upstream body (whatever parses it), and
stderr gets one line with the HTTP status, whose answer it is, and the call id. A runner that
saved only stdout recorded 115 Moz quota 403s as a bare "cli_error" (2026-09-04)."""
import httpx
body = b'{"error":"The account does not have enough quota remaining for current period."}'
relayed = httpx.Response(403, content=body, headers={
"content-type": "application/json", "X-Treg-Call-Id": "c2075d8d79eb4436aafc310e81081c1b"})
with pytest.raises(SystemExit):
cli._show(relayed)
out, err = capsys.readouterr()
assert json.loads(out) == json.loads(body)
assert "HTTP 403" in err and "provider answered" in err and "c2075d8d79eb4436aafc310e81081c1b" in err
assert "charged" not in err # no cost header → no claim about money
refused = httpx.Response(402, content=b'{"detail":{"error":"route_max_cost"}}', headers={
"content-type": "application/json", "X-Treg-Error": "1"})
with pytest.raises(SystemExit):
cli._show(refused)
_, err = capsys.readouterr()
assert "HTTP 402" in err and "treg refused" in err
ok = httpx.Response(200, content=b'{"ok":true}', headers={"content-type": "application/json"})
cli._show(ok)
out, err = capsys.readouterr()
assert json.loads(out) == {"ok": True} and err == ""
def test_show_prints_the_charge_and_call_id_for_a_metered_success(capsys):
"""A metered 2xx gets one stderr line with the settled charge and the call id; stdout is the exact
body. No cost header (own key, or a non-call response) → nothing extra. A replay says so."""
import httpx
metered = httpx.Response(200, content=b'{"results":[{"x":1}],"next_token":"abc"}', headers={
"content-type": "application/json", "X-Treg-Cost-Micro": "6667",
"X-Treg-Call-Id": "125117d50cd3470cb25ba11f3d9789ad"})
cli._show(metered)
out, err = capsys.readouterr()
assert json.loads(out) == {"results": [{"x": 1}], "next_token": "abc"}
assert err.strip() == "treg: charged $0.006667 · call id 125117d50cd3470cb25ba11f3d9789ad"
own_key = httpx.Response(200, content=b'{"ok":true}', headers={
"content-type": "application/json", "X-Treg-Call-Id": "deadbeef"})
cli._show(own_key)
_, err = capsys.readouterr()
assert err == ""
replay = httpx.Response(200, content=b'{}', headers={
"content-type": "application/json", "X-Treg-Cost-Micro": "0", "X-Treg-Original-Cost-Micro": "6667",
"X-Treg-Idempotent-Replay": "true", "X-Treg-Call-Id": "c1"})
cli._show(replay)
_, err = capsys.readouterr()
assert "$0.006667 by the original call" in err and "nothing new charged" in err
async_submission = httpx.Response(200, content=b'{"run_id":"r1"}', headers={
"content-type": "application/json", "X-Treg-Cost-Micro": "2400000",
"X-Treg-Async": "{}", "X-Treg-Call-Id": "async1"})
cli._show(async_submission)
_, err = capsys.readouterr()
assert err.strip() == "treg: reserved up to $2.4 for async settlement · call id async1"
# ---------------------------------------------------------------------------------------------
# The hub family (phase 4)
def test_hub_init_writes_the_four_files(tmp_path, monkeypatch):
from treg import cli
import argparse
args = argparse.Namespace(name="leads-db", script=True, dir=str(tmp_path / "leads"))
cli.cmd_hub_init(args, {})
folder = tmp_path / "leads"
assert {p.name for p in folder.iterdir()} == {"recipe.json", "run.js", "check.json", "README.md"}
import json
m = json.loads((folder / "recipe.json").read_text())
assert m["name"] == "leads-db" and m["script"] == "run.js" and "required" not in json.dumps(m)
body = cli._hub_read_folder(str(folder))
assert set(body) == {"manifest", "check", "readme", "script"}
steps = argparse.Namespace(name="flow", script=False, dir=str(tmp_path / "flow"))
cli.cmd_hub_init(steps, {})
assert not (tmp_path / "flow" / "run.js").exists() and "steps" in json.loads((tmp_path / "flow" / "recipe.json").read_text())
def test_hub_is_in_the_help_and_parses():
from treg import cli
assert any(name == "hub" for _, rows in cli.HELP_GROUPS for name, _ in rows)
parser = cli.build_parser() if hasattr(cli, "build_parser") else None
if parser is not None:
a = parser.parse_args(["hub", "run", ".", "--input", "domain=figma.com", "--input", "limit=3"])
assert a.hub_cmd == "run" and a.input == ["domain=figma.com", "limit=3"]
e = parser.parse_args(["hub", "earnings", "acme.leads-db", "--days", "30", "--csv"])
assert e.hub_cmd == "earnings" and e.days == 30 and e.csv
pr = parser.parse_args(["hub", "price", "acme.leads-db", "0.02"])
assert pr.hub_cmd == "price" and pr.price_usd == 0.02
rt = parser.parse_args(["hub", "retire", "acme.leads-db"])
assert rt.hub_cmd == "retire" and rt.tool_id == "acme.leads-db"
def test_catalog_get_prints_a_hub_tool_contract(capsys):
from treg import cli
e = {"id": "acme.leads-db", "kind": "hub", "version": 2, "provider": "acme", "made_of": 2, "recipe": "script",
"price_line": "seller $0.01 + steps", "cost": {"usd": 0.01}, "status": "live",
"check": {"status": "passed", "checked_at": "2026-09-09T20:11:00"},
"inputs": {"domain": {"type": "string", "example": "figma.com", "note": "one domain"},
"limit": {"type": "int", "default": 20, "max": 100}},
"output": {"fields": ["rows", "count"]},
"call_template": {"cli": "treg call acme.leads-db --data '{\"domain\": \"figma.com\"}'", "http": "POST https://treg.to/call/acme.leads-db"},
"page": "https://treg.to/hub/acme.leads-db"}
cli._catalog_get_hub(e)
out = capsys.readouterr().out
assert "seller $0.01 + steps" in out and "domain" in out and "(required)" in out and "rows, count" in out
assert "treg call acme.leads-db" in out and "https://treg.to/hub/acme.leads-db" in out
def test_a_hub_refusal_names_the_missing_tool_in_its_fix_commands(capsys):
"""From the case study walk (2026-09-09): the refusal must name the tool the manifest wants,
one command per line, in the order to run them."""
from treg import cli
class R:
status_code = 422
headers = {"content-type": "application/json"}
def json(self):
return {"detail": {"error": "manifest_invalid", "field": "uses[0]",
"rule": "'supabase' is not one of your team's tools (register it first: treg tool add)"}}
import pytest as _pytest
with _pytest.raises(SystemExit):
cli._hub_report(R(), json_out=False)
out = capsys.readouterr().out
assert "uses[0]" in out
assert "treg secret add SUPABASE_KEY --value" in out
assert "treg tool add supabase --base-url" in out
lines = [l for l in out.splitlines() if "treg " in l]
assert len(lines) >= 3 and all(l.count("treg ") == 1 for l in lines) # one command per line
def test_host_prints_the_url_alone_and_the_full_response_under_json(monkeypatch, tmp_path, capsys):
"""`$(treg host face.jpg)` must yield the bare URL; `--json` is the GLOBAL flag main() pops from
argv, so cmd_host reads _JSON_OVERRIDE like every other command (a per-subcommand flag is dead)."""
class Response:
status_code = 201
def json(self):
return {"url": "http://x/m/tok", "token": "tok", "content_type": "image/png",
"size": 3, "expires_at": "2026-09-21T00:00:00Z"}
class Client:
def __enter__(self): return self
def __exit__(self, *args): return False
def post(self, path, **kwargs):
assert path == "/media" and kwargs["content"] == b"png" and kwargs["headers"]["content-type"] == "image/png"
return Response()
monkeypatch.setattr(cli, "_client", lambda cfg, **k: Client())
monkeypatch.setattr(cli, "_load_config", lambda: {"base_url": "http://x", "token": "t"})
f = tmp_path / "face.png"
f.write_bytes(b"png")
cli.main(["host", str(f)])
assert capsys.readouterr().out == "http://x/m/tok\n"
cli.main(["host", str(f), "--json"])
assert json.loads(capsys.readouterr().out)["token"] == "tok"
def test_org_rename_follows_slug_change_locally(monkeypatch, tmp_path):
"""`treg org rename --slug` rewrites active_org; the pinned token is untouched because the server
keeps the old slug as an alias."""
monkeypatch.setattr(cli, "CONFIG_PATH", tmp_path / "config.json")
saved = {}
monkeypatch.setattr(cli, "_save_config", lambda cfg: saved.update(cfg))
monkeypatch.setattr(cli, "_active_org_id", lambda cfg, c: 7)
calls = []
class Response:
status_code = 200
def json(self):
return {"org_id": 7, "org": "team-b", "previous_slug": "team-a", "name": "Team B"}
class Client:
def __enter__(self): return self
def __exit__(self, *a): return False
def patch(self, path, json=None):
calls.append((path, json))
return Response()
monkeypatch.setattr(cli, "_client", lambda cfg: Client())
cfg = {"base_url": "http://x", "token": "TK", "active_org": "team-a"}
cli.cmd_org_rename(type("A", (), {"name": "Team B", "slug": "team-b"})(), cfg)
assert calls == [("/orgs/7", {"name": "Team B", "slug": "team-b"})]
assert cfg["active_org"] == "team-b" and cfg["token"] == "TK" and saved["active_org"] == "team-b"