diff --git a/backend/tests/test_kr_name_pollution_20260916.py b/backend/tests/test_kr_name_pollution_20260916.py index 77d2b376..3ee0ee0e 100644 --- a/backend/tests/test_kr_name_pollution_20260916.py +++ b/backend/tests/test_kr_name_pollution_20260916.py @@ -111,7 +111,8 @@ class TestPassThroughGuards: db.refresh(m) resp = client.put(f"/api/cma/maps/{m.id}", headers=auth_header(token), - json={"dimensions": _dims_with_krs("营业收入≥2", 25000)}) + json={"dimensions": _dims_with_krs("营业收入≥2", 25000), + "base_updated_at": m.updated_at.isoformat()}) assert resp.status_code == 200, resp.text kr = db.query(KR).filter(KR.objective_id.isnot(None)).first() @@ -136,7 +137,8 @@ class TestPassThroughGuards: db.refresh(m) resp = client.put(f"/api/cma/maps/{m.id}", headers=auth_header(token), - json={"dimensions": _dims_with_krs("渠道留存率≥90", 90)}) + json={"dimensions": _dims_with_krs("渠道留存率≥90", 90), + "base_updated_at": m.updated_at.isoformat()}) assert resp.status_code == 200, resp.text kr = db.query(KR).filter(KR.objective_id.isnot(None)).first() assert kr is not None and kr.title == "渠道留存率≥90" diff --git a/backend/tests/test_map_versions.py b/backend/tests/test_map_versions.py index 01efdc28..5c3e08c0 100644 --- a/backend/tests/test_map_versions.py +++ b/backend/tests/test_map_versions.py @@ -42,6 +42,19 @@ def _create_published_map(client: TestClient, token: str, title: str) -> int: return map_id +def _base_updated_at(db: Session, map_id: int) -> str: + """客户端载入快照时取得的服务端基准(乐观并发契约 base_updated_at,2026-10-12)。 + + PUT /api/cma/maps/{id} 覆写 dimensions/canvas_data 时须带此基准,取「写前读到的 + updated_at」——这是同一写通道契约的一部分(cma-strategy-map-autosave-stale-overwrite), + 不是把用例改绿:不带基准的写会被 409 拒绝(fail-closed)。 + """ + db.expire_all() + m = db.query(StrategicMap).filter(StrategicMap.id == map_id).first() + assert m is not None + return m.updated_at.isoformat() + + class TestMapVersions: """战略地图版本 endpoints""" @@ -102,7 +115,8 @@ class TestMapVersions: {"name": "KR新", "kpi_code": "F_REVENUE", "weight": 100, "target_value": "100"} ]}]}] upd = client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token), - json={"dimensions": new_dims, "canvas_data": {"connections": [{"from": "a", "to": "b"}]}}) + json={"dimensions": new_dims, "canvas_data": {"connections": [{"from": "a", "to": "b"}]}, + "base_updated_at": _base_updated_at(db, map_id)}) assert upd.status_code == 200 # 回滚到 v1.0 → 应自动备份当前(v1.0→v1.1) 且内容还原为 v1.0 @@ -147,13 +161,15 @@ class TestMapVersions: v1_id = versions[0]["id"] client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token), json={"dimensions": [{"key": "finance", "name": "财务层", "icon": "💰", "color": "#F56C6C", - "objectives": []}]}) + "objectives": []}], + "base_updated_at": _base_updated_at(db, map_id)}) rb1 = client.post(f"/api/cma/maps/{map_id}/versions/{v1_id}/rollback", headers=auth_header(token)).json() assert rb1["backup_version"] == "v1.1" # 再次改内容再回滚 → 备份 v1.2(不撞 v1.1) client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token), json={"dimensions": [{"key": "customer", "name": "客户层", "icon": "👥", "color": "#409EFF", - "objectives": []}]}) + "objectives": []}], + "base_updated_at": _base_updated_at(db, map_id)}) rb2 = client.post(f"/api/cma/maps/{map_id}/versions/{v1_id}/rollback", headers=auth_header(token)).json() assert rb2["backup_version"] == "v1.2"