"""Regression tests for port configuration and tool registration. These tests prevent the recurrence of two bugs found on 2026-07-01: 1. **Port conflict (login hijack)**: vite.config.ts hardcoded proxy target to ``localhost:8000``, which was occupied by the ``geo_backend`` Docker container. Login requests were silently proxied to the wrong service, returning a "请求参数校验失败" error because geo_backend expected an ``email`` field while the frontend sent ``username``. 2. **Missing tool registration**: ``benchmark_runner.yaml`` referenced ``file_read`` but the tool's actual name is ``read_file``, and ``ReadFileTool`` was never registered in ``app.py``'s lifespan — causing "Tool not found: file_read" at skill-load time. 3. **.env.dev not loaded**: ``load_dotenv`` only looked for ``.env``, missing ``.env.dev`` (the in-tree dev preset with DATABASE_URL / REDIS_URL / port allocation), causing Bitable init failure. The tests are intentionally static (grep-based) — they verify that the canonical port numbers and tool names are consistent across all config layers, without booting a server. """ from __future__ import annotations import re from pathlib import Path import pytest # Project root — tests/unit/server/ → ../../../.. PROJECT_ROOT = Path(__file__).resolve().parents[3] # --------------------------------------------------------------------------- # 1. Port configuration — no legacy 8000/8001/5173 in canonical files # --------------------------------------------------------------------------- # Files where hardcoded 8000 / 8001 / 5173 are bugs (should use 18001 etc.) _FILES_NO_LEGACY_PORTS = [ "agentkit.yaml", "src/agentkit/server/config.py", "src/agentkit/server/client.py", "src/agentkit/cli/main.py", "src/agentkit/cli/templates.py", "src/agentkit/cli/onboarding.py", "src/agentkit/cli/admin_client.py", "src/agentkit/cli/admin.py", "src/agentkit/cli/pair.py", "src/agentkit/cli/init.py", "src/agentkit/client/sync.py", "src/agentkit/tools/bitable_tool.py", "src/agentkit/memory/http_rag.py", "src/agentkit/memory/adapters/generic_http.py", "src/agentkit/server/frontend/vite.config.ts", "src/agentkit/server/frontend/src-tauri/src/lib.rs", "src/agentkit/server/frontend/src-tauri/tauri.conf.json", "src/agentkit/server/frontend/src/stores/settings.ts", "src/agentkit/server/frontend/src/stores/auth.ts", "src/agentkit/server/frontend/playwright.config.ts", ] # Patterns that indicate a legacy hardcoded port (not inside a comment-only line). # Matches :8000, :8001, :5173, :8002, port=8000, port=8001, port: 8001 etc. _LEGACY_PORT_RE = re.compile( r"""(?: # Match any of: :8000 # URL-style :8000 | :8001 # URL-style :8001 | :8002 # URL-style :8002 | :5173 # Vite default :5173 | port['"]?\s*[:=]\s*800[012]\b # port: 8001 / port = 8001 / port' = 8001 | port['"]?\s*[:=]\s*5173\b # port: 5173 )""", re.VERBOSE, ) @pytest.mark.parametrize("rel_path", _FILES_NO_LEGACY_PORTS) def test_no_legacy_hardcoded_ports(rel_path: str) -> None: """Canonical config/source files must not hardcode 8000/8001/5173/8002. These ports conflict with Docker services (geo_backend:8000) or other dev projects (Vite:5173). The canonical ports are 18001 (backend), 18002 (GUI), 15173 (Vite dev), 15174 (HMR). Exceptions: comments mentioning the old ports for historical context are allowed — we only flag actual code/config lines. """ fpath = PROJECT_ROOT / rel_path if not fpath.exists(): pytest.skip(f"File not found: {rel_path}") content = fpath.read_text(encoding="utf-8") offending = [] for lineno, line in enumerate(content.splitlines(), 1): stripped = line.strip() # Skip comment-only lines (Python #, YAML #, Rust //, TS //) if stripped.startswith(("#", "//")) or stripped.startswith("///"): continue if _LEGACY_PORT_RE.search(line): offending.append(f" {rel_path}:{lineno}: {stripped}") assert not offending, ( f"Legacy port (8000/8001/5173/8002) found in {rel_path}:\n" + "\n".join(offending) + "\nUse 18001 (backend) / 18002 (GUI) / 15173 (Vite) / 15174 (HMR)." ) # --------------------------------------------------------------------------- # 2. Tool registration — ReadFileTool registered + yaml name matches # --------------------------------------------------------------------------- def test_readfiletool_registered_in_app_py() -> None: """app.py lifespan must register ReadFileTool so benchmark_runner works.""" app_py = (PROJECT_ROOT / "src/agentkit/server/app.py").read_text(encoding="utf-8") assert "from agentkit.tools.file_read import ReadFileTool" in app_py, ( "ReadFileTool import missing from app.py — benchmark_runner skill " "will fail with 'Tool not found: read_file'" ) assert "register(ReadFileTool()" in app_py, ( "ReadFileTool registration call missing from app.py lifespan" ) def test_benchmark_runner_yaml_uses_correct_tool_name() -> None: """benchmark_runner.yaml must reference 'read_file' (not 'file_read').""" yaml_path = PROJECT_ROOT / "configs/skills/benchmark_runner.yaml" if not yaml_path.exists(): pytest.skip("benchmark_runner.yaml not found") content = yaml_path.read_text(encoding="utf-8") # The tools section should contain read_file, not file_read / file_write assert "file_read" not in content or "read_file" in content, ( "benchmark_runner.yaml references 'file_read' — the tool's actual " "name is 'read_file'. This causes 'Tool not found' at load time." ) # --------------------------------------------------------------------------- # 3. .env.dev loading — load_dotenv supports .env.dev candidate # --------------------------------------------------------------------------- def test_load_dotenv_supports_env_dev() -> None: """app.py must load .env.dev (not just .env) for dev port/DB config.""" app_py = (PROJECT_ROOT / "src/agentkit/server/app.py").read_text(encoding="utf-8") assert ".env.dev" in app_py, ( "app.py does not load .env.dev — DATABASE_URL / REDIS_URL / port " "overrides in .env.dev will be silently ignored, causing Bitable " "init failure and Redis 'not_configured' status." ) def test_env_dev_contains_required_keys() -> None: """ .env.dev must define all keys needed for full functional testing.""" env_dev = PROJECT_ROOT / ".env.dev" if not env_dev.exists(): pytest.skip(".env.dev not found") content = env_dev.read_text(encoding="utf-8") required_keys = [ "AGENTKIT_SERVER_PORT", "BACKEND_PORT", "VITE_DEV_PORT", "VITE_HMR_PORT", "AGENTKIT_REMOTE_PORT", "DATABASE_URL", "REDIS_URL", "AGENTKIT_SESSION_BACKEND", "AGENTKIT_BUS_BACKEND", "AGENTKIT_TASK_STORE_BACKEND", ] missing = [k for k in required_keys if k not in content] assert not missing, f".env.dev missing required keys: {missing}"