Skip to content

Commit a3c8001

Browse files
committed
More updates to v1.0.0
1 parent 3aea0d6 commit a3c8001

9 files changed

Lines changed: 156 additions & 17 deletions

testing/automated/test_input_models.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pytest
33
from pydantic import ValidationError
44

5-
from input_models.models import InterfacesQuery, IntentQuery, KBQuery, OspfQuery, RoutingQuery
5+
from input_models.models import InterfacesQuery, IntentQuery, KBQuery, OspfQuery, RoutingQuery, TracerouteInput
66

77

88
class TestOspfQuery:
@@ -72,6 +72,33 @@ def test_valid(self):
7272
assert q.device == "R1"
7373

7474

75+
class TestTracerouteInput:
76+
def test_valid_minimal(self):
77+
q = TracerouteInput(device="R1", destination="10.0.0.1")
78+
assert q.device == "R1"
79+
assert q.destination == "10.0.0.1"
80+
assert q.source is None
81+
assert q.vrf is None
82+
83+
def test_valid_full(self):
84+
q = TracerouteInput(device="R1", destination="10.0.0.1", source="192.168.1.1", vrf="VRF1")
85+
assert q.source == "192.168.1.1"
86+
assert q.vrf == "VRF1"
87+
88+
def test_vrf_injection_blocked(self):
89+
with pytest.raises(ValidationError):
90+
TracerouteInput(device="R1", destination="10.0.0.1", vrf="VRF1; reboot")
91+
92+
def test_json_string_parsing(self):
93+
q = TracerouteInput.model_validate('{"device": "R1", "destination": "10.0.0.1"}')
94+
assert q.device == "R1"
95+
assert q.destination == "10.0.0.1"
96+
97+
def test_missing_destination_rejected(self):
98+
with pytest.raises(ValidationError):
99+
TracerouteInput(device="R1")
100+
101+
75102
class TestKBQuery:
76103
def test_valid_full(self):
77104
q = KBQuery(query="OSPF timers", vendor="cisco_ios", topic="rfc", top_k=3)

testing/automated/test_intent_tool.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"""UT-014: query_intent tool."""
22
from unittest.mock import patch
33

4-
import pytest
5-
64
from input_models.models import IntentQuery
75
from tools.intent import query_intent
86

testing/automated/test_list_devices.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
"""UT-011: list_devices tool."""
2-
import pytest
3-
42
from input_models.models import DeviceListQuery
53
from tools.inventory_tool import list_devices
64

testing/automated/test_mcp_server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33

44
class TestMcpToolRegistration:
5-
async def test_seven_tools_registered(self):
6-
"""The MCP server must expose exactly 7 tools."""
5+
async def test_eight_tools_registered(self):
6+
"""The MCP server must expose exactly 8 tools."""
77
from server.MCPServer import mcp
88
tools = await mcp.list_tools()
9-
assert len(tools) == 7
9+
assert len(tools) == 8
1010

1111
async def test_tool_names(self):
12-
"""All seven expected tool names are registered."""
12+
"""All eight expected tool names are registered."""
1313
from server.MCPServer import mcp
1414
tools = await mcp.list_tools()
1515
names = {t.name for t in tools}
@@ -21,4 +21,5 @@ async def test_tool_names(self):
2121
"query_intent",
2222
"get_status",
2323
"list_devices",
24+
"traceroute",
2425
}

testing/automated/test_platform_map.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def test_routing_queries_complete(self, cli_style):
2626
def test_interfaces_present(self, cli_style):
2727
assert "interface_status" in PLATFORM_MAP[cli_style]["interfaces"]
2828

29+
@pytest.mark.parametrize("cli_style", EXPECTED_CLI_STYLES)
30+
def test_traceroute_present(self, cli_style):
31+
assert "traceroute" in PLATFORM_MAP[cli_style]["tools"], f"{cli_style} missing traceroute"
32+
2933

3034
class TestApplyVrf:
3135
def test_dict_with_vrf(self):
@@ -78,6 +82,39 @@ def test_routeros_ip_route(self):
7882
assert result == "/ip route print without-paging"
7983

8084

