feat(m162): Modrinth 在线内容源(匿名)+ 远端条目按 sha512 补全 + 离线态
core:新增 addon::online —— 按 Modrinth v2 真实形状建模(search facets / project /
project versions / version_file by hash),网络出口只有 Transport 一个 trait,生产实现
EngineTransport 走既有 DownloadEngine::download_bytes(零新依赖);错误分大类,网络类
失败映射成 NotConnected(= 未联网),不再用空列表冒充「没有结果」。
core:online::download::download_verified —— sha1 交给引擎流式校验、sha512 落盘后复核
(download::HashAlgo 没有 Sha512,改它超出 scope,已登记 finding),不符/失败删文件重试,
最多 3 次尝试且不留半截文件。
core:远端条目补全 —— PackFetcher::modrinth_file_by_sha512(默认 Unsupported = 补不出),
import 管线与新增的 scan_pack_online 都会在「有 path 但缺 hashes.sha1」时按 sha512 反查;
补出的 sha1 走 M160 同一套 judge 与合并判定,补不出仍进 unknown_remote 计数(口径未改)。
DownloadEngineFetcher 改用 download_verified,FetchRequest 增 sha512。
presenter:Modrinth 检索/详情改由 core 的 online 客户端提供(不再自己拼 URL、不再读错
date_modified);ContentFailure{kind} 取代裸字符串,无网 = Offline;新增
ContentDetailRequest/ContentDetail + LoadContentDetail 命令与 CoreEvent::ContentDetail 通道;
预检改走 scan_pack_online;内容库安装也走 download_verified(sha1+sha512)。
app-iced:Status::Offline 独立一态(未联网 ≠ 空列表 ≠ 普通失败),详情面板消费新 DTO
显示可用版本;选中项目即拉详情。i18n 新增 6 个键并同步金表。
测试:online 模块 24 个离线用例(录制 JSON + 假字节:搜索/详情/版本/按摘要反查/校验失败
即删/重试/离线分类/补全),import 侧 6 个(离线计数不变、补全后判冲突、补错摘要 ⇒ Failed),
presenter/app-iced 各补失败分类与详情状态机用例。
This commit is contained in:
1 parent
0241f793b2
commit
81278f55af
50 files changed
+3794
-222
No files matched your search
@@ -6,8 +6,8 @@
|
||||
use super::*;
|
||||
use unlucky_i18n::Lang;
|
||||
use unlucky_presenter::dto::{
|
||||
ContentHit, ContentKind, ContentQuery, ContentResults, ContentSource, InstanceRuntime,
|
||||
LoaderKind,
|
||||
ContentDetail, ContentFailure, ContentFailureKind, ContentHit, ContentKind, ContentQuery,
|
||||
ContentResults, ContentSource, ContentVersionView, InstanceRuntime, LoaderKind,
|
||||
};
|
||||
|
||||
fn hit(versions: &[&str], loaders: &[&str]) -> ContentHit {
|
||||
@@ -200,10 +200,69 @@ fn the_four_states_are_derived_from_three_fields() {
|
||||
#[test]
|
||||
fn a_failure_outranks_the_loading_state() {
|
||||
let mut state = state_with(None, true);
|
||||
state.content.failed = Some("网络不通".to_string());
|
||||
state.content.failed = Some(ContentFailure::new(ContentFailureKind::Server, "站点 503"));
|
||||
let model = Content::from_snapshot(&state, &PageState::default(), Lang::En.catalog());
|
||||
assert_eq!(model.status, Status::Failed("网络不通".to_string()));
|
||||
assert_eq!(model.status, Status::Failed("站点 503".to_string()));
|
||||
assert!(model.status.failed());
|
||||
assert!(!model.status.offline());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_network_is_its_own_state_not_an_empty_list() {
|
||||
// M162:无网必须画「未联网」,不能与「没有匹配的内容」共用一个空态。
|
||||
let mut state = state_with(None, false);
|
||||
state.content.results = Some(results(Vec::new(), 0));
|
||||
state.content.failed = Some(ContentFailure::offline("connect timed out"));
|
||||
let model = Content::from_snapshot(&state, &PageState::default(), Lang::En.catalog());
|
||||
assert_eq!(
|
||||
model.status,
|
||||
Status::Offline("connect timed out".to_string())
|
||||
);
|
||||
assert!(model.status.offline());
|
||||
assert!(model.status.failed());
|
||||
assert_ne!(model.status, Status::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versions_come_from_the_detail_channel_of_the_selected_project() {
|
||||
let mut state = state_with(Some(results(vec![hit(&["1.20.1"], &["fabric"])], 0)), false);
|
||||
let mut page = PageState {
|
||||
selected: Some("p1".to_string()),
|
||||
..PageState::default()
|
||||
};
|
||||
// 详情还没到:说明「正在取」,不是「一个版本都没有」
|
||||
state.content.request_detail("p1");
|
||||
let model = Content::from_snapshot(&state, &page, Lang::En.catalog());
|
||||
assert!(model.available_versions.is_empty());
|
||||
assert!(model.versions_note.is_some());
|
||||
|
||||
state.content.apply_detail(ContentDetail {
|
||||
project: "p1".to_string(),
|
||||
title: "Sodium".to_string(),
|
||||
description: "fast".to_string(),
|
||||
body: String::new(),
|
||||
versions: vec![ContentVersionView {
|
||||
id: "v1".to_string(),
|
||||
name: "Sodium 0.6.0".to_string(),
|
||||
number: "mc1.20.1-0.6.0".to_string(),
|
||||
kind: "release".to_string(),
|
||||
game_versions: vec!["1.20.1".to_string()],
|
||||
loaders: vec!["fabric".to_string()],
|
||||
published: "2026-02-01T00:00:00Z".to_string(),
|
||||
file_name: Some("sodium.jar".to_string()),
|
||||
file_size: Some(2048),
|
||||
}],
|
||||
});
|
||||
let model = Content::from_snapshot(&state, &page, Lang::En.catalog());
|
||||
assert_eq!(model.available_versions.len(), 1);
|
||||
assert_eq!(model.available_versions[0].number, "mc1.20.1-0.6.0");
|
||||
assert!(model.available_versions[0].line.contains("mc1.20.1-0.6.0"));
|
||||
assert!(model.versions_note.is_none());
|
||||
|
||||
// 换了选中项:上一个项目的版本列表不许画在这个项目下面
|
||||
page.selected = Some("p2".to_string());
|
||||
let model = Content::from_snapshot(&state, &page, Lang::En.catalog());
|
||||
assert!(model.available_versions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user