fix(cma): scope action-plan KR enrichment to session entity (batch3 follow-up)

list_plans enriched kr_title via KR.id only -> legacy rows whose kr_id points at another entity's KR echoed the foreign KR title. Add KR.entity_id filter (read-side sibling of batch3 item 4).

Local commit only; no push (push triggers Gitea deploy hook).
This commit is contained in:
Hermes CI Fix
2026-09-18 19:03:49 +08:00
parent 9fb9d14a58
commit 50baa16f09
2 changed files with 58 additions and 2 deletions
+4 -1
View File
@@ -124,7 +124,10 @@ def list_plans(
item["kpi_dimension"] = kpi.dimension if kpi else None
# 附带KR信息(行动挂KR 2026-08-27)
if p.kr_id is not None:
kr = db.query(KR).filter(KR.id == p.kr_id).first()
# 账套隔离(2026-09-18 batch3 读侧同族残余):kr_id 可能指向对端账套 KR(存量脏行,
# 写入侧 create_plan/update_plan 已校验归属但旧行仍存在)→ 富化查询必须带账套,
# 否则列表跨账套回显对端 KR 标题
kr = db.query(KR).filter(KR.id == p.kr_id, KR.entity_id == entity_id).first()
item["kr_title"] = kr.title if kr else None
item["kr_metric_kpi_id"] = kr.metric_kpi_id if kr else None
else:
@@ -127,7 +127,7 @@ def _mk_allocation(db: Session, activity: AbcActivity, eid: int, product_code: s
def _mk_plan(db: Session, kpi_id: int, eid: int, title: str, **kw) -> ActionPlan:
plan = ActionPlan(entity_id=eid, kpi_id=kpi_id, title=title,
plan = ActionPlan(entity_id=eid, kpi_id=kpi_id, kr_id=kw.get("kr_id"), title=title,
assignee=kw.get("assignee", "张三"), priority="medium",
status=kw.get("status", "pending"), progress=0)
db.add(plan)
@@ -394,6 +394,59 @@ def test_plan_stats_reverse_direction(client: TestClient, db: Session):
assert s2["pending"] == 1
# ──────────────────────────────────────────────
# D6. GET /action-plans 的 KR 富化按会话账套(batch3 读侧同族残余)
# ──────────────────────────────────────────────
# 缺陷:list_plans 富化 kr_title 的查询 db.query(KR).filter(KR.id == p.kr_id) 无账套校验
# → 存量脏行(action_plans.kr_id 指向对端账套 KR)会让列表跨账套回显对端 KR 标题。
# 写入侧已在 batch3 修(update_plan/create_plan 校验 kr_id 归属),本用例覆盖读侧。
def _list_plans(client: TestClient, token: str) -> dict:
r = client.get(PLANS, headers=auth_header(token))
assert r.status_code == 200, r.text
return {row["id"]: row for row in r.json()["data"]}
def test_list_plans_kr_enrichment_isolated_by_entity(client: TestClient, db: Session):
"""双向:本账套计划挂对端 KR(存量脏行)→ 列表不回显对端 KR 标题;挂本账套 KR → 正常回显"""
k1, k2 = _seed_two_entities(db)
kr1 = _mk_objective_kr(db, 1, "e1 KR")
kr2 = _mk_objective_kr(db, 2, "e2 KR")
# e1 账套:一条挂本账套 KR(正对照)+ 一条脏行挂 e2 的 KR
good1 = _mk_plan(db, k1.id, 1, "zz e1 挂本账套 KR", kr_id=kr1.id)
dirty1 = _mk_plan(db, k1.id, 1, "zz e1 脏行挂对端 KR", kr_id=kr2.id)
# e2 账套:反向同理
good2 = _mk_plan(db, k2.id, 2, "zz e2 挂本账套 KR", kr_id=kr2.id)
dirty2 = _mk_plan(db, k2.id, 2, "zz e2 脏行挂对端 KR", kr_id=kr1.id)
rows1 = _list_plans(client, _login(client, 1))
assert good1.id in rows1 and dirty1.id in rows1, f"e1 会话看不到本账套计划: {sorted(rows1)}"
assert rows1[good1.id]["kr_title"] == "e1 KR", (
f"e1 本账套 KR 标题未回显: {rows1[good1.id].get('kr_title')!r}"
)
assert rows1[dirty1.id]["kr_title"] is None, (
f"e1 会话跨账套回显了 e2 的 KR 标题 {rows1[dirty1.id]['kr_title']!r}"
"(缺陷:list_plans 富化 kr 查询缺 KR.entity_id 校验)"
)
assert rows1[dirty1.id]["kr_metric_kpi_id"] is None, (
f"e1 会话跨账套回显了 e2 KR 的 metric_kpi_id = {rows1[dirty1.id]['kr_metric_kpi_id']!r}"
)
rows2 = _list_plans(client, _login(client, 2))
assert good2.id in rows2 and dirty2.id in rows2, f"e2 会话看不到本账套计划: {sorted(rows2)}"
assert rows2[good2.id]["kr_title"] == "e2 KR", (
f"e2 本账套 KR 标题未回显: {rows2[good2.id].get('kr_title')!r}"
)
assert rows2[dirty2.id]["kr_title"] is None, (
f"e2 会话跨账套回显了 e1 的 KR 标题 {rows2[dirty2.id]['kr_title']!r}"
)
assert rows2[dirty2.id]["kr_metric_kpi_id"] is None
# 计划本体归属未被本改动影响:e1 列表不含 e2 的两条计划
assert good2.id not in rows1 and dirty2.id not in rows1, "e1 列表混入了 e2 账套计划"
# ──────────────────────────────────────────────
# 鉴权底线(改前改后都应通过)
# ──────────────────────────────────────────────