Add ops task overview, chart ahead window, and WebCRT lifecycle status.

Unify live runners in /audit/tasks (including UME sync and connect tests), extend port-traffic compare past now via ahead_hours, and distinguish WebCRT connecting/ready/detached.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-02 14:05:59 +08:00
parent 3f0787da11
commit 3908b78dbb
28 changed files with 1083 additions and 54 deletions

View file

@ -236,6 +236,46 @@ class CompareTargetsTests(unittest.TestCase):
self.assertGreaterEqual(len(out.baseline), 2)
self.assertEqual(out.meta.offset_hours, 2.0)
def test_ahead_hours_extends_baseline_past_now(self):
# Yesterday sample 30min after "now" clock — only visible when ahead_hours > 0.
future_raw = self.now - timedelta(days=1) + timedelta(minutes=30)
self.db.add(
PortTrafficSample(
id=uuid4().hex,
target_row_id=self.target_id,
series_id=self.series_id,
ts=future_raw,
in_bps=777.0,
out_bps=888.0,
raw_ok=True,
)
)
self.db.commit()
without = compare_targets(
self.db,
target_row_id=self.target_id,
range_hours=2,
baseline="day",
ahead_hours=0,
to_ts=self.now,
)
self.assertFalse(any(abs((p.ts_raw or p.ts) - future_raw).total_seconds() < 1 for p in without.baseline))
with_ahead = compare_targets(
self.db,
target_row_id=self.target_id,
range_hours=2,
baseline="day",
ahead_hours=1,
to_ts=self.now,
)
self.assertEqual(with_ahead.meta.ahead_hours, 1.0)
hit = [p for p in with_ahead.baseline if p.in_bps == 777.0]
self.assertEqual(len(hit), 1)
self.assertGreater(hit[0].ts, self.now)
self.assertLessEqual(hit[0].ts, self.now + timedelta(hours=1, seconds=5))
if __name__ == "__main__":
unittest.main()

View file

@ -34,12 +34,15 @@ from netx_api.topology_schemas import (
CISCO_LLDP_BRIEF = """
R2#show lldp neighbors
Capability codes:
(R) Router, (B) Bridge
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
R1 Gi0/0 120 R Gi0/1
R3 Gi0/1 120 R Gi0/0
r1 Gi0/1 120 B,R Ethernet1/0/1
Total entries displayed: 1
"""
CISCO_LLDP_DETAIL = """
@ -72,8 +75,16 @@ gei-0/1/0/1 0011.2233.4455 gei-0/1/0/2 R1
class LldpParserTests(unittest.TestCase):
def test_cisco_brief(self) -> None:
hits = lldp.parse_cisco_lldp(CISCO_LLDP_BRIEF)
self.assertGreaterEqual(len(hits), 1)
self.assertTrue(any(h.remote_name.upper().startswith("R1") for h in hits))
self.assertEqual(len(hits), 1)
self.assertEqual(hits[0].remote_name.lower(), "r1")
self.assertEqual(hits[0].local_port, "Gi0/1")
self.assertEqual(hits[0].remote_port, "Ethernet1/0/1")
def test_cisco_detail_fallback(self) -> None:
hits = lldp.parse_cisco_lldp(CISCO_LLDP_DETAIL)
self.assertEqual(len(hits), 1)
self.assertEqual(hits[0].remote_name.lower(), "r1")
self.assertEqual(hits[0].remote_ip, "192.168.0.1")
def test_huawei(self) -> None:
hits = lldp.parse_huawei_lldp(HUAWEI_LLDP)
@ -83,7 +94,7 @@ class LldpParserTests(unittest.TestCase):
def test_pick_command_lldp_only(self) -> None:
cmd, tag = lldp.pick_neighbor_command(protocol="cdp", vendor="Cisco", device_type="cisco_ios")
self.assertEqual(tag, "lldp")
self.assertIn("lldp", cmd.lower())
self.assertEqual(cmd, "show lldp neighbors detail")
class FabricTopologyTests(unittest.TestCase):

View file

@ -503,6 +503,8 @@ class WebcrtServiceTests(unittest.TestCase):
rows=24,
conn=conn, # type: ignore[arg-type]
)
sess.state = "ready"
sess.connect_finished_at = time.time()
sess.attached = True
with svc._sessions_lock:
svc._sessions["grace"] = sess
@ -522,6 +524,42 @@ class WebcrtServiceTests(unittest.TestCase):
svc._reap_sessions()
self.assertIsNone(svc.get_session("grace"))
@patch.object(svc, "_audit")
def test_list_sessions_lifecycle_ready_vs_detached(self, _mock_audit: MagicMock) -> None:
conn = _FakeConn()
sess = svc.WebcrtSession(
session_id="life",
ne_id="ne1",
ne_name="lab",
ne_ip="1.2.3.4",
protocol="ssh",
cols=80,
rows=24,
conn=conn, # type: ignore[arg-type]
)
sess.state = "ready"
sess.attached = True
with svc._sessions_lock:
svc._sessions["life"] = sess
ready_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
self.assertEqual(ready_rows["life"]["lifecycle"], "ready")
self.assertTrue(ready_rows["life"]["attached"])
svc.detach_session("life", grace_sec=120.0, attach_gen=0)
detached_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
self.assertEqual(detached_rows["life"]["lifecycle"], "detached")
self.assertFalse(detached_rows["life"]["attached"])
self.assertIsNotNone(detached_rows["life"]["detach_deadline"])
sess.state = "connecting"
sess.attached = False
connecting_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
self.assertEqual(connecting_rows["life"]["lifecycle"], "connecting")
self.assertIsInstance(connecting_rows["life"]["elapsed_ms"], int)
svc.close_session("life", reason="test")
self.assertEqual(svc.list_sessions()["total"], 0)
@patch.object(svc, "_audit")
def test_attach_timeout_reaper(self, _mock_audit: MagicMock) -> None:
conn = _FakeConn()