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 209ce27..bafc5dd 100644 --- a/docs/architecture/architecture-bible.md +++ b/docs/architecture/architecture-bible.md @@ -950,6 +950,7 @@ node scripts/release/verify-release-bundle.mjs <产物目录> ## 文档维护记录 +- 2026-08-20:单条历史记录的发送视图在返回任何结果前确认当前成员范围,并用该范围与历史可信关系的交集组装本机发送情况;历史关系和旧发送事实继续保留,当前范围不可用时查询失败。该修改补齐既有当前成员范围规则,不改变分层或所有权。 - 2026-08-21:补充当前成员范围不可用时的脱敏原因分类,区分旧成员记录读取、空间保护关系读取以及缺少当前成员历史;只记录固定类别、保护模式、布尔值和数量,不记录设备名称、文件名、路径或用户内容。不改变成员判断、持久化或对外接口。 - 2026-08-21:修复已初始化旧资料完全缺少空间收敛记录时无法恢复的问题。除确认的首次安装外,缺少记录的既有安装会建立并加密保存旧资料迁移来源;已有记录始终优先,全新安装保持原行为,恢复过程不补造签名成员历史。不改变负责人、对外接口或持久化格式。