缺陷 cma-strategy-map-autosave-stale-overwrite 的护栏(PUT /api/cma/maps/{id} 覆写
dimensions/canvas_data 须带 base_updated_at,否则 409 fail-closed)改变了同一写通道的
契约 ⇒ 全仓清点调用点,在调用点取基准(写前读 updated_at),**不放宽护栏**:
- tests/test_map_versions.py: 新增 _base_updated_at(db, map_id) 助手;3 处覆写 dimensions 的
PUT 补基准(test_rollback_backs_up_current_and_restores_content 会直接失败;
test_rollback_again_uses_next_free_backup_version 两处此前已静默降级——PUT 被拒但用例仍绿,
即「改了内容再回滚」的前提实际未发生)。
- tests/test_kr_name_pollution_20260916.py: 2 处 KR 透传护栏用例补基准。
改前/改后全量对拍(同树 pythonpath):
baseline 22 failed / fixed2 18 failed;diff 仅剩 tests/test_doc_center_20260924.py::
TestProdContentCanary::test_prod_content_readable —— 基线树同样失败(/srv/hermes/shared/
cma-content 于 14:05 被写入 manual 分类,共享内容目录变化),与本单无关。
本单新增的 5 例在基线树全红、改后全绿。
任务: yanxue-20261012-cma-map-autosave-stale-overwrite-defect-fullstack
This commit is contained in:
@@ -111,7 +111,8 @@ class TestPassThroughGuards:
|
|||||||
db.refresh(m)
|
db.refresh(m)
|
||||||
|
|
||||||
resp = client.put(f"/api/cma/maps/{m.id}", headers=auth_header(token),
|
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
|
assert resp.status_code == 200, resp.text
|
||||||
|
|
||||||
kr = db.query(KR).filter(KR.objective_id.isnot(None)).first()
|
kr = db.query(KR).filter(KR.objective_id.isnot(None)).first()
|
||||||
@@ -136,7 +137,8 @@ class TestPassThroughGuards:
|
|||||||
db.refresh(m)
|
db.refresh(m)
|
||||||
|
|
||||||
resp = client.put(f"/api/cma/maps/{m.id}", headers=auth_header(token),
|
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
|
assert resp.status_code == 200, resp.text
|
||||||
kr = db.query(KR).filter(KR.objective_id.isnot(None)).first()
|
kr = db.query(KR).filter(KR.objective_id.isnot(None)).first()
|
||||||
assert kr is not None and kr.title == "渠道留存率≥90"
|
assert kr is not None and kr.title == "渠道留存率≥90"
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ def _create_published_map(client: TestClient, token: str, title: str) -> int:
|
|||||||
return map_id
|
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:
|
class TestMapVersions:
|
||||||
"""战略地图版本 endpoints"""
|
"""战略地图版本 endpoints"""
|
||||||
|
|
||||||
@@ -102,7 +115,8 @@ class TestMapVersions:
|
|||||||
{"name": "KR新", "kpi_code": "F_REVENUE", "weight": 100, "target_value": "100"}
|
{"name": "KR新", "kpi_code": "F_REVENUE", "weight": 100, "target_value": "100"}
|
||||||
]}]}]
|
]}]}]
|
||||||
upd = client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token),
|
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
|
assert upd.status_code == 200
|
||||||
|
|
||||||
# 回滚到 v1.0 → 应自动备份当前(v1.0→v1.1) 且内容还原为 v1.0
|
# 回滚到 v1.0 → 应自动备份当前(v1.0→v1.1) 且内容还原为 v1.0
|
||||||
@@ -147,13 +161,15 @@ class TestMapVersions:
|
|||||||
v1_id = versions[0]["id"]
|
v1_id = versions[0]["id"]
|
||||||
client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token),
|
client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token),
|
||||||
json={"dimensions": [{"key": "finance", "name": "财务层", "icon": "💰", "color": "#F56C6C",
|
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()
|
rb1 = client.post(f"/api/cma/maps/{map_id}/versions/{v1_id}/rollback", headers=auth_header(token)).json()
|
||||||
assert rb1["backup_version"] == "v1.1"
|
assert rb1["backup_version"] == "v1.1"
|
||||||
# 再次改内容再回滚 → 备份 v1.2(不撞 v1.1)
|
# 再次改内容再回滚 → 备份 v1.2(不撞 v1.1)
|
||||||
client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token),
|
client.put(f"/api/cma/maps/{map_id}", headers=auth_header(token),
|
||||||
json={"dimensions": [{"key": "customer", "name": "客户层", "icon": "👥", "color": "#409EFF",
|
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()
|
rb2 = client.post(f"/api/cma/maps/{map_id}/versions/{v1_id}/rollback", headers=auth_header(token)).json()
|
||||||
assert rb2["backup_version"] == "v1.2"
|
assert rb2["backup_version"] == "v1.2"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user