luaskills 0.4.0

LuaSkills core runtime library for loading, invoking, and managing Lua skill packages.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
use crate::lua_skill::validate_luaskills_identifier;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
#[cfg(windows)]
use windows_sys::Win32::Storage::FileSystem::ReplaceFileW;

/// One flattened skill-config record exposed to hosts and FFI consumers.
/// 暴露给宿主与 FFI 消费方的单条扁平化技能配置记录。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkillConfigEntry {
    /// Stable skill identifier that owns the current config key.
    /// 拥有当前配置键的稳定技能标识符。
    pub skill_id: String,
    /// Stable config key stored under the current skill namespace.
    /// 存放在当前技能命名空间下的稳定配置键。
    pub key: String,
    /// String config value stored for the current `(skill_id, key)` pair.
    /// 当前 `(skill_id, key)` 对应存储的字符串配置值。
    pub value: String,
}

/// One persisted skill-config document stored in the unified runtime config file.
/// 存储在统一运行时配置文件中的单个技能配置文档。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
struct SkillConfigDocument {
    /// Per-skill string key-value map grouped by stable skill identifiers.
    /// 按稳定技能标识符分组的每技能字符串键值映射。
    #[serde(default)]
    skills: BTreeMap<String, BTreeMap<String, String>>,
}

/// Shared unified store that owns the single runtime skill config file.
/// 拥有单一运行时技能配置文件的共享统一存储。
#[derive(Debug)]
pub struct SkillConfigStore {
    /// Optional explicit file path injected by the host.
    /// 由宿主注入的可选显式配置文件路径。
    explicit_file_path: Option<PathBuf>,
    /// Lazily captured default runtime root used when the host does not pass one explicit path.
    /// 当宿主未传显式路径时懒加载记录的默认运行时根目录。
    default_runtime_root: Mutex<Option<PathBuf>>,
}

impl SkillConfigStore {
    /// Create one unified skill-config store from an optional explicit file path.
    /// 基于一个可选显式文件路径创建统一技能配置存储。
    pub fn new(explicit_file_path: Option<PathBuf>) -> Result<Self, String> {
        let explicit_file_path = explicit_file_path
            .map(|path| resolve_explicit_skill_config_file_path(&path))
            .transpose()?;
        Ok(Self {
            explicit_file_path,
            default_runtime_root: Mutex::new(None),
        })
    }

    /// Return whether the host has already pinned one explicit unified config file path.
    /// 返回宿主是否已经固定了一条显式统一配置文件路径。
    pub fn has_explicit_file_path(&self) -> bool {
        self.explicit_file_path.is_some()
    }

    /// Capture the runtime root used by the default config path when no explicit file path exists.
    /// 在不存在显式文件路径时记录默认配置路径所使用的运行时根目录。
    pub fn set_default_runtime_root(&self, runtime_root: &Path) -> Result<(), String> {
        let mut guard = self
            .default_runtime_root
            .lock()
            .map_err(|_| "skill config runtime-root lock poisoned".to_string())?;
        *guard = Some(runtime_root.to_path_buf());
        Ok(())
    }

    /// Return the effective unified skill-config file path.
    /// 返回生效中的统一技能配置文件路径。
    pub fn file_path(&self) -> Result<PathBuf, String> {
        if let Some(path) = self.explicit_file_path.as_ref() {
            return Ok(path.clone());
        }
        let guard = self
            .default_runtime_root
            .lock()
            .map_err(|_| "skill config runtime-root lock poisoned".to_string())?;
        let runtime_root = guard.as_ref().ok_or_else(|| {
            "skill config file path is unresolved; set host_options.skill_config_file_path or load at least one skill root first".to_string()
        })?;
        Ok(runtime_root.join("config").join("skill_config.json"))
    }

    /// List flattened config records for one optional skill namespace.
    /// 列出某个可选技能命名空间下的扁平化配置记录。
    pub fn list_entries(&self, skill_id: Option<&str>) -> Result<Vec<SkillConfigEntry>, String> {
        let document = self.with_document_read(|document| Ok(document.clone()))?;
        match skill_id {
            Some(skill_id) => {
                let normalized_skill_id = validate_skill_config_skill_id(skill_id)?;
                Ok(document
                    .skills
                    .get(&normalized_skill_id)
                    .into_iter()
                    .flat_map(|items| {
                        items.iter().map(|(key, value)| SkillConfigEntry {
                            skill_id: normalized_skill_id.clone(),
                            key: key.clone(),
                            value: value.clone(),
                        })
                    })
                    .collect())
            }
            None => Ok(document
                .skills
                .iter()
                .flat_map(|(skill_id, items)| {
                    items.iter().map(|(key, value)| SkillConfigEntry {
                        skill_id: skill_id.clone(),
                        key: key.clone(),
                        value: value.clone(),
                    })
                })
                .collect()),
        }
    }

