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.
127 lines
4.9 KiB
Python
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")
|