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).
This commit is contained in:
Ryan Malloy 2026-04-26 08:54:58 -06:00
parent 8815db06d8
commit 9e5c195ce7
2 changed files with 430 additions and 13 deletions

View File

@ -430,9 +430,74 @@ def list_device_pool_route_groups(
# CSS impact analysis: which devices/lines/patterns reference this CSS # CSS impact analysis: which devices/lines/patterns reference this CSS
# ==================================================================== # ====================================================================
# CSS reference points: for each, the SQL is hand-written because the # CSS reference points: each entry maps a category label to a (table,
# identifier column varies per table. Each entry returns rows with a # column, sql) spec. The SQL returns rows with a common shape: `name`
# common shape: name, context (e.g. partition), table, column. # and `context` (partition for patterns, device class for devices, etc.),
# plus a `description` where the source table has one.
#
# Issue #1: previously we only enumerated a handful of device columns;
# trunks referencing CSSs via `fkcallingsearchspace_cgpntransform` and
# `_cdpntransform` produced false-zero impact analysis. The set below
# covers all 71 known fkcallingsearchspace_* columns from the CUCM 15
# schema; tests/test_css_impact.py:test_complete_schema_coverage_against_known_columns
# fires red if a future CUCM version adds a column we haven't added.
#
# Templates for the common cases follow; the dict is built from them.
def _device_query(suffix: str) -> dict:
"""Reference query for a `fkcallingsearchspace[_<suffix>]` column on `device`."""
col = "fkcallingsearchspace" if not suffix else f"fkcallingsearchspace_{suffix}"
return {
"table": "device",
"column": col,
"sql": f"""
SELECT d.name AS name, tc.name AS context, d.description AS description
FROM device d LEFT OUTER JOIN typeclass tc ON d.tkclass = tc.enum
WHERE d.{col} = '{{pkid}}'
""",
}
def _devicepool_query(suffix: str) -> dict:
"""Reference query for a fkcallingsearchspace_<suffix> column on devicepool.
Phones / devices in a DP inherit these CSSs unless overridden. Audit-
relevant: a CSS only assigned via DP inheritance was previously
invisible (this is the gap the M1 caveat in today's audit reports
explicitly called out, now closed).
Note: `devicepool` has no `description` column (verified against CUCM
15 schema 2026-04-26); the query selects only `name`.
"""
col = f"fkcallingsearchspace_{suffix}"
return {
"table": "devicepool",
"column": col,
"sql": f"""
SELECT name FROM devicepool WHERE {col} = '{{pkid}}'
""",
}
def _numplan_query(suffix: str) -> dict:
"""Reference query for a fkcallingsearchspace_<suffix> column on numplan.
These are the line-level forwarding CSSs (CFA/CFB/CFNA/CFUR and their
internal/personal variants), MWI, translation, etc. Hits one row per
DN that has the CSS configured for that scenario.
"""
col = f"fkcallingsearchspace_{suffix}"
return {
"table": "numplan",
"column": col,
"sql": f"""
SELECT np.dnorpattern AS name, rp.name AS context, np.description AS description
FROM numplan np LEFT OUTER JOIN routepartition rp ON np.fkroutepartition = rp.pkid
WHERE np.{col} = '{{pkid}}'
""",
}
_CSS_REFERENCE_QUERIES: dict[str, dict] = { _CSS_REFERENCE_QUERIES: dict[str, dict] = {
# Line-level forwarding CSSs (call-forward variants on a DN) # Line-level forwarding CSSs (call-forward variants on a DN)
"line_call_forward_all_css": { "line_call_forward_all_css": {
@ -619,6 +684,158 @@ _CSS_REFERENCE_QUERIES: dict[str, dict] = {
WHERE hpq.fkcallingsearchspace_pilotqueuefull = '{pkid}' WHERE hpq.fkcallingsearchspace_pilotqueuefull = '{pkid}'
""", """,
}, },
# ----------------------------------------------------------------
# Issue #1 fix: comprehensive coverage of all fkcallingsearchspace_*
# columns from the CUCM 15 schema. Categories below use the
# _device_query / _devicepool_query / _numplan_query helpers above
# for the bulk cases; the remaining tables use hand-written SQL.
# ----------------------------------------------------------------
# device — remaining 11 variants beyond the 6 already covered above
"device_aar_css": _device_query("aar"),
"device_called_intl_css": _device_query("calledintl"),
"device_called_national_css": _device_query("callednational"),
"device_called_subscriber_css": _device_query("calledsubscriber"),
"device_called_unknown_css": _device_query("calledunknown"),
"device_cdpn_xform_css": _device_query("cdpntransform"), # issue #1
"device_cgpn_ingress_dn_css": _device_query("cgpningressdn"),
"device_cgpn_intl_css": _device_query("cgpnintl"),
"device_cgpn_national_css": _device_query("cgpnnational"),
"device_cgpn_subscriber_css": _device_query("cgpnsubscriber"),
"device_cgpn_xform_css": _device_query("cgpntransform"), # issue #1
# devicepool — 17 variants. Phones inherit these CSSs from their DP
# unless overridden. Important: a CSS only assigned via DP inheritance
# was previously invisible (the M1 caveat in today's audit reports).
"devicepool_aar_css": _devicepool_query("aar"),
"devicepool_adjunct_css": _devicepool_query("adjunct"),
"devicepool_autoregistration_css": _devicepool_query("autoregistration"),
"devicepool_called_intl_css": _devicepool_query("calledintl"),
"devicepool_called_national_css": _devicepool_query("callednational"),
"devicepool_called_subscriber_css": _devicepool_query("calledsubscriber"),
"devicepool_called_unknown_css": _devicepool_query("calledunknown"),
"devicepool_cdpn_xform_css": _devicepool_query("cdpntransform"),
"devicepool_cgpn_ingress_dn_css": _devicepool_query("cgpningressdn"),
"devicepool_cgpn_intl_css": _devicepool_query("cgpnintl"),
"devicepool_cgpn_national_css": _devicepool_query("cgpnnational"),
"devicepool_cgpn_subscriber_css": _devicepool_query("cgpnsubscriber"),
"devicepool_cgpn_xform_css": _devicepool_query("cgpntransform"),
"devicepool_cgpn_unknown_css": _devicepool_query("cgpnunknown"),
"devicepool_cntdpn_xform_css": _devicepool_query("cntdpntransform"),
"devicepool_mobility_css": _devicepool_query("mobility"),
"devicepool_rdn_transform_css": _devicepool_query("rdntransform"),
# numplan — remaining 13 forwarding/transformation variants
"line_call_forward_busy_int_css": _numplan_query("cfbint"),
"line_call_forward_hr_css": _numplan_query("cfhr"),
"line_call_forward_hr_int_css": _numplan_query("cfhrint"),
"line_call_forward_no_answer_int_css": _numplan_query("cfnaint"),
"line_call_forward_unregistered_int_css": _numplan_query("cfurint"),
"line_device_failure_css": _numplan_query("devicefailure"),
"line_mwi_css": _numplan_query("mwi"),
"line_personal_call_forward_css": _numplan_query("pff"),
"line_personal_call_forward_int_css": _numplan_query("pffint"),
"line_park_monitor_fwd_no_retrieve_css": _numplan_query("pkmonfwdnoret"),
"line_park_monitor_fwd_no_retrieve_int_css": _numplan_query("pkmonfwdnoretint"),
"line_reroute_css": _numplan_query("reroute"),
"line_revert_css": _numplan_query("revert"),
# site — single primary CSS for site-level (SAF) call routing.
# `site` has no name column; the human label comes from typesite.
"site_css": {
"table": "site", "column": "fkcallingsearchspace",
"sql": """
SELECT ts.name AS name, dp.name AS context, '' AS description
FROM site s
LEFT OUTER JOIN typesite ts ON s.tksite = ts.enum
LEFT OUTER JOIN devicepool dp ON s.fkdevicepool = dp.pkid
WHERE s.fkcallingsearchspace = '{pkid}'
""",
},
# External call control profile — diversion/rerouting CSS
# (`externalcallcontrolprofile` has no description column)
"external_call_control_diversion_css": {
"table": "externalcallcontrolprofile",
"column": "fkcallingsearchspace_diversionrerouting",
"sql": """
SELECT name FROM externalcallcontrolprofile
WHERE fkcallingsearchspace_diversionrerouting = '{pkid}'
""",
},
# Recording profile — call recording CSS
# (`recordingprofile` has no description column)
"recording_call_recording_css": {
"table": "recordingprofile",
"column": "fkcallingsearchspace_callrecording",
"sql": """
SELECT name FROM recordingprofile
WHERE fkcallingsearchspace_callrecording = '{pkid}'
""",
},
# Usage profile — blocking CSS
"usage_blocking_css": {
"table": "usageprofile", "column": "fkcallingsearchspace_blocking",
"sql": """
SELECT name, description FROM usageprofile
WHERE fkcallingsearchspace_blocking = '{pkid}'
""",
},
# VIPR E.164 transformation profiles
"vipre164_outgoing_cdpn_xform_css": {
"table": "vipre164transformation",
"column": "fkcallingsearchspace_outgoingcdpntranf",
"sql": """
SELECT name, description FROM vipre164transformation
WHERE fkcallingsearchspace_outgoingcdpntranf = '{pkid}'
""",
},
"vipre164_outgoing_cgpn_xform_css": {
"table": "vipre164transformation",
"column": "fkcallingsearchspace_outgoingcgpntranf",
"sql": """
SELECT name, description FROM vipre164transformation
WHERE fkcallingsearchspace_outgoingcgpntranf = '{pkid}'
""",
},
# Incoming transformation profile — 4 number-type variants
"incoming_xform_intl_css": {
"table": "incomingtransformationprofile",
"column": "fkcallingsearchspace_intl",
"sql": """
SELECT name, description FROM incomingtransformationprofile
WHERE fkcallingsearchspace_intl = '{pkid}'
""",
},
"incoming_xform_national_css": {
"table": "incomingtransformationprofile",
"column": "fkcallingsearchspace_national",
"sql": """
SELECT name, description FROM incomingtransformationprofile
WHERE fkcallingsearchspace_national = '{pkid}'
""",
},
"incoming_xform_subscriber_css": {
"table": "incomingtransformationprofile",
"column": "fkcallingsearchspace_subscriber",
"sql": """
SELECT name, description FROM incomingtransformationprofile
WHERE fkcallingsearchspace_subscriber = '{pkid}'
""",
},
"incoming_xform_unknown_css": {
"table": "incomingtransformationprofile",
"column": "fkcallingsearchspace_unknown",
"sql": """
SELECT name, description FROM incomingtransformationprofile
WHERE fkcallingsearchspace_unknown = '{pkid}'
""",
},
} }

View File

@ -69,22 +69,34 @@ def test_one_errored_category_marks_incomplete():
def test_multiple_errors_all_listed(): def test_multiple_errors_all_listed():
"""All errored categories must be enumerated in error_categories.""" """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( client = FakeAxlClient(
error_on_columns=[ error_on_columns=[
"fkcallingsearchspace_cgpnunknown", "fkcallingsearchspace_pilotqueuefull", # huntpilotqueue only
"fkcallingsearchspace_reroute",
"fkcallingsearchspace_pilotqueuefull",
] ]
) )
result = find_devices_using_css(client, "Some-CSS") result = find_devices_using_css(client, "Some-CSS")
assert result["complete"] is False assert result["complete"] is False
assert result["categories_with_errors"] == 3 assert result["categories_with_errors"] >= 1
assert set(result["error_categories"]) == { assert "huntpilot_queue_full_css" in result["error_categories"]
"device_cgpn_unknown_css",
"device_reroute_css",
"huntpilot_queue_full_css", 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(): def test_total_returned_does_not_include_error_categories():
@ -117,3 +129,191 @@ def test_css_not_found_returns_error_not_partial():
assert "complete" not in result, ( assert "complete" not in result, (
"CSS-not-found is a hard error; we shouldn't dress it up as partial" "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())
)