    /// List the complete key-value map owned by one skill namespace.
    /// 列出某个技能命名空间拥有的完整键值映射。
    pub fn list_skill_values(&self, skill_id: &str) -> Result<BTreeMap<String, String>, String> {
        let normalized_skill_id = validate_skill_config_skill_id(skill_id)?;
        let document = self.with_document_read(|document| Ok(document.clone()))?;
        Ok(document
            .skills
            .get(&normalized_skill_id)
            .cloned()
            .unwrap_or_default())
    }

    /// Read one string config value stored under one `(skill_id, key)` pair.
    /// 读取某个 `(skill_id, key)` 对下存储的单个字符串配置值。
    pub fn get_value(&self, skill_id: &str, key: &str) -> Result<Option<String>, String> {
        let normalized_skill_id = validate_skill_config_skill_id(skill_id)?;
        let normalized_key = validate_skill_config_key(key)?;
        self.with_document_read(|document| {
            Ok(document
                .skills
                .get(&normalized_skill_id)
                .and_then(|items| items.get(&normalized_key))
                .cloned())
        })
    }

    /// Return whether one `(skill_id, key)` pair currently exists in the store.
    /// 返回某个 `(skill_id, key)` 对当前是否存在于存储中。
    pub fn has_value(&self, skill_id: &str, key: &str) -> Result<bool, String> {
        Ok(self.get_value(skill_id, key)?.is_some())
    }

    /// Insert or replace one string config value under one `(skill_id, key)` pair.
    /// 在某个 `(skill_id, key)` 对下插入或替换单个字符串配置值。
    pub fn set_value(&self, skill_id: &str, key: &str, value: &str) -> Result<(), String> {
        let normalized_skill_id = validate_skill_config_skill_id(skill_id)?;
        let normalized_key = validate_skill_config_key(key)?;
        self.with_document_mut(|document| {
            document
                .skills
                .entry(normalized_skill_id)
                .or_default()
                .insert(normalized_key, value.to_string());
            Ok(())
        })
    }

    /// Delete one config key under one skill namespace and report whether one value was removed.
    /// 删除某个技能命名空间下的单个配置键,并返回是否移除了一个值。
    pub fn delete_value(&self, skill_id: &str, key: &str) -> Result<bool, String> {
        let normalized_skill_id = validate_skill_config_skill_id(skill_id)?;
        let normalized_key = validate_skill_config_key(key)?;
        self.with_document_mut(|document| {
            let deleted = document
                .skills
                .get_mut(&normalized_skill_id)
                .and_then(|items| items.remove(&normalized_key))
                .is_some();
            if let Some(items) = document.skills.get(&normalized_skill_id) {
                if items.is_empty() {
                    document.skills.remove(&normalized_skill_id);
                }
            }
            Ok(deleted)
        })
    }

    /// Execute one read-only document operation under one process-wide lock derived from the effective config file path.
    /// 在由生效配置文件路径派生的进程级锁下执行一次只读文档操作。
    fn with_document_read<T, F>(&self, action: F) -> Result<T, String>
    where
        F: FnOnce(&SkillConfigDocument) -> Result<T, String>,
    {
        let file_path = self.file_path()?;
        let path_lock = shared_skill_config_path_lock(&file_path)?;
        let _path_guard = path_lock
            .lock()
            .map_err(|_| "skill config shared io lock poisoned".to_string())?;
        let document = self.read_document_from(&file_path)?;
        action(&document)
    }

    /// Execute one read-modify-write document operation under one process-wide lock derived from the effective config file path.
    /// 在由生效配置文件路径派生的进程级锁下执行一次读改写文档操作。
    fn with_document_mut<T, F>(&self, action: F) -> Result<T, String>
    where
        F: FnOnce(&mut SkillConfigDocument) -> Result<T, String>,
    {
        let file_path = self.file_path()?;
        let path_lock = shared_skill_config_path_lock(&file_path)?;
        let _path_guard = path_lock
            .lock()
            .map_err(|_| "skill config shared io lock poisoned".to_string())?;
        let mut document = self.read_document_from(&file_path)?;
        let result = action(&mut document)?;
        self.write_document_to(&file_path, &document)?;
        Ok(result)
    }

