test(auth): port AUTH test plan docs + lint/format pass

- Port backend/docs/AUTH_TEST_PLAN.md and AUTH_UPGRADE.md from PR #1728
- Rename metadata.user_id → metadata.owner_id in AUTH_TEST_PLAN.md
  (4 occurrences from the original PR doc)
- ruff auto-fix UP037 in sentinel type annotations: drop quotes around
  "str | None | _AutoSentinel" now that from __future__ import
  annotations makes them implicit string forms
- ruff format: 2 files (app/gateway/app.py, runtime/user_context.py)

Note on test coverage additions:
- conftest.py autouse fixture was already added in commit 4 (had to
  be co-located with the repository changes to keep pre-existing
  persistence tests passing)
- cross-user isolation E2E tests (test_owner_isolation.py) deferred
  — enforcement is already proven by the 98-test repository suite
  via the autouse fixture + explicit _AUTO sentinel exercises
- New test cases (TC-API-17..20, TC-ATK-13, TC-MIG-01..07) listed
  in AUTH_TEST_PLAN.md are deferred to a follow-up PR — they are
  manual-QA test cases rather than pytest code, and the spec-level
  coverage is already met by test_user_context.py + the 98-test
  repository suite.

Final test results:
- Auth suite (test_auth*, test_langgraph_auth, test_ensure_admin,
  test_user_context): 186 passed
- Persistence suite (test_run_event_store, test_run_repository,
  test_thread_meta_repo, test_feedback): 98 passed
- Lint: ruff check + ruff format both clean
This commit is contained in:
greatmengqi
2026-04-08 11:12:30 +08:00
parent e5ad92474c
commit 3aa3e37532
7 changed files with 1937 additions and 32 deletions
@@ -33,7 +33,7 @@ class FeedbackRepository:
run_id: str,
thread_id: str,
rating: int,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
message_id: str | None = None,
comment: str | None = None,
) -> dict:
@@ -61,7 +61,7 @@ class FeedbackRepository:
self,
feedback_id: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> dict | None:
resolved_owner_id = resolve_owner_id(owner_id, method_name="FeedbackRepository.get")
async with self._sf() as session:
@@ -78,7 +78,7 @@ class FeedbackRepository:
run_id: str,
*,
limit: int = 100,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> list[dict]:
resolved_owner_id = resolve_owner_id(owner_id, method_name="FeedbackRepository.list_by_run")
stmt = select(FeedbackRow).where(FeedbackRow.thread_id == thread_id, FeedbackRow.run_id == run_id)
@@ -94,7 +94,7 @@ class FeedbackRepository:
thread_id: str,
*,
limit: int = 100,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> list[dict]:
resolved_owner_id = resolve_owner_id(owner_id, method_name="FeedbackRepository.list_by_thread")
stmt = select(FeedbackRow).where(FeedbackRow.thread_id == thread_id)
@@ -109,7 +109,7 @@ class FeedbackRepository:
self,
feedback_id: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> bool:
resolved_owner_id = resolve_owner_id(owner_id, method_name="FeedbackRepository.delete")
async with self._sf() as session:
@@ -69,7 +69,7 @@ class RunRepository(RunStore):
*,
thread_id,
assistant_id=None,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
status="pending",
multitask_strategy="reject",
metadata=None,
@@ -102,7 +102,7 @@ class RunRepository(RunStore):
self,
run_id,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
):
resolved_owner_id = resolve_owner_id(owner_id, method_name="RunRepository.get")
async with self._sf() as session:
@@ -117,7 +117,7 @@ class RunRepository(RunStore):
self,
thread_id,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
limit=100,
):
resolved_owner_id = resolve_owner_id(owner_id, method_name="RunRepository.list_by_thread")
@@ -141,7 +141,7 @@ class RunRepository(RunStore):
self,
run_id,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
):
resolved_owner_id = resolve_owner_id(owner_id, method_name="RunRepository.delete")
async with self._sf() as session:
@@ -32,7 +32,7 @@ class ThreadMetaRepository(ThreadMetaStore):
thread_id: str,
*,
assistant_id: str | None = None,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
display_name: str | None = None,
metadata: dict | None = None,
) -> dict:
@@ -59,7 +59,7 @@ class ThreadMetaRepository(ThreadMetaStore):
self,
thread_id: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> dict | None:
resolved_owner_id = resolve_owner_id(owner_id, method_name="ThreadMetaRepository.get")
async with self._sf() as session:
@@ -98,7 +98,7 @@ class ThreadMetaRepository(ThreadMetaStore):
status: str | None = None,
limit: int = 100,
offset: int = 0,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> list[dict]:
"""Search threads with optional metadata and status filters.
@@ -140,7 +140,7 @@ class ThreadMetaRepository(ThreadMetaStore):
thread_id: str,
display_name: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> None:
"""Update the display_name (title) for a thread."""
resolved_owner_id = resolve_owner_id(owner_id, method_name="ThreadMetaRepository.update_display_name")
@@ -155,7 +155,7 @@ class ThreadMetaRepository(ThreadMetaStore):
thread_id: str,
status: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> None:
resolved_owner_id = resolve_owner_id(owner_id, method_name="ThreadMetaRepository.update_status")
async with self._sf() as session:
@@ -169,7 +169,7 @@ class ThreadMetaRepository(ThreadMetaStore):
thread_id: str,
metadata: dict,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> None:
"""Merge ``metadata`` into ``metadata_json``.
@@ -194,7 +194,7 @@ class ThreadMetaRepository(ThreadMetaStore):
self,
thread_id: str,
*,
owner_id: "str | None | _AutoSentinel" = AUTO,
owner_id: str | None | _AutoSentinel = AUTO,
) -> None:
resolved_owner_id = resolve_owner_id(owner_id, method_name="ThreadMetaRepository.delete")
async with self._sf() as session:
@@ -49,9 +49,7 @@ class CurrentUser(Protocol):
id: str
_current_user: Final[ContextVar["CurrentUser | None"]] = ContextVar(
"deerflow_current_user", default=None
)
_current_user: Final[ContextVar[CurrentUser | None]] = ContextVar("deerflow_current_user", default=None)
def set_current_user(user: CurrentUser) -> Token[CurrentUser | None]:
@@ -104,9 +102,9 @@ def require_current_user() -> CurrentUser:
class _AutoSentinel:
"""Singleton marker meaning 'resolve owner_id from contextvar'."""
_instance: "_AutoSentinel | None" = None
_instance: _AutoSentinel | None = None
def __new__(cls) -> "_AutoSentinel":
def __new__(cls) -> _AutoSentinel:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@@ -119,7 +117,7 @@ AUTO: Final[_AutoSentinel] = _AutoSentinel()
def resolve_owner_id(
value: "str | None | _AutoSentinel",
value: str | None | _AutoSentinel,
*,
method_name: str = "repository method",
) -> str | None:
@@ -139,10 +137,6 @@ def resolve_owner_id(
if isinstance(value, _AutoSentinel):
user = _current_user.get()
if user is None:
raise RuntimeError(
f"{method_name} called with owner_id=AUTO but no user context is set; "
"pass an explicit owner_id, set the contextvar via auth middleware, "
"or opt out with owner_id=None for migration/CLI paths."
)
raise RuntimeError(f"{method_name} called with owner_id=AUTO but no user context is set; pass an explicit owner_id, set the contextvar via auth middleware, or opt out with owner_id=None for migration/CLI paths.")
return user.id
return value