mcaxl/tests/test_axl_error_hints.py
Ryan Malloy 9427e3d4df route_plan + client: device_grep + AXL anti-pattern error hints
Two complementary additions from cucx-docs's prompt-suggestions handoff
(see axl/agent-threads/cucx-prompt-suggestions/ for the source thread).

device_grep(pattern, classes=None) — fuzzy device discovery by name OR
description, optionally filtered by tkclass.name. Surfaces "wait, there
are TWO of these?" findings (parallel fax servers, duplicate CUBEs,
vestigial conference bridges) by grouping matches by class so the
structure of what matched is visible at a glance. CUCM-style % wildcards
work; case-insensitive matching via UPPER(); single quotes properly
escaped via _esc.

axl_sql error hints — when AXL returns an error AND the query contains
the trigger phrase, append a path-correction hint to the error message.
Two patterns shipped:

  - "Column (fkdevice) not found" + numplan in query → suggest the
    devicenumplanmap M:N join (the literal multi-attempt schema-discovery
    experience cucx-docs hit at Bingham — numplan has no direct fkdevice)

  - "not in database" + sipdestination in query → suggest sipdestinationgroup
    + sipprofile + axl_list_tables(pattern='sip%') for discovery (the
    `sipdestination` table is reasonable-sounding but doesn't exist)

Hints are surgical (both error fragment AND query trigger must match)
to keep false-positive risk near zero. Validator behavior unchanged —
this is post-execution error augmentation, not gate enhancement. Failing
queries now raise RuntimeError(augmented) when a hint applies; otherwise
the original exception passes through unchanged.

Tests: +19 (8 device_grep + 11 error-hints with end-to-end mock through
execute_sql_query). Full suite 219 → 238 passing.

Live-cluster smoke test still pending (TLS handshake intermittent
this session). Sequencing nit from cucx-docs's msg 003 (move error-hint
earlier) honored — bundled with device_grep in this single commit.
2026-05-05 17:41:15 -06:00

127 lines
4.9 KiB
Python

"""Tests for the AXL anti-pattern error-hint enhancement.
Source: cucx-docs handoff (msg 003 in
axl/agent-threads/cucx-prompt-suggestions/) — three recurring operator
mistakes with cluster-side error messages that don't suggest the right
path. Hints are surgical: only fire when both the error fragment AND
the query trigger phrase match.
"""
import pytest
from mcaxl.client import _augment_axl_error
class TestNumplanFkdeviceHint:
def test_fires_on_matching_error_and_query(self):
err = "Column (fkdevice) not found in any table in the query (or SLV is undefined)."
query = "SELECT name FROM numplan WHERE fkdevice = 'foo'"
out = _augment_axl_error(err, query)
assert err in out
assert "devicenumplanmap" in out
assert "M:N" in out
def test_no_fire_on_error_alone(self):
# Same error fragment but query doesn't mention numplan
err = "Column (fkdevice) not found in any table in the query."
query = "SELECT name FROM device WHERE fkdevice = 'foo'"
assert _augment_axl_error(err, query) == err
def test_no_fire_on_unrelated_error(self):
err = "Some completely different error about a different column."
query = "SELECT name FROM numplan"
assert _augment_axl_error(err, query) == err
def test_case_insensitive_query_match(self):
# User's query in mixed case should still trigger
err = "Column (fkdevice) not found in any table in the query."
query = "SELECT name FROM NumPlan np WHERE np.fkdevice IS NOT NULL"
out = _augment_axl_error(err, query)
assert "devicenumplanmap" in out
class TestSipDestinationHint:
def test_fires_on_matching_error_and_query(self):
err = "Table (sipdestination) is not in database."
query = "SELECT * FROM sipdestination"
out = _augment_axl_error(err, query)
assert "sipdestinationgroup" in out
assert "axl_list_tables" in out
def test_no_fire_when_query_doesnt_reference_table(self):
err = "Table (xyz) is not in database."
query = "SELECT * FROM xyz"
assert _augment_axl_error(err, query) == err
class TestNoMatch:
def test_unrelated_error_returns_unchanged(self):
err = "Permission denied for user 'CCMSysUser'."
query = "SELECT * FROM device"
assert _augment_axl_error(err, query) == err
def test_empty_error_returns_empty(self):
assert _augment_axl_error("", "SELECT 1") == ""
def test_identity_check_for_no_hint(self):
"""Caller compares identity to decide whether to wrap the original
exception. Make sure no-hint path returns the literal input."""
err = "Random AXL error"
query = "SELECT 1"
out = _augment_axl_error(err, query)
assert out is err # not just equal — same object
class TestEndToEnd:
"""Confirm the augmentation propagates through execute_sql_query.
Uses a mock that raises the AXL exception from inside zeep's call;
the augmenter should wrap it as a RuntimeError with the hint
appended.
"""
def test_execute_sql_query_wraps_with_hint(self):
from unittest.mock import MagicMock
from mcaxl.client import AxlClient
from mcaxl.cache import AxlCache
# Real cache, but in a temp location so tests don't pollute
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as td:
cache = AxlCache(Path(td) / "test.sqlite", default_ttl=0, cluster_id="test")
client = AxlClient(cache)
# Bypass _ensure_connected by setting service directly
mock_service = MagicMock()
mock_service.executeSQLQuery.side_effect = RuntimeError(
"Server raised fault: Column (fkdevice) not found in any table"
)
client._service = mock_service
client._connected_at = 0.0
with pytest.raises(RuntimeError, match="devicenumplanmap"):
client.execute_sql_query(
"SELECT name FROM numplan WHERE fkdevice IS NOT NULL"
)
def test_execute_sql_query_passes_through_unrelated_error(self):
from unittest.mock import MagicMock
from mcaxl.client import AxlClient
from mcaxl.cache import AxlCache
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as td:
cache = AxlCache(Path(td) / "test.sqlite", default_ttl=0, cluster_id="test")
client = AxlClient(cache)
mock_service = MagicMock()
mock_service.executeSQLQuery.side_effect = RuntimeError(
"Some unrelated cluster error"
)
client._service = mock_service
client._connected_at = 0.0
# Original exception type + message preserved when no hint applies
with pytest.raises(RuntimeError, match="Some unrelated cluster error"):
client.execute_sql_query("SELECT * FROM device")