mcaxl/tests/test_css_impact.py
Ryan Malloy 9e5c195ce7 Fix issue #1: comprehensive CSS reference coverage (51 new categories)
Closes bingham/mcp-cucm-axl#1

route_devices_using_css missed device.fkcallingsearchspace_cgpntransform
and _cdpntransform — the columns trunks use to attach calling-party and
called-party number transformation CSSs. A CSS only referenced via these
columns showed up as "0 references" in impact analysis, leading an
operator to conclude safe-to-delete and break outbound transformations.

Same failure shape as Hamilton CRITICAL #2 (false-zero impact analysis)
but at a different schema layer: that fix added 7 reference points
covering the obvious cases; this fix closes the rest.

What's covered now (71 fkcallingsearchspace_* columns total across 14
tables in CUCM 15):

  Templates added for the bulk cases:
    _device_query(suffix)      — device.fkcallingsearchspace_<suffix>
    _devicepool_query(suffix)  — devicepool.fkcallingsearchspace_<suffix>
    _numplan_query(suffix)     — numplan.fkcallingsearchspace_<suffix>

  Categories added (51 new):
    11× device variants (incl. _cgpntransform and _cdpntransform — the issue)
    17× devicepool inheritance variants (closes M1 caveat from audit reports)
    13× numplan forwarding/transformation variants (cfbint/cfhr/etc.)
    site, externalcallcontrolprofile, recordingprofile, usageprofile,
    vipre164transformation×2, incomingtransformationprofile×4

Schema gotchas discovered and codified:
  - devicepool, externalcallcontrolprofile, recordingprofile have no
    `description` column (verified against syscolumns 2026-04-26)
  - site has neither `name` nor `description` — uses `tksite` enum joined
    against `typesite.name` for the human-readable form

Live verification on cucm-pub.binghammemorial.org (CUCM 15.0.1.12900-234):
  XFORM-Outbound-ANI:  0 → 1 ref  (PSTN-Router-SIP-Trk via _cgpntransform)
  XFORM-Outbound-DNIS: 0 → 1 ref  (PSTN-Router-SIP-Trk via _cdpntransform)
  E911CSS:             unchanged at 0, but now with `complete: True`
                       — upgrades from "appears orphan with caveat" to
                       "confirmed orphan" since DP variants now covered
  Internal-CSS:        163 → 174 refs (DP + extra numplan variants)

Tests (128 → 134, +6):
  test_issue_1_cgpntransform_column_enumerated
  test_issue_1_cdpntransform_column_enumerated
  test_finds_trunk_via_cgpntransform_reference (mock-driven E2E)
  test_complete_schema_coverage_against_known_columns
    — encodes the 71-column snapshot from CUCM 15. If a future CUCM
      version adds a new fkcallingsearchspace_* column, the test fires
      red so the contributor knows to add it to _CSS_REFERENCE_QUERIES.
  test_no_duplicate_table_column_pairs
    — guards against double-counting if two categories accidentally
      reference the same column.
  test_error_in_multiple_tables_propagates
    — verifies error reporting works across the new shared-suffix cases
      (e.g., _cgpnunknown on both device AND devicepool).
2026-04-26 08:54:58 -06:00

320 lines
14 KiB
Python

