Skip to content

Commit 93795b6

Browse files
committed
feat(flags): 功能开关机制与 feed 捕获默认关
1 parent 03e8393 commit 93795b6

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

‎app/src-tauri/src/feature_flags.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//! 功能开关(v0.11.1;v4 §11.3:feed 相关能力默认关闭)。
2+
//!
3+
//! @ai-context: 项目"增强能力默认关闭"传统的统一开关位——feed_capture 控制
4+
//! 碎片快速捕获面的可见性与 command 准入(后端不信前端隐藏);
5+
//! JSON 持久化同 vocab/ai_settings 模式(缺失/损坏回退默认不阻断)。
6+
7+
use serde::{Deserialize, Serialize};
8+
9+
/// 功能开关集(新增开关必须默认 false——保守默认纪律)。
10+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11+
#[serde(rename_all = "camelCase", default)]
12+
pub struct FeatureFlags {
13+
/// feed 碎片快速捕获(v0.11.1 默认关——Phase 2 验证前不默认启用)
14+
pub feed_capture: bool,
15+
}
16+
17+
impl Default for FeatureFlags {
18+
fn default() -> Self {
19+
Self { feed_capture: false }
20+
}
21+
}
22+
23+
impl FeatureFlags {
24+
/// 从 JSON 文件加载(缺失/损坏 → 内置默认,诚实降级不阻断启动)。
25+
pub fn load(path: &std::path::Path) -> Self {
26+
match std::fs::read_to_string(path) {
27+
Ok(s) => serde_json::from_str(&s).unwrap_or_default(),
28+
Err(_) => Self::default(),
29+
}
30+
}
31+
32+
/// 持久化(先写临时文件再 rename——原子写,防半截 JSON)。
33+
pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
34+
let json = serde_json::to_string_pretty(self).unwrap_or_default();
35+
let tmp = path.with_extension("json.tmp");
36+
std::fs::write(&tmp, json)?;
37+
std::fs::rename(&tmp, path)?;
38+
Ok(())
39+
}
40+
41+
/// 按名读取(白名单;未知开关 → None,诚实不猜)。
42+
pub fn get(&self, name: &str) -> Option<bool> {
43+
match name {
44+
"feed_capture" => Some(self.feed_capture),
45+
_ => None,
46+
}
47+
}
48+
49+
/// 按名写入(白名单;未知开关 → false 返回)。
50+
pub fn set(&mut self, name: &str, value: bool) -> bool {
51+
match name {
52+
"feed_capture" => {
53+
self.feed_capture = value;
54+
true
55+
}
56+
_ => false,
57+
}
58+
}
59+
}
60+
61+
#[cfg(test)]
62+
#[path = "feature_flags_tests.rs"]
63+
mod tests;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! feature_flags 单测(AAA 模式)。
2+
3+
use crate::feature_flags::FeatureFlags;
4+
5+
#[test]
6+
fn defaults_all_off() {
7+
// Arrange/Act
8+
let flags = FeatureFlags::default();
9+
// Assert:保守默认——feed 捕获默认关
10+
assert!(!flags.feed_capture);
11+
}
12+
13+
#[test]
14+
fn set_get_roundtrip_and_unknown_rejected() {
15+
// Arrange
16+
let mut flags = FeatureFlags::default();
17+
// Act/Assert:已知开关可写可读
18+
assert!(flags.set("feed_capture", true));
19+
assert_eq!(flags.get("feed_capture"), Some(true));
20+
// 未知开关诚实拒绝(不猜不造)
21+
assert!(!flags.set("nonexistent", true));
22+
assert_eq!(flags.get("nonexistent"), None);
23+
}
24+
25+
#[test]
26+
fn load_missing_or_corrupt_falls_back_default() {
27+
// Arrange:不存在的路径与损坏内容
28+
let missing = std::path::PathBuf::from("./no_such_feature_flags.json");
29+
let dir = std::env::temp_dir().join("ff_corrupt_test");
30+
let _ = std::fs::create_dir_all(&dir);
31+
let corrupt = dir.join("corrupt.json");
32+
let _ = std::fs::write(&corrupt, "{ not json !!");
33+
// Act
34+
let a = FeatureFlags::load(&missing);
35+
let b = FeatureFlags::load(&corrupt);
36+
// Assert:均回退默认(不阻断启动纪律)
37+
assert_eq!(a, FeatureFlags::default());
38+
assert_eq!(b, FeatureFlags::default());
39+
}
40+
41+
#[test]
42+
fn save_and_reload_persists() {
43+
// Arrange
44+
let dir = std::env::temp_dir().join("ff_save_test");
45+
let _ = std::fs::create_dir_all(&dir);
46+
let path = dir.join("flags.json");
47+
let mut flags = FeatureFlags::default();
48+
flags.feed_capture = true;
49+
// Act
50+
flags.save(&path).expect("save");
51+
let loaded = FeatureFlags::load(&path);
52+
// Assert
53+
assert!(loaded.feed_capture);
54+
}

0 commit comments

Comments
 (0)