fix(representation): add missing deleted_at filter to working representation queries (#456)

RepresentationManager._query_documents_recent() and
._query_documents_most_derived() do not filter soft-deleted documents,
unlike every other document query function in the codebase. This causes
the deriver's working representation to include documents that are being
garbage-collected.

Refs #444

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hart 2026-03-28 13:39:57 -04:00 committed by GitHub
parent 7275372128
commit ef3426d81b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 138 additions and 0 deletions

View File

@ -366,6 +366,7 @@ class RepresentationManager:
models.Document.workspace_name == self.workspace_name,
models.Document.observer == self.observer,
models.Document.observed == self.observed,
models.Document.deleted_at.is_(None),
*(
[models.Document.session_name == session_name]
if session_name is not None
@ -391,6 +392,7 @@ class RepresentationManager:
models.Document.workspace_name == self.workspace_name,
models.Document.observer == self.observer,
models.Document.observed == self.observed,
models.Document.deleted_at.is_(None),
)
.order_by(models.Document.times_derived.desc())
)

View File

@ -0,0 +1,136 @@
import pytest
from nanoid import generate as generate_nanoid
from sqlalchemy import func, update
from sqlalchemy.ext.asyncio import AsyncSession
from src import models
from src.crud.representation import RepresentationManager
class TestRepresentationManagerSoftDelete:
"""Tests that RepresentationManager query methods exclude soft-deleted documents."""
async def _setup(
self,
db_session: AsyncSession,
test_workspace: models.Workspace,
test_peer: models.Peer,
) -> tuple[models.Peer, models.Session, models.Collection, RepresentationManager]:
"""Create peers, session, collection, and a RepresentationManager."""
test_peer2 = models.Peer(
name=str(generate_nanoid()), workspace_name=test_workspace.name
)
db_session.add(test_peer2)
await db_session.flush()
test_session = models.Session(
name=str(generate_nanoid()), workspace_name=test_workspace.name
)
db_session.add(test_session)
await db_session.flush()
collection = models.Collection(
workspace_name=test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
)
db_session.add(collection)
await db_session.flush()
manager = RepresentationManager(
test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
)
return test_peer2, test_session, collection, manager
@pytest.mark.asyncio
async def test_query_documents_recent_excludes_soft_deleted(
self,
db_session: AsyncSession,
sample_data: tuple[models.Workspace, models.Peer],
):
"""Soft-deleted documents must not appear in the recent-documents query."""
test_workspace, test_peer = sample_data
test_peer2, test_session, _, manager = await self._setup(
db_session, test_workspace, test_peer
)
# Create two documents
doc_live = models.Document(
workspace_name=test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
content="Live observation",
session_name=test_session.name,
)
doc_deleted = models.Document(
workspace_name=test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
content="Deleted observation",
session_name=test_session.name,
)
db_session.add_all([doc_live, doc_deleted])
await db_session.flush()
# Soft-delete one
await db_session.execute(
update(models.Document)
.where(models.Document.id == doc_deleted.id)
.values(deleted_at=func.now())
)
await db_session.commit()
results = await manager._query_documents_recent(db_session, top_k=10) # pyright: ignore[reportPrivateUsage]
result_ids = [doc.id for doc in results]
assert doc_live.id in result_ids
assert doc_deleted.id not in result_ids
@pytest.mark.asyncio
async def test_query_documents_most_derived_excludes_soft_deleted(
self,
db_session: AsyncSession,
sample_data: tuple[models.Workspace, models.Peer],
):
"""Soft-deleted documents must not appear in the most-derived query."""
test_workspace, test_peer = sample_data
test_peer2, test_session, _, manager = await self._setup(
db_session, test_workspace, test_peer
)
# Create two documents with different times_derived
doc_live = models.Document(
workspace_name=test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
content="Live observation",
session_name=test_session.name,
times_derived=5,
)
doc_deleted = models.Document(
workspace_name=test_workspace.name,
observer=test_peer.name,
observed=test_peer2.name,
content="Deleted high-derived observation",
session_name=test_session.name,
times_derived=100,
)
db_session.add_all([doc_live, doc_deleted])
await db_session.flush()
# Soft-delete the high-derived one
await db_session.execute(
update(models.Document)
.where(models.Document.id == doc_deleted.id)
.values(deleted_at=func.now())
)
await db_session.commit()
results = await manager._query_documents_most_derived(db_session, top_k=10) # pyright: ignore[reportPrivateUsage]
result_ids = [doc.id for doc in results]
assert doc_live.id in result_ids
assert doc_deleted.id not in result_ids