132 lines
5.7 KiB
Python
132 lines
5.7 KiB
Python
# ============================================================
|
||
# 功能开关(Feature Flags)黄金测试
|
||
# 覆盖:默认全开、文件配置生效、损坏/非法回退默认 + 显式 error、
|
||
# 未知键如实上报、非 bool 值该键回退、env 路径覆盖、/api/features 端点契约。
|
||
# ============================================================
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
from server.agent_core.feature_flags import FEATURE_CATALOG, load_feature_flags
|
||
from tests.auth_provider import install_test_auth
|
||
|
||
|
||
def _write(path: Path, data: object) -> str:
|
||
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||
return str(path)
|
||
|
||
|
||
def test_missing_file_defaults_all_enabled(tmp_path: Path):
|
||
"""文件缺失 → 除 defaultOff 键(fallback 默认关)外全部默认开启,source=default,无 error。"""
|
||
result = load_feature_flags(str(tmp_path / "features.json"))
|
||
assert result["source"] == "default"
|
||
assert result["error"] is None
|
||
assert result["unknown"] == []
|
||
assert result["defaultOff"] == ["fallback"]
|
||
assert result["features"]["fallback"]["enabled"] is False
|
||
assert all(info["enabled"] for key, info in result["features"].items()
|
||
if key not in result["defaultOff"])
|
||
assert set(result["features"]) == set(FEATURE_CATALOG)
|
||
|
||
|
||
def test_file_config_disables_features(tmp_path: Path):
|
||
"""显式 false 隐藏功能;未列出的键保持默认开启;未知键如实上报。"""
|
||
path = _write(tmp_path / "features.json", {
|
||
"version": 1,
|
||
"features": {"drawing": False, "mesh": False, "unknown-thing": True},
|
||
})
|
||
result = load_feature_flags(path)
|
||
assert result["source"] == "file"
|
||
assert result["error"] is None
|
||
assert result["features"]["drawing"]["enabled"] is False
|
||
assert result["features"]["mesh"]["enabled"] is False
|
||
assert result["features"]["orders"]["enabled"] is True
|
||
assert result["unknown"] == ["unknown-thing"]
|
||
|
||
|
||
def test_corrupt_json_fails_open_with_error(tmp_path: Path):
|
||
"""损坏 JSON → 回退默认值(fallback 默认关,其余默认开)+ 显式 error(不锁死界面,不静默吞错)。"""
|
||
path = tmp_path / "features.json"
|
||
path.write_text("{not-json", encoding="utf-8")
|
||
result = load_feature_flags(str(path))
|
||
assert result["source"] == "default"
|
||
assert result["error"] is not None
|
||
assert result["features"]["fallback"]["enabled"] is False
|
||
assert all(info["enabled"] for key, info in result["features"].items()
|
||
if key not in result["defaultOff"])
|
||
|
||
|
||
def test_invalid_shape_and_non_bool_values(tmp_path: Path):
|
||
"""顶层结构非法 / features 非对象 / 值非 bool → 相应回退默认并显式报告。"""
|
||
bad_version = load_feature_flags(_write(tmp_path / "v.json", {"version": 2, "features": {}}))
|
||
assert bad_version["source"] == "default" and bad_version["error"] is not None
|
||
|
||
bad_section = load_feature_flags(_write(tmp_path / "s.json", {"version": 1, "features": [1]}))
|
||
assert bad_section["source"] == "default" and bad_section["error"] is not None
|
||
|
||
non_bool = load_feature_flags(_write(tmp_path / "b.json", {
|
||
"version": 1, "features": {"orders": "no", "drawing": False},
|
||
}))
|
||
assert non_bool["source"] == "file"
|
||
assert non_bool["features"]["orders"]["enabled"] is True # 非 bool 回退默认开启
|
||
assert non_bool["features"]["drawing"]["enabled"] is False # 合法 false 仍生效
|
||
assert non_bool["error"] is not None and "orders" in non_bool["error"]
|
||
|
||
|
||
def test_env_path_override(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||
"""APS_FEATURES_PATH 覆盖默认路径。"""
|
||
path = _write(tmp_path / "custom.json", {"version": 1, "features": {"gov": False}})
|
||
monkeypatch.setenv("APS_FEATURES_PATH", path)
|
||
result = load_feature_flags()
|
||
assert result["source"] == "file"
|
||
assert result["path"] == path
|
||
assert result["features"]["gov"]["enabled"] is False
|
||
|
||
|
||
@pytest.fixture()
|
||
def features_app(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "features.db"))
|
||
monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json"))
|
||
from server.db.database import reset_engine
|
||
from server.state import store as world_store
|
||
|
||
install_test_auth(monkeypatch, "tenant-a-000000000000000000000000001")
|
||
world_store._stores.clear()
|
||
reset_engine()
|
||
from server.gateway.app import create_app
|
||
|
||
app = create_app()
|
||
yield app, monkeypatch, tmp_path
|
||
reset_engine()
|
||
world_store._stores.clear()
|
||
|
||
|
||
def test_features_endpoint_contract(features_app):
|
||
"""/api/features 端点:默认配置契约 + 配置文件生效 + 中文标签齐全 + defaultOff 标注。"""
|
||
app, monkeypatch, tmp_path = features_app
|
||
client = TestClient(app)
|
||
assert client.post("/api/auth/login", json={"username": "planner"}).status_code == 200
|
||
|
||
body = client.get("/api/features").json()
|
||
assert body["version"] == 1
|
||
assert body["source"] == "default"
|
||
assert body["defaultOff"] == ["fallback"]
|
||
assert body["features"]["fallback"]["enabled"] is False
|
||
assert all(info["enabled"] for key, info in body["features"].items()
|
||
if key not in body["defaultOff"])
|
||
assert all(info["label"] for info in body["features"].values())
|
||
|
||
config = _write(tmp_path / "features.json", {
|
||
"version": 1, "features": {"drawing": False, "regen": False},
|
||
})
|
||
monkeypatch.setenv("APS_FEATURES_PATH", config)
|
||
body = client.get("/api/features").json()
|
||
assert body["source"] == "file"
|
||
assert body["features"]["drawing"]["enabled"] is False
|
||
assert body["features"]["regen"]["enabled"] is False
|
||
assert body["features"]["orders"]["enabled"] is True
|