    /// Load the current persisted document, treating a missing file as one empty config set.
    /// 加载当前持久化文档,并把缺失文件视为一份空配置集合。
    fn read_document_from(&self, file_path: &Path) -> Result<SkillConfigDocument, String> {
        if !file_path.exists() {
            return Ok(SkillConfigDocument::default());
        }
        let text = fs::read_to_string(&file_path).map_err(|error| {
            format!(
                "failed to read skill config file '{}': {}",
                file_path.display(),
                error
            )
        })?;
        serde_json::from_str::<SkillConfigDocument>(&text).map_err(|error| {
            format!(
                "failed to parse skill config file '{}': {}",
                file_path.display(),
                error
            )
        })
    }

    /// Persist one complete document with one temp-file write followed by one replacement rename.
    /// 通过“先写临时文件再替换重命名”的方式持久化整份文档。
    fn write_document_to(
        &self,
        file_path: &Path,
        document: &SkillConfigDocument,
    ) -> Result<(), String> {
        let parent = file_path.parent().ok_or_else(|| {
            format!(
                "skill config file '{}' has no parent directory",
                file_path.display()
            )
        })?;
        fs::create_dir_all(parent).map_err(|error| {
            format!(
                "failed to create skill config directory '{}': {}",
                parent.display(),
                error
            )
        })?;
        let serialized = serde_json::to_vec_pretty(document)
            .map_err(|error| format!("failed to serialize skill config document: {}", error))?;
        let temp_path = file_path.with_extension("json.tmp");
        {
            let mut file = fs::File::create(&temp_path).map_err(|error| {
                format!(
                    "failed to create skill config temp file '{}': {}",
                    temp_path.display(),
                    error
                )
            })?;
            file.write_all(&serialized).map_err(|error| {
                format!(
                    "failed to write skill config temp file '{}': {}",
                    temp_path.display(),
                    error
                )
            })?;
            file.flush().map_err(|error| {
                format!(
                    "failed to flush skill config temp file '{}': {}",
                    temp_path.display(),
                    error
                )
            })?;
            file.sync_all().map_err(|error| {
                format!(
                    "failed to sync skill config temp file '{}': {}",
                    temp_path.display(),
                    error
                )
            })?;
        }
        replace_file_atomically(&temp_path, &file_path).map_err(|error| {
            format!(
                "failed to promote skill config temp file '{}' to '{}': {}",
                temp_path.display(),
                file_path.display(),
                error
            )
        })
    }
}

/// Return the process-wide lock registry keyed by effective skill-config file path.
/// 返回按生效技能配置文件路径建立索引的进程级锁注册表。
fn skill_config_lock_registry() -> &'static Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>> {
    static REGISTRY: OnceLock<Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
}

/// Resolve one stable lock key from one effective skill-config file path.
/// 基于单个生效技能配置文件路径解析稳定锁键。
fn skill_config_lock_key(file_path: &Path) -> Result<PathBuf, String> {
    let resolved_path = if file_path.is_absolute() {
        file_path.to_path_buf()
    } else {
        std::env::current_dir()
            .map(|cwd| cwd.join(file_path))
            .map_err(|error| {
                format!(
                    "failed to resolve current directory for skill config lock: {}",
                    error
                )
            })?
    };
    Ok(normalize_skill_config_lock_identity_path(
        &normalize_skill_config_lock_path(&resolved_path),
    ))
}

/// Resolve one explicit host-provided skill-config file path into one fixed absolute path.
/// 将单个宿主显式提供的技能配置文件路径解析成固定的绝对路径。
fn resolve_explicit_skill_config_file_path(file_path: &Path) -> Result<PathBuf, String> {
    let resolved_path = if file_path.is_absolute() {
        file_path.to_path_buf()
    } else {
        std::env::current_dir()
            .map(|cwd| cwd.join(file_path))
            .map_err(|error| {
                format!(
                    "failed to resolve current directory for explicit skill config path: {}",
                    error
                )
            })?
    };
    Ok(normalize_skill_config_lock_path(&resolved_path))
}

