feat: 配置API(分组校验管线/整份版本/回滚/重置出厂/试金石/平台设置)

- config_validation.validate_group: 九组校验管线(类型/键 allowlist/数值
  范围/prompts 大小上限 50KB·100KB/wall 全量交 compile_wall/push.prefix
  跨租户查重 spec E1),错误清单中文带字段路径
- config_routes: GET/PUT config(整组替换原样入版本,MAX+1,热 reload)/
  reset(按 Tenant.profile 取出厂档案组)/versions 列表·详情/rollback
  (旧版复制为新版本,历史不可变)/wall/test 试金石(逐句判定,不落库
  不 bump)/settings 平台设置 CRUD(仅超管,resolver 接线归 Phase 3)
- db.write_config_version: 版本写入公共路径(创建向导与配置 API 单一
  事实源);Tenant 落 profile 列(重置出厂按它取对应档案)
- _profile_snapshot/_blankify 迁 tenant_source(两路由模块共用);
  db_source_app/super_client 夹具收拢 conftest(两测试文件共用)
- 测试: 519 → 537 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 10:07:40 +08:00
parent ec2e17d5e3
commit 7502176190
10 changed files with 1412 additions and 89 deletions
+46
View File
@@ -143,6 +143,52 @@ AUTO_HOST = "chat.agent.hnxinyouhe.com"
# Task 4 收拢到 conftest 单一定义)
NOPE_HOST = "nope.example.com"
# ---- Phase 2 Task 6: DB 源 app/超管 client 夹具 -----------------------------------
# 自 test_tenant_admin_api 收拢到 conftest(Task 5 时代该文件私有,Task 6 的
# 配置 API/试金石测试与租户管理测试共用同一套 DB 源闭环环境)。
# 跨循环纪律见 db_source_app docstring 与 test_tenant_admin_api 模块头。
@pytest.fixture
def db_source_app(tmp_path):
"""app + 生产形态 registry(DbTenantSource 指向临时 platform.db)。
platform_engine 也指同一临时库(lifespan 在 portal 循环里 ensure_schema
+ bootstrap 超管 admin/admin123)。teardown 复位 app.state(照抄
app_test_client 的关残留窗纪律;engine dispose 由 lifespan finally)。
"""
from app.platform.tenant_source import DbTenantSource
engine = make_platform_engine(tmp_path / "platform.db")
app.state.registry = TenantRegistry(
DbTenantSource(engine), data_dir=tmp_path / "tenants")
app.state.platform_engine = engine
try:
yield app
finally:
app.state.registry = None
app.state.platform_engine = None
@pytest.fixture
def super_client(db_source_app, tmp_path):
"""超管 client:lifespan bootstrap 的 admin/admin123 登录 + 首登改密
(过 must_change 409 门)。token 挂 client.platform_token。"""
with app_test_client(db_source_app, "http://testserver", tmp_path) as c:
r = c.post("/api/platform/auth/login",
json={"username": "admin", "password": "admin123"})
assert r.status_code == 200
c.platform_token = r.json()["token"]
r = c.post("/api/platform/auth/change-password", headers=_bearer_of(c),
json={"old_password": "admin123",
"new_password": "sup3r-pass-1"})
assert r.status_code == 200
yield c
def _bearer_of(client):
return {"Authorization": f"Bearer {client.platform_token}"}
def build_app(seed_dir, data_dir):
"""app 工厂(Task 7): 建 Registry → 预置 app.state.registry → 返回 app。