| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- //! Tests for `recipe_repos` — hermetic `file://` git repos (no network).
- use super::{
- LAST_SYNC_FILE, ReposRegistry, archive_at, auth_url, load_registry, save_registry, sync_one,
- sync_repos, unarchive_at,
- };
- use crate::config::RecipeRepo;
- use std::path::PathBuf;
- fn git_ok(args: &[&str]) {
- let out = std::process::Command::new("git")
- .args(args)
- .output()
- .expect("run git");
- assert!(
- out.status.success(),
- "git {args:?} failed: {}",
- String::from_utf8_lossy(&out.stderr)
- );
- }
- /// Create a temp git repo with `files` (`rel path -> content`), committed;
- /// returns (dir, `file://` url).
- fn make_repo(tag: &str, files: &[(&str, &str)]) -> (PathBuf, String) {
- let dir = std::env::temp_dir().join(format!("sm121-rr-{tag}-{}", std::process::id()));
- let _ = std::fs::remove_dir_all(&dir);
- std::fs::create_dir_all(&dir).unwrap();
- for (rel, content) in files {
- let p = dir.join(rel);
- if let Some(parent) = p.parent() {
- std::fs::create_dir_all(parent).unwrap();
- }
- std::fs::write(&p, content).unwrap();
- }
- let d = dir.display().to_string();
- git_ok(&["-C", &d, "init", "-q"]);
- git_ok(&["-C", &d, "config", "user.email", "t@sm121.test"]);
- git_ok(&["-C", &d, "config", "user.name", "sm121-test"]);
- git_ok(&["-C", &d, "add", "-A"]);
- git_ok(&["-C", &d, "commit", "-qm", "init"]);
- (dir, format!("file://{}", d))
- }
- fn repo(url: String) -> RecipeRepo {
- RecipeRepo { url, token: None }
- }
- fn cache_base(tag: &str) -> PathBuf {
- let p = std::env::temp_dir().join(format!("sm121-rr-cache-{tag}-{}", std::process::id()));
- let _ = std::fs::remove_dir_all(&p);
- p
- }
- #[test]
- fn clone_then_pull_picks_up_new_commits() {
- let (_src_a, a_url) = make_repo(
- "a",
- &[
- (
- "recipes/alpha/recipe.toml",
- "engine = \"vllm\"\nversion = \"v1\"\n",
- ),
- ("recipes/beta/repo-note.txt", "static\n"),
- ],
- );
- let (src_b, b_url) = make_repo(
- "b",
- &[(
- "recipes/beta/recipe.toml",
- "engine = \"sglang\"\nversion = \"b1\"\n",
- )],
- );
- let base = cache_base("pull");
- let reg = ReposRegistry::default();
- // first sync: clones both, in list order
- let roots = sync_repos(
- &[repo(a_url.clone()), repo(b_url.clone())],
- &base,
- ®,
- false,
- );
- assert_eq!(roots.len(), 2, "both repos synced: {roots:?}");
- assert!(roots[0].join("recipes/alpha/recipe.toml").is_file());
- assert!(roots[1].join("recipes/beta/recipe.toml").is_file());
- assert!(roots[0].join(LAST_SYNC_FILE).is_file(), "last-sync stamped");
- // new commit in B → the subsequent sync pulls it
- let b2 = src_b.join("recipes/gamma/recipe.toml");
- std::fs::create_dir_all(b2.parent().unwrap()).unwrap();
- std::fs::write(&b2, "engine = \"vllm\"\n").unwrap();
- let d = src_b.display().to_string();
- git_ok(&["-C", &d, "add", "-A"]);
- git_ok(&["-C", &d, "commit", "-qm", "gamma"]);
- let roots = sync_repos(&[repo(a_url), repo(b_url)], &base, ®, false);
- assert!(
- roots[1].join("recipes/gamma/recipe.toml").is_file(),
- "pull landed gamma"
- );
- let _ = _src_a;
- }
- #[test]
- fn first_repo_shadows_later_on_name_collision() {
- let (_a, a_url) = make_repo(
- "shadow-a",
- &[(
- "recipes/dup/recipe.toml",
- "engine = \"vllm\"\norigin = \"a\"\n",
- )],
- );
- let (_b, b_url) = make_repo(
- "shadow-b",
- &[(
- "recipes/dup/recipe.toml",
- "engine = \"vllm\"\norigin = \"b\"\n",
- )],
- );
- let base = cache_base("shadow");
- let roots = sync_repos(
- &[repo(a_url.clone()), repo(b_url.clone())],
- &base,
- &ReposRegistry::default(),
- false,
- );
- assert_eq!(roots.len(), 2);
- // roots land in recipe_sources as their `recipes/` dirs, in list order →
- // the earlier (higher-priority) repo's copy is the one the search finds first
- let cfg: crate::config::Config = toml::from_str(&format!(
- "recipe_sources = [\"{}\", \"{}\"]\n",
- roots[0].join("recipes").display(),
- roots[1].join("recipes").display()
- ))
- .unwrap();
- let found = crate::config::recipe_file("dup", &cfg.recipe_sources)
- .unwrap_or_else(|| panic!("recipe 'dup' not found under the synced roots"));
- assert!(
- found.starts_with(roots[0].join("recipes").display().to_string().as_str()),
- "first repo shadows the later one: {found:?}"
- );
- }
- #[test]
- fn archived_repo_is_skipped() {
- let (_a, a_url) = make_repo(
- "arch-a",
- &[("recipes/alpha/recipe.toml", "engine = \"vllm\"\n")],
- );
- let (_b, b_url) = make_repo(
- "arch-b",
- &[("recipes/beta/recipe.toml", "engine = \"sglang\"\n")],
- );
- let base = cache_base("arch");
- let reg_path = base.join("repos.json");
- archive_at(®_path, &a_url).unwrap();
- let reg = load_registry(®_path);
- assert!(reg.is_archived(&a_url));
- let roots = sync_repos(
- &[repo(a_url.clone()), repo(b_url.clone())],
- &base,
- ®,
- false,
- );
- assert_eq!(roots.len(), 1, "only the un-archived repo synced");
- assert!(roots[0].join("recipes/beta/recipe.toml").is_file());
- // unarchive → it syncs again
- unarchive_at(®_path, &a_url).unwrap();
- let reg = load_registry(®_path);
- assert!(!reg.is_archived(&a_url));
- let roots = sync_repos(&[repo(a_url), repo(b_url)], &base, ®, false);
- assert_eq!(roots.len(), 2);
- }
- #[test]
- fn unreachable_repo_is_skipped_not_fatal() {
- let (_b, b_url) = make_repo(
- "reach-b",
- &[("recipes/beta/recipe.toml", "engine = \"sglang\"\n")],
- );
- let dead = format!(
- "file://{}/no-such-sm121-repo-{}",
- std::env::temp_dir().display(),
- std::process::id()
- );
- let base = cache_base("reach");
- let roots = sync_repos(
- &[repo(dead), repo(b_url)],
- &base,
- &ReposRegistry::default(),
- false,
- );
- assert_eq!(roots.len(), 1, "the live repo still syncs: {roots:?}");
- assert!(roots[0].join("recipes/beta/recipe.toml").is_file());
- }
- #[test]
- fn force_reclones_from_scratch() {
- let (_src, url) = make_repo(
- "force",
- &[("recipes/beta/recipe.toml", "engine = \"sglang\"\n")],
- );
- let base = cache_base("force");
- let reg = ReposRegistry::default();
- let roots = sync_repos(&[repo(url.clone())], &base, ®, false);
- std::fs::write(roots[0].join("junk.txt"), "x").unwrap();
- let roots = sync_repos(&[repo(url)], &base, ®, true);
- assert!(
- !roots[0].join("junk.txt").exists(),
- "force re-clone dropped the local junk"
- );
- assert!(roots[0].join("recipes/beta/recipe.toml").is_file());
- }
- #[test]
- fn registry_missing_is_fresh() {
- let p = cache_base("reg-missing").join("repos.json");
- let reg = load_registry(&p);
- assert!(reg.archived.is_empty());
- }
- #[test]
- fn registry_corrupt_is_backed_up_and_recreated() {
- let p = cache_base("reg-bak").join("repos.json");
- std::fs::create_dir_all(p.parent().unwrap()).unwrap();
- std::fs::write(&p, "{ not json").unwrap();
- let reg = load_registry(&p);
- assert!(
- reg.archived.is_empty(),
- "corrupt registry degrades to fresh"
- );
- assert!(
- p.with_extension("json.bak").exists(),
- "corrupt file preserved as .bak"
- );
- // and it is writable again
- save_registry(&p, ®).unwrap();
- }
- #[test]
- fn archive_unarchive_round_trip() {
- let p = cache_base("reg-rt").join("repos.json");
- archive_at(&p, "https://git.example/x.git").unwrap();
- assert!(load_registry(&p).is_archived("https://git.example/x.git"));
- // idempotent
- archive_at(&p, "https://git.example/x.git").unwrap();
- assert_eq!(load_registry(&p).archived.len(), 1);
- unarchive_at(&p, "https://git.example/x.git").unwrap();
- assert!(!load_registry(&p).is_archived("https://git.example/x.git"));
- // unarchiving a never-archived url is a no-op
- unarchive_at(&p, "https://git.example/y.git").unwrap();
- }
- #[test]
- fn token_is_embedded_only_for_http_urls() {
- let https = RecipeRepo {
- url: "https://git.kilork.org/sm121/recipes-production.git".into(),
- token: Some("tok123".into()),
- };
- assert_eq!(
- auth_url(&https),
- "https://tok123@git.kilork.org/sm121/recipes-production.git"
- );
- let ssh = RecipeRepo {
- url: "git@git.kilork.org:sm121/recipes.git".into(),
- token: Some("ignored".into()),
- };
- assert_eq!(
- auth_url(&ssh),
- ssh.url,
- "ssh urls pass through (the token is a no-op)"
- );
- let no_token = RecipeRepo {
- url: "https://git.kilork.org/x.git".into(),
- token: None,
- };
- assert_eq!(auth_url(&no_token), no_token.url);
- }
- #[test]
- fn sync_one_stamps_last_sync() {
- let (_src, url) = make_repo("stamp", &[("recipes/beta/recipe.toml", "x = 1\n")]);
- let base = cache_base("stamp");
- let dir = super::repo_cache_dir(&base, &url);
- sync_one(&url, &dir, false).unwrap();
- let ts: i64 = std::fs::read_to_string(dir.join(LAST_SYNC_FILE))
- .unwrap()
- .trim()
- .parse()
- .unwrap();
- let now = crate::stamp::now_secs();
- assert!(ts > now - 60 && ts <= now, "stamp is a recent epoch: {ts}");
- }
|