fischer-agentkit/tests/unit/server/test_port_config_regression.py

175 lines
7.1 KiB
Python

"""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}"