The aliases.py public surface had 18 tests covering happy-paths,
audit discipline, domain isolation, lowercase normalization, and
expand-query semantics. Three direct gaps:
- list_term_aliases (no test at all): filter-by-domain, filter-by-
term-substring, unreachable-db fail-closed, missing-table fail-
closed
- _tokenize_fts_query (only via expand-query smoke): preserve
quoted phrases, parentheses-as-tokens (OR-expansion contract),
unterminated-quote fallback
- _quote_for_fts + _match_quoting (no direct test): pass-through
quoted, defensive quote-multi-word, quoted-reference symmetry
10 new tests; aliases.py test count 18 → 28. Same fixture pattern
as the existing tests (sqlite tmp DB with SCHEMA_SQL applied).
375 lines
12 KiB
Python
375 lines
12 KiB
Python
"""Tests for the citation + term alias mechanisms (#000041 + #000042).
|
|
|
|
Pure unit + small-DB tests over the helpers in arborist/qa/aliases.py.
|
|
The end-to-end resolver-uses-alias path is exercised separately via
|
|
the `arborist warrant-resolve --use-aliases` CLI on real shards.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from arborist.qa.aliases import (
|
|
add_citation_alias,
|
|
add_term_alias,
|
|
domain_for_pillar,
|
|
expand_query_with_term_aliases,
|
|
list_citation_aliases,
|
|
list_term_aliases,
|
|
lookup_citation_aliases,
|
|
lookup_term_aliases,
|
|
remove_citation_alias,
|
|
remove_term_alias,
|
|
)
|
|
from arborist.store import SCHEMA_SQL
|
|
|
|
|
|
@pytest.fixture
|
|
def db(tmp_path):
|
|
"""Fresh SQLite DB with the alias schema applied."""
|
|
p = tmp_path / "aliases.db"
|
|
conn = sqlite3.connect(str(p))
|
|
try:
|
|
conn.executescript(SCHEMA_SQL)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return str(p)
|
|
|
|
|
|
# --- pillar domain mapping -------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"pillar,expected",
|
|
[
|
|
("I", "logic"),
|
|
("IV", "geometry"),
|
|
("VII", "combinatorics"),
|
|
("IX", "lambda-calculus"),
|
|
("UNKNOWN", ""),
|
|
("", ""),
|
|
],
|
|
)
|
|
def test_domain_for_pillar(pillar, expected):
|
|
assert domain_for_pillar(pillar) == expected
|
|
|
|
|
|
# --- citation aliases -----------------------------------------------
|
|
|
|
|
|
def test_citation_alias_add_lookup_remove(db):
|
|
inserted = add_citation_alias(
|
|
db,
|
|
original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
|
|
substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
|
|
substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
|
|
substitute_title="Principles of Mathematical Logic",
|
|
decision_by="fox 2026-05-09",
|
|
decision_rationale="Hilbert-Ackermann 1928 is PD; covers same propositional axioms",
|
|
)
|
|
assert inserted is True
|
|
|
|
aliases = lookup_citation_aliases(
|
|
db, "Introduction to Mathematical Logic by Elliott Mendelson"
|
|
)
|
|
assert len(aliases) == 1
|
|
a = aliases[0]
|
|
assert a.substitute_title == "Principles of Mathematical Logic"
|
|
assert "David Hilbert" in a.substitute_authors
|
|
|
|
# Idempotent — second add returns False.
|
|
inserted_again = add_citation_alias(
|
|
db,
|
|
original_ref="Introduction to Mathematical Logic by Elliott Mendelson",
|
|
substitute_ref="Principles of Mathematical Logic by Hilbert and Ackermann",
|
|
substitute_authors=["David Hilbert", "Wilhelm Ackermann"],
|
|
substitute_title="Principles of Mathematical Logic",
|
|
decision_by="fox 2026-05-09",
|
|
)
|
|
assert inserted_again is False
|
|
|
|
# Remove + verify gone.
|
|
removed = remove_citation_alias(
|
|
db,
|
|
"Introduction to Mathematical Logic by Elliott Mendelson",
|
|
"Principles of Mathematical Logic by Hilbert and Ackermann",
|
|
)
|
|
assert removed is True
|
|
assert lookup_citation_aliases(
|
|
db, "Introduction to Mathematical Logic by Elliott Mendelson"
|
|
) == []
|
|
|
|
|
|
def test_citation_alias_audit_discipline(db):
|
|
"""Empty `decision_by` raises — fail-closed audit."""
|
|
with pytest.raises(ValueError, match="decision_by must be non-empty"):
|
|
add_citation_alias(
|
|
db,
|
|
original_ref="A",
|
|
substitute_ref="B",
|
|
substitute_authors=[],
|
|
substitute_title="",
|
|
decision_by="",
|
|
)
|
|
|
|
|
|
def test_citation_alias_list_filter(db):
|
|
add_citation_alias(
|
|
db,
|
|
original_ref="Mendelson 1997",
|
|
substitute_ref="HA 1928",
|
|
substitute_authors=["Hilbert"],
|
|
substitute_title="Principles",
|
|
decision_by="fox",
|
|
)
|
|
add_citation_alias(
|
|
db,
|
|
original_ref="Stanley §1.2",
|
|
substitute_ref="Brualdi §3.5",
|
|
substitute_authors=["Brualdi"],
|
|
substitute_title="Combinatorics",
|
|
decision_by="fox",
|
|
)
|
|
all_a = list_citation_aliases(db)
|
|
assert len(all_a) == 2
|
|
filtered = list_citation_aliases(db, original_filter="Mendelson")
|
|
assert len(filtered) == 1
|
|
assert filtered[0].original_ref == "Mendelson 1997"
|
|
|
|
|
|
# --- term aliases ----------------------------------------------------
|
|
|
|
|
|
def test_term_alias_add_lookup(db):
|
|
inserted = add_term_alias(
|
|
db,
|
|
term="incidence",
|
|
alternate_term="connection",
|
|
domain="geometry",
|
|
decision_by="fox 2026-05-09",
|
|
decision_rationale="Hilbert 1902 Townsend uses 'connection'",
|
|
)
|
|
assert inserted is True
|
|
|
|
# Forward lookup
|
|
alts = lookup_term_aliases(db, "incidence", "geometry")
|
|
assert "connection" in alts
|
|
|
|
# Bidirectional — registered (incidence, connection) also returns
|
|
# "incidence" when looking up by "connection".
|
|
reverse = lookup_term_aliases(db, "connection", "geometry")
|
|
assert "incidence" in reverse
|
|
|
|
|
|
def test_term_alias_domain_isolation(db):
|
|
"""Same term in different domains stay distinct."""
|
|
add_term_alias(
|
|
db, term="set", alternate_term="class",
|
|
domain="set-theory", decision_by="fox",
|
|
)
|
|
add_term_alias(
|
|
db, term="set", alternate_term="ensemble",
|
|
domain="combinatorics", decision_by="fox",
|
|
)
|
|
assert lookup_term_aliases(db, "set", "set-theory") == ["class"]
|
|
assert lookup_term_aliases(db, "set", "combinatorics") == ["ensemble"]
|
|
|
|
|
|
def test_term_alias_lowercase_normalization(db):
|
|
"""Term + alternate + domain stored lowercased; lookup is
|
|
case-insensitive."""
|
|
add_term_alias(
|
|
db, term="Incidence", alternate_term="Connection",
|
|
domain="GEOMETRY", decision_by="fox",
|
|
)
|
|
assert lookup_term_aliases(db, "incidence", "geometry") == ["connection"]
|
|
assert lookup_term_aliases(db, "INCIDENCE", "Geometry") == ["connection"]
|
|
|
|
|
|
def test_term_alias_remove(db):
|
|
add_term_alias(
|
|
db, term="x", alternate_term="y",
|
|
domain="z", decision_by="fox",
|
|
)
|
|
removed = remove_term_alias(db, "x", "y", "z")
|
|
assert removed is True
|
|
assert lookup_term_aliases(db, "x", "z") == []
|
|
|
|
|
|
def test_term_alias_audit_discipline(db):
|
|
"""Empty fields raise — fail-closed."""
|
|
with pytest.raises(ValueError, match="decision_by must be non-empty"):
|
|
add_term_alias(
|
|
db, term="a", alternate_term="b",
|
|
domain="c", decision_by="",
|
|
)
|
|
with pytest.raises(ValueError, match="domain must be non-empty"):
|
|
add_term_alias(
|
|
db, term="a", alternate_term="b",
|
|
domain="", decision_by="fox",
|
|
)
|
|
|
|
|
|
# --- query expansion -------------------------------------------------
|
|
|
|
|
|
def test_expand_query_with_term_aliases_basic(db):
|
|
add_term_alias(
|
|
db, term="incidence", alternate_term="connection",
|
|
domain="geometry", decision_by="fox",
|
|
)
|
|
expanded = expand_query_with_term_aliases(
|
|
"incidence axiom", "geometry", db
|
|
)
|
|
assert "(incidence OR connection)" in expanded
|
|
# Other tokens pass through.
|
|
assert "axiom" in expanded
|
|
|
|
|
|
def test_expand_query_preserves_phrase_syntax(db):
|
|
add_term_alias(
|
|
db, term="line incidence", alternate_term="line connection",
|
|
domain="geometry", decision_by="fox",
|
|
)
|
|
expanded = expand_query_with_term_aliases(
|
|
'"line incidence"', "geometry", db
|
|
)
|
|
assert '"line incidence"' in expanded
|
|
assert '"line connection"' in expanded
|
|
assert " OR " in expanded
|
|
|
|
|
|
def test_expand_query_no_aliases_passes_through(db):
|
|
"""No registered aliases → query unchanged."""
|
|
q = "modus ponens"
|
|
expanded = expand_query_with_term_aliases(q, "logic", db)
|
|
assert expanded == q
|
|
|
|
|
|
def test_expand_query_unreachable_db_returns_original():
|
|
"""Bad DB path → query unchanged (fail-closed; aliases are opt-in)."""
|
|
expanded = expand_query_with_term_aliases(
|
|
"incidence", "geometry", "/nonexistent/path/aliases.db"
|
|
)
|
|
assert expanded == "incidence"
|
|
|
|
|
|
# --- list_term_aliases (gap-fill 2026-05-10) -------------------------
|
|
|
|
|
|
def test_list_term_aliases_filters_by_domain(db):
|
|
"""`list_term_aliases(domain=X)` returns only rows in that
|
|
domain. Same pattern as list_citation_aliases filter."""
|
|
add_term_alias(
|
|
db, term="set", alternate_term="class",
|
|
domain="set-theory", decision_by="fox",
|
|
)
|
|
add_term_alias(
|
|
db, term="line", alternate_term="ray",
|
|
domain="geometry", decision_by="fox",
|
|
)
|
|
add_term_alias(
|
|
db, term="incidence", alternate_term="connection",
|
|
domain="geometry", decision_by="fox",
|
|
)
|
|
from arborist.qa.aliases import list_term_aliases
|
|
|
|
geom = list_term_aliases(db, domain="geometry")
|
|
assert len(geom) == 2
|
|
assert all(a.domain == "geometry" for a in geom)
|
|
set_th = list_term_aliases(db, domain="set-theory")
|
|
assert len(set_th) == 1
|
|
assert set_th[0].term == "set"
|
|
|
|
|
|
def test_list_term_aliases_filters_by_term_substring(db):
|
|
"""`term_filter` is a substring match (LIKE %X%) on term."""
|
|
add_term_alias(db, term="incidence", alternate_term="connection",
|
|
domain="geometry", decision_by="fox")
|
|
add_term_alias(db, term="line incidence", alternate_term="line connection",
|
|
domain="geometry", decision_by="fox")
|
|
add_term_alias(db, term="parallel", alternate_term="parallels",
|
|
domain="geometry", decision_by="fox")
|
|
from arborist.qa.aliases import list_term_aliases
|
|
|
|
inc = list_term_aliases(db, term_filter="incidence")
|
|
assert len(inc) == 2
|
|
assert all("incidence" in a.term for a in inc)
|
|
|
|
|
|
def test_list_term_aliases_unreachable_db_returns_empty():
|
|
"""Same fail-closed contract as expand_query_with_term_aliases:
|
|
nonexistent DB → empty list, no exception."""
|
|
from arborist.qa.aliases import list_term_aliases
|
|
|
|
assert list_term_aliases("/nonexistent/path/aliases.db") == []
|
|
|
|
|
|
def test_list_term_aliases_handles_missing_table(tmp_path):
|
|
"""A SQLite file without the term_aliases table → empty list,
|
|
not an OperationalError."""
|
|
import sqlite3
|
|
|
|
bad_db = tmp_path / "no-schema.db"
|
|
sqlite3.connect(str(bad_db)).execute("CREATE TABLE foo (x TEXT)").connection.close()
|
|
from arborist.qa.aliases import list_term_aliases
|
|
|
|
assert list_term_aliases(str(bad_db)) == []
|
|
|
|
|
|
# --- internal helper coverage (small surfaces, easy regression risk) -
|
|
|
|
|
|
def test_tokenize_fts_query_preserves_quoted_phrase():
|
|
from arborist.qa.aliases import _tokenize_fts_query
|
|
|
|
assert _tokenize_fts_query('"line incidence"') == ['"line incidence"']
|
|
assert _tokenize_fts_query('foo "bar baz" qux') == [
|
|
"foo", '"bar baz"', "qux"
|
|
]
|
|
|
|
|
|
def test_tokenize_fts_query_handles_parentheses_as_tokens():
|
|
"""OR-expansion produces queries like `(a OR b)`; the tokenizer
|
|
must preserve `(` and `)` as standalone tokens (round-trip)."""
|
|
from arborist.qa.aliases import _tokenize_fts_query
|
|
|
|
assert _tokenize_fts_query("(a OR b)") == ["(", "a", "OR", "b", ")"]
|
|
|
|
|
|
def test_tokenize_fts_query_unterminated_quote_falls_back():
|
|
"""A user query with a stray opening quote shouldn't lose data —
|
|
the unmatched-quote tail becomes one token."""
|
|
from arborist.qa.aliases import _tokenize_fts_query
|
|
|
|
out = _tokenize_fts_query('foo "bar baz')
|
|
assert out[0] == "foo"
|
|
assert "bar baz" in out[-1]
|
|
|
|
|
|
def test_quote_for_fts_passes_through_already_quoted():
|
|
from arborist.qa.aliases import _quote_for_fts
|
|
|
|
assert _quote_for_fts('"line incidence"') == '"line incidence"'
|
|
assert _quote_for_fts("incidence") == "incidence"
|
|
|
|
|
|
def test_quote_for_fts_quotes_multiword_bare_token():
|
|
"""Defense-in-depth: a bare token that contains a space gets
|
|
quoted (shouldn't happen via _tokenize_fts_query but the helper
|
|
is defensive)."""
|
|
from arborist.qa.aliases import _quote_for_fts
|
|
|
|
assert _quote_for_fts("two words") == '"two words"'
|
|
|
|
|
|
def test_match_quoting_quoted_reference():
|
|
from arborist.qa.aliases import _match_quoting
|
|
|
|
# Quoted reference → quoted alt.
|
|
assert _match_quoting('"line incidence"', "line connection") == '"line connection"'
|
|
# Bare reference → bare alt.
|
|
assert _match_quoting("incidence", "connection") == "connection"
|