Ship gated eye polish, fabric levels, and collection UI refresh.

Topology MCP adds pull/compact/bundle/suggest-hubs with a no-template skill path; API gains fabric level and NE collection policy; web list pages get paging and denser collect/network workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 16:13:05 +08:00
parent b1aee86701
commit b81e5869a6
91 changed files with 10395 additions and 1513 deletions

View file

@ -0,0 +1,71 @@
from __future__ import annotations
import unittest
from datetime import datetime, timedelta
from unittest.mock import MagicMock
from netx_api.collection_policy import (
DEFAULT_HISTORY_KEEP,
_normalize_interval_hours,
history_keep_value,
next_due_at,
policy_to_out,
)
from netx_api.collection_schemas import CollectionPolicyUpdate
from netx_api.models import NeCollectionJob, NeCollectionPolicy
class NeCollectionPolicyDefaultsTests(unittest.TestCase):
def test_default_history_keep_is_three(self) -> None:
self.assertEqual(DEFAULT_HISTORY_KEEP, 3)
row = NeCollectionPolicy(id=1, enabled=False, history_keep=3)
self.assertEqual(history_keep_value(row), 3)
out = policy_to_out(row)
self.assertFalse(out.enabled)
self.assertEqual(out.history_keep, 3)
def test_normalize_interval_hours_from_days(self) -> None:
row = NeCollectionPolicy(id=1, interval_hours=0, interval_days=2)
self.assertEqual(_normalize_interval_hours(row), 48)
def test_policy_update_schema_bounds(self) -> None:
body = CollectionPolicyUpdate(history_keep=3, interval_hours=24, enabled=False)
data = body.model_dump(exclude_unset=True)
self.assertEqual(data["history_keep"], 3)
self.assertFalse(data["enabled"])
class NeCollectionNextDueTests(unittest.TestCase):
def test_next_due_none_when_disabled(self) -> None:
db = MagicMock()
policy = NeCollectionPolicy(id=1, enabled=False, interval_hours=24)
self.assertIsNone(next_due_at(db, policy))
def test_next_due_now_when_no_prior_schedule(self) -> None:
db = MagicMock()
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
policy = NeCollectionPolicy(id=1, enabled=True, interval_hours=24)
due = next_due_at(db, policy)
self.assertIsNotNone(due)
assert due is not None
self.assertLessEqual(abs((due - datetime.now()).total_seconds()), 5)
def test_next_due_from_last_scheduled_done(self) -> None:
db = MagicMock()
ended = datetime.now() - timedelta(hours=2)
last = NeCollectionJob(
id="abc",
status="done",
trigger_mode="schedule",
ended_at=ended,
)
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = last
policy = NeCollectionPolicy(id=1, enabled=True, interval_hours=24)
due = next_due_at(db, policy)
self.assertIsNotNone(due)
assert due is not None
self.assertEqual(due, ended + timedelta(hours=24))
if __name__ == "__main__":
unittest.main()

View file

@ -977,11 +977,14 @@ class FabricTopologyTests(unittest.TestCase):
),
)
prev = clf.preview_classify(self.db)
self.assertGreaterEqual(prev.level_matched, 4)
self.assertGreaterEqual(prev.role_matched, 4)
applied = clf.apply_classify(self.db)
self.assertGreaterEqual(applied.level_updated, 4)
self.assertGreaterEqual(applied.role_updated, 4)
self.db.refresh(nodes[0])
self.assertEqual(nodes[0].role, "core")
self.assertEqual(nodes[0].level, 1.0)
self.assertEqual(nodes[0].region_folder_id, region.id)
dry = clf.generate_slices(
@ -1026,6 +1029,11 @@ class FabricTopologyTests(unittest.TestCase):
),
)
self.assertEqual(bulk.updated, 1)
self.assertEqual(bulk.level, 3.0)
acc = next(n for n in nodes if n.name.startswith("ACC-"))
self.db.refresh(acc)
self.assertEqual(acc.level, 3.0)
self.assertEqual(acc.role, "access")
def test_project_neighbors_respects_max_nodes(self) -> None:
suffix = uuid4().hex[:8]