Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,190 @@
|
||||
//! 测试替身:五个端口的 **mock 实现**(无盘、无网络、无时间依赖)。
|
||||
//!
|
||||
//! 放在库内(而不是只在 `#[cfg(test)]`)是刻意的:`crates/app` 装配时也要能给
|
||||
//! 自己的单测造一套「无盘」环境,且 P1/M39 的 MCP 契约测试要复用同一批假数据。
|
||||
//!
|
||||
//! 按端口拆文件(`port.rs` 挨 400 行上限的同款理由):`settings` 配置、`sessions`
|
||||
//! 会话日志在下面,`game` 实例/Java/账户与 `diagnose` 诊断各一个子模块。
|
||||
//! 对外名字不变,`unlucky_agent::testing::MockSettings` 之类照旧可用(走 re-export)。
|
||||
|
||||
pub mod diagnose;
|
||||
pub mod game;
|
||||
pub mod settings;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::port::{GameSessionPort, GameSessionSummary, LogEvent, LogPage, LogQuery, PortResult};
|
||||
|
||||
pub use diagnose::MockDiagnose;
|
||||
pub use game::{MockAccounts, MockGame};
|
||||
pub use settings::MockSettings;
|
||||
|
||||
/// 内存会话端口。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MockSessions {
|
||||
sessions: Vec<GameSessionSummary>,
|
||||
events: BTreeMap<String, Vec<LogEvent>>,
|
||||
}
|
||||
|
||||
impl MockSessions {
|
||||
/// 空数据。
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 加一个会话及其事件流。
|
||||
pub fn add(&mut self, summary: GameSessionSummary, events: Vec<LogEvent>) {
|
||||
self.events.insert(summary.session_id.clone(), events);
|
||||
self.sessions.push(summary);
|
||||
}
|
||||
|
||||
/// 便捷:造一个崩溃会话(`exit_code=1` + stderr + crash_found)。
|
||||
#[must_use]
|
||||
pub fn crashing_session(session_id: &str) -> (GameSessionSummary, Vec<LogEvent>) {
|
||||
let summary = GameSessionSummary {
|
||||
session_id: session_id.to_string(),
|
||||
instance_id: Some("inst-1".to_string()),
|
||||
instance_name: Some("1.20.1 Forge".to_string()),
|
||||
game_version: Some("1.20.1".to_string()),
|
||||
started_at: Some("2026-10-29T12:45:01.123Z".to_string()),
|
||||
ended_at: Some("2026-10-29T12:46:31.123Z".to_string()),
|
||||
duration_ms: 90_000,
|
||||
exit_code: Some(1),
|
||||
exit_type: Some("application_error".to_string()),
|
||||
silent_failure: false,
|
||||
window_detected: true,
|
||||
event_count: 5,
|
||||
crash_found: true,
|
||||
diagnosis_top: Some("forge_missing_library".to_string()),
|
||||
};
|
||||
let events = vec![
|
||||
event(
|
||||
0,
|
||||
"stdout",
|
||||
json!({ "line": "Starting Minecraft 1.20.1", "level": "info" }),
|
||||
),
|
||||
event(
|
||||
1,
|
||||
"stderr",
|
||||
json!({ "line": "java.lang.NoClassDefFoundError: cpw/mods/bootstraplauncher/BootstrapLauncher", "level": "error" }),
|
||||
),
|
||||
event(
|
||||
2,
|
||||
"crash_found",
|
||||
json!({ "source": "crash_report", "path": r"C:\Users\aska\.minecraft\crash-reports\crash-1.txt", "excerpt": "Description: Mod loading failure" }),
|
||||
),
|
||||
event(
|
||||
3,
|
||||
"exit",
|
||||
json!({ "code": 1, "exit_type": "application_error", "silent_failure": false }),
|
||||
),
|
||||
event(
|
||||
4,
|
||||
"session_end",
|
||||
json!({ "reason": "exit", "duration_ms": 90000 }),
|
||||
),
|
||||
];
|
||||
(summary, events)
|
||||
}
|
||||
}
|
||||
|
||||
fn event(seq: u64, kind: &str, payload: Value) -> LogEvent {
|
||||
LogEvent {
|
||||
seq,
|
||||
ts: format!("2026-10-29T12:45:0{seq}.000Z"),
|
||||
t_ms: seq * 1_000,
|
||||
kind: kind.to_string(),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
impl GameSessionPort for MockSessions {
|
||||
fn list_sessions(
|
||||
&self,
|
||||
limit: usize,
|
||||
instance_id: Option<&str>,
|
||||
) -> PortResult<Vec<GameSessionSummary>> {
|
||||
let mut out: Vec<GameSessionSummary> = self
|
||||
.sessions
|
||||
.iter()
|
||||
.filter(|s| instance_id.is_none_or(|id| s.instance_id.as_deref() == Some(id)))
|
||||
.cloned()
|
||||
.collect();
|
||||
out.sort_by(|a, b| b.started_at.cmp(&a.started_at));
|
||||
out.truncate(limit);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn read_log(&self, session_id: &str, query: &LogQuery) -> PortResult<LogPage> {
|
||||
let events = self
|
||||
.events
|
||||
.get(session_id)
|
||||
.ok_or_else(|| format!("会话 `{session_id}` 不存在"))?;
|
||||
let mut kind_counts = BTreeMap::new();
|
||||
for event in events {
|
||||
*kind_counts.entry(event.kind.clone()).or_insert(0u64) += 1;
|
||||
}
|
||||
let matched: Vec<&LogEvent> = events
|
||||
.iter()
|
||||
.filter(|e| query.from_seq.is_none_or(|from| e.seq >= from))
|
||||
.filter(|e| query.to_seq.is_none_or(|to| e.seq <= to))
|
||||
.filter(|e| query.kinds.is_empty() || query.kinds.contains(&e.kind))
|
||||
.collect();
|
||||
let max = if query.max_events == 0 {
|
||||
matched.len()
|
||||
} else {
|
||||
query.max_events
|
||||
};
|
||||
let truncated = matched.len() > max;
|
||||
Ok(LogPage {
|
||||
session_id: session_id.to_string(),
|
||||
total_events: events.len() as u64,
|
||||
matched_events: matched.len() as u64,
|
||||
truncated,
|
||||
kind_counts,
|
||||
first_ts: events.first().map(|e| e.ts.clone()),
|
||||
last_ts: events.last().map(|e| e.ts.clone()),
|
||||
events: matched.into_iter().take(max).cloned().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 一次性造好全部端口的 [`crate::port::Ports`](含一个崩溃会话,够跑通对话与日志工具)。
|
||||
#[must_use]
|
||||
pub fn mock_ports() -> (crate::port::Ports, std::sync::Arc<MockSettings>) {
|
||||
use std::sync::Arc;
|
||||
let settings = Arc::new(MockSettings::new());
|
||||
let (summary, events) = MockSessions::crashing_session("20261029T124501-9f3c2a");
|
||||
let mut sessions = MockSessions::new();
|
||||
sessions.add(summary, events);
|
||||
let ports = crate::port::Ports {
|
||||
setting: settings.clone(),
|
||||
sessions: Arc::new(sessions),
|
||||
diagnose: Arc::new(MockDiagnose::new()),
|
||||
game: Arc::new(MockGame::new()),
|
||||
accounts: Arc::new(MockAccounts::new()),
|
||||
};
|
||||
(ports, settings)
|
||||
}
|
||||
|
||||
/// 测试里常用的「全部工具名」集合(契约测试与 MCP 投影比对用)。
|
||||
#[must_use]
|
||||
pub fn expected_v1_tool_names() -> BTreeSet<&'static str> {
|
||||
[
|
||||
"get_config",
|
||||
"get_config_schema",
|
||||
"set_config",
|
||||
"list_game_sessions",
|
||||
"read_game_log",
|
||||
"diagnose_crash",
|
||||
"list_instances",
|
||||
"list_versions",
|
||||
"get_java_status",
|
||||
"list_accounts",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
Reference in new issue
Block a user