Cover remaining uncovered lines in bcda (client, store, log, pipe/cclf, flatten), bib (sync, spider, translate, ingest, item, store, format, ui), cms (express wrappers, log edge cases), api (base, gitea, rustfs, woodpecker, zotero), bls (table import), and pfs (pragma on race guard). 37,687 statements, 0 missed — 11,061 tests passing.
735 lines
23 KiB
Python
735 lines
23 KiB
Python
"""Tests for bib.store — SQLite-backed bibliography store."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bib.client import connect
|
|
from bib.item import (
|
|
Download,
|
|
Manual,
|
|
Regulation,
|
|
Rule,
|
|
Source,
|
|
)
|
|
from bib.store import Store, _generate_key
|
|
|
|
# ── client.connect ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestConnect:
|
|
def test_connect_returns_store(self) -> None:
|
|
"""client.py line 55: connect() returns Store."""
|
|
s = connect(":memory:")
|
|
assert isinstance(s, Store)
|
|
s.close()
|
|
|
|
|
|
# ── Key generation ──────────────────────────────────────────────
|
|
|
|
|
|
class TestGenerateKey:
|
|
def test_length(self) -> None:
|
|
key = _generate_key()
|
|
assert len(key) == 8
|
|
|
|
def test_alphanumeric(self) -> None:
|
|
key = _generate_key()
|
|
assert key.isalnum()
|
|
|
|
def test_uppercase(self) -> None:
|
|
key = _generate_key()
|
|
assert key == key.upper()
|
|
|
|
def test_unique(self) -> None:
|
|
keys = {_generate_key() for _ in range(100)}
|
|
assert len(keys) > 90
|
|
|
|
|
|
# ── Store initialization ────────────────────────────────────────
|
|
|
|
|
|
class TestStoreInit:
|
|
def test_in_memory(self) -> None:
|
|
s = Store(":memory:")
|
|
assert s._db_path == ":memory:"
|
|
s.close()
|
|
|
|
def test_file_based(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db)
|
|
assert Path(s._db_path) == db
|
|
assert db.exists()
|
|
s.close()
|
|
|
|
def test_storage_dir_default(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db)
|
|
assert s._storage == tmp_path / "storage"
|
|
s.close()
|
|
|
|
def test_storage_dir_custom(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
custom = tmp_path / "my_files"
|
|
s = Store(db, storage_dir=custom)
|
|
assert s._storage == custom
|
|
s.close()
|
|
|
|
def test_storage_dir_memory_default(self) -> None:
|
|
s = Store(":memory:")
|
|
assert s._storage == Path("storage")
|
|
s.close()
|
|
|
|
def test_schema_created(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db)
|
|
con = s._con()
|
|
tables = con.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
).fetchall()
|
|
names = {r["name"] for r in tables}
|
|
expected = {
|
|
"items",
|
|
"tags",
|
|
"item_tags",
|
|
"collections",
|
|
"collection_items",
|
|
"attachments",
|
|
"notes",
|
|
"creators",
|
|
"item_creators",
|
|
"bib_meta",
|
|
}
|
|
assert expected.issubset(names)
|
|
s.close()
|
|
|
|
def test_close_idempotent(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db)
|
|
s.close()
|
|
s.close() # should not raise
|
|
|
|
|
|
# ── Item CRUD ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestCreate:
|
|
def test_create_returns_key(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="CY 2026 PFS Final Rule"))
|
|
assert isinstance(key, str)
|
|
assert len(key) == 8
|
|
s.close()
|
|
|
|
def test_create_uses_supplied_key(self) -> None:
|
|
s = Store(":memory:")
|
|
item = Rule(key="MYKEY123", title="Test Rule")
|
|
key = s.create(item)
|
|
assert key == "MYKEY123"
|
|
s.close()
|
|
|
|
def test_create_with_tags(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(
|
|
Rule(title="Test"),
|
|
tags=["module:pfs", "year:2026"],
|
|
)
|
|
item = s.get(key)
|
|
assert "module:pfs" in item.tags
|
|
assert "year:2026" in item.tags
|
|
s.close()
|
|
|
|
def test_create_stamps_access(self) -> None:
|
|
s = Store(":memory:")
|
|
rule = Rule(title="Test")
|
|
assert rule.access_date == ""
|
|
key = s.create(rule)
|
|
item = s.get(key)
|
|
assert item.access_date != ""
|
|
s.close()
|
|
|
|
|
|
class TestGet:
|
|
def test_get_round_trip(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(
|
|
Rule(
|
|
title="PFS Final Rule",
|
|
fr_volume="90",
|
|
fr_page="98452",
|
|
)
|
|
)
|
|
item = s.get(key)
|
|
assert isinstance(item, Rule)
|
|
assert item.title == "PFS Final Rule"
|
|
assert item.fr_volume == "90"
|
|
s.close()
|
|
|
|
def test_get_missing_raises(self) -> None:
|
|
s = Store(":memory:")
|
|
with pytest.raises(KeyError, match="Item not found"):
|
|
s.get("NOEXIST1")
|
|
s.close()
|
|
|
|
def test_get_preserves_item_type(self) -> None:
|
|
s = Store(":memory:")
|
|
for cls, kwargs in [
|
|
(Rule, {"title": "R"}),
|
|
(Manual, {"title": "M"}),
|
|
(Regulation, {"title": "Reg"}),
|
|
(Download, {"title": "D"}),
|
|
(Source, {"title": "S"}),
|
|
]:
|
|
key = s.create(cls(**kwargs))
|
|
item = s.get(key)
|
|
assert isinstance(item, cls)
|
|
s.close()
|
|
|
|
|
|
class TestUpdate:
|
|
def test_update_title(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Old Title"))
|
|
s.update(key, title="New Title")
|
|
assert s.get(key).title == "New Title"
|
|
s.close()
|
|
|
|
def test_update_no_fields_noop(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"))
|
|
s.update(key) # should not raise
|
|
assert s.get(key).title == "Test"
|
|
s.close()
|
|
|
|
def test_update_tags(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"), tags=["old"])
|
|
s.update(key, tags=["new1", "new2"])
|
|
item = s.get(key)
|
|
assert "new1" in item.tags
|
|
assert "new2" in item.tags
|
|
assert "old" not in item.tags
|
|
s.close()
|
|
|
|
def test_update_missing_raises(self) -> None:
|
|
s = Store(":memory:")
|
|
with pytest.raises(KeyError):
|
|
s.update("NOEXIST1", tags=["x"])
|
|
s.close()
|
|
|
|
|
|
class TestDelete:
|
|
def test_delete_removes_item(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="To Delete"))
|
|
s.delete(key)
|
|
with pytest.raises(KeyError):
|
|
s.get(key)
|
|
s.close()
|
|
|
|
def test_delete_nonexistent_no_error(self) -> None:
|
|
s = Store(":memory:")
|
|
s.delete("NOEXIST1") # should not raise
|
|
s.close()
|
|
|
|
|
|
class TestUpsert:
|
|
def test_upsert_creates_new(self) -> None:
|
|
s = Store(":memory:")
|
|
item = Rule(
|
|
title="New Rule",
|
|
url="https://example.com/rule1",
|
|
)
|
|
key = s.upsert(item)
|
|
assert s.get(key).title == "New Rule"
|
|
s.close()
|
|
|
|
def test_upsert_updates_existing_by_url(self) -> None:
|
|
s = Store(":memory:")
|
|
item1 = Rule(
|
|
title="First",
|
|
url="https://example.com/rule1",
|
|
)
|
|
key1 = s.upsert(item1)
|
|
item2 = Rule(
|
|
title="Updated",
|
|
url="https://example.com/rule1",
|
|
)
|
|
key2 = s.upsert(item2)
|
|
assert key1 == key2
|
|
assert s.get(key2).title == "Updated"
|
|
s.close()
|
|
|
|
def test_upsert_no_url_always_creates(self) -> None:
|
|
s = Store(":memory:")
|
|
k1 = s.upsert(Rule(title="A"))
|
|
k2 = s.upsert(Rule(title="B"))
|
|
assert k1 != k2
|
|
s.close()
|
|
|
|
def test_upsert_with_tags(self) -> None:
|
|
s = Store(":memory:")
|
|
item = Rule(
|
|
title="Test",
|
|
url="https://example.com/t",
|
|
)
|
|
key = s.upsert(item, tags=["module:pfs"])
|
|
got = s.get(key)
|
|
assert "module:pfs" in got.tags
|
|
s.close()
|
|
|
|
def test_upsert_with_collection(self) -> None:
|
|
s = Store(":memory:")
|
|
coll_map = s.ensure_collections({"Rules": {}})
|
|
coll_key = coll_map["Rules"]
|
|
item = Rule(
|
|
title="Test",
|
|
url="https://example.com/c",
|
|
)
|
|
key = s.upsert(item, collection=coll_key)
|
|
got = s.get(key)
|
|
assert coll_key in got.collections
|
|
s.close()
|
|
|
|
|
|
# ── Query / list ────────────────────────────────────────────────
|
|
|
|
|
|
class TestListItems:
|
|
@pytest.fixture
|
|
def populated_store(self) -> Store:
|
|
s = Store(":memory:")
|
|
s.create(
|
|
Rule(title="PFS Rule"),
|
|
tags=["module:pfs", "year:2026"],
|
|
)
|
|
s.create(
|
|
Manual(title="CPM Chapter 12"),
|
|
tags=["module:pfs"],
|
|
)
|
|
s.create(
|
|
Download(title="RVU Files"),
|
|
tags=["module:pfs", "file:rvu"],
|
|
)
|
|
s.create(
|
|
Source(title="Guidance Doc"),
|
|
tags=["year:2025"],
|
|
)
|
|
return s
|
|
|
|
def test_list_all(self, populated_store: Store) -> None:
|
|
items = populated_store.list_items()
|
|
assert len(items) == 4
|
|
populated_store.close()
|
|
|
|
def test_list_by_tag(self, populated_store: Store) -> None:
|
|
items = populated_store.list_items(tag="year:2026")
|
|
assert len(items) == 1
|
|
assert items[0].title == "PFS Rule"
|
|
populated_store.close()
|
|
|
|
def test_list_by_item_type(self, populated_store: Store) -> None:
|
|
items = populated_store.list_items(item_type="rule")
|
|
assert len(items) == 1
|
|
assert isinstance(items[0], Rule)
|
|
populated_store.close()
|
|
|
|
def test_list_by_query_title(self, populated_store: Store) -> None:
|
|
items = populated_store.list_items(query="RVU")
|
|
assert len(items) == 1
|
|
assert items[0].title == "RVU Files"
|
|
populated_store.close()
|
|
|
|
def test_list_no_results(self, populated_store: Store) -> None:
|
|
items = populated_store.list_items(tag="nonexistent")
|
|
assert items == []
|
|
populated_store.close()
|
|
|
|
def test_count(self, populated_store: Store) -> None:
|
|
assert populated_store.count() == 4
|
|
assert populated_store.count(tag="module:pfs") == 3
|
|
populated_store.close()
|
|
|
|
def test_to_dataframe(self, populated_store: Store) -> None:
|
|
import polars as pl
|
|
|
|
df = populated_store.to_dataframe()
|
|
assert isinstance(df, pl.DataFrame)
|
|
assert len(df) == 4
|
|
assert "key" in df.columns
|
|
assert "title" in df.columns
|
|
assert "tags" in df.columns
|
|
populated_store.close()
|
|
|
|
def test_to_dataframe_with_filter(self, populated_store: Store) -> None:
|
|
df = populated_store.to_dataframe(tag="year:2026")
|
|
assert len(df) == 1
|
|
populated_store.close()
|
|
|
|
|
|
# ── Tags ────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTags:
|
|
def test_add_tag(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"))
|
|
s.add_tag(key, "new-tag")
|
|
item = s.get(key)
|
|
assert "new-tag" in item.tags
|
|
s.close()
|
|
|
|
def test_add_tag_idempotent(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"), tags=["t1"])
|
|
s.add_tag(key, "t1")
|
|
item = s.get(key)
|
|
assert item.tags.count("t1") == 1
|
|
s.close()
|
|
|
|
def test_add_tag_missing_item(self) -> None:
|
|
s = Store(":memory:")
|
|
with pytest.raises(KeyError, match="Item not found"):
|
|
s.add_tag("NOEXIST1", "tag")
|
|
s.close()
|
|
|
|
def test_remove_tag(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"), tags=["keep", "remove"])
|
|
s.remove_tag(key, "remove")
|
|
item = s.get(key)
|
|
assert "keep" in item.tags
|
|
assert "remove" not in item.tags
|
|
s.close()
|
|
|
|
def test_remove_tag_nonexistent_noop(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"))
|
|
s.remove_tag(key, "nope") # should not raise
|
|
s.close()
|
|
|
|
def test_list_tags(self) -> None:
|
|
s = Store(":memory:")
|
|
s.create(Rule(title="A"), tags=["module:pfs"])
|
|
s.create(Rule(title="B"), tags=["module:pfs"])
|
|
s.create(
|
|
Rule(title="C"),
|
|
tags=["module:aco", "year:2026"],
|
|
)
|
|
tags = s.list_tags()
|
|
names = {t["name"] for t in tags}
|
|
assert "module:pfs" in names
|
|
assert "module:aco" in names
|
|
assert "year:2026" in names
|
|
pfs_tag = next(t for t in tags if t["name"] == "module:pfs")
|
|
assert pfs_tag["count"] == 2
|
|
s.close()
|
|
|
|
def test_list_tags_namespace(self) -> None:
|
|
s = Store(":memory:")
|
|
s.create(
|
|
Rule(title="A"),
|
|
tags=["module:pfs", "year:2026"],
|
|
)
|
|
s.create(Rule(title="B"), tags=["module:aco"])
|
|
tags = s.list_tags(namespace="module")
|
|
names = [t["name"] for t in tags]
|
|
assert "module:pfs" in names
|
|
assert "module:aco" in names
|
|
# year tag should not appear
|
|
assert all("year" not in n for n in names)
|
|
s.close()
|
|
|
|
def test_list_tags_empty(self) -> None:
|
|
s = Store(":memory:")
|
|
assert s.list_tags() == []
|
|
s.close()
|
|
|
|
|
|
# ── Collections ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestCollections:
|
|
def test_ensure_collections(self) -> None:
|
|
s = Store(":memory:")
|
|
result = s.ensure_collections(
|
|
{
|
|
"Federal Register": {},
|
|
"CMS Manuals": {},
|
|
}
|
|
)
|
|
assert "Federal Register" in result
|
|
assert "CMS Manuals" in result
|
|
assert len(result["Federal Register"]) == 8
|
|
s.close()
|
|
|
|
def test_ensure_collections_idempotent(self) -> None:
|
|
s = Store(":memory:")
|
|
r1 = s.ensure_collections({"Rules": {}})
|
|
r2 = s.ensure_collections({"Rules": {}})
|
|
assert r1["Rules"] == r2["Rules"]
|
|
s.close()
|
|
|
|
def test_ensure_nested_collections(self) -> None:
|
|
s = Store(":memory:")
|
|
result = s.ensure_collections(
|
|
{
|
|
"CMS": {
|
|
"Rules": {},
|
|
"Manuals": {},
|
|
},
|
|
}
|
|
)
|
|
assert "CMS" in result
|
|
assert "Rules" in result
|
|
assert "Manuals" in result
|
|
s.close()
|
|
|
|
def test_list_collections(self) -> None:
|
|
s = Store(":memory:")
|
|
s.ensure_collections({"Alpha": {}, "Beta": {}})
|
|
colls = s.list_collections()
|
|
names = [c["name"] for c in colls]
|
|
assert "Alpha" in names
|
|
assert "Beta" in names
|
|
s.close()
|
|
|
|
def test_list_collections_empty(self) -> None:
|
|
s = Store(":memory:")
|
|
assert s.list_collections() == []
|
|
s.close()
|
|
|
|
def test_list_collections_item_count(self) -> None:
|
|
s = Store(":memory:")
|
|
coll_map = s.ensure_collections({"Rules": {}})
|
|
coll_key = coll_map["Rules"]
|
|
s.create(
|
|
Rule(title="R1"),
|
|
collection=coll_key,
|
|
)
|
|
colls = s.list_collections()
|
|
rules_coll = next(c for c in colls if c["name"] == "Rules")
|
|
assert rules_coll["item_count"] == 1
|
|
s.close()
|
|
|
|
def test_list_items_by_collection(self) -> None:
|
|
s = Store(":memory:")
|
|
coll_map = s.ensure_collections({"Rules": {}})
|
|
coll_key = coll_map["Rules"]
|
|
s.create(Rule(title="In"), collection=coll_key)
|
|
s.create(Rule(title="Out"))
|
|
items = s.list_items(collection=coll_key)
|
|
assert len(items) == 1
|
|
assert items[0].title == "In"
|
|
s.close()
|
|
|
|
|
|
# ── Attachments ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestAttachments:
|
|
def test_attach_file(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db, storage_dir=tmp_path / "storage")
|
|
key = s.create(Rule(title="Test"))
|
|
src = tmp_path / "doc.pdf"
|
|
src.write_bytes(b"fake pdf content")
|
|
att_key = s.attach_file(key, src)
|
|
assert isinstance(att_key, str)
|
|
assert len(att_key) == 8
|
|
dest = tmp_path / "storage" / att_key / "doc.pdf"
|
|
assert dest.exists()
|
|
s.close()
|
|
|
|
def test_attach_file_missing_item(self, tmp_path: Path) -> None:
|
|
db = tmp_path / "bib.sqlite"
|
|
s = Store(db)
|
|
src = tmp_path / "doc.pdf"
|
|
src.write_bytes(b"data")
|
|
with pytest.raises(KeyError, match="Item not found"):
|
|
s.attach_file("NOEXIST1", src)
|
|
s.close()
|
|
|
|
|
|
# ── Notes ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestNotes:
|
|
def test_attach_note(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"))
|
|
note_id = s.attach_note(key, "Some notes here", title="My Note")
|
|
assert isinstance(note_id, int)
|
|
assert note_id > 0
|
|
s.close()
|
|
|
|
def test_attach_note_missing_item(self) -> None:
|
|
s = Store(":memory:")
|
|
with pytest.raises(KeyError, match="Item not found"):
|
|
s.attach_note("NOEXIST1", "content")
|
|
s.close()
|
|
|
|
|
|
# ── Citation formatting ─────────────────────────────────────────
|
|
|
|
|
|
class TestCitationFormatting:
|
|
def test_format_citation_returns_string(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(
|
|
Rule(
|
|
title="CY 2026 PFS Final Rule",
|
|
institution="CMS",
|
|
date_published="2025-11-01",
|
|
)
|
|
)
|
|
citation = s.format_citation(key)
|
|
assert isinstance(citation, str)
|
|
assert len(citation) > 0
|
|
s.close()
|
|
|
|
def test_format_bibliography(self) -> None:
|
|
s = Store(":memory:")
|
|
k1 = s.create(
|
|
Rule(
|
|
title="Rule One",
|
|
institution="CMS",
|
|
)
|
|
)
|
|
k2 = s.create(
|
|
Manual(
|
|
title="Manual One",
|
|
institution="CMS",
|
|
)
|
|
)
|
|
bib = s.format_bibliography([k1, k2])
|
|
assert isinstance(bib, str)
|
|
assert "Rule One" in bib
|
|
assert "Manual One" in bib
|
|
s.close()
|
|
|
|
|
|
# ── __del__ ────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestStoreDel:
|
|
def test_del_closes_connection(self) -> None:
|
|
s = Store(":memory:")
|
|
s._con() # force connection open
|
|
assert s._connection is not None
|
|
s.__del__()
|
|
assert s._connection is None
|
|
|
|
def test_del_on_closed_store(self) -> None:
|
|
s = Store(":memory:")
|
|
s.close()
|
|
s.__del__() # should not raise
|
|
|
|
def test_del_catches_close_exception(self) -> None:
|
|
"""Lines 90-91: __del__ catches exceptions from close()."""
|
|
from unittest.mock import patch
|
|
|
|
s = Store(":memory:")
|
|
s._con() # force connection open
|
|
with patch.object(s, "close", side_effect=RuntimeError("boom")):
|
|
s.__del__() # should not raise
|
|
|
|
|
|
class TestSyncTagsEmptyName:
|
|
def test_empty_tag_name_skipped(self) -> None:
|
|
"""Line 399: _sync_tags skips empty tag names."""
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"), tags=["keep", "", "also-keep"])
|
|
item = s.get(key)
|
|
assert "keep" in item.tags
|
|
assert "also-keep" in item.tags
|
|
assert "" not in item.tags
|
|
s.close()
|
|
|
|
|
|
# ── Upsert URL dedup with tags/collections ─────────────────────────
|
|
|
|
|
|
class TestUpsertDedup:
|
|
def test_upsert_adds_tags_on_existing(self) -> None:
|
|
"""Lines 222-225: upsert with tags= on existing URL."""
|
|
from bib.tag import Tag
|
|
|
|
s = Store(":memory:")
|
|
item1 = Rule(
|
|
title="First",
|
|
url="https://example.com/dup",
|
|
)
|
|
key = s.upsert(item1, tags=[Tag.module("pfs")])
|
|
|
|
# Upsert same URL with Tag objects in tags=
|
|
item2 = Rule(
|
|
title="Updated",
|
|
url="https://example.com/dup",
|
|
)
|
|
key2 = s.upsert(item2, tags=[Tag.year(2026)])
|
|
assert key == key2
|
|
|
|
got2 = s.get(key2)
|
|
# item2 had no initial tags, then Tag.year(2026) was added via tags=
|
|
assert "year:2026" in got2.tags
|
|
assert got2.title == "Updated"
|
|
s.close()
|
|
|
|
def test_upsert_adds_collection_on_existing(self) -> None:
|
|
"""Line 227: upsert with collection= on existing URL."""
|
|
s = Store(":memory:")
|
|
coll_map = s.ensure_collections({"Rules": {}})
|
|
coll_key = coll_map["Rules"]
|
|
|
|
item1 = Rule(title="First", url="https://example.com/dup2")
|
|
key = s.upsert(item1)
|
|
|
|
item2 = Rule(title="Updated", url="https://example.com/dup2")
|
|
key2 = s.upsert(item2, collection=coll_key)
|
|
assert key == key2
|
|
|
|
got = s.get(key2)
|
|
assert coll_key in got.collections
|
|
s.close()
|
|
|
|
def test_upsert_with_string_tags_on_existing(self) -> None:
|
|
"""Ensure tags= with plain strings works on dedup path."""
|
|
s = Store(":memory:")
|
|
item1 = Rule(title="First", url="https://example.com/dup3")
|
|
key = s.upsert(item1)
|
|
|
|
item2 = Rule(title="Updated", url="https://example.com/dup3")
|
|
key2 = s.upsert(item2, tags=["plain-tag"])
|
|
assert key == key2
|
|
|
|
got = s.get(key2)
|
|
assert "plain-tag" in got.tags
|
|
s.close()
|
|
|
|
|
|
# ── _sync_collections with unknown key ─────────────────────────────
|
|
|
|
|
|
class TestSyncCollections:
|
|
def test_unknown_collection_key_skipped(self) -> None:
|
|
s = Store(":memory:")
|
|
key = s.create(Rule(title="Test"))
|
|
# Get item_id
|
|
con = s._con()
|
|
row = con.execute("SELECT id FROM items WHERE key = ?", (key,)).fetchone()
|
|
item_id = row["id"]
|
|
|
|
# Should not raise when collection key doesn't exist
|
|
s._sync_collections(item_id, ["NOEXIST1"])
|
|
|
|
# Verify no collection_items were created
|
|
ci = con.execute(
|
|
"SELECT count(*) FROM collection_items WHERE item_id = ?",
|
|
(item_id,),
|
|
).fetchone()
|
|
assert ci[0] == 0
|
|
s.close()
|