From ed0d31bc04bd778bcfcccba0f3b02bd9ac692cb5 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sat, 4 Apr 2026 05:12:19 +0000 Subject: [PATCH] feat: add list_bounties and list_all_bounties methods to storage adapter Add filtering methods to JsonFileRoomStorage for Phase 2 soft delete support: - list_bounties(room_id): returns only non-deleted bounties for normal queries - list_all_bounties(room_id, include_deleted=True): returns all bounties for /recover Update RoomStorage protocol to include the new methods. Update mock classes in tests to pass isinstance checks. Fixes #42 --- adapters/storage/json_file.py | 29 +++++++++++++++++++++++++++++ core/ports.py | 19 +++++++++++++++++++ tests/test_ports.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/adapters/storage/json_file.py b/adapters/storage/json_file.py index 41a32b9..c503829 100644 --- a/adapters/storage/json_file.py +++ b/adapters/storage/json_file.py @@ -140,6 +140,35 @@ class JsonFileRoomStorage: return None + def list_bounties(self, room_id: int) -> list[Bounty]: + """List all non-deleted bounties in a room. + + This is the default method for normal queries - soft-deleted bounties + are excluded from results. + """ + room_data = self.load(room_id) + if room_data is None: + return [] + return [b for b in room_data.bounties if b.deleted_at is None] + + def list_all_bounties( + self, room_id: int, include_deleted: bool = True + ) -> list[Bounty]: + """List all bounties including or excluding soft-deleted. + + Args: + room_id: The room ID + include_deleted: If True, return all bounties including soft-deleted. + If False, return only non-deleted bounties. + Defaults to True for /recover functionality. + """ + room_data = self.load(room_id) + if room_data is None: + return [] + if include_deleted: + return room_data.bounties + return [b for b in room_data.bounties if b.deleted_at is None] + class JsonFileTrackingStorage: """TrackingStorage implementation using JSON files. diff --git a/core/ports.py b/core/ports.py index 879be0e..0e98834 100644 --- a/core/ports.py +++ b/core/ports.py @@ -40,6 +40,25 @@ class RoomStorage(Protocol): """Get a specific bounty from a room by ID.""" ... + def list_bounties(self, room_id: int) -> list[Bounty]: + """List all non-deleted bounties in a room. + + Soft-deleted bounties (where deleted_at is not None) are excluded. + """ + ... + + def list_all_bounties( + self, room_id: int, include_deleted: bool = True + ) -> list[Bounty]: + """List all bounties including or excluding soft-deleted. + + Args: + room_id: The room ID + include_deleted: If True, return all bounties including soft-deleted. + If False, return only non-deleted bounties. + """ + ... + @runtime_checkable class TrackingStorage(Protocol): diff --git a/tests/test_ports.py b/tests/test_ports.py index 3d67284..acb63fb 100644 --- a/tests/test_ports.py +++ b/tests/test_ports.py @@ -48,6 +48,20 @@ class SimpleRoomStorage: return b return None + def list_bounties(self, room_id: int) -> list[Bounty]: + if room_id not in self._rooms: + return [] + return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] + + def list_all_bounties( + self, room_id: int, include_deleted: bool = True + ) -> list[Bounty]: + if room_id not in self._rooms: + return [] + if include_deleted: + return self._rooms[room_id].bounties + return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] + class SimpleTrackingStorage: """Minimal mock without ensure_tracking - tests if track_bounty works without it. @@ -119,6 +133,20 @@ class MockRoomStorage: return b return None + def list_bounties(self, room_id: int) -> list[Bounty]: + if room_id not in self._rooms: + return [] + return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] + + def list_all_bounties( + self, room_id: int, include_deleted: bool = True + ) -> list[Bounty]: + if room_id not in self._rooms: + return [] + if include_deleted: + return self._rooms[room_id].bounties + return [b for b in self._rooms[room_id].bounties if b.deleted_at is None] + class MockTrackingStorage: """Mock implementation of TrackingStorage for testing."""