/// Normalize one skill-config lock path with stable lexical component folding.
/// 使用稳定的词法组件折叠规则规范化单个技能配置锁路径。
fn normalize_skill_config_lock_path(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    let mut can_pop_normal = false;
    for component in path.components() {
        match component {
            Component::Prefix(prefix) => {
                normalized.push(prefix.as_os_str());
                can_pop_normal = false;
            }
            Component::RootDir => {
                normalized.push(component.as_os_str());
                can_pop_normal = false;
            }
            Component::CurDir => {}
            Component::ParentDir => {
                if can_pop_normal && normalized.pop() {
                    can_pop_normal = !matches!(
                        normalized.components().next_back(),
                        Some(Component::Prefix(_)) | Some(Component::RootDir) | None
                    );
                } else if !path.is_absolute() {
                    normalized.push(component.as_os_str());
                    can_pop_normal = false;
                }
            }
            Component::Normal(part) => {
                normalized.push(part);
                can_pop_normal = true;
            }
        }
    }
    normalized
}

/// Normalize one lexically folded lock path into one platform-stable lock identity.
/// 将一个已完成词法规整的锁路径进一步规范为平台稳定的锁标识。
fn normalize_skill_config_lock_identity_path(path: &Path) -> PathBuf {
    #[cfg(windows)]
    {
        return normalize_windows_skill_config_lock_identity_path(path);
    }
    #[cfg(not(windows))]
    {
        path.to_path_buf()
    }
}

/// Normalize one Windows lock path so case aliases and verbatim prefixes collapse to one shared identity.
/// 规范化单个 Windows 锁路径,使大小写别名与 verbatim 前缀收敛到同一共享标识。
#[cfg(windows)]
fn normalize_windows_skill_config_lock_identity_path(path: &Path) -> PathBuf {
    let rendered = path.to_string_lossy();
    let without_verbatim = if let Some(stripped) = rendered.strip_prefix(r"\\?\UNC\") {
        format!(r"\\{}", stripped)
    } else if let Some(stripped) = rendered.strip_prefix(r"\\?\") {
        stripped.to_string()
    } else {
        rendered.into_owned()
    };
    PathBuf::from(without_verbatim.to_lowercase())
}

/// Return one process-wide shared mutex for the current effective skill-config file path.
/// 返回当前生效技能配置文件路径对应的进程级共享互斥锁。
fn shared_skill_config_path_lock(file_path: &Path) -> Result<Arc<Mutex<()>>, String> {
    let lock_key = skill_config_lock_key(file_path)?;
    let mut registry = skill_config_lock_registry()
        .lock()
        .map_err(|_| "skill config lock registry poisoned".to_string())?;
    Ok(registry
        .entry(lock_key)
        .or_insert_with(|| Arc::new(Mutex::new(())))
        .clone())
}

/// Validate one skill identifier used by the unified config store.
/// 校验统一配置存储使用的单个技能标识符。
fn validate_skill_config_skill_id(skill_id: &str) -> Result<String, String> {
    let normalized = skill_id.trim();
    validate_luaskills_identifier(normalized, "skill_id")
        .map(|_| normalized.to_string())
        .map_err(|error| format!("invalid skill config skill_id: {}", error))
}

/// Validate one config key used inside one skill namespace.
/// 校验技能命名空间内使用的单个配置键。
fn validate_skill_config_key(key: &str) -> Result<String, String> {
    let normalized = key.trim();
    if normalized.is_empty() {
        return Err("skill config key must not be empty".to_string());
    }
    Ok(normalized.to_string())
}

/// Replace one destination file with one temp file using one platform-safe atomic commit strategy.
/// 使用平台安全的原子提交策略,以临时文件替换目标文件。
fn replace_file_atomically(
    temp_path: &Path,
    destination_path: &Path,
) -> Result<(), std::io::Error> {
    #[cfg(windows)]
    {
        use std::os::windows::ffi::OsStrExt;

        if !destination_path.exists() {
            return fs::rename(temp_path, destination_path);
        }

        let destination_wide: Vec<u16> = destination_path
            .as_os_str()
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();
        let temp_wide: Vec<u16> = temp_path
            .as_os_str()
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();

        let replaced = unsafe {
            ReplaceFileW(
                destination_wide.as_ptr(),
                temp_wide.as_ptr(),
                std::ptr::null(),
                0,
                std::ptr::null_mut(),
                std::ptr::null_mut(),
            )
        };
        if replaced == 0 {
            return Err(std::io::Error::last_os_error());
        }
        Ok(())
    }
    #[cfg(not(windows))]
    {
        fs::rename(temp_path, destination_path)
    }
}

#[cfg(test)]
mod tests {
    use super::{SkillConfigEntry, SkillConfigStore, shared_skill_config_path_lock};
    use std::collections::BTreeMap;
    use std::fs;
    use std::path::{Path, PathBuf};
    use std::sync::Arc;
    use std::time::{SystemTime, UNIX_EPOCH};

