diff --git a/src/mcaxl/route_plan.py b/src/mcaxl/route_plan.py index e11f6d3..ab284b9 100644 --- a/src/mcaxl/route_plan.py +++ b/src/mcaxl/route_plan.py @@ -1734,22 +1734,7 @@ def translation_chain(client: "AxlClient", number: str, css_name: str | None = N """ # Pull every pattern in scope; we filter in Python with wildcard logic. - sql = f""" - SELECT - np.dnorpattern AS pattern, - tpu.name AS pattern_type, - rp.name AS partition_name, - np.callingpartytransformationmask AS calling_party_xform_mask, - np.calledpartytransformationmask AS called_party_xform_mask, - np.prefixdigitsout AS prefix_digits_out, - ddi.name AS digit_discard_instructions, - rf.name AS route_filter, - np.description AS description - FROM numplan np - LEFT OUTER JOIN routepartition rp ON np.fkroutepartition = rp.pkid - LEFT OUTER JOIN typepatternusage tpu ON np.tkpatternusage = tpu.enum - LEFT OUTER JOIN digitdiscardinstruction ddi ON np.fkdigitdiscardinstruction = ddi.pkid - LEFT OUTER JOIN routefilter rf ON np.fkroutefilter = rf.pkid + # # tkpatternusage candidates included in the match scan: # 2 = Device (DN) — directly-dialable directory numbers # 3 = Translation — translation patterns @@ -1766,6 +1751,22 @@ def translation_chain(client: "AxlClient", number: str, css_name: str | None = N # was the cause of the false-positive: candidates_evaluated stayed # 23 vs 26 numplan rows in the reachable partition, with the 3-row # gap being exactly the 3 Device DNs. + sql = f""" + SELECT + np.dnorpattern AS pattern, + tpu.name AS pattern_type, + rp.name AS partition_name, + np.callingpartytransformationmask AS calling_party_xform_mask, + np.calledpartytransformationmask AS called_party_xform_mask, + np.prefixdigitsout AS prefix_digits_out, + ddi.name AS digit_discard_instructions, + rf.name AS route_filter, + np.description AS description + FROM numplan np + LEFT OUTER JOIN routepartition rp ON np.fkroutepartition = rp.pkid + LEFT OUTER JOIN typepatternusage tpu ON np.tkpatternusage = tpu.enum + LEFT OUTER JOIN digitdiscardinstruction ddi ON np.fkdigitdiscardinstruction = ddi.pkid + LEFT OUTER JOIN routefilter rf ON np.fkroutefilter = rf.pkid WHERE np.tkpatternusage IN (2, 3, 5, 7) AND np.dnorpattern IS NOT NULL {css_filter} diff --git a/tests/test_cti_failsafe_reachability.py b/tests/test_cti_failsafe_reachability.py index f51eced..27a2636 100644 --- a/tests/test_cti_failsafe_reachability.py +++ b/tests/test_cti_failsafe_reachability.py @@ -466,6 +466,39 @@ class TestDeviceDnInTranslationChainCandidates: "empirical proof." ) + def test_no_python_comment_chars_leak_into_sql(self): + """Sentinel — a `#` in any captured query is almost certainly a + Python comment that escaped its f-string. Informix doesn't use + `#` for comments (it uses `--` and `/* */`); CUCM's data + dictionary doesn't use `#` in table or column names either. + + Caught a real regression in 2026-05-09 cti-audit-prompts + thread: a Python explanatory comment was placed *inside* the + translation_chain f-string (after the JOIN clauses, before + WHERE). Informix returned "A syntax error has occurred" on + every live call. Offline tests passed because the FakeAxlClient + dispatched on substring matches and didn't parse the SQL. + + This test wouldn't have caught the original bug if it predated + the fix (the FakeAxlClient still wouldn't fail) — but adding + it now means a future contributor can't reintroduce the same + class of mistake in any cti_failsafe_reachability call path. + """ + client = FakeAxlClient(cti_rp_rows=[ + _cti_row("Test-RP", "Generic", cfna="912", cfna_css="SomeCSS"), + ]) + cti_failsafe_reachability(client) + for q in client.queries: + # Allow `#` in column-comment-like positions in SELECT lists? + # No — CUCM's data dictionary has no such columns. A `#` + # anywhere in any query my tool emits is a defect. + assert "#" not in q, ( + f"Python `#` character leaked into SQL — likely a Python " + f"comment inside an f-string. Informix will reject this " + f"with 'A syntax error has occurred'. Offending query:\n" + f"{q[:200]}..." + ) + def test_cti_rp_to_cti_rp_failsafe_does_not_false_positive(self): """The motivating Bingham case: 911-CTI-RP CFNA → 912 (Device DN of 912-CTI-RP) under 911CER-CSS reaching 911CER-PT.