From 2794a898bc5692be3386a06368e12d6df575d37f Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Thu, 20 Aug 2026 21:44:59 +0800 Subject: [PATCH] fix(history): enforce scope lookup for every delivery view Apply AI Review suggestion Verified with confidence: 100/100 AI-Review: https://github.com/UniClipboard/Engine/pull/47#discussion_r3822008336 Resolves: https://github.com/UniClipboard/Engine/pull/47#discussion_r3822008336 --- .../clipboard/sync/get_entry_delivery_view.rs | 77 ++++++++++++++++--- docs/architecture/architecture-bible.md | 2 +- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/crates/uc-application/src/clipboard/sync/get_entry_delivery_view.rs b/crates/uc-application/src/clipboard/sync/get_entry_delivery_view.rs index ffcc438..5f45edf 100644 --- a/crates/uc-application/src/clipboard/sync/get_entry_delivery_view.rs +++ b/crates/uc-application/src/clipboard/sync/get_entry_delivery_view.rs @@ -136,6 +136,15 @@ impl GetEntryDeliveryViewUseCase { .map_err(|e| GetEntryDeliveryViewError::Storage(e.to_string()))? .ok_or_else(|| GetEntryDeliveryViewError::EntryNotFound(entry_id.to_string()))?; + let current_peers: HashSet = self + .peer_scope + .snapshot() + .await + .map_err(|e| GetEntryDeliveryViewError::Storage(format!("current peer scope: {e:?}")))? + .peer_device_ids + .into_iter() + .collect(); + // 2. 历史 entry 直接降级:不合成 Pending,deliveries 留空, // 视图层据此渲染"无投递记录"。 if !entry.delivery_tracked { @@ -210,15 +219,6 @@ impl GetEntryDeliveryViewUseCase { // 5. 本机 entry:当前成员范围与 trusted_peer 取交集后 LEFT JOIN // delivery 表。可信关系和投递事实为历史验证保留,不能单独恢复当前资格。 - let current_peers: HashSet = self - .peer_scope - .snapshot() - .await - .map_err(|e| GetEntryDeliveryViewError::Storage(format!("current peer scope: {e:?}")))? - .peer_device_ids - .into_iter() - .collect(); - let trusted = self .trusted_peer_repo .list() @@ -732,6 +732,25 @@ mod tests { assert!(view.deliveries.is_empty()); } + #[tokio::test] + async fn historical_entry_fails_when_current_peer_scope_is_unavailable() { + let entries = Arc::new(FakeEntryRepo::new()); + entries.insert(make_entry("e1", "ev1", false)); + let uc = build_uc_full_with_scope( + entries, + Arc::new(FakeEventRepo::new()), + Arc::new(FakeTrustedPeerRepo::new(vec![])), + Arc::new(FakeDeliveryRepo::new(vec![])), + Arc::new(FakeMemberRepo::new(vec![])), + Arc::new(make_empty_mobile_device_repo()), + Arc::new(FailingPeerScope), + ); + + let err = uc.execute(&entry_id("e1")).await.unwrap_err(); + + assert!(matches!(err, GetEntryDeliveryViewError::Storage(_))); + } + // ── 分支 3: source_device = None → Historical fallback ───────────── #[tokio::test] @@ -754,6 +773,25 @@ mod tests { ); } + #[tokio::test] + async fn missing_source_device_fails_when_current_peer_scope_is_unavailable() { + let entries = Arc::new(FakeEntryRepo::new()); + entries.insert(make_entry("e1", "ev1", true)); + let uc = build_uc_full_with_scope( + entries, + Arc::new(FakeEventRepo::new()), + Arc::new(FakeTrustedPeerRepo::new(vec![])), + Arc::new(FakeDeliveryRepo::new(vec![])), + Arc::new(FakeMemberRepo::new(vec![])), + Arc::new(make_empty_mobile_device_repo()), + Arc::new(FailingPeerScope), + ); + + let err = uc.execute(&entry_id("e1")).await.unwrap_err(); + + assert!(matches!(err, GetEntryDeliveryViewError::Storage(_))); + } + // ── 分支 4: 远端 entry → Remote, deliveries 空 ───────────────────── #[tokio::test] @@ -782,6 +820,27 @@ mod tests { ); } + #[tokio::test] + async fn remote_entry_fails_when_current_peer_scope_is_unavailable() { + let entries = Arc::new(FakeEntryRepo::new()); + entries.insert(make_entry("e1", "ev1", true)); + let events = Arc::new(FakeEventRepo::new()); + events.set_source(&event_id("ev1"), Some(peer("origin-peer"))); + let uc = build_uc_full_with_scope( + entries, + events, + Arc::new(FakeTrustedPeerRepo::new(vec![])), + Arc::new(FakeDeliveryRepo::new(vec![])), + Arc::new(FakeMemberRepo::new(vec![])), + Arc::new(make_empty_mobile_device_repo()), + Arc::new(FailingPeerScope), + ); + + let err = uc.execute(&entry_id("e1")).await.unwrap_err(); + + assert!(matches!(err, GetEntryDeliveryViewError::Storage(_))); + } + // ── 分支 5: 本机 entry · 无 peer → Local, deliveries 空 ───────────── #[tokio::test] diff --git a/docs/architecture/architecture-bible.md b/docs/architecture/architecture-bible.md index 4565015..e57126e 100644 --- a/docs/architecture/architecture-bible.md +++ b/docs/architecture/architecture-bible.md @@ -941,7 +941,7 @@ node scripts/release/verify-release-bundle.mjs <产物目录> ## 文档维护记录 -- 2026-08-20:单条历史记录的发送视图改用当前成员范围与历史可信关系的交集;历史关系和旧发送事实继续保留,当前范围不可用时查询失败。该修改补齐既有当前成员范围规则,不改变分层或所有权。 +- 2026-08-20:单条历史记录的发送视图在返回任何结果前确认当前成员范围,并用该范围与历史可信关系的交集组装本机发送情况;历史关系和旧发送事实继续保留,当前范围不可用时查询失败。该修改补齐既有当前成员范围规则,不改变分层或所有权。 - 2026-08-20:全新安装首次启动确认没有既有设置后立即保存当前应用版本,避免完成设置后的第二次启动因版本游标仍为空而被误判为旧资料升级。既有资料缺少版本游标时仍进入旧资料独立化,并且只有独立化完整成功后才推进版本游标,失败时保留重试能力。不改变对外接口或持久化格式。