From c96a5dbbeb8043f2a51970e4c0d89af06acca899 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Mon, 6 Jul 2026 09:55:31 +0200 Subject: [PATCH] fix(types): satisfy mypy on the new notification/LLM code CI runs `mypy src` (not part of the local pytest/ruff/black gate); the feature commit tripped 22 type errors. No behaviour change: - events.py: rename the SSE notification payload local so it no longer collides with the str-typed `payload` earlier in the generator. - inbox.py: type: ignore on the `update().where(read == False)` DML predicate and the CursorResult.rowcount reads. - tests: narrow Optional returns (a typed `_upsert` helper for InboxDatabase, `assert ... is not None` for credential loads) and annotate a bare dict. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs --- backend/src/module/api/events.py | 6 +-- backend/src/module/database/inbox.py | 6 +-- backend/src/test/test_database_inbox.py | 44 ++++++++++++------- .../src/test/test_database_llm_credential.py | 1 + .../src/test/test_llm_credentials_store.py | 1 + backend/src/test/test_llm_plugins_grey.py | 2 +- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/backend/src/module/api/events.py b/backend/src/module/api/events.py index da7744b7..32fe0c12 100644 --- a/backend/src/module/api/events.py +++ b/backend/src/module/api/events.py @@ -125,13 +125,13 @@ async def _event_generator(request: Request, ctx: AppContext): rev = inbox_revision() if rev != last_inbox_rev: try: - payload = await _notification_payload() + notif_payload = await _notification_payload() except Exception: logger.debug("SSE: notification payload unavailable", exc_info=True) else: last_inbox_rev = rev - payload["revision"] = rev - yield {"event": "notification", "data": json.dumps(payload)} + notif_payload["revision"] = rev + yield {"event": "notification", "data": json.dumps(notif_payload)} await asyncio.sleep(_TICK_SECONDS) tick += _TICK_SECONDS diff --git a/backend/src/module/database/inbox.py b/backend/src/module/database/inbox.py index bfab8094..3bf6a52f 100644 --- a/backend/src/module/database/inbox.py +++ b/backend/src/module/database/inbox.py @@ -115,11 +115,11 @@ class InboxDatabase: async def mark_all_read(self) -> int: result = await self.session.execute( update(InboxMessage) - .where(InboxMessage.read == False) # noqa: E712 + .where(InboxMessage.read == False) # type: ignore[arg-type] # noqa: E712 .values(read=True) ) await self.session.commit() - return int(result.rowcount or 0) + return int(result.rowcount or 0) # type: ignore[attr-defined] async def delete(self, message_id: int) -> bool: message = await self.session.get(InboxMessage, message_id) @@ -132,7 +132,7 @@ class InboxDatabase: async def clear(self) -> int: result = await self.session.execute(delete(InboxMessage)) await self.session.commit() - return int(result.rowcount or 0) + return int(result.rowcount or 0) # type: ignore[attr-defined] async def prune(self, keep: int = DEFAULT_KEEP) -> int: """删除最近 ``keep`` 条以外的旧消息,返回删除数量。""" diff --git a/backend/src/test/test_database_inbox.py b/backend/src/test/test_database_inbox.py index 4c6f4575..acb88b55 100644 --- a/backend/src/test/test_database_inbox.py +++ b/backend/src/test/test_database_inbox.py @@ -8,6 +8,13 @@ from module.database.inbox import InboxDatabase from module.models import InboxMessage +async def _upsert(db: InboxDatabase, **kwargs) -> InboxMessage: + """upsert 的非空封装:本文件里除 once 去重测试外都必然返回一行。""" + msg = await db.upsert(**kwargs) + assert msg is not None + return msg + + async def _rows(db_session) -> list[InboxMessage]: result = await db_session.execute(select(InboxMessage)) return list(result.scalars().all()) @@ -15,7 +22,8 @@ async def _rows(db_session) -> list[InboxMessage]: async def test_upsert_inserts_new_message(db_session): db = InboxDatabase(db_session) - msg = await db.upsert( + msg = await _upsert( + db, kind="rss_failure", severity="error", title="RSS 失效", @@ -32,7 +40,8 @@ async def test_upsert_inserts_new_message(db_session): async def test_upsert_unread_same_dedup_coalesces(db_session): db = InboxDatabase(db_session) - first = await db.upsert( + first = await _upsert( + db, kind="downloader_unavailable", severity="error", payload=json.dumps({"reason": "unreachable"}), @@ -43,7 +52,8 @@ async def test_upsert_unread_same_dedup_coalesces(db_session): db_session.add(first) await db_session.commit() - second = await db.upsert( + second = await _upsert( + db, kind="downloader_unavailable", severity="error", payload=json.dumps({"reason": "credentials"}), @@ -54,16 +64,16 @@ async def test_upsert_unread_same_dedup_coalesces(db_session): assert len(rows) == 1 assert second.id == first.id assert second.count == 2 - assert json.loads(second.payload)["reason"] == "credentials" # 最新覆盖 + assert json.loads(second.payload or "{}")["reason"] == "credentials" # 最新覆盖 assert second.updated_at > "2020-01-01T00:00:00+00:00" async def test_upsert_read_same_dedup_inserts_new_row(db_session): db = InboxDatabase(db_session) - first = await db.upsert(kind="rss_failure", dedup_key="rss:1") + first = await _upsert(db, kind="rss_failure", dedup_key="rss:1") await db.mark_read(first.id) - second = await db.upsert(kind="rss_failure", dedup_key="rss:1") + second = await _upsert(db, kind="rss_failure", dedup_key="rss:1") rows = await _rows(db_session) assert len(rows) == 2 @@ -89,15 +99,15 @@ async def test_upsert_once_skips_when_any_row_exists(db_session): async def test_upsert_without_dedup_always_inserts(db_session): db = InboxDatabase(db_session) - await db.upsert(kind="update_applied") - await db.upsert(kind="update_applied") + await _upsert(db, kind="update_applied") + await _upsert(db, kind="update_applied") assert len(await _rows(db_session)) == 2 async def test_prune_keeps_newest(db_session): db = InboxDatabase(db_session) for i in range(5): - msg = await db.upsert(kind="download_failure") + msg = await _upsert(db, kind="download_failure") msg.updated_at = f"2026-01-0{i + 1}T00:00:00+00:00" db_session.add(msg) await db_session.commit() @@ -113,14 +123,14 @@ async def test_prune_keeps_newest(db_session): async def test_upsert_prunes_beyond_keep(db_session): db = InboxDatabase(db_session) for _ in range(4): - await db.upsert(kind="download_failure", keep=3) + await _upsert(db, kind="download_failure", keep=3) assert len(await _rows(db_session)) == 3 async def test_unread_count_and_mark_read(db_session): db = InboxDatabase(db_session) - first = await db.upsert(kind="rss_failure") - await db.upsert(kind="download_failure") + first = await _upsert(db, kind="rss_failure") + await _upsert(db, kind="download_failure") assert await db.unread_count() == 2 assert await db.mark_read(first.id) is True @@ -130,8 +140,8 @@ async def test_unread_count_and_mark_read(db_session): async def test_mark_all_read(db_session): db = InboxDatabase(db_session) - await db.upsert(kind="rss_failure") - await db.upsert(kind="download_failure") + await _upsert(db, kind="rss_failure") + await _upsert(db, kind="download_failure") changed = await db.mark_all_read() @@ -141,8 +151,8 @@ async def test_mark_all_read(db_session): async def test_delete_and_clear(db_session): db = InboxDatabase(db_session) - first = await db.upsert(kind="rss_failure") - await db.upsert(kind="download_failure") + first = await _upsert(db, kind="rss_failure") + await _upsert(db, kind="download_failure") assert await db.delete(first.id) is True assert await db.delete(first.id) is False @@ -153,7 +163,7 @@ async def test_delete_and_clear(db_session): async def test_list_pagination_and_unread_filter(db_session): db = InboxDatabase(db_session) for i in range(3): - msg = await db.upsert(kind=f"kind_{i}") + msg = await _upsert(db, kind=f"kind_{i}") msg.updated_at = f"2026-01-0{i + 1}T00:00:00+00:00" db_session.add(msg) await db_session.commit() diff --git a/backend/src/test/test_database_llm_credential.py b/backend/src/test/test_database_llm_credential.py index fda54892..bb15d653 100644 --- a/backend/src/test/test_database_llm_credential.py +++ b/backend/src/test/test_database_llm_credential.py @@ -23,6 +23,7 @@ async def test_upsert_inserts_then_updates(db_session): await db.upsert("github-copilot", TokenSet(access_token="at-2")) updated = await db.get("github-copilot") + assert updated is not None assert updated.access_token == "at-2" assert await db.count() == 1 # 同 provider 只有一行 diff --git a/backend/src/test/test_llm_credentials_store.py b/backend/src/test/test_llm_credentials_store.py index 8ee4d1dc..3938f8da 100644 --- a/backend/src/test/test_llm_credentials_store.py +++ b/backend/src/test/test_llm_credentials_store.py @@ -44,6 +44,7 @@ class TestCredentialStore: await store.save(TokenSet(access_token="at-2")) await store.clear() + assert loaded is not None assert loaded.access_token == "at" db.llm_credential.get.assert_awaited_once_with("github-copilot") db.llm_credential.upsert.assert_awaited_once() diff --git a/backend/src/test/test_llm_plugins_grey.py b/backend/src/test/test_llm_plugins_grey.py index 91da9a64..23a2412c 100644 --- a/backend/src/test/test_llm_plugins_grey.py +++ b/backend/src/test/test_llm_plugins_grey.py @@ -333,7 +333,7 @@ class TestCodexChatgpt: await _install(tmp_path, keypair, "codex-chatgpt") cls = registry.resolve("codex-chatgpt") - refreshed_saved = {} + refreshed_saved: dict = {} def handler(request): url = str(request.url)