mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
Four findings from @willem-bd on #4401: - FeedbackDialog stays mounted across messages, so selected tags and the comment survived an ESC/click-outside dismiss and pre-filled the next thumbs-down. Reset on every close path via a wrapped onOpenChange. - Neither the dialog's handleSubmit nor handleDialogSubmit caught a failed enrichment PUT, so a rejection went unhandled and the user got no signal. Catch in the dialog (where the rejection lands), toast, and keep the input for a retry. - rate_run awaited the RunLookup port before Feedback.create validated the rating, contradicting its own "before any I/O happens" docstring: an invalid rating on an unknown run surfaced as RunNotFoundError. Validate first, restoring the legacy router's 400-before-404 ordering. The service test now uses an unknown run id so it actually pins that order. - InMemoryFeedbackRepository moved out of test_feedback.py into tests/feedback_fakes.py (with FakeRunLookup) so two test modules share it without one importing the other; conftest states the tests-dir sys.path dependency explicitly, which also makes it work under --import-mode=importlib. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
1.9 KiB
Python
49 lines
1.9 KiB
Python
"""Shared zero-IO doubles for the feedback context.
|
|
|
|
Lives in its own module rather than inside a test file so both the port
|
|
contract suite (``test_feedback.py``) and the service tests
|
|
(``test_feedback_service.py``) can import it as an ordinary module, without
|
|
relying on the tests directory happening to be on ``sys.path``.
|
|
"""
|
|
|
|
from dataclasses import replace
|
|
|
|
from deerflow.domain.feedback import Feedback
|
|
|
|
|
|
class InMemoryFeedbackRepository:
|
|
"""Zero-IO fake keyed by aggregate identity (thread, run, user)."""
|
|
|
|
def __init__(self) -> None:
|
|
self._rows: dict[tuple[str, str, str | None], Feedback] = {}
|
|
|
|
async def save(self, feedback: Feedback) -> Feedback:
|
|
key = (feedback.thread_id, feedback.run_id, feedback.user_id)
|
|
existing = self._rows.get(key)
|
|
if existing is not None:
|
|
feedback = replace(feedback, feedback_id=existing.feedback_id)
|
|
self._rows[key] = feedback
|
|
return feedback
|
|
|
|
async def latest_per_run_in_thread(self, thread_id: str, *, user_id: str | None) -> dict[str, Feedback]:
|
|
return {fb.run_id: fb for (thread, _run, user), fb in self._rows.items() if thread == thread_id and (user_id is None or user == user_id)}
|
|
|
|
async def latest_for_runs(self, thread_id: str, run_ids: set[str], *, user_id: str | None) -> dict[str, Feedback]:
|
|
if not run_ids:
|
|
return {}
|
|
per_run = await self.latest_per_run_in_thread(thread_id, user_id=user_id)
|
|
return {run_id: fb for run_id, fb in per_run.items() if run_id in run_ids}
|
|
|
|
async def remove_for_run(self, thread_id: str, run_id: str, *, user_id: str | None) -> bool:
|
|
return self._rows.pop((thread_id, run_id, user_id), None) is not None
|
|
|
|
|
|
class FakeRunLookup:
|
|
"""RunLookup double backed by a run_id -> thread_id mapping."""
|
|
|
|
def __init__(self, runs: dict[str, str]):
|
|
self._runs = runs
|
|
|
|
async def thread_of(self, run_id: str) -> str | None:
|
|
return self._runs.get(run_id)
|