    /// Create one unique temporary runtime root used by config-store tests.
    /// 创建一个供配置存储测试使用的唯一临时运行时根目录。
    fn unique_temp_runtime_root(label: &str) -> PathBuf {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time before unix epoch")
            .as_nanos();
        std::env::temp_dir().join(format!("luaskills_skill_config_{}_{}", label, nonce))
    }

    /// Verify the store resolves the default config file under `<runtime_root>/config/skill_config.json`.
    /// 验证存储会把默认配置文件解析到 `<runtime_root>/config/skill_config.json`。
    #[test]
    fn skill_config_store_resolves_default_path_from_runtime_root() {
        let runtime_root = unique_temp_runtime_root("default_path");
        let store = SkillConfigStore::new(None).expect("create default path store");
        store
            .set_default_runtime_root(&runtime_root)
            .expect("set runtime root");
        assert_eq!(
            store.file_path().expect("resolve config path"),
            runtime_root.join("config").join("skill_config.json")
        );
    }

    /// Verify the default runtime root tracks the latest root instead of keeping the first one forever.
    /// 验证默认运行时根目录会跟随最新根目录更新,而不是永久保留第一次的值。
    #[test]
    fn skill_config_store_updates_default_path_when_runtime_root_changes() {
        let first_root = unique_temp_runtime_root("default_path_first");
        let second_root = unique_temp_runtime_root("default_path_second");
        let store = SkillConfigStore::new(None).expect("create update path store");
        store
            .set_default_runtime_root(&first_root)
            .expect("set first runtime root");
        store
            .set_default_runtime_root(&second_root)
            .expect("set second runtime root");
        assert_eq!(
            store.file_path().expect("resolve updated config path"),
            second_root.join("config").join("skill_config.json")
        );
    }

    /// Verify config values persist inside one explicit unified file path.
    /// 验证配置值会持久化到单个显式统一文件路径中。
    #[test]
    fn skill_config_store_persists_values_in_explicit_file() {
        let runtime_root = unique_temp_runtime_root("persist");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let store = SkillConfigStore::new(Some(file_path.clone())).expect("create explicit store");
        store
            .set_value("demo-skill", "api_token", "sk-123")
            .expect("set config value");
        assert_eq!(
            store
                .get_value("demo-skill", "api_token")
                .expect("get config value"),
            Some("sk-123".to_string())
        );
        assert!(file_path.exists());
        let reloaded =
            SkillConfigStore::new(Some(file_path)).expect("create reloaded explicit store");
        assert_eq!(
            reloaded
                .get_value("demo-skill", "api_token")
                .expect("reload config value"),
            Some("sk-123".to_string())
        );
    }

    /// Verify the store returns flattened records for hosts that need one cross-skill management view.
    /// 验证存储会为需要跨技能管理视图的宿主返回扁平化记录列表。
    #[test]
    fn skill_config_store_lists_flattened_entries() {
        let runtime_root = unique_temp_runtime_root("list");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let store = SkillConfigStore::new(Some(file_path)).expect("create flattened-list store");
        store
            .set_value("alpha-skill", "api_token", "alpha-token")
            .expect("set alpha token");
        store
            .set_value("beta-skill", "endpoint", "https://example.test")
            .expect("set beta endpoint");
        assert_eq!(
            store.list_entries(None).expect("list entries"),
            vec![
                SkillConfigEntry {
                    skill_id: "alpha-skill".to_string(),
                    key: "api_token".to_string(),
                    value: "alpha-token".to_string(),
                },
                SkillConfigEntry {
                    skill_id: "beta-skill".to_string(),
                    key: "endpoint".to_string(),
                    value: "https://example.test".to_string(),
                },
            ]
        );
    }

    /// Verify the store exposes one per-skill key-value map for Lua `vulcan.config.list()`.
    /// 验证存储会为 Lua `vulcan.config.list()` 暴露单个技能级键值映射。
    #[test]
    fn skill_config_store_lists_one_skill_value_map() {
        let runtime_root = unique_temp_runtime_root("skill_map");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let store = SkillConfigStore::new(Some(file_path)).expect("create skill-map store");
        store
            .set_value("demo-skill", "api_token", "sk-123")
            .expect("set api token");
        store
            .set_value("demo-skill", "endpoint", "https://example.test")
            .expect("set endpoint");
        let mut expected = BTreeMap::new();
        expected.insert("api_token".to_string(), "sk-123".to_string());
        expected.insert("endpoint".to_string(), "https://example.test".to_string());
        assert_eq!(
            store
                .list_skill_values("demo-skill")
                .expect("list one skill values"),
            expected
        );
    }