"""Hamilton review MAJOR #3: find_devices_using_css must surface partial failures.
The function is per-category resilient by design — if one schema query fails,
the others still produce results. But the top-level summary previously hid
that some categories errored out: `total_returned` and `any_truncated` only
reflected the SUCCESSFUL categories. An LLM consuming "47 references, low
impact" wouldn't know that 5 categories errored and the real number is
likely much higher.
After the fix: the response includes `complete: bool`, `categories_with_errors`,
and `error_categories`, so an LLM (or human auditor) can see the partial-failure
state and act on it.
"""
import pytest
from mcp_cucm_axl.route_plan import find_devices_using_css
class FakeAxlClient:
"""Minimal stand-in for AxlClient that lets us simulate per-query failures.
Returns a fake CSS pkid for the lookup query, then either a single fake row
or an exception based on substring matching.
"""
def __init__(self, error_on_columns: list[str] | None = None):
self.error_on_columns = error_on_columns or []
self.queries: list[str] = []
def execute_sql_query(self, sql: str) -> dict:
self.queries.append(sql)
# The CSS lookup query — return a fake pkid
if "callingsearchspace WHERE name" in sql:
return {"row_count": 1, "rows": [{"pkid": "fake-css-pkid"}]}
# Any query referencing an "error trigger" column → simulate failure
for trigger in self.error_on_columns:
if trigger in sql:
raise RuntimeError(f"simulated cluster failure on {trigger}")
# Otherwise return one fake reference row so the category isn't empty
return {
"row_count": 1,
"rows": [{"name": "FakeRef", "context": "FakePart", "description": "fake"}],
}
def test_no_errors_reports_complete():
"""Baseline: when every category succeeds, complete=True and no error fields populated."""
client = FakeAxlClient()
result = find_devices_using_css(client, "Some-CSS")
assert result["complete"] is True
assert result["categories_with_errors"] == 0
assert result["error_categories"] == []
# And total_returned reflects the successful categories
assert result["total_returned"] >= 1
def test_one_errored_category_marks_incomplete():
"""The audit-trust failure mode: one category errors out and the summary lies.
Fix: complete=False, categories_with_errors >= 1.
"""
client = FakeAxlClient(error_on_columns=["fkcallingsearchspace_cgpnunknown"])
result = find_devices_using_css(client, "Some-CSS")
assert result["complete"] is False, (
"complete must be False when any category errored"
)
assert result["categories_with_errors"] >= 1
assert "device_cgpn_unknown_css" in result["error_categories"]
def test_multiple_errors_all_listed():
"""All errored categories must be enumerated in error_categories.
After issue #1 fix, several column suffixes (`_cgpnunknown`,
`_reroute`) appear on BOTH the device and devicepool tables, so
a single suffix in error_on_columns hits multiple categories.
The test verifies the relevant categories are surfaced.
"""
client = FakeAxlClient(
error_on_columns=[
"fkcallingsearchspace_pilotqueuefull", # huntpilotqueue only
]
)
result = find_devices_using_css(client, "Some-CSS")
assert result["complete"] is False
assert result["categories_with_errors"] >= 1
assert "huntpilot_queue_full_css" in result["error_categories"]
def test_error_in_multiple_tables_propagates():
"""A column suffix shared across tables (e.g., `_cgpnunknown` on both
device AND devicepool) errors out in BOTH categories — both must
appear in error_categories."""
client = FakeAxlClient(
error_on_columns=["fkcallingsearchspace_cgpnunknown"],
)
result = find_devices_using_css(client, "Some-CSS")
assert "device_cgpn_unknown_css" in result["error_categories"]
assert "devicepool_cgpn_unknown_css" in result["error_categories"]
def test_total_returned_does_not_include_error_categories():
"""An errored category contributes 0 to total_returned (correct behavior).
What's NEW: the response also flags that the count is partial.
"""
client = FakeAxlClient(error_on_columns=["fkcallingsearchspace_cgpnunknown"])
result = find_devices_using_css(client, "Some-CSS")
# The count itself is unchanged from before — what's new is the warning
assert result["complete"] is False
# The error category has no rows in references_by_category
err_cat = result["references_by_category"].get("device_cgpn_unknown_css", {})
assert "error" in err_cat
def test_css_not_found_returns_error_not_partial():
"""If the CSS lookup itself fails (CSS doesn't exist), we return the
'not found' error early, NOT a partial-failure response. Distinct
failure modes deserve distinct shapes.
"""
class CssNotFoundClient:
def execute_sql_query(self, sql):
if "callingsearchspace WHERE name" in sql:
return {"row_count": 0, "rows": []}
return {"row_count": 1, "rows": [{}]}
result = find_devices_using_css(CssNotFoundClient(), "Nonexistent-CSS")
assert "error" in result
assert "complete" not in result, (
"CSS-not-found is a hard error; we shouldn't dress it up as partial"
)
# ---- Issue #1 regression tests --------------------------------------------
# https://git.supported.systems/bingham/mcp-cucm-axl/issues/1
#
# Pre-fix: route_devices_using_css missed device.fkcallingsearchspace_cgpntransform
# and device.fkcallingsearchspace_cdpntransform. These are the columns trunks
# use to attach calling-party / called-party transformation CSSs. A CSS only
# referenced via these columns showed up as "0 references" — operator running
# impact analysis would conclude safe-to-delete and break outbound transforms.
from mcp_cucm_axl.route_plan import _CSS_REFERENCE_QUERIES
def test_issue_1_cgpntransform_column_enumerated():
"""The specific column that triggered the issue is in our reference set."""
columns = {
(spec["table"], spec["column"])
for spec in _CSS_REFERENCE_QUERIES.values()
}
assert ("device", "fkcallingsearchspace_cgpntransform") in columns, (
"device.fkcallingsearchspace_cgpntransform must be enumerated; "
"see Gitea issue #1 — false-zero impact analysis on calling-party "
"transformation CSSs (e.g., XFORM-Outbound-ANI)"
)
def test_issue_1_cdpntransform_column_enumerated():
"""The sibling column (called-party transformation) is also enumerated."""
columns = {
(spec["table"], spec["column"])
for spec in _CSS_REFERENCE_QUERIES.values()
}
assert ("device", "fkcallingsearchspace_cdpntransform") in columns, (
"device.fkcallingsearchspace_cdpntransform must be enumerated; "
"same bug pattern as _cgpntransform (issue #1)"
)
def test_finds_trunk_via_cgpntransform_reference():
"""End-to-end: a trunk referencing a CSS via _cgpntransform should
appear in the impact analysis."""
class TrunkRefClient:
"""Returns 1 row only when queried for fkcallingsearchspace_cgpntransform."""
def execute_sql_query(self, sql):
if "callingsearchspace WHERE name" in sql:
return {"row_count": 1, "rows": [{"pkid": "fake-css-pkid"}]}
if "fkcallingsearchspace_cgpntransform" in sql:
return {
"row_count": 1,
"rows": [{
"name": "PSTN-Router-SIP-Trk",
"context": "Trunk",
"description": "the trunk that references this CSS",
}],
}
return {"row_count": 0, "rows": []}
result = find_devices_using_css(TrunkRefClient(), "XFORM-Outbound-ANI")
# Total must be ≥ 1 (the trunk reference), not 0
assert result["total_returned"] >= 1, (
"trunk referenced via _cgpntransform must surface in total_returned"
)
# And the specific category should be populated
cgpn_cat = result["references_by_category"].get("device_cgpn_xform_css")
assert cgpn_cat is not None and cgpn_cat.get("returned_count") == 1, (
f"device_cgpn_xform_css category should have 1 row; "
f"got: {result['references_by_category']}"
)
# ---- Comprehensive coverage --------------------------------------------
# The 71-column snapshot from CUCM 15.0.1.12900-234. If a future CUCM
# version adds a new fkcallingsearchspace_* column, this test fires red
# so the contributor knows to add it to _CSS_REFERENCE_QUERIES.
# Format: (table, column). Sourced from a SELECT against syscolumns
# 2026-04-26. Update when a new CUCM release lands.
_KNOWN_CSS_COLUMNS_FROM_CUCM_15 = frozenset({
# device — primary + 16 variants
("device", "fkcallingsearchspace"),
("device", "fkcallingsearchspace_aar"),
("device", "fkcallingsearchspace_calledintl"),
("device", "fkcallingsearchspace_callednational"),
("device", "fkcallingsearchspace_calledsubscriber"),
("device", "fkcallingsearchspace_calledunknown"),
("device", "fkcallingsearchspace_cdpntransform"),
("device", "fkcallingsearchspace_cgpningressdn"),
("device", "fkcallingsearchspace_cgpnintl"),
("device", "fkcallingsearchspace_cgpnnational"),
("device", "fkcallingsearchspace_cgpnsubscriber"),
("device", "fkcallingsearchspace_cgpntransform"),
("device", "fkcallingsearchspace_cgpnunknown"),
("device", "fkcallingsearchspace_rdntransform"),
("device", "fkcallingsearchspace_refer"),
("device", "fkcallingsearchspace_reroute"),
("device", "fkcallingsearchspace_restrict"),
# devicenumplanmap
("devicenumplanmap", "fkcallingsearchspace_monitoring"),
# devicepool — 16 variants (DP-level inheritance)
("devicepool", "fkcallingsearchspace_aar"),
("devicepool", "fkcallingsearchspace_adjunct"),
("devicepool", "fkcallingsearchspace_autoregistration"),
("devicepool", "fkcallingsearchspace_calledintl"),
("devicepool", "fkcallingsearchspace_callednational"),
("devicepool", "fkcallingsearchspace_calledsubscriber"),
("devicepool", "fkcallingsearchspace_calledunknown"),
("devicepool", "fkcallingsearchspace_cdpntransform"),
("devicepool", "fkcallingsearchspace_cgpningressdn"),
("devicepool", "fkcallingsearchspace_cgpnintl"),
("devicepool", "fkcallingsearchspace_cgpnnational"),
("devicepool", "fkcallingsearchspace_cgpnsubscriber"),
("devicepool", "fkcallingsearchspace_cgpntransform"),
("devicepool", "fkcallingsearchspace_cgpnunknown"),
("devicepool", "fkcallingsearchspace_cntdpntransform"),
("devicepool", "fkcallingsearchspace_mobility"),
("devicepool", "fkcallingsearchspace_rdntransform"),
# External call control + h323 + sipdevice + huntpilotqueue
("externalcallcontrolprofile", "fkcallingsearchspace_diversionrerouting"),
("h323device", "fkcallingsearchspace_cntdpntransform"),
("sipdevice", "fkcallingsearchspace_cntdpntransform"),
("huntpilotqueue", "fkcallingsearchspace_maxwaittime"),
("huntpilotqueue", "fkcallingsearchspace_noagent"),
("huntpilotqueue", "fkcallingsearchspace_pilotqueuefull"),
# incomingtransformationprofile (4)
("incomingtransformationprofile", "fkcallingsearchspace_intl"),
("incomingtransformationprofile", "fkcallingsearchspace_national"),
("incomingtransformationprofile", "fkcallingsearchspace_subscriber"),
("incomingtransformationprofile", "fkcallingsearchspace_unknown"),
# numplan — 18 forwarding/transformation CSSs
("numplan", "fkcallingsearchspace_cfapt"),
("numplan", "fkcallingsearchspace_cfb"),
("numplan", "fkcallingsearchspace_cfbint"),
("numplan", "fkcallingsearchspace_cfhr"),
("numplan", "fkcallingsearchspace_cfhrint"),
("numplan", "fkcallingsearchspace_cfna"),
("numplan", "fkcallingsearchspace_cfnaint"),
("numplan", "fkcallingsearchspace_cfur"),
("numplan", "fkcallingsearchspace_cfurint"),
("numplan", "fkcallingsearchspace_devicefailure"),
("numplan", "fkcallingsearchspace_mwi"),
("numplan", "fkcallingsearchspace_pff"),
("numplan", "fkcallingsearchspace_pffint"),
("numplan", "fkcallingsearchspace_pkmonfwdnoret"),
("numplan", "fkcallingsearchspace_pkmonfwdnoretint"),
("numplan", "fkcallingsearchspace_reroute"),
("numplan", "fkcallingsearchspace_revert"),
("numplan", "fkcallingsearchspace_sharedlineappear"),
("numplan", "fkcallingsearchspace_translation"),
# Profile tables + simple primary fkcallingsearchspace
("recordingprofile", "fkcallingsearchspace_callrecording"),
("routelist", "fkcallingsearchspace"),
("site", "fkcallingsearchspace"),
("usageprofile", "fkcallingsearchspace_blocking"),
("vipre164transformation", "fkcallingsearchspace_outgoingcdpntranf"),
("vipre164transformation", "fkcallingsearchspace_outgoingcgpntranf"),
("voicemessagingpilot", "fkcallingsearchspace"),
})
def test_complete_schema_coverage_against_known_columns():
"""If CUCM adds a new column type or we missed one, this test surfaces it.
Counts: 71 columns total in the CUCM 15.0.1.12900-234 snapshot.
"""
actual = {
(spec["table"], spec["column"])
for spec in _CSS_REFERENCE_QUERIES.values()
}
missing = _KNOWN_CSS_COLUMNS_FROM_CUCM_15 - actual
assert not missing, (
f"_CSS_REFERENCE_QUERIES is missing {len(missing)} known columns:\n"
+ "\n".join(f" {t}.{c}" for t, c in sorted(missing))
)
def test_no_duplicate_table_column_pairs():
"""Each (table, column) pair should map to exactly one category label.
Two categories pointing at the same column would double-count references."""
seen: dict[tuple, list[str]] = {}
for label, spec in _CSS_REFERENCE_QUERIES.items():
key = (spec["table"], spec["column"])
seen.setdefault(key, []).append(label)
duplicates = {k: v for k, v in seen.items() if len(v) > 1}
assert not duplicates, (
f"duplicate (table, column) pairs would double-count:\n"
+ "\n".join(f" {k}: {v}" for k, v in duplicates.items())
)