test(acl): give search/inject fixtures real owner ids; fail soft on name lookup
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Successful in 1m4s

Run 2892: 3 pre-existing tests broke, 392 passed. Same shape as the last
breakage — a DB-touching call landed in a path unit tests exercise, and their
fixtures had auto-MagicMock user_id attributes that compare as "someone else's",
sending the code off to look up a username.

Fixed the fixtures rather than the assertion: _fake_note in the search tests and
_note in the plugin-context tests now take a real user_id defaulting to the
caller those tests bind. That makes "is this shared?" meaningful in both files
instead of accidental, which is what the new provenance behaviour actually needs
from them.

Separately, and not as cover for the above: owner_names_for now fails soft.
A lookup error yields no names and callers render "another user". The part that
matters — that the record is NOT the caller's — comes from comparing owner ids,
not from this query, so losing an attribution is cosmetic where failing the whole
search would not be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLwAaV4DQEmVyn496HnEvt
This commit is contained in:
2026-07-25 23:10:12 -04:00
parent ef1dbdfc86
commit e9cd3435ad
3 changed files with 31 additions and 10 deletions
+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