Files
treg/tests/test_org_rename.py
Jason ZhouandClaude Fable 5.1 e9c3ebd69c feat(orgs): rename a team's name and slug
PATCH /orgs/{id} (admin+) changes name and/or slug. The slug is baked into signed team
keys, ~/.treg, MCP pins and agent addresses, so a slug change retires the old one into
Org.previous_slug: _resolve_org still honours it, no other team may claim it, and
pre-rename agents keep their short names. Dashboard form under Team settings and
`treg org rename`. Migration 0040.

Two migration-pinned tests inserted an Org through the current ORM at an old revision;
they now insert only the columns that exist there.

Fragments: multi-tenancy, data-model, api, cli, dashboard; USAGE.md, skill.md, llms.txt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 08:54:12 +10:00

62 lines
3.3 KiB
Python

"""PATCH /orgs/{id}: change a team's name and slug; the retired slug stays an alias."""
from httpx import AsyncClient
from tests.test_orgs_mgmt import _h, _register, _team_with_member, c # noqa: F401 (fixture)
async def test_admin_renames_name_and_slug_member_cannot(c: AsyncClient):
org_id, otok, mtok, *_ = await _team_with_member(c)
r = await c.patch(f"/orgs/{org_id}", headers=_h(mtok), json={"name": "Nope"})
assert r.status_code == 403
r = await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"name": " Team B "})
assert r.status_code == 200, r.text
assert r.json() == {"org_id": org_id, "org": "team-a", "previous_slug": None, "name": "Team B"}
r = await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"slug": "team-b"})
assert r.status_code == 200, r.text
assert r.json() == {"org_id": org_id, "org": "team-b", "previous_slug": "team-a", "name": "Team B"}
orgs = (await c.get("/orgs", headers=_h(otok))).json()
assert [o["slug"] for o in orgs if o["org_id"] == org_id] == ["team-b"]
async def test_old_slug_keeps_resolving_for_pinned_credentials(c: AsyncClient):
org_id, otok, mtok, *_ = await _team_with_member(c)
# otok was minted by POST /orgs with org=team-a baked into its claim
await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"slug": "team-b"})
for slug in ("team-a", "team-b"):
r = await c.post("/agents/checkin", headers={**_h(otok), "X-Treg-Org": slug})
assert r.status_code == 200, (slug, r.text)
assert r.json()["org"] == "team-b"
assert (await c.get("/tools", headers=_h(mtok))).status_code == 200
async def test_retired_slug_cannot_be_taken_by_another_team(c: AsyncClient):
org_id, otok, *_ = await _team_with_member(c)
await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"slug": "team-b"})
other = await _register(c, "other@x.dev")
r = await c.post("/orgs", headers=_h(other), json={"name": "Team A"})
assert r.json()["org"] == "team-a-2"
other_id, other_tok = r.json()["org_id"], r.json()["token"]
for taken in ("team-a", "team-b"):
r = await c.patch(f"/orgs/{other_id}", headers=_h(other_tok), json={"slug": taken})
assert r.status_code == 409, (taken, r.text)
async def test_bad_slugs_rejected(c: AsyncClient):
org_id, otok, *_ = await _team_with_member(c)
for bad in ("Bad Slug!", "ab", "sbx-0123456789ab", "x" * 41):
r = await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"slug": bad})
assert r.status_code == 400, (bad, r.text)
assert (await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"name": " "})).status_code == 400
assert (await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={})).status_code == 422
async def test_agent_named_before_rename_keeps_its_short_name(c: AsyncClient):
org_id, otok, *_ = await _team_with_member(c)
r = await c.post(f"/orgs/{org_id}/agents", headers=_h(otok), json={"name": "deploy"})
assert r.status_code == 200, r.text
await c.patch(f"/orgs/{org_id}", headers=_h(otok), json={"slug": "team-b"})
names = {a["name"] for a in (await c.get(f"/orgs/{org_id}/agents", headers=_h(otok))).json()}
assert "deploy" in names
keys = (await c.get(f"/orgs/{org_id}/api-keys", headers=_h(otok))).json()
assert any(k.get("assigned_name") == "deploy" or k.get("name") == "deploy" for k in keys), keys