Files
Hermes CI Fix 87cb9e7db2 feat: OpenCode执行层试点 — 补测试5个 + list_objectives隔离修复
- OpenCode执行模式: 补 test_maps.py 5个测试(create_objective/snapshot隔离/entity注入/跨企业)
- OpenCode诚实标注: list_objectives 后端漏entity过滤 → 修复(跨企业返回空)
- 记忆层闭环: Hermes验证(pytest 536 passed) + 技能已更新(opencode-independent-review 双角色)
- 执行层试点验证: OpenCode能写代码+发现后端缺口, 最小闭环跑通
2026-08-26 23:45:20 +08:00

287 lines
10 KiB
Python

"""
战略地图模块测试
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import Entity, User, UserEntity
from tests.conftest import create_test_user, get_token_for_user, auth_header
def _create_second_entity(db: Session, user: User) -> int:
"""创建第二个企业账套并授权测试用户,返回 entity_id"""
db.add(Entity(id=2, name="第二企业", short_name="企业2", status="active"))
db.add(UserEntity(user_id=user.id, entity_id=2, granted_by=1))
db.commit()
return 2
def _token_for_entity(client: TestClient, entity_id: int) -> str:
"""以指定账套登录获取token"""
resp = client.post("/api/cma/auth/login", json={
"username": "testadmin",
"password": "admin123",
"entity_id": entity_id,
})
assert resp.status_code == 200
data = resp.json()
return data.get("token") or data.get("access_token")
class TestMaps:
"""战略地图CRUD测试"""
def test_list_maps_empty(self, client: TestClient, db: Session):
"""空列表"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.get("/api/cma/maps", headers=auth_header(token))
assert resp.status_code == 200
assert resp.json()["data"] == []
def test_create_with_template(self, client: TestClient, db: Session):
"""创建带模板的地图"""
create_test_user(db)
token = get_token_for_user(client)
resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "测试模板地图"},
)
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "测试模板地图"
assert data["status"] == "draft"
assert len(data["dimensions"]) == 4
assert len(data["dimensions"][0]["objectives"]) > 0
def test_update_map(self, client: TestClient, db: Session):
"""编辑地图"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "待编辑地图"},
)
map_id = create_resp.json()["id"]
update_resp = client.put(
f"/api/cma/maps/{map_id}",
headers=auth_header(token),
json={"title": "已编辑地图"},
)
assert update_resp.status_code == 200
assert update_resp.json()["title"] == "已编辑地图"
def test_publish_map_creates_version(self, client: TestClient, db: Session):
"""发布地图触发版本快照"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "待发布地图"},
)
map_id = create_resp.json()["id"]
# 发布
client.put(
f"/api/cma/maps/{map_id}",
headers=auth_header(token),
json={"status": "published"},
)
ver_resp = client.get(
f"/api/cma/maps/{map_id}/versions",
headers=auth_header(token),
)
assert ver_resp.status_code == 200
versions = ver_resp.json()["data"]
assert len(versions) >= 1
assert versions[0]["version"] == "v1.0"
def test_add_connection(self, client: TestClient, db: Session):
"""添加因果连线"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "连线测试"},
)
map_id = create_resp.json()["id"]
resp = client.post(
f"/api/cma/maps/{map_id}/connections",
headers=auth_header(token),
json={"from": "learning-0", "to": "process-0"},
)
assert resp.status_code == 200
assert len(resp.json()["connections"]) == 1
def test_same_dim_connection_fails(self, client: TestClient, db: Session):
"""同维度连线被拒绝"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "同维度测试"},
)
map_id = create_resp.json()["id"]
resp = client.post(
f"/api/cma/maps/{map_id}/connections",
headers=auth_header(token),
json={"from": "finance-0", "to": "finance-1"},
)
assert resp.status_code == 200
# 同维度连线现在被允许了,不再是旧的拒绝逻辑
# assert "不能" in resp.json()["detail"]
def test_create_objective(self, client: TestClient, db: Session):
"""在地图上新增目标并返回目标对象"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "新增目标测试"},
)
map_id = create_resp.json()["id"]
resp = client.post(
f"/api/cma/maps/{map_id}/objectives",
headers=auth_header(token),
json={"dimension_key": "finance", "name": "新增营收目标", "icon": "target"},
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "新增营收目标"
assert data["dimension_key"] == "finance"
assert data["map_id"] == map_id
assert data["entity_id"] == create_resp.json()["entity_id"]
assert "id" in data
def test_create_objective_wrong_entity_404(self, client: TestClient, db: Session):
"""跨账套新增目标返回404(地图不属于当前企业)"""
create_test_user(db)
token1 = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token1),
json={"title": "账户A地图"},
)
map_id = create_resp.json()["id"]
# 第二个企业账套
user = db.query(User).filter(User.username == "testadmin").first()
_create_second_entity(db, user)
token2 = _token_for_entity(client, 2)
resp = client.post(
f"/api/cma/maps/{map_id}/objectives",
headers=auth_header(token2),
json={"dimension_key": "finance", "name": "越权目标"},
)
assert resp.status_code == 404
def test_create_snapshot_inherits_entity(self, client: TestClient, db: Session):
"""手动快照创建版本并继承地图企业"""
create_test_user(db)
token1 = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token1),
json={"title": "快照继承测试"},
)
map_id = create_resp.json()["id"]
map_entity = create_resp.json()["entity_id"]
snap_resp = client.post(
f"/api/cma/maps/{map_id}/versions/snapshot",
headers=auth_header(token1),
json={"comment": "首次快照"},
)
assert snap_resp.status_code == 200
snap = snap_resp.json()
assert snap["map_id"] == map_id
assert snap["version"] == "v1.0"
assert snap["entity_id"] == map_entity
def test_update_objective_rejects_entity_id_injection(self, client: TestClient, db: Session):
"""更新目标时注入entity_id被忽略,企业归属保持不变"""
create_test_user(db)
token = get_token_for_user(client)
create_resp = client.post(
"/api/cma/maps/create-with-template",
headers=auth_header(token),
json={"title": "注入防护测试"},
)
map_id = create_resp.json()["id"]
orig_entity = create_resp.json()["entity_id"]
obj_resp = client.post(
f"/api/cma/maps/{map_id}/objectives",
headers=auth_header(token),
json={"dimension_key": "finance", "name": "原目标"},
)
obj_id = obj_resp.json()["id"]
update_resp = client.put(
f"/api/cma/maps/{map_id}/objectives/{obj_id}",
headers=auth_header(token),
json={"entity_id": 999, "name": "改名目标"},
)
assert update_resp.status_code == 200
data = update_resp.json()
# entity_id 不在白名单字段中,注入被忽略 → 归属不变
assert data["entity_id"] == orig_entity
assert data["entity_id"] != 999
assert data["name"] == "改名目标"
def test_cross_entity_list_objectives_empty(self, client: TestClient, db: Session):
"""跨账套查询目标列表返回空(目标不属于当前企业)"""
create_test_user(db)
token1 = get_token_for_user(client)
# 企业1创建空白地图(无任何目标)
create_resp = client.post(
"/api/cma/maps",
headers=auth_header(token1),
json={"title": "企业1地图"},
)
assert create_resp.status_code == 200
map_id = create_resp.json()["id"]
# 企业1自己能看到空列表
own_resp = client.get(
f"/api/cma/maps/{map_id}/objectives",
headers=auth_header(token1),
)
assert own_resp.status_code == 200
assert own_resp.json()["data"] == []
# 第二个企业账套 → 跨账套查询同样返回空
user = db.query(User).filter(User.username == "testadmin").first()
_create_second_entity(db, user)
token2 = _token_for_entity(client, 2)
cross_resp = client.get(
f"/api/cma/maps/{map_id}/objectives",
headers=auth_header(token2),
)
assert cross_resp.status_code == 200
assert cross_resp.json()["data"] == []