85+
class TestTracerouteVrf:
86+
def test_ios_traceroute_with_vrf(self):
87+
device = {"cli_style": "ios"}
88+
result = get_action(device, "tools", "traceroute", vrf="VRF1")
89+
assert result == "traceroute vrf VRF1"
90+
91+
def test_ios_traceroute_no_vrf(self):
92+
device = {"cli_style": "ios"}
93+
result = get_action(device, "tools", "traceroute")
94+
assert result == "traceroute"
95+
96+
def test_eos_traceroute_with_vrf(self):
97+
device = {"cli_style": "eos"}
98+
result = get_action(device, "tools", "traceroute", vrf="VRF1")
99+
assert result == "traceroute vrf VRF1"
100+
101+
def test_junos_traceroute_with_vrf(self):
102+
device = {"cli_style": "junos"}
103+
result = get_action(device, "tools", "traceroute", vrf="VRF1")
104+
assert result == "traceroute routing-instance VRF1"
105+
106+
def test_routeros_traceroute_no_vrf_variant(self):
107+
device = {"cli_style": "routeros"}
108+
result = get_action(device, "tools", "traceroute")
109+
assert result == "/tool/traceroute"
110+
111+
def test_routeros_traceroute_ignores_vrf(self):
112+
"""RouterOS has no VRF variant — VRF is silently ignored."""
113+
device = {"cli_style": "routeros"}
114+
result = get_action(device, "tools", "traceroute", vrf="VRF1")
115+
assert result == "/tool/traceroute"
116+
117+
81118
class TestGetAction:
82119
def test_resolves_ios_ospf_neighbors(self):
83120
device = {"cli_style": "ios"}

testing/automated/test_security.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from pydantic import ValidationError
99

10-
from input_models.models import OspfQuery, RoutingQuery
10+
from input_models.models import OspfQuery, RoutingQuery, TracerouteInput
1111

1212

1313
# ── VRF injection: each pattern MUST be rejected by the Pydantic validator ────
@@ -48,6 +48,13 @@ def test_vrf_injection_blocked_routing(bad_vrf):
4848
RoutingQuery(device="R1", query="ip_route", vrf=bad_vrf)
4949

5050

51+
@pytest.mark.parametrize("bad_vrf", _INJECTION_PATTERNS)
52+
def test_vrf_injection_blocked_traceroute(bad_vrf):
53+
"""Every injection payload must raise ValidationError in TracerouteInput."""
54+
with pytest.raises(ValidationError):
55+
TracerouteInput(device="R1", destination="10.0.0.1", vrf=bad_vrf)
56+
57+
5158
# ── VRF positive cases: valid names MUST be accepted ─────────────────────────
5259

