aps-agent/tests/golden/test_feature_flags.py

132 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 功能开关(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