Drafter reuse-recall (both halves) + the multi-user ACL work it exposed #78

Merged
bvandeusen merged 14 commits from dev into main 2026-07-26 00:25:27 -04:00
3 changed files with 31 additions and 10 deletions
Showing only changes of commit e9cd3435ad - Show all commits
+13 -1
View File
@@ -209,9 +209,17 @@ def notes_visibility_clause(user_id: int, scope: str = "own"):
async def owner_names_for(user_ids: set[int]) -> dict[int, str]: async def owner_names_for(user_ids: set[int]) -> dict[int, str]:
"""{user_id: username} in one query. Empty input costs nothing.""" """{user_id: username} in one query. Empty input costs nothing.
Fails soft: on a lookup error the caller gets no names and renders "another
user" instead. Losing an attribution is a cosmetic downgrade, and the part
that matters — that the record ISN'T the caller's — comes from comparing
owner ids, not from this. Failing the whole search over a username would be
the worse trade.
"""
if not user_ids: if not user_ids:
return {} return {}
try:
async with async_session() as session: async with async_session() as session:
rows = ( rows = (
await session.execute( await session.execute(
@@ -219,6 +227,10 @@ async def owner_names_for(user_ids: set[int]) -> dict[int, str]:
) )
).all() ).all()
return {uid: name for uid, name in rows} return {uid: name for uid, name in rows}
except Exception:
logger.warning("Owner-name lookup failed; falling back to unnamed "
"attribution", exc_info=True)
return {}
def _my_group_ids(user_id: int): def _my_group_ids(user_id: int):
+6 -1
View File
@@ -18,13 +18,18 @@ def _reset_user_ctx():
def _fake_note(*, id: int, title: str, body: str = "", def _fake_note(*, id: int, title: str, body: str = "",
tags: list[str] | None = None, is_task: bool = False) -> MagicMock: tags: list[str] | None = None, is_task: bool = False,
user_id: int = 7) -> MagicMock:
note = MagicMock() note = MagicMock()
note.id = id note.id = id
note.title = title note.title = title
note.body = body note.body = body
note.tags = tags or [] note.tags = tags or []
note.is_task = is_task note.is_task = is_task
# A real int, matching the bound caller by default: results compare it to
# decide whether to attach a shared/owner marker, and an auto-MagicMock would
# read as "someone else's" and send the tool looking up a username.
note.user_id = user_id
return note return note
+5 -1
View File
@@ -10,9 +10,13 @@ def _rule(rid, title, topic_id):
return r return r
def _note(nid, title): def _note(nid, title, user_id=1):
n = MagicMock() n = MagicMock()
n.id, n.title = nid, title n.id, n.title = nid, title
# Real int, defaulting to the caller used in these tests: the injected menu
# compares it to decide whether the line needs a "shared by …" attribution,
# and an auto-MagicMock would read as another user's note.
n.user_id = user_id
return n return n