feat(vocabulary): backend for native tags and categories (#241)
* feat(tags): keep unused tags in the shared vocabulary * feat(vocabulary): add bounded tag and category reads * feat(vocabulary): guard tag create, rename and merge * feat(vocabulary): preview and apply category consolidation * refactor(vocabulary): share canonical token hashing * test(vocabulary): prove retained tags survive restore * test(vocabulary): cover category paging tie-break and clarify contract docs * test(vocabulary): assert rejected tag creations write nothing
This commit is contained in:
@@ -181,6 +181,46 @@ class TestTagSuggestions(DatabaseTestCase):
|
||||
[("asian", 3), ("portrait", 3), ("dog", 2), ("smile", 1), ("snow_day", 1)],
|
||||
)
|
||||
|
||||
def _add_unused(self, *names):
|
||||
with self.db.model.get_connection() as conn:
|
||||
for name in names:
|
||||
conn.execute("INSERT INTO tags(name) VALUES(?)", (name,))
|
||||
|
||||
def test_no_context_appends_unused_names_at_zero_after_used(self):
|
||||
self._add_unused("zz_unused", "asian_unused")
|
||||
used = [
|
||||
("asian", 3),
|
||||
("portrait", 3),
|
||||
("dog", 2),
|
||||
("smile", 1),
|
||||
("snow_day", 1),
|
||||
]
|
||||
self.assertEqual(
|
||||
self._names("", []),
|
||||
used + [("asian_unused", 0), ("zz_unused", 0)],
|
||||
)
|
||||
self.assertEqual(self._names("asian", []), [("asian", 3), ("asian_unused", 0)])
|
||||
|
||||
def test_context_suggestions_never_include_unused_names(self):
|
||||
self._add_unused("zz_unused", "asian_unused")
|
||||
self.assertEqual(
|
||||
self._names("", ["asian"]), [("portrait", 2), ("dog", 1), ("smile", 1)]
|
||||
)
|
||||
|
||||
def test_limit_counts_unused_rows_last(self):
|
||||
self._add_unused("zz_unused", "asian_unused")
|
||||
self.assertEqual(
|
||||
self._names("", [], limit=6),
|
||||
[
|
||||
("asian", 3),
|
||||
("portrait", 3),
|
||||
("dog", 2),
|
||||
("smile", 1),
|
||||
("snow_day", 1),
|
||||
("asian_unused", 0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_prefix_is_case_insensitive_and_anchored_at_the_start(self):
|
||||
self.assertEqual(self._names("P", []), [("portrait", 3)])
|
||||
self.assertEqual(self._names("s", []), [("smile", 1), ("snow_day", 1)])
|
||||
@@ -203,6 +243,24 @@ class TestTagSuggestions(DatabaseTestCase):
|
||||
self.assertEqual(self._names("snow_", []), [("snow_day", 1)])
|
||||
self.assertEqual(self._names("snow%", []), [])
|
||||
|
||||
def test_like_wildcards_never_match_unused_names(self):
|
||||
self._add_unused("snowXday", "snowfall", "plain")
|
||||
self.assertEqual(self._names("_", []), [])
|
||||
self.assertEqual(self._names("%", []), [])
|
||||
self.assertEqual(self._names("snow_", []), [("snow_day", 1)])
|
||||
self.assertEqual(self._names("snow%", []), [])
|
||||
self.assertEqual(
|
||||
self._names("snow", []),
|
||||
[("snow_day", 1), ("snowfall", 0), ("snowXday", 0)],
|
||||
)
|
||||
|
||||
def test_context_limit_ignores_unused_rows(self):
|
||||
self._add_unused("aaa_unused", "portrait_unused")
|
||||
self.assertEqual(
|
||||
self._names("", ["asian"], limit=2), [("portrait", 2), ("dog", 1)]
|
||||
)
|
||||
self.assertEqual(self._names("p", ["asian"], limit=1), [("portrait", 2)])
|
||||
|
||||
|
||||
class TestTagJunctionTables(DatabaseTestCase):
|
||||
"""Test normalized tag storage via junction tables."""
|
||||
@@ -218,6 +276,11 @@ class TestTagJunctionTables(DatabaseTestCase):
|
||||
all_tags = self.db.get_all_tags()
|
||||
self.assertEqual(sorted(all_tags), ["a", "b", "c"])
|
||||
|
||||
def test_get_all_tags_includes_unused_registry_rows(self):
|
||||
pid = self._save("P1", tags=["a", "b"])
|
||||
self.db.set_prompt_tags(pid, ["b"])
|
||||
self.assertEqual(self.db.get_all_tags(), ["a", "b"])
|
||||
|
||||
def test_get_tags_with_counts(self):
|
||||
self._save("P1", tags=["common", "rare"])
|
||||
self._save("P2", tags=["common"])
|
||||
|
||||
Reference in New Issue
Block a user