    /// Verify deleting one config key removes the value and prunes an empty skill namespace.
    /// 验证删除单个配置键会移除对应值并清理空技能命名空间。
    #[test]
    fn skill_config_store_delete_prunes_empty_skill_namespace() {
        let runtime_root = unique_temp_runtime_root("delete");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let store = SkillConfigStore::new(Some(file_path.clone())).expect("create delete store");
        store
            .set_value("demo-skill", "api_token", "sk-123")
            .expect("set api token");
        assert!(
            store
                .delete_value("demo-skill", "api_token")
                .expect("delete api token")
        );
        assert_eq!(
            store
                .get_value("demo-skill", "api_token")
                .expect("read deleted value"),
            None
        );
        let persisted =
            fs::read_to_string(file_path).expect("skill config file should still be readable");
        assert_eq!(persisted.trim(), "{\n  \"skills\": {}\n}");
    }

    /// Verify stores that target the same config file path share one process-wide IO lock.
    /// 验证指向同一配置文件路径的存储会共享同一把进程级 IO 锁。
    #[test]
    fn skill_config_store_uses_process_wide_lock_per_effective_path() {
        let runtime_root = unique_temp_runtime_root("shared_lock");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let first_lock =
            shared_skill_config_path_lock(&file_path).expect("resolve first shared lock");
        let second_lock =
            shared_skill_config_path_lock(&file_path).expect("resolve second shared lock");
        assert!(Arc::ptr_eq(&first_lock, &second_lock));
    }

    /// Verify one relative explicit config file path gets fixed to one absolute path at creation time.
    /// 验证单个相对显式配置文件路径会在创建时固定成绝对路径。
    #[test]
    fn skill_config_store_freezes_relative_explicit_path_at_creation_time() {
        let relative_path = PathBuf::from("config").join("skill_config.json");
        let expected_path = std::env::current_dir()
            .expect("resolve current directory")
            .join(&relative_path);
        let store = SkillConfigStore::new(Some(relative_path))
            .expect("create relative explicit-path store");
        assert_eq!(
            store.file_path().expect("resolve frozen explicit path"),
            expected_path
        );
    }

    /// Verify lexically equivalent config-file paths reuse the same shared lock.
    /// 验证词法等价的配置文件路径会复用同一把共享锁。
    #[test]
    fn skill_config_store_normalizes_equivalent_paths_for_shared_lock() {
        let runtime_root = unique_temp_runtime_root("shared_lock_normalized");
        let file_path = runtime_root.join("custom").join("skill_config.json");
        let alias_path = runtime_root
            .join("custom")
            .join(".")
            .join("child")
            .join("..")
            .join("skill_config.json");
        let first_lock =
            shared_skill_config_path_lock(&file_path).expect("resolve canonical shared lock");
        let second_lock =
            shared_skill_config_path_lock(&alias_path).expect("resolve alias shared lock");
        assert!(Arc::ptr_eq(&first_lock, &second_lock));
    }

    /// Verify Windows path aliases that differ only by drive-letter casing or verbatim prefix reuse the same shared lock.
    /// 验证仅在盘符大小写或 verbatim 前缀上存在差异的 Windows 路径别名会复用同一把共享锁。
    #[cfg(windows)]
    #[test]
    fn skill_config_store_normalizes_windows_aliases_for_shared_lock() {
        let runtime_root = unique_temp_runtime_root("shared_lock_windows_alias");
        let canonical_path = runtime_root.join("custom").join("skill_config.json");
        let canonical_text = canonical_path.to_string_lossy().into_owned();
        let drive_letter = canonical_text
            .chars()
            .next()
            .expect("canonical windows path should have a drive letter");
        let alias_text = format!(
            "{}{}",
            drive_letter.to_ascii_lowercase(),
            &canonical_text[drive_letter.len_utf8()..]
        );
        let verbatim_alias = format!(r"\\?\{}", alias_text);

        let first_lock =
            shared_skill_config_path_lock(&canonical_path).expect("resolve canonical shared lock");
        let second_lock = shared_skill_config_path_lock(Path::new(&verbatim_alias))
            .expect("resolve windows alias shared lock");
        assert!(Arc::ptr_eq(&first_lock, &second_lock));
    }
}