Skip to main content

skillfile_core/
conflict.rs

1use std::path::Path;
2
3use crate::error::SkillfileError;
4use crate::models::ConflictState;
5
6pub const CONFLICT_FILE: &str = ".skillfile/conflict";
7
8/// Read conflict state from `.skillfile/conflict`. Returns `None` if no conflict file exists.
9pub fn read_conflict(repo_root: &Path) -> Result<Option<ConflictState>, SkillfileError> {
10    let p = repo_root.join(CONFLICT_FILE);
11    if !p.exists() {
12        return Ok(None);
13    }
14    let text = std::fs::read_to_string(&p)?;
15    let state: ConflictState = serde_json::from_str(&text)
16        .map_err(|e| SkillfileError::Manifest(format!("invalid conflict file: {e}")))?;
17    Ok(Some(state))
18}
19
20/// Write conflict state to `.skillfile/conflict`.
21pub fn write_conflict(repo_root: &Path, state: &ConflictState) -> Result<(), SkillfileError> {
22    let p = repo_root.join(CONFLICT_FILE);
23    if let Some(parent) = p.parent() {
24        std::fs::create_dir_all(parent)?;
25    }
26    let json = serde_json::to_string_pretty(state)
27        .map_err(|e| SkillfileError::Manifest(format!("failed to serialize conflict: {e}")))?;
28    std::fs::write(&p, format!("{json}\n"))?;
29    Ok(())
30}
31
32/// Remove the conflict file. No-op if it doesn't exist.
33pub fn clear_conflict(repo_root: &Path) -> Result<(), SkillfileError> {
34    let p = repo_root.join(CONFLICT_FILE);
35    if p.exists() {
36        std::fs::remove_file(&p)?;
37    }
38    Ok(())
39}
40
41/// Check whether a conflict file exists.
42#[must_use]
43pub fn has_conflict(repo_root: &Path) -> bool {
44    repo_root.join(CONFLICT_FILE).exists()
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    fn make_state() -> ConflictState {
52        ConflictState {
53            entry: "foo".into(),
54            entity_type: "agent".into(),
55            old_sha: "a".repeat(40),
56            new_sha: "b".repeat(40),
57        }
58    }
59
60    // -------------------------------------------------------------------
61    // read_conflict
62    // -------------------------------------------------------------------
63
64    #[test]
65    fn read_missing_returns_none() {
66        let dir = tempfile::tempdir().unwrap();
67        assert!(read_conflict(dir.path()).unwrap().is_none());
68    }
69
70    #[test]
71    fn write_then_read_roundtrip() {
72        let dir = tempfile::tempdir().unwrap();
73        let state = make_state();
74        write_conflict(dir.path(), &state).unwrap();
75        assert_eq!(read_conflict(dir.path()).unwrap(), Some(state));
76    }
77
78    // -------------------------------------------------------------------
79    // write_conflict
80    // -------------------------------------------------------------------
81
82    #[test]
83    fn write_produces_valid_json_structure() {
84        let dir = tempfile::tempdir().unwrap();
85        let state = ConflictState {
86            entry: "bar".into(),
87            entity_type: "skill".into(),
88            ..make_state()
89        };
90        write_conflict(dir.path(), &state).unwrap();
91        let data: serde_json::Value =
92            serde_json::from_str(&std::fs::read_to_string(dir.path().join(CONFLICT_FILE)).unwrap())
93                .unwrap();
94        assert_eq!(data["entry"], "bar");
95        assert_eq!(data["entity_type"], "skill");
96        assert_eq!(data["old_sha"], "a".repeat(40));
97        assert_eq!(data["new_sha"], "b".repeat(40));
98    }
99
100    #[test]
101    fn write_creates_file() {
102        let dir = tempfile::tempdir().unwrap();
103        write_conflict(dir.path(), &make_state()).unwrap();
104        assert!(dir.path().join(CONFLICT_FILE).exists());
105    }
106
107    // -------------------------------------------------------------------
108    // has_conflict
109    // -------------------------------------------------------------------
110
111    #[test]
112    fn has_conflict_false_when_missing() {
113        let dir = tempfile::tempdir().unwrap();
114        assert!(!has_conflict(dir.path()));
115    }
116
117    #[test]
118    fn has_conflict_true_after_write() {
119        let dir = tempfile::tempdir().unwrap();
120        write_conflict(dir.path(), &make_state()).unwrap();
121        assert!(has_conflict(dir.path()));
122    }
123
124    // -------------------------------------------------------------------
125    // clear_conflict
126    // -------------------------------------------------------------------
127
128    #[test]
129    fn clear_removes_file() {
130        let dir = tempfile::tempdir().unwrap();
131        write_conflict(dir.path(), &make_state()).unwrap();
132        clear_conflict(dir.path()).unwrap();
133        assert!(!has_conflict(dir.path()));
134        assert!(!dir.path().join(CONFLICT_FILE).exists());
135    }
136
137    #[test]
138    fn clear_noop_when_missing() {
139        let dir = tempfile::tempdir().unwrap();
140        clear_conflict(dir.path()).unwrap(); // must not panic
141    }
142}