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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAxVyRwrY7z5NotTtgnwVs
This commit is contained in:
Estrella Pan
2026-07-06 09:55:31 +02:00
parent e6d39e5624
commit c96a5dbbeb
6 changed files with 36 additions and 24 deletions

View File

@@ -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

View File

@@ -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`` 条以外的旧消息,返回删除数量。"""

View File

@@ -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()

View File

@@ -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 只有一行

View File

@@ -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()

View File

@@ -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)