from __future__ import annotations import tempfile import unittest from pathlib import Path from alert_service import AlertService from database import ReviewDatabase class AlertServiceTests(unittest.TestCase): def setUp(self) -> None: self.temp = tempfile.TemporaryDirectory() self.database = ReviewDatabase(Path(self.temp.name) / "review.db") self.owner = self.database.create_user("alert_owner", "salt", "hash") self.other = self.database.create_user("alert_other", "salt", "hash") self.service = AlertService(self.database) def tearDown(self) -> None: self.temp.cleanup() def test_future_manual_alert_is_visible_but_not_unread_until_due(self): alert_id = self.service.create_manual( self.owner["id"], { "title": "复核承接", "content": "开盘不及预期则退出观察", "code": "002141", "remind_date": "2026-07-25", }, ) before = self.service.list_alerts(self.owner["id"], "all", "2026-07-22") due = self.service.list_alerts(self.owner["id"], "unread", "2026-07-25") self.assertEqual(before["items"][0]["id"], alert_id) self.assertFalse(before["items"][0]["due"]) self.assertEqual(before["unread_count"], 0) self.assertEqual(due["unread_count"], 1) self.assertTrue(due["items"][0]["due"]) def test_alert_reads_and_mutations_are_scoped_to_owner(self): alert_id = self.database.save_alert( self.owner["id"], "manual", "甲的提醒", "", "20260722", "", "owner-only" ) self.assertEqual( self.service.list_alerts(self.other["id"], "all", "2026-07-22")["items"], [] ) self.assertFalse(self.database.mark_alert_read(self.other["id"], alert_id)) self.assertFalse(self.database.delete_alert(self.other["id"], alert_id)) self.assertTrue(self.database.mark_alert_read(self.owner["id"], alert_id)) self.assertEqual( self.service.list_alerts(self.owner["id"], "all", "2026-07-22")["unread_count"], 0 ) self.assertTrue(self.database.delete_alert(self.owner["id"], alert_id)) def test_strategy_alerts_are_idempotent(self): tracking = { "batches": [ { "run_id": 9, "strategy_name": "修复策略", "items": [{"code": "002141"}, {"code": "600000"}], "summary": { "observed": 2, "completed": 2, "t1_win_rate": 50.0, "average_t5": 3.25, }, } ] } self.service.sync_strategy_tracking(self.owner["id"], tracking) self.service.sync_strategy_tracking(self.owner["id"], tracking) alerts = self.database.list_alerts( self.owner["id"], "99991231", unread_only=False ) self.assertEqual(len(alerts), 2) self.assertEqual({item["kind"] for item in alerts}, {"strategy_t1", "strategy_t5"}) def test_mark_all_only_changes_due_alerts(self): self.database.save_alert( self.owner["id"], "manual", "今日", "", "20260722", "", "due" ) self.database.save_alert( self.owner["id"], "manual", "未来", "", "20260723", "", "future" ) self.assertEqual( self.database.mark_all_alerts_read(self.owner["id"], "20260722"), 1 ) future = self.service.list_alerts(self.owner["id"], "unread", "2026-07-23") self.assertEqual([item["title"] for item in future["items"]], ["未来"]) if __name__ == "__main__": unittest.main()