feat(persistence): PR6 T6.5 — Run/Feedback/RunEvent repos workspace_id
`RunRepository` (put / get / list_by_thread / delete), `FeedbackRepository` (create / get / list_by_run / list_by_thread / list_by_thread_grouped / upsert / delete / delete_by_run), and `DbRunEventStore` (put / put_batch / list_messages / list_events / list_messages_by_run / count_messages / delete_by_thread / delete_by_run) all accept `workspace_id: str | None | _AutoSentinel = AUTO`. - Write paths stamp `workspace_id` from the contextvar (same shape as the existing `user_id` stamping). For event writes the soft-read `_workspace_id_from_context()` mirrors `_user_id_from_context()` so background worker writes without a contextvar leave the column NULL — consistent with PR5's nullable-during-backfill stance. - Read paths get an extra `WHERE workspace_id = :wid` clause when the resolved value is not None. 7 new tests (3 RunRepo + 2 Feedback + 2 RunEvent) prove cross-workspace reads see zero rows. 92 existing run / feedback / event tests stay green.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""Tests for Run/Feedback/RunEvent repository workspace_id filtering (PR6 T6.5)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.runtime.workspace_context import (
|
||||
reset_current_workspace,
|
||||
set_current_workspace,
|
||||
)
|
||||
|
||||
|
||||
async def _init_engine(tmp_path, *, workspaces: tuple[str, ...] = ()):
|
||||
from deerflow.persistence.engine import get_session_factory, init_engine
|
||||
|
||||
url = f"sqlite+aiosqlite:///{tmp_path / 'test.db'}"
|
||||
await init_engine("sqlite", url=url, sqlite_dir=str(tmp_path))
|
||||
for wid in workspaces:
|
||||
await _seed_workspace(wid)
|
||||
return get_session_factory()
|
||||
|
||||
|
||||
async def _seed_workspace(wid: str) -> None:
|
||||
from deerflow.persistence.engine import get_session_factory
|
||||
from deerflow.persistence.workspace.model import WorkspaceRow
|
||||
|
||||
factory = get_session_factory()
|
||||
async with factory() as session:
|
||||
if await session.get(WorkspaceRow, wid) is not None:
|
||||
return
|
||||
now = datetime.now(UTC)
|
||||
session.add(
|
||||
WorkspaceRow(
|
||||
id=wid,
|
||||
name=f"WS {wid}",
|
||||
slug=wid.replace("_", "-")[:32],
|
||||
status="active",
|
||||
owner_id="test-user-autouse",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _cleanup():
|
||||
from deerflow.persistence.engine import close_engine
|
||||
|
||||
await close_engine()
|
||||
|
||||
|
||||
def _use_workspace(wid: str):
|
||||
return set_current_workspace(SimpleNamespace(id=wid, role="owner"))
|
||||
|
||||
|
||||
class TestRunRepositoryWorkspace:
|
||||
@pytest.mark.anyio
|
||||
async def test_put_records_workspace_id(self, tmp_path):
|
||||
from deerflow.persistence.run import RunRepository
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha",))
|
||||
repo = RunRepository(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
await repo.put("r1", thread_id="t1", user_id="alice")
|
||||
record = await repo.get("r1", user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert record["workspace_id"] == "ws-alpha"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_filters_cross_workspace(self, tmp_path):
|
||||
from deerflow.persistence.run import RunRepository
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha", "ws-beta"))
|
||||
repo = RunRepository(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
await repo.put("r1", thread_id="t1", user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
token = _use_workspace("ws-beta")
|
||||
try:
|
||||
assert await repo.get("r1", user_id="alice") is None
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_by_thread_filters_workspace(self, tmp_path):
|
||||
from deerflow.persistence.run import RunRepository
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha", "ws-beta"))
|
||||
repo = RunRepository(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
await repo.put("r1", thread_id="t1", user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
token = _use_workspace("ws-beta")
|
||||
try:
|
||||
await repo.put("r2", thread_id="t1", user_id="alice")
|
||||
rows = await repo.list_by_thread("t1", user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert [r["run_id"] for r in rows] == ["r2"]
|
||||
|
||||
|
||||
class TestFeedbackRepositoryWorkspace:
|
||||
@pytest.mark.anyio
|
||||
async def test_create_records_workspace_id(self, tmp_path):
|
||||
from deerflow.persistence.feedback.sql import FeedbackRepository
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha",))
|
||||
repo = FeedbackRepository(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
row = await repo.create(run_id="r1", thread_id="t1", rating=1, user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert row["workspace_id"] == "ws-alpha"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_by_thread_filters_workspace(self, tmp_path):
|
||||
from deerflow.persistence.feedback.sql import FeedbackRepository
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha", "ws-beta"))
|
||||
repo = FeedbackRepository(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
await repo.create(run_id="r1", thread_id="t1", rating=1, user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
token = _use_workspace("ws-beta")
|
||||
try:
|
||||
rows = await repo.list_by_thread("t1", user_id="alice")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert rows == []
|
||||
|
||||
|
||||
class TestRunEventStoreWorkspace:
|
||||
@pytest.mark.anyio
|
||||
async def test_put_records_workspace_id(self, tmp_path):
|
||||
from deerflow.runtime.events.store.db import DbRunEventStore
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha",))
|
||||
store = DbRunEventStore(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
row = await store.put(thread_id="t1", run_id="r1", event_type="msg", category="message", content="hi")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert row["workspace_id"] == "ws-alpha"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_messages_filters_cross_workspace(self, tmp_path):
|
||||
from deerflow.runtime.events.store.db import DbRunEventStore
|
||||
|
||||
sf = await _init_engine(tmp_path, workspaces=("ws-alpha", "ws-beta"))
|
||||
store = DbRunEventStore(sf)
|
||||
token = _use_workspace("ws-alpha")
|
||||
try:
|
||||
await store.put(thread_id="t1", run_id="r1", event_type="msg", category="message", content="from-alpha")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
token = _use_workspace("ws-beta")
|
||||
try:
|
||||
rows = await store.list_messages("t1", user_id="test-user-autouse")
|
||||
finally:
|
||||
reset_current_workspace(token)
|
||||
await _cleanup()
|
||||
assert rows == []
|
||||
Reference in New Issue
Block a user