"""Tests for the admin CLI command group (U8). These tests exercise the Typer command callbacks in :mod:`agentkit.cli.admin` by mocking :class:`AdminHttpClient` so no real HTTP traffic is generated. They verify that each command: - Calls the correct endpoint with the correct method/body/params. - Exits 0 on success and prints the expected output. - Handles connection / HTTP errors gracefully with a non-zero exit. """ from __future__ import annotations import os import tempfile from unittest.mock import MagicMock, patch import httpx import pytest from typer.testing import CliRunner from agentkit.cli.admin import admin_app runner = CliRunner() # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def mock_client(): """Patch :class:`AdminHttpClient` and return the mock instance. The mock's ``base_url`` is set so error handlers can format messages. """ with patch("agentkit.cli.admin.AdminHttpClient") as mock_client_class: mock = MagicMock() mock.base_url = "http://localhost:8001" mock_client_class.from_config.return_value = mock yield mock # --------------------------------------------------------------------------- # Department commands # --------------------------------------------------------------------------- class TestDepartmentCommands: def test_dept_list_json(self, mock_client): """department list --json outputs raw JSON.""" mock_client.get.return_value = [ { "id": "dept-1", "name": "HR", "description": "Human Resources", "is_active": True, "created_at": "2026-01-01T00:00:00Z", } ] result = runner.invoke(admin_app, ["department", "list", "--json"]) assert result.exit_code == 0 assert "HR" in result.stdout assert "dept-1" in result.stdout mock_client.get.assert_called_once_with("/api/v1/admin/departments", params=None) def test_dept_list_table(self, mock_client): """department list renders a Rich table by default.""" mock_client.get.return_value = [ { "id": "dept-1", "name": "Engineering", "description": "Eng team", "is_active": True, "created_at": "2026-01-01", } ] result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code == 0 assert "Engineering" in result.stdout assert "Departments" in result.stdout def test_dept_list_empty(self, mock_client): """department list with no results shows empty message.""" mock_client.get.return_value = [] result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code == 0 assert "No departments" in result.stdout def test_dept_create(self, mock_client): """department create posts the correct body.""" mock_client.post.return_value = { "id": "dept-2", "name": "Finance", "description": "Finance team", } result = runner.invoke( admin_app, [ "department", "create", "--name", "Finance", "--description", "Finance team", ], ) assert result.exit_code == 0 assert "Finance" in result.stdout mock_client.post.assert_called_once_with( "/api/v1/admin/departments", json={"name": "Finance", "description": "Finance team"}, ) def test_dept_update(self, mock_client): """department update patches only provided fields.""" mock_client.patch.return_value = {"id": "dept-1", "name": "HR Updated"} result = runner.invoke( admin_app, ["department", "update", "dept-1", "--name", "HR Updated"], ) assert result.exit_code == 0 mock_client.patch.assert_called_once_with( "/api/v1/admin/departments/dept-1", json={"name": "HR Updated"}, ) def test_dept_update_no_fields_errors(self, mock_client): """department update without --name or --description exits non-zero.""" result = runner.invoke(admin_app, ["department", "update", "dept-1"]) assert result.exit_code != 0 assert "Provide" in result.stdout or "Error" in result.stdout def test_dept_delete(self, mock_client): """department delete calls DELETE and prints success.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke(admin_app, ["department", "delete", "dept-1"]) assert result.exit_code == 0 assert "deleted" in result.stdout.lower() mock_client.delete.assert_called_once_with("/api/v1/admin/departments/dept-1", params=None) def test_dept_enable(self, mock_client): """department enable calls the enable endpoint.""" mock_client.post.return_value = {"id": "dept-1", "is_active": True} result = runner.invoke(admin_app, ["department", "enable", "dept-1"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/departments/dept-1/enable", json=None ) def test_dept_disable(self, mock_client): """department disable calls the disable endpoint.""" mock_client.post.return_value = {"id": "dept-1", "is_active": False} result = runner.invoke(admin_app, ["department", "disable", "dept-1"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/departments/dept-1/disable", json=None ) def test_dept_bind_skill(self, mock_client): """department bind-skill posts to the binding endpoint.""" mock_client.post.return_value = {"bound": True} result = runner.invoke( admin_app, ["department", "bind-skill", "dept-1", "content_generator"], ) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/departments/dept-1/skills/content_generator", json=None, ) def test_dept_unbind_skill(self, mock_client): """department unbind-skill deletes the binding.""" mock_client.delete.return_value = {"unbound": True} result = runner.invoke( admin_app, ["department", "unbind-skill", "dept-1", "content_generator"], ) assert result.exit_code == 0 mock_client.delete.assert_called_once_with( "/api/v1/admin/departments/dept-1/skills/content_generator", params=None, ) def test_dept_bind_kb(self, mock_client): """department bind-kb posts to the KB binding endpoint.""" mock_client.post.return_value = {"bound": True} result = runner.invoke( admin_app, ["department", "bind-kb", "dept-1", "kb-src-1"], ) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/departments/dept-1/kb/kb-src-1", json=None, ) def test_dept_unbind_kb(self, mock_client): """department unbind-kb deletes the KB binding.""" mock_client.delete.return_value = {"unbound": True} result = runner.invoke( admin_app, ["department", "unbind-kb", "dept-1", "kb-src-1"], ) assert result.exit_code == 0 mock_client.delete.assert_called_once_with( "/api/v1/admin/departments/dept-1/kb/kb-src-1", params=None, ) def test_dept_list_skills(self, mock_client): """department list-skills returns bound skill names.""" mock_client.get.return_value = ["content_generator", "code_reviewer"] result = runner.invoke(admin_app, ["department", "list-skills", "dept-1"]) assert result.exit_code == 0 assert "content_generator" in result.stdout def test_dept_list_quotas(self, mock_client): """department list-quotas returns quota rows.""" mock_client.get.return_value = [ {"quota_type": "token", "limit_value": 100000, "period": "daily"} ] result = runner.invoke(admin_app, ["department", "list-quotas", "dept-1"]) assert result.exit_code == 0 assert "token" in result.stdout def test_dept_set_quota_token(self, mock_client): """department set-quota with token type sends int limit.""" mock_client.put.return_value = {"quota_type": "token", "limit_value": 100000} result = runner.invoke( admin_app, [ "department", "set-quota", "dept-1", "--type", "token", "--limit", "100000", ], ) assert result.exit_code == 0 mock_client.put.assert_called_once_with( "/api/v1/admin/departments/dept-1/quotas", json={ "quota_type": "token", "limit_value": 100000, "period": "daily", }, ) def test_dept_set_quota_whitelist(self, mock_client): """department set-quota with model_whitelist sends list.""" mock_client.put.return_value = {"quota_type": "model_whitelist"} result = runner.invoke( admin_app, [ "department", "set-quota", "dept-1", "--type", "model_whitelist", "--limit", "gpt-4,claude-3", ], ) assert result.exit_code == 0 mock_client.put.assert_called_once_with( "/api/v1/admin/departments/dept-1/quotas", json={ "quota_type": "model_whitelist", "limit_value": ["gpt-4", "claude-3"], "period": "daily", }, ) def test_dept_delete_quota(self, mock_client): """department delete-quota passes query params.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke( admin_app, [ "department", "delete-quota", "dept-1", "--type", "token", "--period", "monthly", ], ) assert result.exit_code == 0 mock_client.delete.assert_called_once_with( "/api/v1/admin/departments/dept-1/quotas", params={"quota_type": "token", "period": "monthly"}, ) # --------------------------------------------------------------------------- # User commands # --------------------------------------------------------------------------- class TestUserCommands: def test_user_list(self, mock_client): """user list returns users table.""" mock_client.get.return_value = [ { "id": "user-1", "username": "alice", "email": "alice@example.com", "role": "member", "is_active": True, } ] result = runner.invoke(admin_app, ["user", "list"]) assert result.exit_code == 0 assert "alice" in result.stdout mock_client.get.assert_called_once_with("/api/v1/admin/users", params=None) def test_user_list_with_department_filter(self, mock_client): """user list --department-id passes the filter.""" mock_client.get.return_value = [] result = runner.invoke(admin_app, ["user", "list", "--department-id", "dept-1"]) assert result.exit_code == 0 mock_client.get.assert_called_once_with( "/api/v1/admin/users", params={"department_id": "dept-1"} ) def test_user_create(self, mock_client): """user create posts the user body.""" mock_client.post.return_value = { "id": "user-2", "username": "bob", "email": "bob@example.com", } result = runner.invoke( admin_app, [ "user", "create", "--username", "bob", "--email", "bob@example.com", "--password", "secret123", ], ) assert result.exit_code == 0 assert "bob" in result.stdout mock_client.post.assert_called_once_with( "/api/v1/admin/users", json={ "username": "bob", "email": "bob@example.com", "password": "secret123", "role": "member", }, ) def test_user_create_with_departments(self, mock_client): """user create --department-ids splits the comma list.""" mock_client.post.return_value = {"id": "user-3", "username": "carol"} result = runner.invoke( admin_app, [ "user", "create", "--username", "carol", "--email", "carol@example.com", "--password", "secret123", "--department-ids", "dept-1,dept-2", ], ) assert result.exit_code == 0 body = mock_client.post.call_args.kwargs["json"] assert body["department_ids"] == ["dept-1", "dept-2"] def test_user_update_role(self, mock_client): """user update --role patches only the role field.""" mock_client.patch.return_value = {"id": "user-1", "role": "admin"} result = runner.invoke(admin_app, ["user", "update", "user-1", "--role", "admin"]) assert result.exit_code == 0 mock_client.patch.assert_called_once_with( "/api/v1/admin/users/user-1", json={"role": "admin"}, ) def test_user_delete(self, mock_client): """user delete soft-deletes the user.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke(admin_app, ["user", "delete", "user-1"]) assert result.exit_code == 0 mock_client.delete.assert_called_once_with("/api/v1/admin/users/user-1", params=None) def test_user_reset_password(self, mock_client): """user reset-password posts the new password.""" mock_client.post.return_value = {"reset": True} result = runner.invoke( admin_app, ["user", "reset-password", "user-1", "--password", "newpass"], ) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/users/user-1/reset-password", json={"new_password": "newpass"}, ) def test_user_assign_department(self, mock_client): """user assign-department posts to the assignment endpoint.""" mock_client.post.return_value = {"assigned": True} result = runner.invoke(admin_app, ["user", "assign-department", "user-1", "dept-1"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/users/user-1/departments/dept-1", json=None, ) def test_user_remove_department(self, mock_client): """user remove-department deletes the assignment.""" mock_client.delete.return_value = {"removed": True} result = runner.invoke(admin_app, ["user", "remove-department", "user-1", "dept-1"]) assert result.exit_code == 0 mock_client.delete.assert_called_once_with( "/api/v1/admin/users/user-1/departments/dept-1", params=None, ) # --------------------------------------------------------------------------- # LLM commands # --------------------------------------------------------------------------- class TestLlmCommands: def test_llm_list_providers(self, mock_client): """llm list-providers returns the providers table.""" mock_client.get.return_value = [ { "name": "openai", "type": "openai", "base_url": "", "api_key": "sk-***", "max_tokens": 4096, "timeout": 60.0, } ] result = runner.invoke(admin_app, ["llm", "list-providers"]) assert result.exit_code == 0 assert "openai" in result.stdout def test_llm_add_provider(self, mock_client): """llm add-provider posts the provider config.""" mock_client.post.return_value = {"name": "anthropic"} result = runner.invoke( admin_app, [ "llm", "add-provider", "--name", "anthropic", "--api-key", "sk-ant-xxx", ], ) assert result.exit_code == 0 body = mock_client.post.call_args.kwargs["json"] assert body["name"] == "anthropic" assert body["api_key"] == "sk-ant-xxx" def test_llm_update_provider(self, mock_client): """llm update-provider patches the provider.""" mock_client.patch.return_value = {"name": "openai"} result = runner.invoke( admin_app, ["llm", "update-provider", "openai", "--max-tokens", "8192"], ) assert result.exit_code == 0 mock_client.patch.assert_called_once_with( "/api/v1/admin/llm/providers/openai", json={"max_tokens": 8192}, ) def test_llm_delete_provider(self, mock_client): """llm delete-provider deletes the provider.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke(admin_app, ["llm", "delete-provider", "openai"]) assert result.exit_code == 0 mock_client.delete.assert_called_once_with( "/api/v1/admin/llm/providers/openai", params=None ) def test_llm_set_api_key(self, mock_client): """llm set-api-key posts the new key.""" mock_client.post.return_value = {"name": "openai", "api_key": "sk-***"} result = runner.invoke( admin_app, ["llm", "set-api-key", "openai", "--api-key", "sk-new"], ) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/llm/providers/openai/api-key", json={"api_key": "sk-new"}, ) def test_llm_list_fallbacks(self, mock_client): """llm list-fallbacks returns the fallback map.""" mock_client.get.return_value = {"gpt-4": ["openai/gpt-4", "anthropic/claude-3"]} result = runner.invoke(admin_app, ["llm", "list-fallbacks"]) assert result.exit_code == 0 assert "gpt-4" in result.stdout def test_llm_set_fallback(self, mock_client): """llm set-fallback puts the chain list.""" mock_client.put.return_value = {"model": "gpt-4", "chain": ["a", "b"]} result = runner.invoke( admin_app, ["llm", "set-fallback", "gpt-4", "--chain", "openai/gpt-4,anthropic/claude-3"], ) assert result.exit_code == 0 mock_client.put.assert_called_once_with( "/api/v1/admin/llm/fallbacks/gpt-4", json={"chain": ["openai/gpt-4", "anthropic/claude-3"]}, ) def test_llm_delete_fallback(self, mock_client): """llm delete-fallback deletes the chain.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke(admin_app, ["llm", "delete-fallback", "gpt-4"]) assert result.exit_code == 0 mock_client.delete.assert_called_once_with("/api/v1/admin/llm/fallbacks/gpt-4", params=None) # --------------------------------------------------------------------------- # Skill commands # --------------------------------------------------------------------------- class TestSkillCommands: def test_skill_list(self, mock_client): """skill list calls GET /skills (not /admin/skills).""" mock_client.get.return_value = [ {"name": "content_generator", "agent_type": "llm", "description": "Gen"} ] result = runner.invoke(admin_app, ["skill", "list"]) assert result.exit_code == 0 mock_client.get.assert_called_once_with("/api/v1/skills", params=None) def test_skill_enable(self, mock_client): """skill enable posts to the enable endpoint.""" mock_client.post.return_value = {"enabled": True, "skill_name": "x"} result = runner.invoke(admin_app, ["skill", "enable", "x"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/enable", json=None) def test_skill_disable(self, mock_client): """skill disable posts to the disable endpoint.""" mock_client.post.return_value = {"disabled": True} result = runner.invoke(admin_app, ["skill", "disable", "x"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/disable", json=None) def test_skill_reload(self, mock_client): """skill reload posts to the reload endpoint.""" mock_client.post.return_value = {"name": "x", "reloaded": True} result = runner.invoke(admin_app, ["skill", "reload", "x"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with("/api/v1/admin/skills/x/reload", json=None) def test_skill_import(self, mock_client): """skill import reads the YAML file and posts its content.""" mock_client.post.return_value = {"name": "imported_skill"} with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: f.write("name: imported_skill\ndescription: test\n") f.flush() path = f.name try: result = runner.invoke(admin_app, ["skill", "import", path]) assert result.exit_code == 0 body = mock_client.post.call_args.kwargs["json"] assert "imported_skill" in body["yaml_content"] finally: os.unlink(path) def test_skill_import_missing_file(self, mock_client): """skill import with a missing file exits non-zero.""" result = runner.invoke(admin_app, ["skill", "import", "/nonexistent.yaml"]) assert result.exit_code != 0 assert "not found" in result.stdout.lower() # --------------------------------------------------------------------------- # KB commands # --------------------------------------------------------------------------- class TestKbCommands: def test_kb_list_documents(self, mock_client): """kb list-documents returns the documents table.""" mock_client.get.return_value = { "documents": [ { "id": "doc-1", "filename": "spec.md", "source_id": "src-1", "department_id": "dept-1", "size": 1024, "created_at": "2026-01-01", } ] } result = runner.invoke(admin_app, ["kb", "list-documents"]) assert result.exit_code == 0 assert "spec.md" in result.stdout def test_kb_upload(self, mock_client): """kb upload reads the file and posts the content.""" mock_client.post.return_value = {"id": "doc-2", "filename": "x.md"} with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f: f.write("# Title\nbody") f.flush() path = f.name try: result = runner.invoke( admin_app, [ "kb", "upload", "--filename", "x.md", "--content-file", path, "--source-id", "src-1", ], ) assert result.exit_code == 0 body = mock_client.post.call_args.kwargs["json"] assert body["filename"] == "x.md" assert "# Title" in body["content"] finally: os.unlink(path) def test_kb_delete(self, mock_client): """kb delete deletes the document.""" mock_client.delete.return_value = {"deleted": True} result = runner.invoke(admin_app, ["kb", "delete", "doc-1"]) assert result.exit_code == 0 mock_client.delete.assert_called_once_with("/api/v1/admin/kb/documents/doc-1", params=None) def test_kb_sync(self, mock_client): """kb sync triggers a source sync.""" mock_client.post.return_value = {"synced": True} result = runner.invoke(admin_app, ["kb", "sync", "src-1"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with("/api/v1/admin/kb/sources/src-1/sync", json=None) def test_kb_rebuild(self, mock_client): """kb rebuild triggers a source rebuild.""" mock_client.post.return_value = {"rebuilt": True} result = runner.invoke(admin_app, ["kb", "rebuild", "src-1"]) assert result.exit_code == 0 mock_client.post.assert_called_once_with( "/api/v1/admin/kb/sources/src-1/rebuild", json=None ) # --------------------------------------------------------------------------- # Usage commands # --------------------------------------------------------------------------- class TestUsageCommands: def test_usage_summary(self, mock_client): """usage summary returns aggregated metrics.""" mock_client.get.return_value = { "total_requests": 100, "total_tokens": 50000, "total_cost": 1.23, } result = runner.invoke(admin_app, ["usage", "summary"]) assert result.exit_code == 0 assert "total_requests" in result.stdout assert "100" in result.stdout def test_usage_summary_json(self, mock_client): """usage summary --json outputs raw JSON.""" mock_client.get.return_value = {"total_requests": 5} result = runner.invoke(admin_app, ["usage", "summary", "--json"]) assert result.exit_code == 0 assert "total_requests" in result.stdout assert "5" in result.stdout def test_usage_timeseries(self, mock_client): """usage timeseries returns bucketed rows.""" mock_client.get.return_value = [ {"bucket": "2026-01-01", "requests": 10, "tokens": 1000, "cost": 0.1} ] result = runner.invoke(admin_app, ["usage", "timeseries", "--interval", "day"]) assert result.exit_code == 0 assert "2026-01-01" in result.stdout params = mock_client.get.call_args.kwargs["params"] assert params["interval"] == "day" def test_usage_by_model(self, mock_client): """usage by-model returns per-model rows.""" mock_client.get.return_value = [ {"model": "gpt-4", "requests": 50, "tokens": 20000, "cost": 0.8} ] result = runner.invoke(admin_app, ["usage", "by-model"]) assert result.exit_code == 0 assert "gpt-4" in result.stdout def test_usage_top_users(self, mock_client): """usage top-users returns ranked rows.""" mock_client.get.return_value = [ { "user_id": "user-1", "requests": 100, "tokens": 50000, "cost": 1.5, } ] result = runner.invoke(admin_app, ["usage", "top-users", "--limit", "5"]) assert result.exit_code == 0 assert "user-1" in result.stdout params = mock_client.get.call_args.kwargs["params"] assert params["limit"] == 5 def test_usage_export_stdout(self, mock_client): """usage export prints CSV to stdout.""" mock_client.get_text.return_value = "user_id,tokens\nalice,1000\n" result = runner.invoke(admin_app, ["usage", "export", "--format", "csv"]) assert result.exit_code == 0 assert "user_id" in result.stdout def test_usage_export_to_file(self, mock_client): """usage export --output writes to a file.""" mock_client.get_text.return_value = "user_id,tokens\nalice,1000\n" with tempfile.NamedTemporaryFile(mode="r", suffix=".csv", delete=False) as f: path = f.name try: result = runner.invoke(admin_app, ["usage", "export", "--output", path]) assert result.exit_code == 0 with open(path) as f: content = f.read() assert "user_id" in content finally: os.unlink(path) # --------------------------------------------------------------------------- # Login command # --------------------------------------------------------------------------- class TestLoginCommand: def test_login_success(self): """admin login saves the token to the config file.""" with tempfile.TemporaryDirectory() as tmpdir: config_path = os.path.join(tmpdir, "admin_config.yaml") with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls: mock_instance = MagicMock() mock_instance.login.return_value = "jwt-token-123" mock_cls.return_value = mock_instance result = runner.invoke( admin_app, [ "login", "--username", "admin", "--password", "secret", "--config-path", config_path, ], ) assert result.exit_code == 0 assert "Login successful" in result.stdout import yaml with open(config_path) as f: cfg = yaml.safe_load(f) assert cfg["token"] == "jwt-token-123" assert cfg["server_url"] == "http://localhost:18001" def test_login_with_server_url(self): """admin login --server-url saves the custom URL.""" with tempfile.TemporaryDirectory() as tmpdir: config_path = os.path.join(tmpdir, "admin_config.yaml") with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls: mock_instance = MagicMock() mock_instance.login.return_value = "tok" mock_cls.return_value = mock_instance result = runner.invoke( admin_app, [ "login", "--username", "admin", "--password", "secret", "--server-url", "http://my-server:9000", "--config-path", config_path, ], ) assert result.exit_code == 0 import yaml with open(config_path) as f: cfg = yaml.safe_load(f) assert cfg["server_url"] == "http://my-server:9000" def test_login_failure_auth_error(self): """admin login with bad credentials exits non-zero.""" with tempfile.TemporaryDirectory() as tmpdir: config_path = os.path.join(tmpdir, "admin_config.yaml") with patch("agentkit.cli.admin.AdminHttpClient") as mock_cls: mock_instance = MagicMock() # Simulate a 401 from the server. response = MagicMock(spec=httpx.Response) response.status_code = 401 response.text = "Unauthorized" response.json.return_value = {"detail": "Invalid credentials"} mock_instance.login.side_effect = httpx.HTTPStatusError( "Unauthorized", request=MagicMock(), response=response ) mock_cls.return_value = mock_instance result = runner.invoke( admin_app, [ "login", "--username", "admin", "--password", "wrong", "--config-path", config_path, ], ) assert result.exit_code != 0 assert "Authentication failed" in result.stdout # --------------------------------------------------------------------------- # Error handling # --------------------------------------------------------------------------- class TestErrorHandling: def test_connection_error(self, mock_client): """Connection errors produce a friendly message and exit 1.""" mock_client.get.side_effect = httpx.ConnectError("connection refused") result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code != 0 assert "Cannot connect" in result.stdout def test_auth_error_401(self, mock_client): """401 errors prompt the user to run 'admin login'.""" response = MagicMock(spec=httpx.Response) response.status_code = 401 response.text = "Unauthorized" response.json.return_value = {"detail": "Unauthorized"} mock_client.get.side_effect = httpx.HTTPStatusError( "Unauthorized", request=MagicMock(), response=response ) result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code != 0 assert "Authentication failed" in result.stdout def test_forbidden_error_403(self, mock_client): """403 errors explain admin permission is required.""" response = MagicMock(spec=httpx.Response) response.status_code = 403 response.text = "Forbidden" response.json.return_value = {"detail": "Forbidden"} mock_client.get.side_effect = httpx.HTTPStatusError( "Forbidden", request=MagicMock(), response=response ) result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code != 0 assert "Admin permission required" in result.stdout def test_not_found_error_404(self, mock_client): """404 errors print a not-found message.""" response = MagicMock(spec=httpx.Response) response.status_code = 404 response.text = "Not Found" response.json.return_value = {"detail": "Not found"} mock_client.get.side_effect = httpx.HTTPStatusError( "Not Found", request=MagicMock(), response=response ) result = runner.invoke(admin_app, ["department", "list"]) assert result.exit_code != 0 assert "not found" in result.stdout.lower() def test_conflict_error_409(self, mock_client): """409 errors print the conflict detail.""" response = MagicMock(spec=httpx.Response) response.status_code = 409 response.text = "Conflict" response.json.return_value = {"detail": "Name already exists"} mock_client.post.side_effect = httpx.HTTPStatusError( "Conflict", request=MagicMock(), response=response ) result = runner.invoke( admin_app, ["department", "create", "--name", "dup"], ) assert result.exit_code != 0 assert "Conflict" in result.stdout assert "Name already exists" in result.stdout # --------------------------------------------------------------------------- # Help / registration smoke tests # --------------------------------------------------------------------------- class TestAdminAppRegistration: def test_admin_help_shows_groups(self): """agentkit admin --help lists all sub-groups.""" result = runner.invoke(admin_app, ["--help"]) assert result.exit_code == 0 for group in ("department", "user", "llm", "skill", "kb", "usage", "login"): assert group in result.stdout def test_admin_department_help(self): """agentkit admin department --help lists department commands.""" result = runner.invoke(admin_app, ["department", "--help"]) assert result.exit_code == 0 for cmd in ("list", "create", "update", "delete", "bind-skill", "unbind-skill"): assert cmd in result.stdout def test_admin_registered_on_main_app(self): """The admin sub-app is registered on the main agentkit app.""" from agentkit.cli.main import app as main_app result = runner.invoke(main_app, ["--help"]) assert result.exit_code == 0 assert "admin" in result.stdout