test(e2e): cover tag creation persisting for spend categorization (#34018)

This commit is contained in:
Yassin Kortam 2026-07-20 12:24:50 -07:00 • committed by GitHub
parent 51df801159
commit 72be5a9bc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 1 deletions

View file

@ -26,6 +26,10 @@ from models import (
OrgInfoResponse,
OrgNewBody,
OrgNewResponse,
TagDeleteBody,
TagListEntry,
TagListResponse,
TagNewBody,
TeamData,
TeamDeleteBody,
TeamInfoParams,
@ -259,6 +263,36 @@ class ManagementClient:
)
)
def create_tag(self, body: TagNewBody) -> None:
_ = unwrap(
self.proxy.transport.post(
"/tag/new",
headers=self.proxy.transport.master,
json=body,
response_type=NoBody,
)
)
def delete_tag(self, name: str) -> None:
_ = self.proxy.transport.post(
"/tag/delete",
headers=self.proxy.transport.master,
json=TagDeleteBody(name=name),
response_type=NoBody,
)
def tag_list(self) -> tuple[TagListEntry, ...]:
return tuple(
unwrap(
self.proxy.transport.get(
"/tag/list",
headers=self.proxy.transport.master,
params=NoBody(),
response_type=TagListResponse,
)
).root
)
def chat_status(self, key: str, model: str, content: str) -> StreamingResponse:
return self.proxy.transport.send(
"/chat/completions",

View file

@ -22,7 +22,7 @@ from management_client import (
ROUTE_NOT_ALLOWED_MARKER,
ManagementClient,
)
from models import KeyGenerateBody, OrgNewBody, TeamNewBody, UserNewBody
from models import KeyGenerateBody, OrgNewBody, TagListEntry, TagNewBody, TeamNewBody, UserNewBody
pytestmark = pytest.mark.e2e
@ -271,6 +271,28 @@ class TestOrganizationRoutes:
)
class TestTagRoutes:
@pytest.mark.covers("mgmt.tag.new.happy_path")
def test_new_persists_to_tag_list(self, client: ManagementClient, resources: ResourceManager) -> None:
name = f"e2e-mgmt-tag-{unique_marker()}"
description = "Tag for spend categorization"
assert all(entry.name != name for entry in client.tag_list()), (
f"tag {name!r} was already listed by /tag/list before /tag/new created it"
)
client.create_tag(TagNewBody(name=name, description=description))
resources.defer(lambda: client.delete_tag(name))
def listed() -> TagListEntry | None:
return next((entry for entry in client.tag_list() if entry.name == name), None)
entry = _poll(client, listed, f"/tag/list never listed {name!r} after /tag/new")
assert entry.description == description, (
f"/tag/list reports description {entry.description!r} for {name!r}, configured {description!r}"
)
def _assert_route_forbidden(route: str, outcome: StreamingResponse) -> None:
assert outcome.status_code == 403, (
f"llm-only key POSTing {route} must be denied exactly 403, got {outcome.status_code}: {outcome.body[:300]}"

View file

@ -699,3 +699,26 @@ class OrgInfoResponse(BaseModel):
class OrgDeleteBody(BaseModel):
organization_ids: list[str]
# ---------- tags (management) ----------
class TagNewBody(BaseModel):
name: str
description: str | None = None
class TagDeleteBody(BaseModel):
name: str
class TagListEntry(BaseModel):
name: str
description: str | None = None
class TagListResponse(RootModel[list[TagListEntry]]):
"""GET /tag/list answers with a bare array of tag configs (the stored tags plus
any dynamically-seen spend tags), not an object wrapping them. Read the rows off
.root."""