use super::{ShippedChecks, shipped_checks};
use crate::candidate_prediction::CandidateRequest;
use crate::command_test_support::os_arguments;
use crate::diagnostics::EngineFailureCode;
use crate::policy_content::LifecycleContent;
use crate::policy_engine::{PolicyChecks, PolicyFinding, PolicyOutcome};
use crate::policy_registry::PolicyId;
use crate::policy_test_support::{
ScriptedFacts, linked_worktree, main_worktree, outside_worktree, scripted_facts,
};
use crate::policy_trigger::Trigger;
use crate::rule_linked_worktree::{
GuardedCommand, main_worktree_message, outside_worktree_message,
};
use crate::test_support::{fixture, remove};
use crate::worktree_identity::WorktreeIdentity;
use std::path::PathBuf;
fn checks(values: &[&str], facts: ScriptedFacts) -> ShippedChecks<ScriptedFacts> {
return shipped_checks(
facts,
os_arguments(values),
LifecycleContent::None,
Vec::<PathBuf>::new(),
);
}
fn pre_forward(
policy: PolicyId,
values: &[&str],
facts: ScriptedFacts,
) -> (PolicyOutcome, Vec<String>) {
let mut shipped: ShippedChecks<ScriptedFacts> = checks(values, facts);
let outcome: PolicyOutcome = shipped.check(policy, Trigger::PreForward);
return (outcome, shipped.facts.asked);
}
fn clean() -> PolicyOutcome {
return PolicyOutcome::Findings(Vec::<PolicyFinding>::new());
}
fn one(code: &'static str, message: &str) -> PolicyOutcome {
return PolicyOutcome::Findings(vec![PolicyFinding {
code,
message: String::from(message),
path: None,
location: None,
fix_available: false,
}]);
}
fn located(location: crate::repository_location::RepositoryLocation) -> ScriptedFacts {
let mut facts: ScriptedFacts = scripted_facts();
facts.location = Ok(location);
return facts;
}
fn unlocatable() -> ScriptedFacts {
let mut facts: ScriptedFacts = scripted_facts();
facts.location = Err(String::from("git could not be asked"));
return facts;
}
fn location_only() -> Vec<String> {
return vec![String::from("location")];
}
#[test]
fn require_root_passes_exempt_commands_and_the_top_level() {
for values in [
vec![],
vec!["--version"],
vec!["help"],
vec!["init"],
vec!["clone", "url"],
vec!["config", "--global", "user.name", "x"],
vec!["config", "list"],
] {
assert_eq!(
pre_forward(PolicyId::RequireRoot, values.as_slice(), unlocatable()),
(clean(), Vec::<String>::new()),
"{values:?}"
);
}
for location in [
main_worktree("/r", ""),
linked_worktree("/r", "/w"),
outside_worktree(),
] {
assert_eq!(
pre_forward(PolicyId::RequireRoot, &["status"], located(location)),
(clean(), location_only())
);
}
}
#[test]
fn require_root_rejects_a_directory_below_the_top_level() {
assert_eq!(
pre_forward(
PolicyId::RequireRoot,
&["status"],
located(main_worktree("/r", "sub/dir/"))
),
(
one(
"not-at-root",
"cli-git: not at the root of the git repository. Repo root is /r but effective cwd \
is /r/sub/dir. Tip: cd to /r or pass -C /r before the subcommand."
),
location_only()
)
);
let (outcome, _) = pre_forward(
PolicyId::RequireRoot,
&["config", "user.name", "x"],
located(main_worktree("/r", "a/")),
);
assert!(matches!(outcome, PolicyOutcome::Findings(found) if found.len() == 1));
assert_eq!(
pre_forward(PolicyId::RequireRoot, &["status"], unlocatable()),
(
PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("git could not be asked"),
path: None,
},
location_only()
)
);
}
#[test]
fn require_root_direct_check_always_measures() {
for values in [vec![], vec!["-C", "dir"]] {
let mut at_root: ShippedChecks<ScriptedFacts> =
checks(values.as_slice(), located(main_worktree("/r", "")));
assert_eq!(
at_root.check(PolicyId::RequireRoot, Trigger::DirectCheck),
clean()
);
assert_eq!(at_root.facts.asked, location_only());
let mut below: ShippedChecks<ScriptedFacts> =
checks(values.as_slice(), located(main_worktree("/r", "sub/")));
assert!(matches!(
below.check(PolicyId::RequireRoot, Trigger::DirectCheck),
PolicyOutcome::Findings(found) if found.len() == 1 && found[0].code == "not-at-root"
));
}
}
#[test]
fn linked_worktree_judges_guarded_commands_by_worktree_kind() {
for values in [vec!["status"], vec!["clean", "-n"], vec!["reset", "--soft"]] {
assert_eq!(
pre_forward(
PolicyId::LinkedWorktreeOnly,
values.as_slice(),
unlocatable()
),
(clean(), Vec::<String>::new()),
"{values:?}"
);
}
assert_eq!(
pre_forward(
PolicyId::LinkedWorktreeOnly,
&["reset", "--hard"],
located(main_worktree("/r", ""))
),
(
one(
"linked-worktree-required",
main_worktree_message(GuardedCommand::Reset)
),
location_only()
)
);
assert_eq!(
pre_forward(
PolicyId::LinkedWorktreeOnly,
&["stash"],
located(outside_worktree())
),
(
one(
"linked-worktree-required",
outside_worktree_message(GuardedCommand::Stash)
),
location_only()
)
);
assert_eq!(
pre_forward(
PolicyId::LinkedWorktreeOnly,
&["clean", "-fd"],
located(linked_worktree("/r", "/w"))
),
(clean(), location_only())
);
assert_eq!(
pre_forward(
PolicyId::LinkedWorktreeOnly,
&["clean", "-fd"],
unlocatable()
),
(
PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("git could not be asked"),
path: None,
},
location_only()
)
);
}
#[test]
fn linked_worktree_exempts_an_allowed_tool_cache() {
let root: PathBuf = fixture("checks-tool-cache");
let cache: PathBuf = root.join("cache");
let clone: PathBuf = cache.join("clone");
std::fs::create_dir_all(clone.join(".git")).expect("cache clone");
let mut facts: ScriptedFacts = scripted_facts();
facts.location = Ok(crate::repository_location::RepositoryLocation {
identity: WorktreeIdentity::MainWorktree {
common_dir: clone.join(".git"),
git_dir: clone.join(".git"),
worktree_root: clone,
},
prefix: Vec::<u8>::new(),
});
let mut exempt: ShippedChecks<ScriptedFacts> = checks(&["reset", "--hard"], facts.clone());
exempt.allowed_worktree_dirs = vec![cache];
assert_eq!(
exempt.check(PolicyId::LinkedWorktreeOnly, Trigger::PreForward),
clean()
);
let mut guarded: ShippedChecks<ScriptedFacts> = checks(&["reset", "--hard"], facts);
assert_eq!(
guarded.check(PolicyId::LinkedWorktreeOnly, Trigger::PreForward),
one(
"linked-worktree-required",
main_worktree_message(GuardedCommand::Reset)
)
);
remove(root.as_path());
}
#[test]
fn branch_worktree_rejects_creation_and_guessed_creation() {
assert_eq!(
pre_forward(PolicyId::BranchWorktreeOnly, &["status"], scripted_facts()),
(clean(), Vec::<String>::new())
);
assert_eq!(
pre_forward(
PolicyId::BranchWorktreeOnly,
&["switch", "-c", "topic"],
scripted_facts()
),
(
one(
"branch-creation-requires-worktree",
"cli-git: git switch branch creation is rejected in the current worktree. Use \
`git worktree add -b <branch> <path> [<start-point>]` so new branch work starts \
in its own checkout, or pass --no-enforce-worktree-branch to bypass for this \
invocation."
),
Vec::<String>::new()
)
);
let asked: Vec<String> = vec![String::from("remote-guess:topic")];
assert_eq!(
pre_forward(
PolicyId::BranchWorktreeOnly,
&["switch", "topic"],
scripted_facts()
),
(clean(), asked.clone())
);
let mut guessing: ScriptedFacts = scripted_facts();
guessing.remote_guess = Ok(true);
assert_eq!(
pre_forward(
PolicyId::BranchWorktreeOnly,
&["checkout", "topic"],
guessing
),
(
one(
"branch-creation-requires-worktree",
"cli-git: git checkout for topic branch creation is rejected in the current \
worktree. Use `git worktree add -b <branch> <path> [<start-point>]` so new \
branch work starts in its own checkout, or pass --no-enforce-worktree-branch to \
bypass for this invocation."
),
asked.clone()
)
);
let mut failing: ScriptedFacts = scripted_facts();
failing.remote_guess = Err(String::from("no git"));
assert_eq!(
pre_forward(PolicyId::BranchWorktreeOnly, &["switch", "topic"], failing),
(
PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("no git"),
path: None,
},
asked
)
);
}
#[test]
fn add_explicit_never_asks_for_a_fact() {
assert_eq!(
pre_forward(PolicyId::AddExplicit, &["add", "file"], unlocatable()),
(clean(), Vec::<String>::new())
);
let (outcome, asked) = pre_forward(PolicyId::AddExplicit, &["add", "-A"], unlocatable());
assert!(matches!(
outcome,
PolicyOutcome::Findings(found)
if found.len() == 1 && found[0].code == "bulk-add-rejected" && found[0].message.contains("(-A)")
));
assert_eq!(asked, Vec::<String>::new());
}
fn add_candidates() -> LifecycleContent {
return LifecycleContent::Requested(CandidateRequest::Add(os_arguments(&["file"])));
}
fn unprepared() -> PolicyOutcome {
return PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("the scripted facts prepare no candidates"),
path: None,
};
}
#[test]
fn content_policies_follow_the_lifecycle_content() {
for policy in [
PolicyId::FinalNewline,
PolicyId::MarkdownAutofix,
PolicyId::ForbiddenRootContext,
PolicyId::DependentVersionBump,
PolicyId::ForbiddenStrings,
] {
for trigger in [
Trigger::PreForward,
Trigger::DirectCheck,
Trigger::DirectFix,
] {
let mut without: ShippedChecks<ScriptedFacts> = checks(&["status"], unlocatable());
assert_eq!(without.check(policy, trigger), clean(), "{policy:?}");
assert_eq!(without.facts.asked, Vec::<String>::new());
}
}
let mut with: ShippedChecks<ScriptedFacts> = checks(&["add", "file"], unlocatable());
with.candidates = add_candidates();
assert_eq!(
with.check(PolicyId::DependentVersionBump, Trigger::PreForward),
clean()
);
assert_eq!(with.facts.asked, Vec::<String>::new());
for policy in [
PolicyId::FinalNewline,
PolicyId::MarkdownAutofix,
PolicyId::ForbiddenRootContext,
PolicyId::ForbiddenStrings,
PolicyId::DependentVersionBump,
] {
for trigger in [
Trigger::PreForward,
Trigger::DirectCheck,
Trigger::DirectFix,
] {
if policy == PolicyId::DependentVersionBump && trigger == Trigger::PreForward {
continue;
}
let mut read: ShippedChecks<ScriptedFacts> = checks(&["add", "file"], unlocatable());
read.candidates = add_candidates();
assert_eq!(read.check(policy, trigger), unprepared(), "{policy:?}");
assert_eq!(read.check(policy, trigger), unprepared(), "{policy:?}");
assert_eq!(read.facts.asked, vec![String::from("candidates")]);
}
}
let mut command: ShippedChecks<ScriptedFacts> = checks(&["add", "file"], scripted_facts());
command.candidates = add_candidates();
assert_eq!(
command.check(PolicyId::AddExplicit, Trigger::PreForward),
clean()
);
assert_eq!(command.facts.asked, Vec::<String>::new());
}