use super::super::harness::ENV_LITANY_HOME;
use super::*;
use crate::template::RealGit;
use crate::workspace::fixture;
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
struct StubEnv(HashMap<&'static str, OsString>);
impl EnvLookup for StubEnv {
fn get(&self, key: &str) -> Option<OsString> {
self.0.get(key).cloned()
}
}
fn env(repo: &Path, home: &Path) -> StubEnv {
let mut m = HashMap::new();
m.insert(ENV_CONV_REPO, repo.as_os_str().to_owned());
m.insert(ENV_CONV_BRANCH, OsString::from("a1"));
m.insert(ENV_LITANY_HOME, home.as_os_str().to_owned());
StubEnv(m)
}
fn input(name: &str) -> Cursor<Vec<u8>> {
Cursor::new(serde_json::json!({ "name": name }).to_string().into_bytes())
}
fn manifest(name: &str, body: &str) -> String {
format!("---\nname: {name}\ndescription: d\n---\n{body}")
}
fn workspace_with(files: &[(&str, &str)]) -> (TempDir, PathBuf) {
let (holder, ws) = fixture::workspace();
fixture::spawn_root(&ws, "a1");
fixture::amend_config(&ws, files);
(holder, ws)
}
fn pool_skill(home: &Path, name: &str, body: &str) {
let dir = home.join("skills").join(name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("SKILL.md"), manifest(name, body)).unwrap();
}
fn staged(ws: &Path) -> String {
RealGit::new()
.run_capture(&ws.join("agents/a1"), &["diff", "--cached", "--name-only"])
.unwrap()
}
#[test]
fn a_workspace_skill_is_checked_out_of_the_followed_config_commit() {
let notes = manifest("notes", "the workspace's own lesson");
let (_h, ws) = workspace_with(&[("skills/notes/SKILL.md", notes.as_str())]);
let home = TempDir::new().unwrap();
let mut out = Vec::new();
run(&mut input("notes"), &mut out, &env(&ws, home.path())).unwrap();
let payload: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(payload["status"], "loaded");
assert_eq!(payload["path"], "skills/notes");
let dest = ws.join("agents/a1/skills/notes/SKILL.md");
assert_eq!(std::fs::read_to_string(&dest).unwrap(), notes);
assert!(
staged(&ws).contains("skills/notes/SKILL.md"),
"{}",
staged(&ws)
);
}
#[test]
fn a_name_both_homes_hold_loads_the_followed_tips_body() {
let ours = manifest("notes", "the workspace's own");
let (_h, ws) = workspace_with(&[("skills/notes/SKILL.md", ours.as_str())]);
let home = TempDir::new().unwrap();
pool_skill(home.path(), "notes", "the install's");
let mut out = Vec::new();
run(&mut input("notes"), &mut out, &env(&ws, home.path())).unwrap();
let dest = ws.join("agents/a1/skills/notes/SKILL.md");
assert_eq!(std::fs::read_to_string(&dest).unwrap(), ours);
}
#[test]
fn a_name_neither_home_holds_is_declined_naming_both() {
let notes = manifest("notes", "ours");
let (_h, ws) = workspace_with(&[("skills/notes/SKILL.md", notes.as_str())]);
let home = TempDir::new().unwrap();
pool_skill(home.path(), "git-ops", "theirs");
let err = run(&mut input("nope"), &mut Vec::new(), &env(&ws, home.path())).unwrap_err();
let msg = err.to_string();
assert!(matches!(err, Error::Unknown { .. }), "{msg}");
assert!(msg.contains("notes"), "{msg}");
assert!(msg.contains("git-ops"), "{msg}");
}
#[test]
fn the_archive_is_neither_listed_nor_loadable() {
let old = manifest("old", "retired");
let notes = manifest("notes", "ours");
let (_h, ws) = workspace_with(&[
("skills/notes/SKILL.md", notes.as_str()),
("skills/archived/old/SKILL.md", old.as_str()),
]);
let home = TempDir::new().unwrap();
let err = run(
&mut input("archived"),
&mut Vec::new(),
&env(&ws, home.path()),
)
.unwrap_err();
assert!(matches!(err, Error::Archived(_)), "{err}");
let err = run(
&mut input("archived/old"),
&mut Vec::new(),
&env(&ws, home.path()),
)
.unwrap_err();
assert!(matches!(err, Error::BadName(_)), "{err}");
let err = run(&mut input("nope"), &mut Vec::new(), &env(&ws, home.path())).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("notes"), "{msg}");
assert!(!msg.contains("archived"), "{msg}");
}
#[test]
fn a_branch_with_no_config_ancestry_declines_naming_the_lineage() {
let repo = TempDir::new().unwrap();
let home = TempDir::new().unwrap();
let err = run(
&mut input("notes"),
&mut Vec::new(),
&env(repo.path(), home.path()),
)
.unwrap_err();
assert!(matches!(err, Error::Lineage(_)), "{err}");
}
#[test]
fn a_failed_checkout_of_a_workspace_skill_surfaces_checkout() {
struct RefusesCheckout(RealGit);
impl GitRunner for RefusesCheckout {
fn run(&self, dest: &Path, args: &[&str]) -> std::io::Result<()> {
if args.first() == Some(&"checkout") {
return Err(std::io::Error::other("checkout refused"));
}
self.0.run(dest, args)
}
fn run_capture(&self, dest: &Path, args: &[&str]) -> std::io::Result<String> {
self.0.run_capture(dest, args)
}
}
let notes = manifest("notes", "ours");
let (_h, ws) = workspace_with(&[("skills/notes/SKILL.md", notes.as_str())]);
let home = TempDir::new().unwrap();
let err = run_with(
&mut input("notes"),
&mut Vec::new(),
&env(&ws, home.path()),
&RefusesCheckout(RealGit::new()),
)
.unwrap_err();
assert!(matches!(err, Error::Checkout { .. }), "{err}");
}