use super::*;
#[test]
fn apply_subpath_success_case() {
let dir = TempDir::new().unwrap();
let package_root = dir.path().join("plugins/foo");
std::fs::create_dir_all(&package_root).unwrap();
let subpath = SourceSubpath::new("plugins/foo").unwrap();
let rooted = apply_subpath(&SourceName::from("dep"), dir.path(), Some(&subpath)).unwrap();
assert_eq!(rooted.checkout_root, dir.path());
assert_eq!(rooted.package_root, package_root);
}
#[test]
fn apply_subpath_missing_directory_rejection() {
let dir = TempDir::new().unwrap();
let subpath = SourceSubpath::new("plugins/missing").unwrap();
let err = apply_subpath(&SourceName::from("dep"), dir.path(), Some(&subpath))
.unwrap_err()
.to_string();
assert!(
err.contains("does not exist"),
"missing directory should be rejected: {err}"
);
}
#[test]
fn apply_subpath_file_not_dir_rejection() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("plugins");
std::fs::write(&file_path, "not a directory").unwrap();
let subpath = SourceSubpath::new("plugins").unwrap();
let err = apply_subpath(&SourceName::from("dep"), dir.path(), Some(&subpath))
.unwrap_err()
.to_string();
assert!(
err.contains("not a directory"),
"file subpath should be rejected: {err}"
);
}
#[cfg(unix)]
#[test]
fn apply_subpath_traversal_rejection() {
let dir = TempDir::new().unwrap();
let outside = TempDir::new().unwrap();
let outside_pkg = outside.path().join("pkg");
std::fs::create_dir_all(&outside_pkg).unwrap();
std::os::unix::fs::symlink(outside.path(), dir.path().join("escape")).unwrap();
let subpath = SourceSubpath::new("escape").unwrap();
let err = apply_subpath(&SourceName::from("dep"), dir.path(), Some(&subpath))
.unwrap_err()
.to_string();
assert!(
err.contains("escapes checkout root"),
"symlink traversal should be rejected: {err}"
);
}
#[test]
fn single_source_no_deps() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("source-a");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0), (1, 1, 0)]);
provider.add_source("a", tree, None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("^1.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 1);
assert!(graph.nodes.contains_key("a"));
assert_eq!(graph.order.len(), 1);
assert_eq!(graph.order[0], "a");
let node = &graph.nodes["a"];
assert_eq!(node.resolved_ref.version, Some(Version::new(1, 1, 0)));
}
#[test]
fn two_sources_no_deps() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(2, 0, 0)]);
provider.add_source("a", tree_a, None);
provider.add_source("b", tree_b, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v2.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.order.len(), 2);
assert!(graph.order.contains(&"a".into()));
assert!(graph.order.contains(&"b".into()));
}
#[test]
fn source_with_transitive_dep() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_dep = dir.path().join("dep");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_dep).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("dep", "https://example.com/dep.git", ">=0.5.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions(
"https://example.com/dep.git",
vec![(0, 4, 0), (0, 5, 0), (0, 6, 0), (1, 0, 0)],
);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("dep", tree_dep, None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("v1.0.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 2);
assert!(graph.nodes.contains_key("a"));
assert!(graph.nodes.contains_key("dep"));
let dep_node = &graph.nodes["dep"];
assert_eq!(dep_node.resolved_ref.version, Some(Version::new(1, 0, 0)));
assert_eq!(graph.order, vec!["a", "dep"]);
}
#[test]
fn duplicate_source_identity_detects_same_url_and_subpath() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
std::fs::create_dir_all(tree_a.join("plugins/foo")).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, None);
let subpath = SourceSubpath::new("plugins/foo").unwrap();
let mut dependencies = IndexMap::new();
dependencies.insert(
SourceName::from("a"),
EffectiveDependency {
name: "a".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/shared.git"),
Some(subpath.clone()),
),
spec: git_spec("https://example.com/shared.git", Some("v1.0.0")),
subpath: Some(subpath.clone()),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
dependencies.insert(
SourceName::from("b"),
EffectiveDependency {
name: "b".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/shared.git"),
Some(subpath.clone()),
),
spec: git_spec("https://example.com/shared.git", Some("v1.0.0")),
subpath: Some(subpath),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies,
settings: Settings::default(),
};
let err = resolve(&config, &provider, None, &default_options())
.unwrap_err()
.to_string();
assert!(
err.contains("duplicate source identity"),
"expected duplicate identity error: {err}"
);
}
#[test]
fn source_identity_mismatch_detects_different_subpaths_for_same_name() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_dep = dir.path().join("dep");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(tree_dep.join("plugins/foo")).unwrap();
std::fs::create_dir_all(tree_dep.join("plugins/bar")).unwrap();
let mut manifest_deps = IndexMap::new();
manifest_deps.insert(
"dep".to_string(),
ManifestDep {
url: Some(SourceUrl::from("https://example.com/dep.git")),
path: None,
subpath: Some(SourceSubpath::new("plugins/bar").unwrap()),
version: Some(">=1.0.0".to_string()),
filter: FilterConfig::default(),
},
);
let manifest_a = Manifest {
package: PackageInfo {
name: "a".to_string(),
version: "1.0.0".to_string(),
description: None,
},
dependencies: manifest_deps,
models: IndexMap::new(),
};
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/dep.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("dep", tree_dep, None);
let mut dependencies = IndexMap::new();
dependencies.insert(
SourceName::from("a"),
EffectiveDependency {
name: "a".into(),
id: SourceId::git(SourceUrl::from("https://example.com/a.git")),
spec: git_spec("https://example.com/a.git", Some("v1.0.0")),
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
dependencies.insert(
SourceName::from("dep"),
EffectiveDependency {
name: "dep".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/dep.git"),
Some(SourceSubpath::new("plugins/foo").unwrap()),
),
spec: git_spec("https://example.com/dep.git", Some("v1.0.0")),
subpath: Some(SourceSubpath::new("plugins/foo").unwrap()),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies,
settings: Settings::default(),
};
let err = resolve(&config, &provider, None, &default_options())
.unwrap_err()
.to_string();
assert!(
err.contains("conflicting identities"),
"expected identity mismatch error: {err}"
);
}
#[test]
fn transitive_dep_propagates_subpath_into_source_identity() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_dep = dir.path().join("dep");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(tree_dep.join("plugins/foo")).unwrap();
let mut manifest_deps = IndexMap::new();
manifest_deps.insert(
"dep".to_string(),
ManifestDep {
url: Some(SourceUrl::from("https://example.com/dep.git")),
path: None,
subpath: Some(SourceSubpath::new("plugins/foo").unwrap()),
version: Some(">=1.0.0".to_string()),
filter: FilterConfig::default(),
},
);
let manifest_a = Manifest {
package: PackageInfo {
name: "a".to_string(),
version: "1.0.0".to_string(),
description: None,
},
dependencies: manifest_deps,
models: IndexMap::new(),
};
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/dep.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("dep", tree_dep.clone(), None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("v1.0.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
let dep_node = graph.nodes.get("dep").expect("dep should be resolved");
assert_eq!(
dep_node.source_id,
SourceId::git_with_subpath(
SourceUrl::from("example.com/dep"),
Some(SourceSubpath::new("plugins/foo").unwrap())
)
);
assert_eq!(
dep_node.rooted_ref.package_root,
tree_dep.join("plugins/foo")
);
}
#[test]
fn compatible_constraints_from_two_dependents() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions(
"https://example.com/shared.git",
vec![(1, 0, 0), (1, 2, 0), (1, 5, 0), (2, 0, 0)],
);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 3);
let shared_node = &graph.nodes["shared"];
assert_eq!(
shared_node.resolved_ref.version,
Some(Version::new(2, 0, 0))
);
}
#[test]
fn narrower_second_constraint_upgrades_latest_compatible_selection() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.5.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions(
"https://example.com/shared.git",
vec![(1, 0, 0), (1, 2, 0), (1, 5, 0), (2, 0, 0)],
);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options())
.expect("both constraints are satisfiable; should resolve to 2.0.0");
assert_eq!(
graph.nodes["shared"].resolved_ref.version,
Some(Version::new(2, 0, 0)),
"re-resolution must upgrade shared to the newest version satisfying both >=1.0.0 and >=1.5.0"
);
}
#[test]
fn incompatible_constraints_produce_error() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=2.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "<1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions(
"https://example.com/shared.git",
vec![(0, 5, 0), (1, 0, 0), (2, 0, 0)],
);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let result = resolve(&config, &provider, None, &default_options());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("shared"),
"error should mention the conflicting source: {err}"
);
}
#[test]
fn cycle_does_not_error() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("b", "https://example.com/b.git", ">=1.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("a", "https://example.com/a.git", ">=1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("v1.0.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 2);
assert!(graph.nodes.contains_key("a"));
assert!(graph.nodes.contains_key("b"));
}
#[test]
fn same_version_revisit_skips_and_package_fetches_once() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
write_minimal_package_marker(&tree_shared);
write_skill(&tree_shared, "common");
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert!(graph.nodes.contains_key("shared"));
assert_eq!(provider.fetch_count("shared"), 1);
}
#[test]
fn different_second_constraint_re_resolves_to_satisfying_version() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
write_minimal_package_marker(&tree_shared);
write_skill(&tree_shared, "common");
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=1.0.0")],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", ">=2.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0), (2, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options())
.expect(">=1.0.0 and >=2.0.0 are jointly satisfiable by 2.0.0; should not error");
assert_eq!(
graph.nodes["shared"].resolved_ref.version,
Some(Version::new(2, 0, 0)),
"re-resolution must select shared 2.0.0 (latest satisfying >=1.0.0 ∩ >=2.0.0)"
);
}
#[test]
fn latest_and_pinned_revisit_re_resolves_to_pinned_version() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
write_minimal_package_marker(&tree_shared);
write_skill(&tree_shared, "common");
let mut deps_a = IndexMap::new();
deps_a.insert(
"shared".to_string(),
ManifestDep {
url: Some(SourceUrl::from("https://example.com/shared.git")),
path: None,
subpath: None,
version: None,
filter: FilterConfig::default(),
},
);
let manifest_a = Manifest {
package: PackageInfo {
name: "a".to_string(),
version: "1.0.0".to_string(),
description: None,
},
dependencies: deps_a,
models: IndexMap::new(),
};
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "v1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0), (2, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options())
.expect("Latest + exact-pin are jointly satisfiable by 1.0.0; should not error");
assert_eq!(
graph.nodes["shared"].resolved_ref.version,
Some(Version::new(1, 0, 0)),
"re-resolution must downgrade shared from 2.0.0 to 1.0.0 to satisfy the exact pin"
);
}
#[test]
fn normal_mode_falls_back_when_locked_commit_unreachable() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("a");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0), (1, 1, 0)]);
provider.add_source("a", tree, None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("^1.0")),
)]);
let unreachable_commit = "missing-locked-sha";
provider.mark_unreachable_preferred_commit(unreachable_commit);
let mut lock = LockFile::empty();
lock.dependencies.insert(
"a".into(),
crate::lock::LockedSource {
url: Some("https://example.com/a.git".into()),
path: None,
subpath: None,
version: Some("v1.1.0".into()),
commit: Some(unreachable_commit.into()),
tree_hash: None,
},
);
let graph = resolve(&config, &provider, Some(&lock), &default_options()).unwrap();
assert_eq!(
graph.nodes["a"].resolved_ref.version,
Some(Version::new(1, 1, 0))
);
assert_eq!(
graph.nodes["a"].resolved_ref.commit.as_deref(),
Some("mock-commit")
);
assert_eq!(
provider.seen_preferred_commits(),
vec![Some(unreachable_commit.to_string()), None]
);
}
#[test]
fn frozen_mode_errors_when_locked_commit_unreachable() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("a");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0), (1, 1, 0)]);
provider.add_source("a", tree, None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("^1.0")),
)]);
let unreachable_commit = "missing-locked-sha";
provider.mark_unreachable_preferred_commit(unreachable_commit);
let mut lock = LockFile::empty();
lock.dependencies.insert(
"a".into(),
crate::lock::LockedSource {
url: Some("https://example.com/a.git".into()),
path: None,
subpath: None,
version: Some("v1.1.0".into()),
commit: Some(unreachable_commit.into()),
tree_hash: None,
},
);
let options = ResolveOptions::frozen();
let result = resolve(&config, &provider, Some(&lock), &options);
assert!(matches!(
result,
Err(MarsError::LockedCommitUnreachable { .. })
));
assert_eq!(
provider.seen_preferred_commits(),
vec![Some(unreachable_commit.to_string())]
);
}
#[test]
fn source_without_manifest_has_no_transitive_deps() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("a");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_source("a", tree, None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("v1.0.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 1);
assert!(graph.nodes["a"].deps.is_empty());
}
#[test]
fn path_source_resolves_without_version() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("local-source");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_source("local", tree.clone(), None);
let config = make_config(vec![("local", SourceSpec::Path(tree))]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 1);
let node = &graph.nodes["local"];
assert!(node.resolved_ref.version.is_none());
assert!(node.latest_version.is_none());
}
#[test]
fn local_path_source_resolves_transitive_path_dependencies() {
let dir = TempDir::new().unwrap();
let app = dir.path().join("app");
let shared = dir.path().join("shared");
let planning = dir.path().join("planning");
std::fs::create_dir_all(&app).unwrap();
std::fs::create_dir_all(&shared).unwrap();
std::fs::create_dir_all(&planning).unwrap();
std::fs::write(
app.join("mars.toml"),
"[package]\nname = \"app\"\nversion = \"1.0.0\"\n\n[dependencies.shared]\npath = \"../shared\"\n",
)
.unwrap();
std::fs::write(
shared.join("mars.toml"),
"[package]\nname = \"shared\"\nversion = \"1.0.0\"\n\n[dependencies.planning]\npath = \"../planning\"\n",
)
.unwrap();
std::fs::write(
planning.join("mars.toml"),
"[package]\nname = \"planning\"\nversion = \"1.0.0\"\n",
)
.unwrap();
write_agent(&app, "coder", &["planning"]);
write_skill(&planning, "planning");
let provider = MockProvider::new();
let config = make_config(vec![("app", SourceSpec::Path(app))]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert!(graph.nodes.contains_key("app"));
assert!(graph.nodes.contains_key("shared"));
assert!(graph.nodes.contains_key("planning"));
}
#[test]
fn alphabetical_order_linear_chain() {
let mut nodes = IndexMap::new();
nodes.insert(
"c".into(),
ResolvedNode {
source_name: "c".into(),
source_id: SourceId::git(SourceUrl::from("example.com/c")),
resolved_ref: dummy_ref("c"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["b".into()],
},
);
nodes.insert(
"b".into(),
ResolvedNode {
source_name: "b".into(),
source_id: SourceId::git(SourceUrl::from("example.com/b")),
resolved_ref: dummy_ref("b"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["a".into()],
},
);
nodes.insert(
"a".into(),
ResolvedNode {
source_name: "a".into(),
source_id: SourceId::git(SourceUrl::from("example.com/a")),
resolved_ref: dummy_ref("a"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec![],
},
);
let order = alphabetical_order(&nodes);
assert_eq!(order, vec!["a", "b", "c"]);
}
#[test]
fn alphabetical_order_ignores_dependency_shape() {
let mut nodes = IndexMap::new();
nodes.insert(
"a".into(),
ResolvedNode {
source_name: "a".into(),
source_id: SourceId::git(SourceUrl::from("example.com/a")),
resolved_ref: dummy_ref("a"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["b".into(), "c".into()],
},
);
nodes.insert(
"b".into(),
ResolvedNode {
source_name: "b".into(),
source_id: SourceId::git(SourceUrl::from("example.com/b")),
resolved_ref: dummy_ref("b"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["d".into()],
},
);
nodes.insert(
"c".into(),
ResolvedNode {
source_name: "c".into(),
source_id: SourceId::git(SourceUrl::from("example.com/c")),
resolved_ref: dummy_ref("c"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["d".into()],
},
);
nodes.insert(
"d".into(),
ResolvedNode {
source_name: "d".into(),
source_id: SourceId::git(SourceUrl::from("example.com/d")),
resolved_ref: dummy_ref("d"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec![],
},
);
let order = alphabetical_order(&nodes);
assert_eq!(order, vec!["a", "b", "c", "d"]);
}
#[test]
fn alphabetical_order_no_deps() {
let mut nodes = IndexMap::new();
nodes.insert(
"a".into(),
ResolvedNode {
source_name: "a".into(),
source_id: SourceId::git(SourceUrl::from("example.com/a")),
resolved_ref: dummy_ref("a"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec![],
},
);
nodes.insert(
"b".into(),
ResolvedNode {
source_name: "b".into(),
source_id: SourceId::git(SourceUrl::from("example.com/b")),
resolved_ref: dummy_ref("b"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec![],
},
);
let order = alphabetical_order(&nodes);
assert_eq!(order.len(), 2);
assert_eq!(order, vec!["a", "b"]);
}
#[test]
fn alphabetical_order_is_stable_for_cycles() {
let mut nodes = IndexMap::new();
nodes.insert(
"a".into(),
ResolvedNode {
source_name: "a".into(),
source_id: SourceId::git(SourceUrl::from("example.com/a")),
resolved_ref: dummy_ref("a"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["b".into()],
},
);
nodes.insert(
"b".into(),
ResolvedNode {
source_name: "b".into(),
source_id: SourceId::git(SourceUrl::from("example.com/b")),
resolved_ref: dummy_ref("b"),
rooted_ref: dummy_rooted_ref(),
latest_version: None,
manifest: None,
deps: vec!["a".into()],
},
);
let order = alphabetical_order(&nodes);
assert_eq!(order, vec!["a", "b"]);
}
#[test]
fn apply_subpath_none_yields_checkout_as_package_root() {
let dir = TempDir::new().unwrap();
let rooted = apply_subpath(&SourceName::from("dep"), dir.path(), None).unwrap();
assert_eq!(rooted.checkout_root, dir.path());
assert_eq!(rooted.package_root, dir.path());
}
#[test]
fn resolver_reads_manifest_from_package_root_not_checkout_root() {
let dir = TempDir::new().unwrap();
let checkout = dir.path().join("checkout");
let package_root = checkout.join("plugins/foo");
std::fs::create_dir_all(&package_root).unwrap();
let manifest = Manifest {
package: PackageInfo {
name: "foo".to_string(),
version: "1.0.0".to_string(),
description: None,
},
dependencies: IndexMap::new(),
models: IndexMap::new(),
};
let subpath = SourceSubpath::new("plugins/foo").unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/repo.git", vec![(1, 0, 0)]);
provider.trees.insert("dep".to_string(), checkout.clone());
provider
.manifests
.insert(package_root.clone(), Some(manifest.clone()));
provider.manifests.insert(checkout.clone(), None);
let mut dependencies = IndexMap::new();
dependencies.insert(
SourceName::from("dep"),
EffectiveDependency {
name: "dep".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/repo.git"),
Some(subpath.clone()),
),
spec: git_spec("https://example.com/repo.git", Some("v1.0.0")),
subpath: Some(subpath),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies,
settings: Settings::default(),
};
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
let node = graph.nodes.get("dep").expect("dep should be in graph");
assert!(
node.manifest.is_some(),
"manifest should be loaded from package_root; got None — checkout_root was likely used instead"
);
assert_eq!(node.rooted_ref.package_root, package_root);
assert_eq!(node.rooted_ref.checkout_root, checkout);
}
#[test]
fn two_subpaths_same_url_resolve_to_distinct_package_roots() {
let dir = TempDir::new().unwrap();
let checkout_a = dir.path().join("a");
let checkout_b = dir.path().join("b");
let pkg_a = checkout_a.join("plugins/foo");
let pkg_b = checkout_b.join("plugins/bar");
std::fs::create_dir_all(&pkg_a).unwrap();
std::fs::create_dir_all(&pkg_b).unwrap();
let subpath_foo = SourceSubpath::new("plugins/foo").unwrap();
let subpath_bar = SourceSubpath::new("plugins/bar").unwrap();
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/mono.git", vec![(1, 0, 0)]);
provider.add_source("dep-a", checkout_a.clone(), None);
provider.add_source("dep-b", checkout_b.clone(), None);
let mut dependencies = IndexMap::new();
dependencies.insert(
SourceName::from("dep-a"),
EffectiveDependency {
name: "dep-a".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/mono.git"),
Some(subpath_foo.clone()),
),
spec: git_spec("https://example.com/mono.git", Some("v1.0.0")),
subpath: Some(subpath_foo),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
dependencies.insert(
SourceName::from("dep-b"),
EffectiveDependency {
name: "dep-b".into(),
id: SourceId::git_with_subpath(
SourceUrl::from("https://example.com/mono.git"),
Some(subpath_bar.clone()),
),
spec: git_spec("https://example.com/mono.git", Some("v1.0.0")),
subpath: Some(subpath_bar),
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies,
settings: Settings::default(),
};
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 2);
let node_a = graph.nodes.get("dep-a").expect("dep-a should be resolved");
let node_b = graph.nodes.get("dep-b").expect("dep-b should be resolved");
assert_eq!(node_a.rooted_ref.package_root, pkg_a);
assert_eq!(node_b.rooted_ref.package_root, pkg_b);
assert_ne!(
node_a.rooted_ref.package_root,
node_b.rooted_ref.package_root
);
}
#[test]
fn transitive_dep_without_subpath_has_none_in_source_identity() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_dep = dir.path().join("dep");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_dep).unwrap();
let mut manifest_deps = IndexMap::new();
manifest_deps.insert(
"dep".to_string(),
ManifestDep {
url: Some(SourceUrl::from("https://example.com/dep.git")),
path: None,
subpath: None,
version: Some(">=1.0.0".to_string()),
filter: FilterConfig::default(),
},
);
let manifest_a = Manifest {
package: PackageInfo {
name: "a".to_string(),
version: "1.0.0".to_string(),
description: None,
},
dependencies: manifest_deps,
models: IndexMap::new(),
};
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/dep.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("dep", tree_dep.clone(), None);
let config = make_config(vec![(
"a",
git_spec("https://example.com/a.git", Some("v1.0.0")),
)]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
let dep_node = graph.nodes.get("dep").expect("dep should be in graph");
assert_eq!(
dep_node.source_id,
SourceId::git_with_subpath(SourceUrl::from("example.com/dep"), None)
);
assert_eq!(dep_node.rooted_ref.package_root, tree_dep);
assert_eq!(dep_node.rooted_ref.checkout_root, tree_dep);
}
#[test]
fn ssh_and_https_url_forms_have_same_canonical_source_id() {
let ssh_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"git@example.com:org/repo.git",
)),
None,
);
let https_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://example.com/org/repo.git",
)),
None,
);
assert_eq!(
ssh_id, https_id,
"SSH and HTTPS of the same repo must produce equal SourceIds"
);
}
#[test]
fn ssh_and_https_direct_deps_same_repo_detected_as_duplicate() {
let dir = TempDir::new().unwrap();
let tree = dir.path().join("shared");
std::fs::create_dir_all(&tree).unwrap();
let mut provider = MockProvider::new();
provider.add_versions("git@example.com:org/shared.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/org/shared.git", vec![(1, 0, 0)]);
provider.add_source("dep-a", tree.clone(), None);
provider.add_source("dep-b", tree, None);
let canonical_url = SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://example.com/org/shared.git",
));
let mut deps = IndexMap::new();
deps.insert(
SourceName::from("dep-a"),
EffectiveDependency {
name: "dep-a".into(),
id: SourceId::git_with_subpath(canonical_url.clone(), None),
spec: git_spec("git@example.com:org/shared.git", Some("v1.0.0")),
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
deps.insert(
SourceName::from("dep-b"),
EffectiveDependency {
name: "dep-b".into(),
id: SourceId::git_with_subpath(canonical_url, None),
spec: git_spec("https://example.com/org/shared.git", Some("v1.0.0")),
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies: deps,
settings: Settings::default(),
};
let err = resolve(&config, &provider, None, &default_options())
.unwrap_err()
.to_string();
assert!(
err.contains("duplicate source identity"),
"SSH and HTTPS of same repo should be detected as duplicate: {err}"
);
}
#[test]
fn transitive_dep_https_converges_with_direct_dep_ssh_same_canonical() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![("shared", "https://example.com/org/shared.git", ">=1.0.0")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("git@example.com:org/shared.git", vec![(1, 0, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("shared", tree_shared, None);
let ssh_canonical = SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"git@example.com:org/shared.git",
));
let mut deps = IndexMap::new();
deps.insert(
SourceName::from("a"),
EffectiveDependency {
name: "a".into(),
id: SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://example.com/a.git",
)),
None,
),
spec: git_spec("https://example.com/a.git", Some("v1.0.0")),
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
deps.insert(
SourceName::from("shared"),
EffectiveDependency {
name: "shared".into(),
id: SourceId::git_with_subpath(ssh_canonical, None),
spec: git_spec("git@example.com:org/shared.git", Some("v1.0.0")),
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let config = EffectiveConfig {
dependencies: deps,
settings: Settings::default(),
};
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert!(
graph.nodes.contains_key("shared"),
"shared should be resolved"
);
assert!(graph.nodes.contains_key("a"), "a should be resolved");
}
#[test]
fn different_host_or_path_does_not_produce_false_convergence() {
let github_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://github.com/org/repo.git",
)),
None,
);
let gitlab_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://gitlab.com/org/repo.git",
)),
None,
);
assert_ne!(
github_id, gitlab_id,
"Different hosts must produce distinct SourceIds"
);
let repo_a_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://github.com/org/repo-a.git",
)),
None,
);
let repo_b_id = SourceId::git_with_subpath(
SourceUrl::from(crate::source::canonical::canonicalize_git_url(
"https://github.com/org/repo-b.git",
)),
None,
);
assert_ne!(
repo_a_id, repo_b_id,
"Different repo paths must produce distinct SourceIds"
);
}
#[test]
fn transitive_latest_constraint_upgrades_already_resolved_version() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![(
"shared",
"https://example.com/shared.git",
">=1.0.0, <2.0.0",
)],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0), (1, 2, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 3, "a, b, shared should all be resolved");
let shared_node = &graph.nodes["shared"];
assert_eq!(
shared_node.resolved_ref.version,
Some(Version::new(1, 2, 0)),
"shared must resolve to 1.2.0 (Latest+semver maximizes within >=1.0,<2.0)"
);
}
#[test]
fn transitive_latest_constraint_order_independent_b_first() {
let dir = TempDir::new().unwrap();
let tree_a = dir.path().join("a");
let tree_b = dir.path().join("b");
let tree_shared = dir.path().join("shared");
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_shared).unwrap();
let manifest_a = make_manifest(
"a",
"1.0.0",
vec![(
"shared",
"https://example.com/shared.git",
">=1.0.0, <2.0.0",
)],
);
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0), (1, 2, 0)]);
provider.add_source("a", tree_a, Some(manifest_a));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("shared", tree_shared, None);
let config = make_config(vec![
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
("a", git_spec("https://example.com/a.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert_eq!(graph.nodes.len(), 3, "a, b, shared should all be resolved");
let shared_node = &graph.nodes["shared"];
assert_eq!(
shared_node.resolved_ref.version,
Some(Version::new(1, 2, 0)),
"shared must resolve to 1.2.0 regardless of processing order"
);
}
#[test]
fn restart_fresh_context_drops_removed_transitive_dependency_and_lock_entry() {
let dir = TempDir::new().unwrap();
let tree_a_v1 = dir.path().join("a-v1");
let tree_a_v2 = dir.path().join("a-v2");
let tree_b = dir.path().join("b");
let tree_x = dir.path().join("x");
std::fs::create_dir_all(&tree_a_v1).unwrap();
std::fs::create_dir_all(&tree_a_v2).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_x).unwrap();
let manifest_a_v1 = make_manifest(
"a",
"1.0.0",
vec![("x", "https://example.com/x.git", ">=1.0.0")],
);
let manifest_a_v2 = make_manifest("a", "2.0.0", vec![]);
let manifest_b = make_manifest("b", "1.0.0", vec![("a", "https://example.com/a.git", "")]);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0), (2, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/x.git", vec![(1, 0, 0)]);
provider.add_versioned_source("a", "v1.0.0", tree_a_v1, Some(manifest_a_v1));
provider.add_versioned_source("a", "v2.0.0", tree_a_v2, Some(manifest_a_v2));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("x", tree_x, None);
let config = make_config(vec![
(
"a",
git_spec("https://example.com/a.git", Some(">=1.0.0, <3.0.0")),
),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert!(
!graph.nodes.contains_key("x"),
"X should be absent after fresh-context restart on A@v2"
);
let lock = crate::lock::build(
&graph,
&crate::sync::apply::ApplyResult {
outcomes: Vec::new(),
},
&LockFile::empty(),
std::collections::BTreeMap::new(),
)
.unwrap();
assert!(
!lock.dependencies.contains_key("x"),
"lock dependencies should not keep removed transitive dep X"
);
}
#[test]
fn restart_fresh_context_materializes_new_transitive_dependency_filters() {
let dir = TempDir::new().unwrap();
let tree_a_v1 = dir.path().join("a-v1");
let tree_a_v2 = dir.path().join("a-v2");
let tree_b = dir.path().join("b");
let tree_y = dir.path().join("y");
std::fs::create_dir_all(&tree_a_v1).unwrap();
std::fs::create_dir_all(&tree_a_v2).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
std::fs::create_dir_all(&tree_y).unwrap();
let manifest_a_v1 = make_manifest("a", "1.0.0", vec![]);
let manifest_a_v2 = make_manifest(
"a",
"2.0.0",
vec![("y", "https://example.com/y.git", ">=1.0.0")],
);
let manifest_b = make_manifest("b", "1.0.0", vec![("a", "https://example.com/a.git", "")]);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/a.git", vec![(1, 0, 0), (2, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/y.git", vec![(1, 0, 0)]);
provider.add_versioned_source("a", "v1.0.0", tree_a_v1, Some(manifest_a_v1));
provider.add_versioned_source("a", "v2.0.0", tree_a_v2, Some(manifest_a_v2));
provider.add_source("b", tree_b, Some(manifest_b));
provider.add_source("y", tree_y, None);
let config = make_config(vec![
(
"a",
git_spec("https://example.com/a.git", Some(">=1.0.0, <3.0.0")),
),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
assert!(
graph.nodes.contains_key("y"),
"Y should be present after A re-resolves to v2"
);
let y_filters = graph
.filters
.get("y")
.expect("Y must receive materialization filters on restart");
assert!(
y_filters
.iter()
.any(|filter| matches!(filter, FilterMode::All)),
"Y should receive an unfiltered materialization request so sync includes it"
);
}
#[test]
fn latest_revisit_ignores_locked_commit_even_when_version_matches() {
let dir = TempDir::new().unwrap();
let tree_shared = dir.path().join("shared");
let tree_b = dir.path().join("b");
std::fs::create_dir_all(&tree_shared).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_source("shared", tree_shared, None);
provider.add_source("b", tree_b, Some(manifest_b));
let config = make_config(vec![
(
"shared",
git_spec("https://example.com/shared.git", Some("^1.0")),
),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let locked_commit = "locked-sha-123";
let mut lock = LockFile::empty();
lock.dependencies.insert(
"shared".into(),
crate::lock::LockedSource {
url: Some("https://example.com/shared.git".into()),
path: None,
subpath: None,
version: Some("v1.0.0".into()),
commit: Some(locked_commit.into()),
tree_hash: None,
},
);
let graph = resolve(&config, &provider, Some(&lock), &default_options()).unwrap();
assert_eq!(
graph.nodes["shared"].resolved_ref.version,
Some(Version::new(1, 0, 0))
);
assert_eq!(
graph.nodes["shared"].resolved_ref.commit.as_deref(),
Some("mock-commit"),
"`latest` must force current resolution instead of replaying the locked commit"
);
}
#[test]
fn monotonic_restart_converges_for_more_than_32_packages() {
let dir = TempDir::new().unwrap();
let mut provider = MockProvider::new();
let mut dependencies = IndexMap::new();
const PAIR_COUNT: usize = 40;
for idx in 0..PAIR_COUNT {
let a_name = format!("a-{idx}");
let b_name = format!("b-{idx}");
let a_url = format!("https://example.com/{a_name}.git");
let b_url = format!("https://example.com/{b_name}.git");
let tree_a = dir.path().join(&a_name);
let tree_b = dir.path().join(&b_name);
std::fs::create_dir_all(&tree_a).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let manifest_b = make_manifest(&b_name, "1.0.0", vec![(&a_name, &a_url, "")]);
provider.add_versions(&a_url, vec![(1, 0, 0), (2, 0, 0)]);
provider.add_versions(&b_url, vec![(1, 0, 0)]);
provider.add_source(&a_name, tree_a, None);
provider.add_source(&b_name, tree_b, Some(manifest_b));
let a_spec = git_spec(&a_url, Some(">=1.0.0, <3.0.0"));
dependencies.insert(
SourceName::from(a_name.clone()),
EffectiveDependency {
name: a_name.clone().into(),
id: source_id_for_spec(&a_spec, None),
spec: a_spec,
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
let b_spec = git_spec(&b_url, Some("v1.0.0"));
dependencies.insert(
SourceName::from(b_name.clone()),
EffectiveDependency {
name: b_name.clone().into(),
id: source_id_for_spec(&b_spec, None),
spec: b_spec,
subpath: None,
filter: FilterMode::All,
rename: RenameMap::new(),
is_overridden: false,
original_git: None,
},
);
}
let config = EffectiveConfig {
dependencies,
settings: Settings::default(),
};
let graph = resolve(&config, &provider, None, &default_options())
.expect("monotonic one-restart-per-package convergence should succeed");
assert_eq!(graph.nodes.len(), PAIR_COUNT * 2);
for idx in 0..PAIR_COUNT {
let a_name = format!("a-{idx}");
assert_eq!(
graph
.nodes
.get(a_name.as_str())
.expect("each A package should resolve")
.resolved_ref
.version,
Some(Version::new(2, 0, 0)),
"{a_name} should converge to the latest satisfying version after restart"
);
}
}
#[test]
fn restart_override_preserves_latest_version_metadata() {
let dir = TempDir::new().unwrap();
let tree_shared = dir.path().join("shared");
let tree_b = dir.path().join("b");
std::fs::create_dir_all(&tree_shared).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "")],
);
let mut provider = MockProvider::new();
provider.add_versions(
"https://example.com/shared.git",
vec![(1, 0, 0), (1, 2, 0), (2, 0, 0)],
);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_source("shared", tree_shared, None);
provider.add_source("b", tree_b, Some(manifest_b));
let config = make_config(vec![
(
"shared",
git_spec("https://example.com/shared.git", Some(">=1.0.0, <2.0.0")),
),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let graph = resolve(&config, &provider, None, &default_options()).unwrap();
let shared = graph.nodes.get("shared").expect("shared should resolve");
assert_eq!(shared.resolved_ref.version, Some(Version::new(1, 2, 0)));
assert_eq!(
shared.latest_version,
Some(Version::new(2, 0, 0)),
"latest_version should survive override-based restart"
);
assert!(
provider.fetch_count("shared") > 1,
"shared should be re-resolved at least once to exercise override path"
);
}
#[test]
fn oscillating_ref_selection_errors_with_ref_cycle() {
let dir = TempDir::new().unwrap();
let tree_shared = dir.path().join("shared");
let tree_b = dir.path().join("b");
std::fs::create_dir_all(&tree_shared).unwrap();
std::fs::create_dir_all(&tree_b).unwrap();
let manifest_b = make_manifest(
"b",
"1.0.0",
vec![("shared", "https://example.com/shared.git", "")],
);
let mut provider = MockProvider::new();
provider.add_versions("https://example.com/shared.git", vec![(1, 0, 0)]);
provider.add_versions("https://example.com/b.git", vec![(1, 0, 0)]);
provider.add_source("shared", tree_shared, None);
provider.add_source("b", tree_b, Some(manifest_b));
provider.set_commit_sequence("shared", vec!["osc-a", "osc-b"]);
let config = make_config(vec![
(
"shared",
git_spec("https://example.com/shared.git", Some("^1.0")),
),
("b", git_spec("https://example.com/b.git", Some("v1.0.0"))),
]);
let result = resolve(&config, &provider, None, &default_options());
match result {
Err(MarsError::Resolution(ResolutionError::VersionConflict { name, message })) => {
assert_eq!(name, "shared");
assert!(
message.contains("resolution oscillation detected for `shared`"),
"oscillation message should name package: {message}"
);
assert!(
message.contains("v1.0.0@osc-a") || message.contains("v1.0.0@osc-b"),
"oscillation message should include ref cycle details: {message}"
);
}
other => panic!("expected oscillation VersionConflict, got {other:?}"),
}
}