From e89427297d37f6cb75dc412e86eed30be259f2f2 Mon Sep 17 00:00:00 2001 From: oliver Date: Tue, 22 Sep 2026 14:50:28 +0800 Subject: [PATCH] Rename shared bgp_peer workbook sheet to BGP Status Summary. Batch sheets are keyed by metric_id, so all AF summaries land in one tab; stop inheriting the first profile title (VPNv4). Co-authored-by: Cursor --- netx_api/biz_state/service.py | 28 ++++++++++++-- tests/test_batch_workbook_api.py | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/netx_api/biz_state/service.py b/netx_api/biz_state/service.py index 107bb72..224914d 100644 --- a/netx_api/biz_state/service.py +++ b/netx_api/biz_state/service.py @@ -571,6 +571,25 @@ def run_purge_for_task(db: Session, task_id: str) -> dict[str, Any]: return purge_task_batches(db, task) +# Workbook sheets are keyed by metric_id. Multiple AF-specific collect profiles +# share one metric (e.g. all BGP summaries → bgp_peer); use a neutral title. +_METRIC_SHEET_TITLES: dict[str, str] = { + "bgp_peer": "BGP Status Summary", + "bgp_route": "BGP Neighbor Routes", + "vrrp": "VRRP", + "ip_route": "IPv4 Forwarding", + "ipv6_route": "IPv6 Forwarding", +} + + +def _metric_sheet_title(metric_id: str, fallback: str = "") -> str: + mid = str(metric_id or "").strip() + if mid in _METRIC_SHEET_TITLES: + return _METRIC_SHEET_TITLES[mid] + fb = str(fallback or "").strip() + return fb or mid + + def get_batch(db: Session, batch_id: str) -> dict[str, Any]: """Batch workbook summary: meta + commands + sheet catalog (no metric row payload).""" b = db.get(BizStateBatch, batch_id) @@ -617,8 +636,7 @@ def get_batch(db: Session, batch_id: str) -> dict[str, Any]: if id_ not in sheets_order: sheets_order.append(id_) sheet_cmds.setdefault(id_, []) - if title: - sheet_titles[id_] = title + sheet_titles[id_] = _metric_sheet_title(id_, title) if cmd_info is not None: # Prefer primary collect rows over aux / aux_cached for the same CLI sheet_cmds[id_].append(cmd_info) @@ -704,11 +722,12 @@ def get_batch(db: Session, batch_id: str) -> dict[str, Any]: sheet_cmds.setdefault(mid, []) if mid not in sheet_titles: # Best-effort title from any profile with this metric_id + fallback = mid for p in all_profiles(): if p.metric_id == mid and p.enabled: - sheet_titles[mid] = str(p.title or mid) + fallback = str(p.title or mid) break - sheet_titles.setdefault(mid, mid) + sheet_titles[mid] = _metric_sheet_title(mid, fallback) sheets = [ { @@ -1013,6 +1032,7 @@ def preview_items(db: Session, *, vendor: str, device_type: str, items: list[dic def _resolve_export_profile(profile_id: str): """Resolve collect profile; remap disabled if_intf → config_interface.""" + pid = str(profile_id or "").strip() if not pid: return None diff --git a/tests/test_batch_workbook_api.py b/tests/test_batch_workbook_api.py index d2f9734..4fbdb62 100644 --- a/tests/test_batch_workbook_api.py +++ b/tests/test_batch_workbook_api.py @@ -70,6 +70,71 @@ class BatchWorkbookApiTests(unittest.TestCase): self.assertEqual(out["sheets"][0]["commands"][0]["raw_command"], "show arp | one-line") self.assertTrue(out["sheets"][0].get("title")) + def test_bgp_peer_sheet_uses_status_summary_title(self) -> None: + """Shared metric_id bgp_peer must not inherit first AF profile title.""" + batch = BizStateBatch( + id="b1", + task_id="t1", + status="ok", + command_count=2, + row_count=3, + ) + cmds = [ + BizStateBatchCommand( + id="c1", + batch_id="b1", + profile_id="zte.bgp_vpnv4_summary", + parser_id="bgp_peer", + metric_id="bgp_peer", + raw_command="show bgp vpnv4 unicast summary | one-line", + parse_status="ok", + row_count=2, + ), + BizStateBatchCommand( + id="c2", + batch_id="b1", + profile_id="zte.bgp_ipv4_summary", + parser_id="bgp_peer", + metric_id="bgp_peer", + raw_command="show bgp ipv4 unicast summary | one-line", + parse_status="ok", + row_count=1, + ), + ] + db = MagicMock() + db.get.side_effect = lambda model, pk: batch if pk == "b1" else None + cmd_q = MagicMock() + cmd_q.filter.return_value.order_by.return_value.all.return_value = cmds + metric_count_q = MagicMock() + metric_count_q.filter.return_value.group_by.return_value.all.return_value = [ + ("bgp_peer", 3) + ] + lldp_count_q = MagicMock() + lldp_count_q.filter.return_value.scalar.return_value = 0 + + def query(*_args, **_kwargs): + n = query.n + query.n += 1 + if n == 0: + return cmd_q + if n == 1: + return metric_count_q + return lldp_count_q + + query.n = 0 + db.query.side_effect = query + + with patch( + "netx_api.biz_state.service.batch_protect_info", + return_value={"protected": False, "reasons": []}, + ): + out = get_batch(db, "b1") + + bgp = next(s for s in out["sheets"] if s["metric_id"] == "bgp_peer") + self.assertEqual(bgp["title"], "BGP Status Summary") + self.assertEqual(bgp["row_count"], 3) + self.assertEqual(len(bgp["commands"]), 2) + def test_list_metric_rows_rejects_commands_sheet(self) -> None: db = MagicMock() db.get.return_value = BizStateBatch(id="b1", task_id="t1")