# ============================================================ # 功能开关(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): """文件缺失 → 全部默认开启,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 all(info["enabled"] for info in result["features"].values()) 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 → fail-open 全部开启 + 显式 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 all(info["enabled"] for info in result["features"].values()) 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 端点:默认全开契约 + 配置文件生效 + 中文标签齐全。""" 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 all(info["enabled"] for info in body["features"].values()) 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