"""E2E: "Create custom agent" dialog on the new-session landing page. Covers the user journey of creating a custom agent from the agent picker dropdown, configuring it (name, description, MCP tools), and submitting the form to create a session with the bundled agent. Uses the same route-stubbing approach as ``test_start_session.py``: the server's ``/v1/hosts``, ``/v1/agents``, and ``POST /v1/sessions`` are faked so the tests don't need a real host. The create POST is intercepted to capture the multipart request body for assertion. """ from __future__ import annotations import asyncio import json import re import threading from collections.abc import Coroutine from typing import Any from playwright.async_api import Route, async_playwright, expect # Stubbed host the composer auto-selects. _HOST_ID = "host_e2e" # Bare create endpoint — intercepts POST but lets GET through. _SESSIONS_RE = re.compile(r"/v1/sessions(\?.*)?$") def _run_in_fresh_loop(coro: Coroutine[Any, Any, None]) -> None: """Run *coro* in a dedicated thread with its own event loop.""" captured: dict[str, Exception] = {} def _worker() -> None: try: asyncio.run(coro) except Exception as exc: captured["error"] = exc thread = threading.Thread(target=_worker) thread.start() thread.join() if "error" in captured: raise captured["error"] async def _wait_until(predicate, *, timeout_s: float = 15.0) -> None: loop = asyncio.get_running_loop() deadline = loop.time() + timeout_s while loop.time() < deadline: if predicate(): return await asyncio.sleep(0.05) raise AssertionError(f"condition not met within {timeout_s:.0f}s") def _agents_body() -> str: """Single Claude Code agent for the stub.""" return json.dumps( { "data": [ { "id": "ag_claude_e2e", "name": "claude-native-ui", "display_name": "Claude Code", "description": "Anthropic's coding agent", "harness": None, "skills": [], } ] } ) def _hosts_body() -> str: return json.dumps( { "hosts": [ { "host_id": _HOST_ID, "name": "e2e-host", "owner": "e2e", "status": "online", } ] } ) async def _register_routes( page, *, created_session_id: str, create_requests: list[dict[str, Any]], ) -> None: """Install stubs for hosts, agents, session create, and events.""" async def handle_hosts(route: Route) -> None: await route.fulfill(status=200, content_type="application/json", body=_hosts_body()) async def handle_agents(route: Route) -> None: await route.fulfill(status=200, content_type="application/json", body=_agents_body()) async def handle_events(route: Route) -> None: await route.fulfill( status=200, content_type="application/json", body=json.dumps({"queued": True, "item_id": "ci_e2e"}), ) async def handle_sessions(route: Route) -> None: if route.request.method == "POST": # Capture multipart or JSON create requests. content_type = route.request.headers.get("content-type", "") if "multipart" in content_type: # For multipart, we can't easily parse the binary body in # Playwright, so just record that a multipart POST happened. create_requests.append({"__multipart__": True}) else: create_requests.append(route.request.post_data_json) await route.fulfill( status=200, content_type="application/json", body=json.dumps({"id": created_session_id, "session_id": created_session_id}), ) else: await route.continue_() await page.route("**/v1/hosts", handle_hosts) await page.route("**/v1/agents", handle_agents) await page.route("**/v1/sessions/*/events", handle_events) await page.route(_SESSIONS_RE, handle_sessions) async def _seed_workspace(page) -> None: """Seed a recent workspace so the composer can enable Send.""" await page.add_init_script( f"""window.localStorage.setItem( "omnigent:recent-workspaces", JSON.stringify({{ {_HOST_ID}: ["/work/repo"] }}) );""" ) # ── Tests ────────────────────────────────────────────────────────── def test_create_agent_dialog_opens_from_dropdown( seeded_session: tuple[str, str], ) -> None: """The agent dropdown shows a "Create custom agent" item that opens the dialog.""" base_url, session_id = seeded_session _run_in_fresh_loop(_drive_dialog_opens(base_url, session_id)) async def _drive_dialog_opens(base_url: str, session_id: str) -> None: async with async_playwright() as pw: browser = await pw.chromium.launch() page = await browser.new_page() try: create_requests: list[dict[str, Any]] = [] await _register_routes( page, created_session_id=session_id, create_requests=create_requests ) await _seed_workspace(page) await page.goto(f"{base_url}/") await page.get_by_test_id("new-chat-landing-input").wait_for( state="visible", timeout=30_000 ) # Open the agent dropdown. await page.get_by_test_id("new-chat-landing-agent-select").click() # "Create custom agent" item should be visible. create_item = page.get_by_test_id("new-chat-landing-create-agent") await expect(create_item).to_be_visible() # Click it — dialog should open. await create_item.click() dialog = page.get_by_test_id("create-agent-dialog") await expect(dialog).to_be_visible(timeout=5_000) # Verify form fields are present. await expect(page.get_by_test_id("create-agent-name")).to_be_visible() await expect(page.get_by_test_id("create-agent-description")).to_be_visible() await expect(page.get_by_test_id("create-agent-harness")).to_be_visible() await expect(page.get_by_test_id("create-agent-instructions")).to_be_visible() await expect(page.get_by_test_id("create-agent-add-mcp")).to_be_visible() finally: await browser.close() def test_create_agent_submits_multipart_bundle( seeded_session: tuple[str, str], ) -> None: """Creating a custom agent and sending produces a multipart POST.""" base_url, session_id = seeded_session _run_in_fresh_loop(_drive_create_and_submit(base_url, session_id)) async def _drive_create_and_submit(base_url: str, session_id: str) -> None: async with async_playwright() as pw: browser = await pw.chromium.launch() page = await browser.new_page() try: create_requests: list[dict[str, Any]] = [] await _register_routes( page, created_session_id=session_id, create_requests=create_requests ) await _seed_workspace(page) await page.goto(f"{base_url}/") await page.get_by_test_id("new-chat-landing-input").wait_for( state="visible", timeout=30_000 ) # Open dropdown → Create custom agent. await page.get_by_test_id("new-chat-landing-agent-select").click() await page.get_by_test_id("new-chat-landing-create-agent").click() dialog = page.get_by_test_id("create-agent-dialog") await expect(dialog).to_be_visible(timeout=5_000) # Fill in agent details. await page.get_by_test_id("create-agent-name").fill("test-agent") await page.get_by_test_id("create-agent-description").fill("A test agent") await page.get_by_test_id("create-agent-model").fill("claude-sonnet-4-20250514") await page.get_by_test_id("create-agent-instructions").fill( "You are a test assistant." ) # Submit the dialog. await page.get_by_test_id("create-agent-submit").click() # Dialog should close. await expect(dialog).to_be_hidden(timeout=5_000) # The agent chip should now show the custom agent name. await expect(page.get_by_test_id("new-chat-landing-agent-select")).to_contain_text( "test-agent" ) # Type a message and submit the session. await page.get_by_test_id("new-chat-landing-input").fill("hello world") await page.get_by_test_id("new-chat-landing-submit").click() # The create POST should have been a multipart request (the bundle). await _wait_until(lambda: len(create_requests) == 1) assert create_requests[0].get("__multipart__") is True, ( f"Expected multipart POST, got: {create_requests[0]}" ) finally: await browser.close() def test_create_agent_with_mcp_server( seeded_session: tuple[str, str], ) -> None: """Adding an MCP server in the dialog includes it in the bundle.""" base_url, session_id = seeded_session _run_in_fresh_loop(_drive_mcp_server(base_url, session_id)) async def _drive_mcp_server(base_url: str, session_id: str) -> None: async with async_playwright() as pw: browser = await pw.chromium.launch() page = await browser.new_page() try: create_requests: list[dict[str, Any]] = [] await _register_routes( page, created_session_id=session_id, create_requests=create_requests ) await _seed_workspace(page) await page.goto(f"{base_url}/") await page.get_by_test_id("new-chat-landing-input").wait_for( state="visible", timeout=30_000 ) # Open dropdown → Create custom agent. await page.get_by_test_id("new-chat-landing-agent-select").click() await page.get_by_test_id("new-chat-landing-create-agent").click() dialog = page.get_by_test_id("create-agent-dialog") await expect(dialog).to_be_visible(timeout=5_000) # Fill in agent name and model (both required). await page.get_by_test_id("create-agent-name").fill("mcp-agent") await page.get_by_test_id("create-agent-model").fill("claude-sonnet-4-20250514") # Add an MCP server. await page.get_by_test_id("create-agent-add-mcp").click() # An MCP entry card should appear. mcp_entry = page.get_by_test_id("create-agent-mcp-entry") await expect(mcp_entry).to_be_visible() # Fill in MCP server details (stdio transport is default). await page.get_by_test_id("create-agent-mcp-name").fill("github") await page.get_by_test_id("create-agent-mcp-command").fill("npx") await page.get_by_test_id("create-agent-mcp-args").fill( "-y @modelcontextprotocol/server-github" ) await page.get_by_test_id("create-agent-mcp-env").fill("GITHUB_TOKEN=ghp_test123") # Submit the dialog. await page.get_by_test_id("create-agent-submit").click() await expect(dialog).to_be_hidden(timeout=5_000) # Submit session. await page.get_by_test_id("new-chat-landing-input").fill("list repos") await page.get_by_test_id("new-chat-landing-submit").click() await _wait_until(lambda: len(create_requests) == 1) assert create_requests[0].get("__multipart__") is True finally: await browser.close() def test_create_agent_cancel_closes_dialog( seeded_session: tuple[str, str], ) -> None: """Cancelling the dialog closes it without creating an agent.""" base_url, session_id = seeded_session _run_in_fresh_loop(_drive_cancel(base_url, session_id)) async def _drive_cancel(base_url: str, session_id: str) -> None: async with async_playwright() as pw: browser = await pw.chromium.launch() page = await browser.new_page() try: create_requests: list[dict[str, Any]] = [] await _register_routes( page, created_session_id=session_id, create_requests=create_requests ) await _seed_workspace(page) await page.goto(f"{base_url}/") await page.get_by_test_id("new-chat-landing-input").wait_for( state="visible", timeout=30_000 ) # Open dropdown → Create custom agent. await page.get_by_test_id("new-chat-landing-agent-select").click() await page.get_by_test_id("new-chat-landing-create-agent").click() dialog = page.get_by_test_id("create-agent-dialog") await expect(dialog).to_be_visible(timeout=5_000) # Fill some fields. await page.get_by_test_id("create-agent-name").fill("should-not-persist") # Cancel. cancel_btn = dialog.get_by_role("button", name="Cancel") await cancel_btn.click() # Dialog should close. await expect(dialog).to_be_hidden(timeout=5_000) # The agent chip should still show the original agent (Claude Code). await expect(page.get_by_test_id("new-chat-landing-agent-select")).to_contain_text( "Claude Code" ) finally: await browser.close()