# -*- coding: utf-8 -*- """Integration tests for multi-agent lifecycle management. Covers agent creation (with full options, auto-id, workspace init), updates, ordering, enable/disable routing effects, workspace isolation, deletion behaviour, and error boundaries (duplicate, reserved, invalid format, too short, nonexistent). """ from __future__ import annotations import re import time import pytest from tests.integration.helpers import ( create_agent, default_http_timeout, delete_agent_quietly, scoped, toggle_agent, ) _AGENT_HTTP_TIMEOUT = default_http_timeout(15.0) # ------------------------------------------------------------------ # # creation — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p0 def test_create_agent_with_full_options(app_server) -> None: """Test purpose: - Verify POST /api/agents with id, name, description, and language creates an agent with all fields persisted. Test flow: 1. POST /api/agents with full options. 2. GET /api/agents/{id} and verify all fields. 3. Cleanup. API endpoints: - POST /api/agents - GET /api/agents/{agentId} """ agent_id = "integ_ma_full_01" try: resp = app_server.api_request( "POST", "/api/agents", json={ "id": agent_id, "name": "Full Options Agent", "description": "test agent with all fields", "language": "zh", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code == 201, app_server.logs_tail() body = resp.json() assert body.get("id") == agent_id assert body.get("enabled") is True assert "workspace_dir" in body get_resp = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_resp.status_code == 200, app_server.logs_tail() profile = get_resp.json() assert profile.get("name") == "Full Options Agent" assert profile.get("description") == ("test agent with all fields") assert "workspace_dir" in profile finally: delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p1 def test_create_agent_auto_id_when_omitted(app_server) -> None: """Test purpose: - Verify POST /api/agents without an id returns 201 with an auto-generated id matching the expected pattern. Test flow: 1. POST /api/agents with name only (no id). 2. Assert 201 + returned id is alphanumeric 2-64 chars. 3. Cleanup. API endpoints: - POST /api/agents - DELETE /api/agents/{agentId} """ resp = app_server.api_request( "POST", "/api/agents", json={ "name": "Auto ID Agent", "description": "no explicit id", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code == 201, app_server.logs_tail() body = resp.json() auto_id = body.get("id", "") assert len(auto_id) >= 2 assert re.match( r"^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$", auto_id, ), f"auto id does not match pattern: {auto_id}" delete_agent_quietly(app_server, auto_id) @pytest.mark.integration @pytest.mark.p0 def test_create_agent_workspace_initialized(app_server) -> None: """Test purpose: - Verify that creating an agent initializes the workspace directory with expected subdirectories and files. Test flow: 1. POST /api/agents. 2. GET /api/agents/{id} to find workspace_dir. 3. Check sessions/, memory/, jobs.json, chats.json exist. 4. Cleanup. API endpoints: - POST /api/agents - GET /api/agents/{agentId} """ from pathlib import Path agent_id = "integ_ma_ws_init_01" try: create_agent(app_server, agent_id) get_resp = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_resp.status_code == 200, app_server.logs_tail() ws_dir = Path(get_resp.json().get("workspace_dir", "")) assert ws_dir.is_dir(), f"workspace_dir not found: {ws_dir}" assert (ws_dir / "sessions").is_dir() assert (ws_dir / "memory").is_dir() assert (ws_dir / "jobs.json").is_file() assert (ws_dir / "chats.json").is_file() finally: delete_agent_quietly(app_server, agent_id) # ------------------------------------------------------------------ # # update — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p0 def test_update_agent_roundtrip_with_side_effects(app_server) -> None: """Test purpose: - Verify PUT /api/agents/{id} persists name and description changes and does not mutate other fields (workspace_dir, enabled). Test flow: 1. Create agent and GET full profile as baseline. 2. PUT with modified name + description. 3. GET and verify changes persisted + other fields unchanged. 4. Cleanup. API endpoints: - POST /api/agents - GET /api/agents/{agentId} - PUT /api/agents/{agentId} """ agent_id = "integ_ma_update_01" try: create_agent(app_server, agent_id) get_before = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_before.status_code == 200, app_server.logs_tail() before = get_before.json() updated = dict(before) updated["name"] = "Updated Name" updated["description"] = "Updated description" put_resp = app_server.api_request( "PUT", f"/api/agents/{agent_id}", json=updated, timeout=_AGENT_HTTP_TIMEOUT, ) assert put_resp.status_code == 200, app_server.logs_tail() get_after = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_after.status_code == 200, app_server.logs_tail() after = get_after.json() assert after.get("name") == "Updated Name" assert after.get("description") == "Updated description" assert after.get("workspace_dir") == before.get( "workspace_dir", ) assert after.get("enabled") == before.get("enabled") finally: delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p1 def test_update_default_agent_allowed(app_server) -> None: """Test purpose: - Verify the default agent can be updated (name/description) even though it cannot be deleted or disabled. Test flow: 1. GET /api/agents/default as baseline. 2. PUT with modified description. 3. GET and verify. 4. Restore baseline. API endpoints: - GET /api/agents/default - PUT /api/agents/default """ get_before = app_server.api_request( "GET", "/api/agents/default", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_before.status_code == 200, app_server.logs_tail() before = get_before.json() updated = dict(before) updated["description"] = "integ-test-default-update" try: put_resp = app_server.api_request( "PUT", "/api/agents/default", json=updated, timeout=_AGENT_HTTP_TIMEOUT, ) assert put_resp.status_code == 200, app_server.logs_tail() get_after = app_server.api_request( "GET", "/api/agents/default", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_after.status_code == 200, app_server.logs_tail() assert ( get_after.json().get("description") == "integ-test-default-update" ) finally: app_server.api_request( "PUT", "/api/agents/default", json=before, timeout=_AGENT_HTTP_TIMEOUT, ) # ------------------------------------------------------------------ # # ordering — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p1 def test_new_agent_appended_to_order(app_server) -> None: """Test purpose: - Verify a newly created agent appears at the end of the agent list. Test flow: 1. GET /api/agents and note order. 2. POST create new agent. 3. GET /api/agents and verify new agent is last. 4. Cleanup. API endpoints: - GET /api/agents - POST /api/agents """ agent_id = "integ_ma_order_01" try: list_before = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) assert list_before.status_code == 200, app_server.logs_tail() ids_before = [a["id"] for a in list_before.json().get("agents", [])] create_agent(app_server, agent_id) list_after = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) assert list_after.status_code == 200, app_server.logs_tail() ids_after = [a["id"] for a in list_after.json().get("agents", [])] assert ids_after[-1] == agent_id assert ids_after[: len(ids_before)] == ids_before finally: delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p0 def test_multi_agent_reorder_and_delete_order_adjusts( app_server, ) -> None: """Test purpose: - Verify reorder persists and deletion adjusts the order. Test flow: 1. Create agents A, B, C. 2. GET list → note full order. 3. PUT /api/agents/order with [C, ..., A, B] arrangement. 4. GET list and verify new order. 5. DELETE A → GET list → A removed, C and B remain in order. 6. Cleanup. API endpoints: - POST /api/agents - GET /api/agents - PUT /api/agents/order - DELETE /api/agents/{agentId} """ a_id = "integ_ma_reorder_a" b_id = "integ_ma_reorder_b" c_id = "integ_ma_reorder_c" try: create_agent(app_server, a_id) create_agent(app_server, b_id) create_agent(app_server, c_id) list_resp = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) assert list_resp.status_code == 200, app_server.logs_tail() all_ids = [a["id"] for a in list_resp.json().get("agents", [])] assert a_id in all_ids assert b_id in all_ids assert c_id in all_ids pinned_prefix_length = next( ( index for index, agent in enumerate( list_resp.json().get("agents", []), ) if agent["id"] != "default" and not agent.get("pinned", False) ), len(all_ids), ) new_order = list(all_ids) new_order.remove(c_id) new_order.insert(pinned_prefix_length, c_id) reorder_resp = app_server.api_request( "PUT", "/api/agents/order", json={"agent_ids": new_order}, timeout=_AGENT_HTTP_TIMEOUT, ) assert reorder_resp.status_code == 200, app_server.logs_tail() list_after_reorder = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) reordered_agents = list_after_reorder.json().get("agents", []) ids_reordered = [agent["id"] for agent in reordered_agents] assert ids_reordered == new_order del_resp = app_server.api_request( "DELETE", f"/api/agents/{a_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert del_resp.status_code == 200, app_server.logs_tail() list_after_del = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) ids_final = [a["id"] for a in list_after_del.json().get("agents", [])] assert a_id not in ids_final assert c_id in ids_final assert b_id in ids_final c_idx = ids_final.index(c_id) b_idx = ids_final.index(b_id) assert c_idx < b_idx finally: delete_agent_quietly(app_server, a_id) delete_agent_quietly(app_server, b_id) delete_agent_quietly(app_server, c_id) # ------------------------------------------------------------------ # # enable / disable — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p0 def test_disabled_agent_blocks_scoped_operations(app_server) -> None: """Test purpose: - Verify a disabled agent rejects scoped operations (403) but its profile is still readable via GET /api/agents/{id}. Test flow: 1. Create agent and disable it. 2. GET /api/agents/{id}/tools → assert 403. 3. GET /api/agents/{id} → assert 200 (profile still readable). 4. Cleanup. API endpoints: - POST /api/agents - PATCH /api/agents/{agentId}/toggle - GET /api/agents/{agentId}/tools - GET /api/agents/{agentId} """ agent_id = "integ_ma_disable_01" try: create_agent(app_server, agent_id) toggle_resp = toggle_agent(app_server, agent_id, False) assert toggle_resp.status_code == 200, app_server.logs_tail() tools_resp = app_server.api_request( "GET", scoped(agent_id, "/tools"), timeout=_AGENT_HTTP_TIMEOUT, ) assert tools_resp.status_code == 403, ( f"expected 403, got {tools_resp.status_code}: " f"{app_server.logs_tail()}" ) profile_resp = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert profile_resp.status_code == 200, app_server.logs_tail() finally: toggle_agent(app_server, agent_id, True) delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p1 def test_disabled_agent_re_enable_preserves_state( app_server, ) -> None: """Test purpose: - Verify that disabling and re-enabling an agent preserves workspace files written before disable. Test flow: 1. Create agent and PUT a markdown file to its workspace. 2. Disable agent. 3. Re-enable agent. 4. GET the file → content still present. 5. Cleanup. API endpoints: - POST /api/agents - PUT /api/agents/{agentId}/workspace/files/{md_name} - PATCH /api/agents/{agentId}/toggle - GET /api/agents/{agentId}/workspace/files/{md_name} """ agent_id = "integ_ma_preserve_01" try: create_agent(app_server, agent_id) put_file = app_server.api_request( "PUT", scoped(agent_id, "/workspace/files/persist_test.md"), json={"content": "# persist-test\ndata here"}, timeout=_AGENT_HTTP_TIMEOUT, ) assert put_file.status_code == 200, app_server.logs_tail() toggle_agent(app_server, agent_id, False) time.sleep(0.5) toggle_agent(app_server, agent_id, True) time.sleep(1.0) get_file = app_server.api_request( "GET", scoped(agent_id, "/workspace/files/persist_test.md"), timeout=_AGENT_HTTP_TIMEOUT, ) assert get_file.status_code == 200, app_server.logs_tail() content = get_file.json().get("content", "") assert "persist-test" in content finally: delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p1 def test_toggle_shows_in_agent_list(app_server) -> None: """Test purpose: - Verify that toggling an agent's enabled state is reflected in GET /api/agents list response. Test flow: 1. Create agent (starts enabled). 2. Disable → GET list → agent shows enabled=false. 3. Enable → GET list → agent shows enabled=true. 4. Cleanup. API endpoints: - POST /api/agents - PATCH /api/agents/{agentId}/toggle - GET /api/agents """ agent_id = "integ_ma_toggle_list_01" try: create_agent(app_server, agent_id) toggle_agent(app_server, agent_id, False) list_resp = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) assert list_resp.status_code == 200, app_server.logs_tail() agents = list_resp.json().get("agents", []) target = next( (a for a in agents if a["id"] == agent_id), None, ) assert target is not None assert target.get("enabled") is False toggle_agent(app_server, agent_id, True) list_resp2 = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) agents2 = list_resp2.json().get("agents", []) target2 = next( (a for a in agents2 if a["id"] == agent_id), None, ) assert target2 is not None assert target2.get("enabled") is True finally: delete_agent_quietly(app_server, agent_id) # ------------------------------------------------------------------ # # workspace isolation — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p1 def test_agent_workspace_dir_isolated(app_server) -> None: """Test purpose: - Verify two agents have different workspace directories. Test flow: 1. Create agent_a and agent_b. 2. GET each profile and compare workspace_dir. 3. Assert they differ. 4. Cleanup. API endpoints: - POST /api/agents - GET /api/agents/{agentId} """ a_id = "integ_ma_iso_dir_a" b_id = "integ_ma_iso_dir_b" try: create_agent(app_server, a_id) create_agent(app_server, b_id) get_a = app_server.api_request( "GET", f"/api/agents/{a_id}", timeout=_AGENT_HTTP_TIMEOUT, ) get_b = app_server.api_request( "GET", f"/api/agents/{b_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_a.status_code == 200, app_server.logs_tail() assert get_b.status_code == 200, app_server.logs_tail() ws_a = get_a.json().get("workspace_dir") ws_b = get_b.json().get("workspace_dir") assert ws_a != ws_b, f"same workspace_dir: {ws_a}" finally: delete_agent_quietly(app_server, a_id) delete_agent_quietly(app_server, b_id) @pytest.mark.integration @pytest.mark.p0 def test_agent_chats_isolated(app_server) -> None: """Test purpose: - Verify chats created in agent_a are not visible to agent_b. Test flow: 1. Create agent_a and agent_b. 2. POST /api/agents/{a}/chats to create a chat in agent_a. 3. GET /api/agents/{b}/chats → assert the chat is not present. 4. Cleanup. API endpoints: - POST /api/agents - POST /api/agents/{agentId}/chats - GET /api/agents/{agentId}/chats """ a_id = "integ_ma_iso_chat_a" b_id = "integ_ma_iso_chat_b" try: create_agent(app_server, a_id) create_agent(app_server, b_id) create_chat = app_server.api_request( "POST", scoped(a_id, "/chats"), json={ "name": "isolation-test-chat", "session_id": "console:integ_user", "user_id": "integ_user", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert create_chat.status_code in ( 200, 201, ), app_server.logs_tail() chat_data = create_chat.json() chat_id = chat_data.get("id", chat_data.get("chat_id", "")) get_b_chats = app_server.api_request( "GET", scoped(b_id, "/chats"), timeout=_AGENT_HTTP_TIMEOUT, ) assert get_b_chats.status_code == 200, app_server.logs_tail() b_chats = get_b_chats.json() b_chat_ids = [] if isinstance(b_chats, list): b_chat_ids = [c.get("id", "") for c in b_chats] elif isinstance(b_chats, dict): b_chat_ids = [c.get("id", "") for c in b_chats.get("chats", [])] assert chat_id not in b_chat_ids finally: delete_agent_quietly(app_server, a_id) delete_agent_quietly(app_server, b_id) @pytest.mark.integration @pytest.mark.p1 def test_agent_workspace_files_isolated(app_server) -> None: """Test purpose: - Verify files written to agent_a's workspace are not visible in agent_b's workspace. Test flow: 1. Create agent_a and agent_b. 2. PUT a file in agent_a workspace. 3. GET agent_b workspace files → file not present. 4. Cleanup. API endpoints: - POST /api/agents - PUT /api/agents/{agentId}/workspace/files/{md_name} - GET /api/agents/{agentId}/workspace/files """ a_id = "integ_ma_iso_file_a" b_id = "integ_ma_iso_file_b" try: create_agent(app_server, a_id) create_agent(app_server, b_id) put_file = app_server.api_request( "PUT", scoped(a_id, "/workspace/files/isolated_note.md"), json={"content": "# only-in-a"}, timeout=_AGENT_HTTP_TIMEOUT, ) assert put_file.status_code == 200, app_server.logs_tail() get_b_files = app_server.api_request( "GET", scoped(b_id, "/workspace/files"), timeout=_AGENT_HTTP_TIMEOUT, ) assert get_b_files.status_code == 200, app_server.logs_tail() b_files = get_b_files.json() names = [] if isinstance(b_files, list): names = [f.get("filename", f.get("name", "")) for f in b_files] elif isinstance(b_files, dict): names = [ f.get("filename", f.get("name", "")) for f in b_files.get("files", []) ] assert not any("isolated_note" in n for n in names) finally: delete_agent_quietly(app_server, a_id) delete_agent_quietly(app_server, b_id) # ------------------------------------------------------------------ # # deletion — positive flow # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p1 def test_delete_agent_workspace_dir_preserved(app_server) -> None: """Test purpose: - Verify deleting an agent does NOT remove its workspace directory from disk (by design). Test flow: 1. Create agent and GET its workspace_dir. 2. DELETE agent. 3. Verify workspace_dir still exists on disk. API endpoints: - POST /api/agents - GET /api/agents/{agentId} - DELETE /api/agents/{agentId} """ from pathlib import Path agent_id = "integ_ma_del_ws_01" try: create_agent(app_server, agent_id) get_resp = app_server.api_request( "GET", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert get_resp.status_code == 200, app_server.logs_tail() ws_dir = Path(get_resp.json().get("workspace_dir", "")) assert ws_dir.is_dir() del_resp = app_server.api_request( "DELETE", f"/api/agents/{agent_id}", timeout=_AGENT_HTTP_TIMEOUT, ) assert del_resp.status_code == 200, app_server.logs_tail() assert ws_dir.is_dir(), f"workspace_dir deleted: {ws_dir}" finally: delete_agent_quietly(app_server, agent_id) # ------------------------------------------------------------------ # # error boundaries # ------------------------------------------------------------------ # @pytest.mark.integration @pytest.mark.p0 def test_create_agent_duplicate_id_rejected(app_server) -> None: """Test purpose: - Verify creating an agent with a duplicate id is rejected. Test flow: 1. POST /api/agents with id=X → 201. 2. POST /api/agents with same id=X → 400 or 409. 3. Cleanup. API endpoints: - POST /api/agents """ agent_id = "integ_ma_dup_01" try: create_agent(app_server, agent_id) resp2 = app_server.api_request( "POST", "/api/agents", json={ "id": agent_id, "name": "Duplicate", "description": "", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp2.status_code in (400, 409), ( f"expected 400/409, got {resp2.status_code}: " f"{app_server.logs_tail()}" ) finally: delete_agent_quietly(app_server, agent_id) @pytest.mark.integration @pytest.mark.p1 def test_create_agent_reserved_id_default_rejected( app_server, ) -> None: """Test purpose: - Verify creating an agent with id='default' is rejected. Test flow: 1. POST /api/agents with id=default. 2. Assert 400. API endpoints: - POST /api/agents """ resp = app_server.api_request( "POST", "/api/agents", json={ "id": "default", "name": "Should Fail", "description": "", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code == 400, app_server.logs_tail() @pytest.mark.integration @pytest.mark.p2 def test_create_agent_invalid_id_format_rejected( app_server, ) -> None: """Test purpose: - Verify creating an agent with special characters in id is rejected. Test flow: 1. POST /api/agents with id='agent@#!'. 2. Assert 400 or 422. API endpoints: - POST /api/agents """ resp = app_server.api_request( "POST", "/api/agents", json={ "id": "agent@#!", "name": "Invalid ID", "description": "", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code in (400, 422), ( f"expected 400/422, got {resp.status_code}: " f"{app_server.logs_tail()}" ) @pytest.mark.integration @pytest.mark.p2 def test_create_agent_id_too_short_rejected(app_server) -> None: """Test purpose: - Verify creating an agent with a single-char id (below minimum length of 2) is rejected. Test flow: 1. POST /api/agents with id='a'. 2. Assert 400 or 422. API endpoints: - POST /api/agents """ resp = app_server.api_request( "POST", "/api/agents", json={ "id": "a", "name": "Too Short", "description": "", }, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code in (400, 422), ( f"expected 400/422, got {resp.status_code}: " f"{app_server.logs_tail()}" ) @pytest.mark.integration @pytest.mark.p1 def test_delete_default_agent_rejected(app_server) -> None: """Test purpose: - Verify DELETE /api/agents/default is rejected with 400. Test flow: 1. DELETE /api/agents/default. 2. Assert 400. API endpoints: - DELETE /api/agents/default """ resp = app_server.api_request( "DELETE", "/api/agents/default", timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code == 400, app_server.logs_tail() @pytest.mark.integration @pytest.mark.p2 def test_delete_nonexistent_agent_returns_404(app_server) -> None: """Test purpose: - Verify DELETE for a nonexistent agent returns 404. Test flow: 1. DELETE /api/agents/does_not_exist_xyz. 2. Assert 404. API endpoints: - DELETE /api/agents/{agentId} """ resp = app_server.api_request( "DELETE", "/api/agents/does_not_exist_xyz", timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code == 404, app_server.logs_tail() @pytest.mark.integration @pytest.mark.p1 def test_toggle_default_agent_rejected(app_server) -> None: """Test purpose: - Verify disabling the default agent is rejected with 400. Test flow: 1. PATCH /api/agents/default/toggle with enabled=false. 2. Assert 400. API endpoints: - PATCH /api/agents/default/toggle """ resp = toggle_agent(app_server, "default", False) assert resp.status_code == 400, app_server.logs_tail() @pytest.mark.integration @pytest.mark.p2 def test_reorder_partial_list_rejected(app_server) -> None: """Test purpose: - Verify PUT /api/agents/order with an incomplete agent list is rejected. Test flow: 1. GET /api/agents to get full list. 2. PUT /api/agents/order with only a subset of ids. 3. Assert 400 or 422. API endpoints: - GET /api/agents - PUT /api/agents/order """ list_resp = app_server.api_request( "GET", "/api/agents", timeout=_AGENT_HTTP_TIMEOUT, ) assert list_resp.status_code == 200, app_server.logs_tail() all_ids = [a["id"] for a in list_resp.json().get("agents", [])] assert len(all_ids) >= 2, "need at least 2 agents for test" partial = all_ids[:1] resp = app_server.api_request( "PUT", "/api/agents/order", json={"agent_ids": partial}, timeout=_AGENT_HTTP_TIMEOUT, ) assert resp.status_code in (400, 422), ( f"expected 400/422, got {resp.status_code}: " f"{app_server.logs_tail()}" )