5360
_VALID_VRFS = [
@@ -70,3 +77,9 @@ def test_vrf_valid_names_accepted_ospf(good_vrf):
7077
def test_vrf_valid_names_accepted_routing(good_vrf):
7178
q = RoutingQuery(device="R1", query="ip_route", vrf=good_vrf)
7279
assert q.vrf == good_vrf
80+
81+
82+
@pytest.mark.parametrize("good_vrf", _VALID_VRFS)
83+
def test_vrf_valid_names_accepted_traceroute(good_vrf):
84+
q = TracerouteInput(device="R1", destination="10.0.0.1", vrf=good_vrf)
85+
assert q.vrf == good_vrf

testing/automated/test_tools.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
"""UT-003: Tool layer — get_ospf, get_interfaces, _error_response, and VRF integration."""
1+
"""UT-003: Tool layer — get_ospf, get_interfaces, traceroute, _error_response, and VRF integration."""
22
from unittest.mock import AsyncMock, patch
33

44
import pytest
55

6-
from input_models.models import InterfacesQuery, OspfQuery
6+
from input_models.models import InterfacesQuery, OspfQuery, TracerouteInput
77
from tools import _error_response
8-
from tools.operational import get_interfaces
8+
from tools.operational import get_interfaces, traceroute
99
from tools.ospf import get_ospf
1010

1111

@@ -72,6 +72,48 @@ async def test_valid_device_junos(self):
7272
assert result["cli_style"] == "junos" # R3 is junos in MOCK_DEVICES
7373

7474

75+
class TestTraceroute:
76+
async def test_unknown_device(self):
77+
result = await traceroute(TracerouteInput(device="NONEXISTENT", destination="10.0.0.1"))
78+
assert "error" in result
79+
assert "Unknown device" in result["error"]
80+
81+
async def test_valid_device_ios_basic(self):
82+
with patch("transport.execute_ssh", new_callable=AsyncMock) as mock_ssh:
83+
mock_ssh.return_value = "1 10.0.0.254 1 ms"
84+
result = await traceroute(TracerouteInput(device="R1", destination="10.0.0.1"))
85+
assert result["device"] == "R1"
86+
assert result["cli_style"] == "ios"
87+
assert "traceroute" in result["_command"]
88+
assert "10.0.0.1" in result["_command"]
89+
90+
async def test_ios_source_appended(self):
91+
with patch("transport.execute_ssh", new_callable=AsyncMock) as mock_ssh:
92+
mock_ssh.return_value = "1 10.0.0.254 1 ms"
93+
result = await traceroute(TracerouteInput(device="R1", destination="10.0.0.1", source="192.168.1.1"))
94+
assert "source 192.168.1.1" in result["_command"]
95+
96+
async def test_eos_vrf_in_command(self):
97+
with patch("transport.execute_ssh", new_callable=AsyncMock) as mock_ssh:
98+
mock_ssh.return_value = "mock output"
99+
result = await traceroute(TracerouteInput(device="R2", destination="10.0.0.1", vrf="VRF1"))
100+
assert "vrf VRF1" in result["_command"]
101+
assert "10.0.0.1" in result["_command"]
102+
103+
async def test_routeros_address_syntax(self):
104+
with patch("transport.execute_ssh", new_callable=AsyncMock) as mock_ssh:
105+
mock_ssh.return_value = "mock output"
106+
result = await traceroute(TracerouteInput(device="R5", destination="10.0.0.1"))
107+
assert "address=10.0.0.1" in result["_command"]
108+
109+
async def test_routeros_source_syntax(self):
110+
with patch("transport.execute_ssh", new_callable=AsyncMock) as mock_ssh:
111+
mock_ssh.return_value = "mock output"
112+
result = await traceroute(TracerouteInput(device="R5", destination="10.0.0.1", source="192.168.1.1"))
113+
assert "address=10.0.0.1" in result["_command"]
114+
assert "src-address=192.168.1.1" in result["_command"]
115+
116+
75117
class TestVrfEndToEnd:
76118
async def test_explicit_vrf_in_final_command(self):
77119
"""VRF flows from validated input → get_action substitution → final CLI command."""

testing/live/test_platform_coverage.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""LT-001: Live platform coverage — all vendors × all OSPF queries + interfaces.
1+
"""LT-001: Live platform coverage — all vendors × all OSPF, routing_table, tools + interfaces.
22
33
Generates platform_coverage_results.md with detailed output per test.
44
"""
@@ -7,9 +7,10 @@
77
import pytest
88

99
from core.inventory import devices
10-
from input_models.models import InterfacesQuery, OspfQuery
11-
from tools.operational import get_interfaces
10+
from input_models.models import InterfacesQuery, OspfQuery, RoutingQuery, TracerouteInput
11+
from tools.operational import get_interfaces, traceroute
1212
from tools.ospf import get_ospf
13+
from tools.routing import get_routing
1314

1415
# ── Test devices: one per vendor ─────────────────────────────────────────────
1516
TEST_DEVICES = {
@@ -21,6 +22,9 @@
2122
}
2223

2324
OSPF_QUERIES = ["neighbors", "database", "borders", "config", "interfaces", "details"]
25+
ROUTING_QUERIES = ["ip_route", "route_maps", "prefix_lists", "policy_based_routing", "access_lists"]
26+
TRACEROUTE_DEST = "172.20.20.207" # C1J management IP — core device reachable from all vendors
27+
2428
RESULTS = [] # collected during test run
2529

2630

@@ -124,10 +128,29 @@ async def test_ospf_query(device, query):
124128
assert status != "FAIL", f"{device} ospf/{query}: {result.get('error', result.get('raw', '')[:200])}"
125129

126130

131+
@pytest.mark.parametrize("device", TEST_DEVICES.keys())
132+
@pytest.mark.parametrize("query", ROUTING_QUERIES)
133+
async def test_routing_query(device, query):
134+
"""Test routing_table query against a live device."""
135+
result = await get_routing(RoutingQuery(device=device, query=query))
136+
status = classify(result)
137+
record(device, "routing_table", query, result, status)
138+
assert status != "FAIL", f"{device} routing_table/{query}: {result.get('error', result.get('raw', '')[:200])}"
139+
140+
127141
@pytest.mark.parametrize("device", TEST_DEVICES.keys())
128142
async def test_interfaces(device):
129143
"""Test interface status query against a live device."""
130144
result = await get_interfaces(InterfacesQuery(device=device))
131145
status = classify(result)
132146
record(device, "interfaces", "interface_status", result, status)
133147
assert status != "FAIL", f"{device} interfaces: {result.get('error', result.get('raw', '')[:200])}"
148+
149+
150+
@pytest.mark.parametrize("device", TEST_DEVICES.keys())
151+
async def test_traceroute(device):
152+
"""Test traceroute against a live device, tracing to C1J management IP."""
153+
result = await traceroute(TracerouteInput(device=device, destination=TRACEROUTE_DEST))
154+
status = classify(result)
155+
record(device, "tools", "traceroute", result, status)
156+
assert status != "FAIL", f"{device} traceroute→{TRACEROUTE_DEST}: {result.get('error', result.get('raw', '')[:200])}"

testing/run_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ echo ""
6767
printf "%sLive Tests%s\n" "$BOLD" "$NC"
6868
echo "-----------------------------------------"
6969
if [[ "${1:-}" == "--live" ]]; then
70-
run_suite "LT-001" "Platform Coverage (5 vendors)" "testing/live/test_platform_coverage.py" "--live"
70+
run_suite "LT-001" "Platform Coverage (5 vendors × OSPF+routing+traceroute+interfaces)" "testing/live/test_platform_coverage.py" "--live"
7171
echo ""
7272
echo "Report: testing/live/platform_coverage_results.md"
7373
else

0 commit comments

Comments
 (0)