Skip to main content

sqz_engine/
kiro_integration.rs

1//! Kiro (IDE chat and CLI) integration.
2//!
3//! Kiro's PreToolUse hooks are allow/block only — there is no
4//! `updatedInput`-style rewrite mechanism, so the command-rewriting hook
5//! sqz used to install never worked (and its `when`/`then` JSON wasn't a
6//! valid Kiro hook file either). The integration that actually works in
7//! Kiro chat is:
8//!
9//! 1. A steering file at `.kiro/steering/sqz.md` (always included) that
10//!    teaches the agent to pipe long command output through `sqz compress`.
11//! 2. The `sqz-mcp` server registered in `.kiro/settings/mcp.json`.
12
13use std::path::{Path, PathBuf};
14
15use crate::error::{Result, SqzError};
16
17const KIRO_STEERING_SENTINEL: &str = "<!-- sqz-kiro-steering (auto-installed by sqz init) -->";
18
19pub fn kiro_steering_path(project_dir: &Path) -> PathBuf {
20    project_dir.join(".kiro").join("steering").join("sqz.md")
21}
22
23pub fn kiro_mcp_path(project_dir: &Path) -> PathBuf {
24    project_dir.join(".kiro").join("settings").join("mcp.json")
25}
26
27/// Kiro's user-level config root: `$KIRO_HOME` if set, else `~/.kiro`.
28/// Steering and MCP config placed here apply to every workspace.
29pub fn kiro_user_root() -> PathBuf {
30    kiro_user_root_from(
31        std::env::var("KIRO_HOME").ok(),
32        dirs_next::home_dir(),
33    )
34}
35
36fn kiro_user_root_from(kiro_home: Option<String>, home: Option<PathBuf>) -> PathBuf {
37    if let Some(custom) = kiro_home {
38        let trimmed = custom.trim();
39        if !trimmed.is_empty() {
40            return PathBuf::from(trimmed);
41        }
42    }
43    home.unwrap_or_else(|| PathBuf::from(".")).join(".kiro")
44}
45
46pub fn kiro_user_steering_path() -> PathBuf {
47    kiro_user_root().join("steering").join("sqz.md")
48}
49
50pub fn kiro_user_mcp_path() -> PathBuf {
51    kiro_user_root().join("settings").join("mcp.json")
52}
53
54/// The legacy hook file older sqz versions wrote. It used a schema Kiro
55/// never recognized, so it can be deleted safely when we detect it.
56pub fn kiro_legacy_hook_path(project_dir: &Path) -> PathBuf {
57    project_dir.join(".kiro").join("hooks").join("sqz-compress.json")
58}
59
60pub fn kiro_steering_content(sqz_path: &str) -> String {
61    format!(
62        "---\ninclusion: always\n---\n{KIRO_STEERING_SENTINEL}\n\n{}",
63        crate::codex_integration::sqz_guidance(sqz_path)
64    )
65}
66
67pub fn kiro_user_steering_content(sqz_path: &str) -> String {
68    kiro_steering_content(sqz_path)
69}
70
71/// State of a steering file sqz may manage.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum SteeringState {
74    Missing,
75    /// No sqz sentinel: the user wrote it, sqz never touches it.
76    UserAuthored,
77    /// Matches what this version of sqz writes.
78    Current,
79    /// sqz-managed but written by an older version (or a different
80    /// template), so the agent is reading outdated guidance.
81    Stale,
82}
83
84/// The sqz binary path a managed steering file was written for, read back
85/// from its first example line.
86pub(crate) fn embedded_sqz_path(content: &str) -> Option<&str> {
87    content.lines().find_map(|line| {
88        line.split_once("git status 2>&1 | ")
89            .and_then(|(_, rest)| rest.strip_suffix(" compress"))
90    })
91}
92
93/// Compare a steering file against what this version would write for the
94/// same sqz path. `global` selects the user-level template.
95pub fn kiro_steering_state(path: &Path, global: bool) -> SteeringState {
96    let Ok(content) = std::fs::read_to_string(path) else {
97        return SteeringState::Missing;
98    };
99    if !content.contains(KIRO_STEERING_SENTINEL) {
100        return SteeringState::UserAuthored;
101    }
102    let Some(sqz_path) = embedded_sqz_path(&content) else {
103        return SteeringState::Stale;
104    };
105    let expected = if global {
106        kiro_user_steering_content(sqz_path)
107    } else {
108        kiro_steering_content(sqz_path)
109    };
110    if content.replace("\r\n", "\n") == expected {
111        SteeringState::Current
112    } else {
113        SteeringState::Stale
114    }
115}
116
117/// Whether sqz's user-level steering is installed. Kiro applies
118/// `~/.kiro/steering` to every workspace, so a project copy next to it is
119/// the same guidance sent twice on every request.
120pub fn user_steering_installed() -> bool {
121    matches!(
122        kiro_steering_state(&kiro_user_steering_path(), true),
123        SteeringState::Current | SteeringState::Stale
124    )
125}
126
127/// Rewrite a stale sqz-managed steering file with this version's template,
128/// keeping the sqz binary path it already points at when that binary still
129/// exists (else `fallback_sqz_path`). Returns `Ok(false)` for missing,
130/// current, or user-authored files.
131pub fn refresh_kiro_steering(path: &Path, global: bool, fallback_sqz_path: &str) -> Result<bool> {
132    if kiro_steering_state(path, global) != SteeringState::Stale {
133        return Ok(false);
134    }
135    let existing = std::fs::read_to_string(path)
136        .map_err(|e| SqzError::Other(format!("read {}: {e}", path.display())))?;
137    let sqz_path = embedded_sqz_path(&existing)
138        .filter(|p| Path::new(p).is_file())
139        .unwrap_or(fallback_sqz_path)
140        .to_string();
141    let content = if global {
142        kiro_user_steering_content(&sqz_path)
143    } else {
144        kiro_steering_content(&sqz_path)
145    };
146    install_kiro_steering_at(path, &content)
147}
148
149/// Write the steering file. Refreshes sqz-managed files in place, leaves
150/// a user-authored `.kiro/steering/sqz.md` alone.
151pub fn install_kiro_steering(project_dir: &Path, sqz_path: &str) -> Result<bool> {
152    install_kiro_steering_at(&kiro_steering_path(project_dir), &kiro_steering_content(sqz_path))
153}
154
155/// Write the user-level steering file (`~/.kiro/steering/sqz.md`), which
156/// Kiro applies to every workspace.
157pub fn install_kiro_user_steering(sqz_path: &str) -> Result<bool> {
158    install_kiro_steering_at(&kiro_user_steering_path(), &kiro_user_steering_content(sqz_path))
159}
160
161pub(crate) fn install_kiro_steering_at(path: &Path, content: &str) -> Result<bool> {
162    if path.exists() {
163        let existing = std::fs::read_to_string(&path).map_err(|e| {
164            SqzError::Other(format!("read {}: {e}", path.display()))
165        })?;
166        if !existing.contains(KIRO_STEERING_SENTINEL) {
167            return Ok(false);
168        }
169        if existing == content {
170            return Ok(false);
171        }
172    }
173    if let Some(parent) = path.parent() {
174        std::fs::create_dir_all(parent)
175            .map_err(|e| SqzError::Other(format!("create {}: {e}", parent.display())))?;
176    }
177    std::fs::write(&path, content)
178        .map_err(|e| SqzError::Other(format!("write {}: {e}", path.display())))?;
179    Ok(true)
180}
181
182pub fn remove_kiro_steering(project_dir: &Path) -> Result<bool> {
183    remove_kiro_steering_at(&kiro_steering_path(project_dir))
184}
185
186pub fn remove_kiro_user_steering() -> Result<bool> {
187    remove_kiro_steering_at(&kiro_user_steering_path())
188}
189
190pub(crate) fn remove_kiro_steering_at(path: &Path) -> Result<bool> {
191    if !path.exists() {
192        return Ok(false);
193    }
194    let content = std::fs::read_to_string(path)
195        .map_err(|e| SqzError::Other(format!("read {}: {e}", path.display())))?;
196    if !content.contains(KIRO_STEERING_SENTINEL) {
197        return Ok(false);
198    }
199    std::fs::remove_file(path)
200        .map_err(|e| SqzError::Other(format!("remove {}: {e}", path.display())))?;
201    Ok(true)
202}
203
204/// Delete the legacy broken hook file if present and sqz-authored.
205pub fn remove_kiro_legacy_hook(project_dir: &Path) -> Result<bool> {
206    let path = kiro_legacy_hook_path(project_dir);
207    if !path.exists() {
208        return Ok(false);
209    }
210    let content = std::fs::read_to_string(&path).unwrap_or_default();
211    if !(content.contains("\"sqz compress\"") && content.contains("hook kiro")) {
212        return Ok(false);
213    }
214    std::fs::remove_file(&path)
215        .map_err(|e| SqzError::Other(format!("remove {}: {e}", path.display())))?;
216    Ok(true)
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum KiroMcpInstall {
221    Created,
222    Merged,
223    AlreadyPresent,
224    /// File exists but is not strict JSON; left untouched.
225    SkippedUnparseable,
226}
227
228pub fn install_kiro_mcp_config(project_dir: &Path) -> Result<KiroMcpInstall> {
229    install_kiro_mcp_config_at(&kiro_mcp_path(project_dir))
230}
231
232/// Register sqz-mcp in the user-level `~/.kiro/settings/mcp.json`, which
233/// Kiro merges into every workspace.
234pub fn install_kiro_user_mcp_config() -> Result<KiroMcpInstall> {
235    install_kiro_mcp_config_at(&kiro_user_mcp_path())
236}
237
238pub(crate) fn install_kiro_mcp_config_at(path: &Path) -> Result<KiroMcpInstall> {
239    let sqz_entry = serde_json::json!({
240        "command": "sqz-mcp",
241        "args": ["--transport", "stdio"],
242        "disabled": false
243    });
244
245    if !path.exists() {
246        if let Some(parent) = path.parent() {
247            std::fs::create_dir_all(parent)
248                .map_err(|e| SqzError::Other(format!("create {}: {e}", parent.display())))?;
249        }
250        let root = serde_json::json!({ "mcpServers": { "sqz": sqz_entry } });
251        std::fs::write(path, serde_json::to_string_pretty(&root).unwrap_or_default())
252            .map_err(|e| SqzError::Other(format!("write {}: {e}", path.display())))?;
253        return Ok(KiroMcpInstall::Created);
254    }
255
256    let text = std::fs::read_to_string(path)
257        .map_err(|e| SqzError::Other(format!("read {}: {e}", path.display())))?;
258    let mut root: serde_json::Value = if text.trim().is_empty() {
259        serde_json::json!({})
260    } else {
261        match serde_json::from_str(&text) {
262            Ok(v) => v,
263            Err(_) => return Ok(KiroMcpInstall::SkippedUnparseable),
264        }
265    };
266
267    let Some(root_obj) = root.as_object_mut() else {
268        return Ok(KiroMcpInstall::SkippedUnparseable);
269    };
270    let servers = root_obj
271        .entry("mcpServers".to_string())
272        .or_insert_with(|| serde_json::json!({}));
273    if !servers.is_object() {
274        *servers = serde_json::json!({});
275    }
276    let servers_obj = servers.as_object_mut().expect("just ensured object");
277
278    if servers_obj.contains_key("sqz") {
279        return Ok(KiroMcpInstall::AlreadyPresent);
280    }
281    servers_obj.insert("sqz".to_string(), sqz_entry);
282
283    let mut out = serde_json::to_string_pretty(&root).unwrap_or_default();
284    if text.ends_with('\n') {
285        out.push('\n');
286    }
287    std::fs::write(path, out)
288        .map_err(|e| SqzError::Other(format!("write {}: {e}", path.display())))?;
289    Ok(KiroMcpInstall::Merged)
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum KiroMcpRemove {
294    Removed,
295    NotPresent,
296    SkippedUnparseable,
297}
298
299pub fn remove_kiro_mcp_config(project_dir: &Path) -> Result<KiroMcpRemove> {
300    remove_kiro_mcp_config_at(&kiro_mcp_path(project_dir))
301}
302
303pub fn remove_kiro_user_mcp_config() -> Result<KiroMcpRemove> {
304    remove_kiro_mcp_config_at(&kiro_user_mcp_path())
305}
306
307pub(crate) fn remove_kiro_mcp_config_at(path: &Path) -> Result<KiroMcpRemove> {
308    if !path.exists() {
309        return Ok(KiroMcpRemove::NotPresent);
310    }
311    let text = std::fs::read_to_string(path)
312        .map_err(|e| SqzError::Other(format!("read {}: {e}", path.display())))?;
313    let mut root: serde_json::Value = match serde_json::from_str(&text) {
314        Ok(v) => v,
315        Err(_) => {
316            if text.contains("\"sqz\"") {
317                return Ok(KiroMcpRemove::SkippedUnparseable);
318            }
319            return Ok(KiroMcpRemove::NotPresent);
320        }
321    };
322
323    let Some(root_obj) = root.as_object_mut() else {
324        return Ok(KiroMcpRemove::NotPresent);
325    };
326    let Some(servers) = root_obj.get_mut("mcpServers").and_then(|s| s.as_object_mut()) else {
327        return Ok(KiroMcpRemove::NotPresent);
328    };
329    if servers.remove("sqz").is_none() {
330        return Ok(KiroMcpRemove::NotPresent);
331    }
332    if servers.is_empty() {
333        root_obj.remove("mcpServers");
334    }
335    let mut out = serde_json::to_string_pretty(&root).unwrap_or_default();
336    if text.ends_with('\n') {
337        out.push('\n');
338    }
339    std::fs::write(path, out)
340        .map_err(|e| SqzError::Other(format!("write {}: {e}", path.display())))?;
341    Ok(KiroMcpRemove::Removed)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn steering_install_creates_file() {
350        let dir = tempfile::tempdir().unwrap();
351        assert!(install_kiro_steering(dir.path(), "/usr/bin/sqz").unwrap());
352        let content = std::fs::read_to_string(kiro_steering_path(dir.path())).unwrap();
353        assert!(content.starts_with("---\ninclusion: always\n---"));
354        assert!(content.contains(KIRO_STEERING_SENTINEL));
355        assert!(content.contains("/usr/bin/sqz compress"));
356    }
357
358    #[test]
359    fn steering_install_is_idempotent() {
360        let dir = tempfile::tempdir().unwrap();
361        assert!(install_kiro_steering(dir.path(), "sqz").unwrap());
362        assert!(!install_kiro_steering(dir.path(), "sqz").unwrap());
363    }
364
365    #[test]
366    fn steering_install_refreshes_stale_managed_file() {
367        let dir = tempfile::tempdir().unwrap();
368        install_kiro_steering(dir.path(), "/old/sqz").unwrap();
369        assert!(install_kiro_steering(dir.path(), "/new/sqz").unwrap());
370        let content = std::fs::read_to_string(kiro_steering_path(dir.path())).unwrap();
371        assert!(content.contains("/new/sqz"));
372        assert!(!content.contains("/old/sqz"));
373    }
374
375    #[test]
376    fn steering_install_leaves_user_file_alone() {
377        let dir = tempfile::tempdir().unwrap();
378        let path = kiro_steering_path(dir.path());
379        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
380        std::fs::write(&path, "my own sqz notes\n").unwrap();
381        assert!(!install_kiro_steering(dir.path(), "sqz").unwrap());
382        assert_eq!(std::fs::read_to_string(&path).unwrap(), "my own sqz notes\n");
383    }
384
385    #[test]
386    fn steering_remove_only_deletes_managed_file() {
387        let dir = tempfile::tempdir().unwrap();
388        install_kiro_steering(dir.path(), "sqz").unwrap();
389        assert!(remove_kiro_steering(dir.path()).unwrap());
390        assert!(!kiro_steering_path(dir.path()).exists());
391
392        let path = kiro_steering_path(dir.path());
393        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
394        std::fs::write(&path, "user file\n").unwrap();
395        assert!(!remove_kiro_steering(dir.path()).unwrap());
396        assert!(path.exists());
397    }
398
399    #[test]
400    fn legacy_hook_is_cleaned_up() {
401        let dir = tempfile::tempdir().unwrap();
402        let hook = kiro_legacy_hook_path(dir.path());
403        std::fs::create_dir_all(hook.parent().unwrap()).unwrap();
404        std::fs::write(&hook, r#"{"name":"sqz compress","then":{"command":"sqz hook kiro"}}"#).unwrap();
405        assert!(remove_kiro_legacy_hook(dir.path()).unwrap());
406        assert!(!hook.exists());
407    }
408
409    #[test]
410    fn legacy_cleanup_skips_user_hooks() {
411        let dir = tempfile::tempdir().unwrap();
412        let hook = kiro_legacy_hook_path(dir.path());
413        std::fs::create_dir_all(hook.parent().unwrap()).unwrap();
414        std::fs::write(&hook, r#"{"version":"v1","hooks":[{"name":"mine"}]}"#).unwrap();
415        assert!(!remove_kiro_legacy_hook(dir.path()).unwrap());
416        assert!(hook.exists());
417    }
418
419    #[test]
420    fn mcp_creates_fresh_config() {
421        let dir = tempfile::tempdir().unwrap();
422        assert_eq!(
423            install_kiro_mcp_config(dir.path()).unwrap(),
424            KiroMcpInstall::Created
425        );
426        let parsed: serde_json::Value = serde_json::from_str(
427            &std::fs::read_to_string(kiro_mcp_path(dir.path())).unwrap(),
428        )
429        .unwrap();
430        assert_eq!(parsed["mcpServers"]["sqz"]["command"], "sqz-mcp");
431        assert_eq!(parsed["mcpServers"]["sqz"]["disabled"], false);
432    }
433
434    #[test]
435    fn mcp_merges_preserving_other_servers() {
436        let dir = tempfile::tempdir().unwrap();
437        let path = kiro_mcp_path(dir.path());
438        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
439        std::fs::write(&path, r#"{"mcpServers":{"aws-docs":{"command":"uvx"}}}"#).unwrap();
440        assert_eq!(install_kiro_mcp_config_at(&path).unwrap(), KiroMcpInstall::Merged);
441        let parsed: serde_json::Value =
442            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
443        assert_eq!(parsed["mcpServers"]["aws-docs"]["command"], "uvx");
444        assert_eq!(parsed["mcpServers"]["sqz"]["command"], "sqz-mcp");
445    }
446
447    #[test]
448    fn mcp_leaves_existing_sqz_entry_alone() {
449        let dir = tempfile::tempdir().unwrap();
450        let path = kiro_mcp_path(dir.path());
451        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
452        std::fs::write(&path, r#"{"mcpServers":{"sqz":{"command":"/custom/sqz-mcp"}}}"#).unwrap();
453        assert_eq!(
454            install_kiro_mcp_config_at(&path).unwrap(),
455            KiroMcpInstall::AlreadyPresent
456        );
457        let parsed: serde_json::Value =
458            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
459        assert_eq!(parsed["mcpServers"]["sqz"]["command"], "/custom/sqz-mcp");
460    }
461
462    #[test]
463    fn mcp_skips_unparseable_config() {
464        let dir = tempfile::tempdir().unwrap();
465        let path = kiro_mcp_path(dir.path());
466        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
467        let bad = "{ not json ";
468        std::fs::write(&path, bad).unwrap();
469        assert_eq!(
470            install_kiro_mcp_config_at(&path).unwrap(),
471            KiroMcpInstall::SkippedUnparseable
472        );
473        assert_eq!(std::fs::read_to_string(&path).unwrap(), bad);
474    }
475
476    #[test]
477    fn mcp_remove_is_surgical() {
478        let dir = tempfile::tempdir().unwrap();
479        let path = kiro_mcp_path(dir.path());
480        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
481        std::fs::write(
482            &path,
483            r#"{"mcpServers":{"sqz":{"command":"sqz-mcp"},"aws-docs":{"command":"uvx"}}}"#,
484        )
485        .unwrap();
486        assert_eq!(remove_kiro_mcp_config_at(&path).unwrap(), KiroMcpRemove::Removed);
487        let parsed: serde_json::Value =
488            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
489        assert!(parsed["mcpServers"].get("sqz").is_none());
490        assert_eq!(parsed["mcpServers"]["aws-docs"]["command"], "uvx");
491    }
492
493    #[test]
494    fn user_root_honors_kiro_home() {
495        assert_eq!(
496            kiro_user_root_from(Some("/custom/kiro".into()), Some(PathBuf::from("/home/u"))),
497            PathBuf::from("/custom/kiro")
498        );
499        assert_eq!(
500            kiro_user_root_from(Some("  ".into()), Some(PathBuf::from("/home/u"))),
501            PathBuf::from("/home/u/.kiro")
502        );
503        assert_eq!(
504            kiro_user_root_from(None, Some(PathBuf::from("/home/u"))),
505            PathBuf::from("/home/u/.kiro")
506        );
507    }
508
509    #[test]
510    fn user_and_project_steering_carry_the_shared_guidance() {
511        let content = kiro_user_steering_content("/usr/bin/sqz");
512        assert!(content.contains(KIRO_STEERING_SENTINEL));
513        assert!(content.ends_with(&crate::codex_integration::sqz_guidance("/usr/bin/sqz")));
514        assert_eq!(content, kiro_steering_content("/usr/bin/sqz"));
515    }
516
517    #[test]
518    fn user_level_steering_roundtrip_via_at_helpers() {
519        let dir = tempfile::tempdir().unwrap();
520        let path = dir.path().join("steering").join("sqz.md");
521        let content = kiro_user_steering_content("/usr/bin/sqz");
522        assert!(install_kiro_steering_at(&path, &content).unwrap());
523        assert!(path.exists());
524        // Idempotent on identical content.
525        assert!(!install_kiro_steering_at(&path, &content).unwrap());
526        // Removes only sqz-authored files.
527        assert!(remove_kiro_steering_at(&path).unwrap());
528        assert!(!path.exists());
529        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
530        std::fs::write(&path, "my own steering\n").unwrap();
531        assert!(!remove_kiro_steering_at(&path).unwrap());
532        assert!(path.exists());
533    }
534
535    #[test]
536    fn steering_state_tracks_current_stale_and_user_files() {
537        let dir = tempfile::tempdir().unwrap();
538        let path = dir.path().join("sqz.md");
539        assert_eq!(kiro_steering_state(&path, true), SteeringState::Missing);
540
541        std::fs::write(&path, kiro_user_steering_content("/opt/bin/sqz")).unwrap();
542        assert_eq!(kiro_steering_state(&path, true), SteeringState::Current);
543        assert_eq!(kiro_steering_state(&path, false), SteeringState::Current);
544
545        // An older template: sentinel present, content no longer matches.
546        let old = kiro_user_steering_content("/opt/bin/sqz").replace("sqz_recall", "recall");
547        std::fs::write(&path, old).unwrap();
548        assert_eq!(kiro_steering_state(&path, true), SteeringState::Stale);
549
550        std::fs::write(&path, "my own notes about sqz\n").unwrap();
551        assert_eq!(kiro_steering_state(&path, true), SteeringState::UserAuthored);
552    }
553
554    #[test]
555    fn refresh_keeps_the_existing_binary_path_when_it_exists() {
556        let dir = tempfile::tempdir().unwrap();
557        let bin = dir.path().join("sqz");
558        std::fs::write(&bin, "").unwrap();
559        let bin = bin.to_string_lossy().to_string();
560        let path = dir.path().join("steering.md");
561        let stale = kiro_user_steering_content(&bin).replace("sqz_recall", "recall");
562        std::fs::write(&path, stale).unwrap();
563
564        assert!(refresh_kiro_steering(&path, true, "/fallback/sqz").unwrap());
565        assert_eq!(std::fs::read_to_string(&path).unwrap(), kiro_user_steering_content(&bin));
566        assert!(!refresh_kiro_steering(&path, true, "/fallback/sqz").unwrap(), "now current");
567    }
568
569    #[test]
570    fn refresh_falls_back_when_the_old_binary_is_gone() {
571        let dir = tempfile::tempdir().unwrap();
572        let path = dir.path().join("steering.md");
573        let stale = kiro_steering_content("/deleted/target/debug/sqz").replace("sqz_recall", "recall");
574        std::fs::write(&path, stale).unwrap();
575        assert!(refresh_kiro_steering(&path, false, "/fallback/sqz").unwrap());
576        assert!(std::fs::read_to_string(&path).unwrap().contains("/fallback/sqz compress"));
577    }
578
579    #[test]
580    fn refresh_never_touches_user_files() {
581        let dir = tempfile::tempdir().unwrap();
582        let path = dir.path().join("steering.md");
583        std::fs::write(&path, "mine\n").unwrap();
584        assert!(!refresh_kiro_steering(&path, true, "/fallback/sqz").unwrap());
585        assert_eq!(std::fs::read_to_string(&path).unwrap(), "mine\n");
586    }
587
588    #[test]
589    fn embedded_path_round_trips_through_template() {
590        let content = kiro_steering_content("/Users/me/.cargo/bin/sqz");
591        assert_eq!(embedded_sqz_path(&content), Some("/Users/me/.cargo/bin/sqz"));
592    }
593
594    #[test]
595    fn user_mcp_merge_preserves_existing_servers() {
596        let dir = tempfile::tempdir().unwrap();
597        let path = dir.path().join("settings").join("mcp.json");
598        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
599        std::fs::write(
600            &path,
601            r#"{"mcpServers":{"builder-mcp":{"command":"/x/builder-mcp","args":[]}}}"#,
602        )
603        .unwrap();
604        assert_eq!(
605            install_kiro_mcp_config_at(&path).unwrap(),
606            KiroMcpInstall::Merged
607        );
608        let parsed: serde_json::Value =
609            serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
610        assert_eq!(parsed["mcpServers"]["builder-mcp"]["command"], "/x/builder-mcp");
611        assert_eq!(parsed["mcpServers"]["sqz"]["command"], "sqz-mcp");
612    }
613}