use crate::adopt::plan::{AdoptionPlan, ChainSupport, DetectedGate, GateType, ProposedCheckSource};
pub fn render_plan(plan: &AdoptionPlan) -> String {
if plan.findings.is_empty() {
return "No existing gates detected. Run `klasp init` for a fresh klasp.toml.\n"
.to_string();
}
let mut out = String::from("Detected existing gates:\n");
for gate in &plan.findings {
out.push('\n');
out.push_str(&render_gate(gate));
}
out.push_str("\nNext:\n");
out.push_str(" klasp init --adopt --mode mirror\n");
out.push_str(" klasp install --agent all\n");
out.push_str(" klasp doctor\n");
out
}
fn render_gate(gate: &DetectedGate) -> String {
let label = gate_label(gate);
let name = gate_human_name(&gate.gate_type);
let mut out = format!("{label} {name}\n");
out.push_str(&format!(" {}\n", gate.source_path.display()));
out.push_str(&format!(" {}\n", summarise_checks(gate)));
for warning in &gate.warnings {
out.push_str(&format!(" {warning}\n"));
}
out
}
fn gate_label(gate: &DetectedGate) -> &'static str {
if gate.chain_support == ChainSupport::Unsafe || !gate.warnings.is_empty() {
"WARN"
} else {
"OK"
}
}
fn gate_human_name(gt: &GateType) -> String {
match gt {
GateType::PreCommitFramework => "pre-commit framework".to_string(),
GateType::Husky { hook } => format!("husky {}", hook.as_str()),
GateType::Lefthook => "lefthook".to_string(),
GateType::PlainGitHook { hook } => format!("plain git hook ({})", hook.as_str()),
GateType::LintStaged => "lint-staged".to_string(),
GateType::Tooling(name) => name.clone(),
}
}
fn summarise_checks(gate: &DetectedGate) -> String {
let Some(first) = gate.proposed_checks.first() else {
return "mirror: (no checks proposed; inspect only)".to_string();
};
match &first.source {
ProposedCheckSource::PreCommit { .. } => "mirror: type = \"pre_commit\"".to_string(),
ProposedCheckSource::Shell { command } => {
format!("mirror: command = \"{command}\"")
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::adopt::plan::{
AdoptionPlan, ChainSupport, DetectedGate, GateType, HookStage, ProposedCheck,
ProposedCheckSource, TriggerKind,
};
fn pre_commit_gate() -> DetectedGate {
DetectedGate {
gate_type: GateType::PreCommitFramework,
source_path: PathBuf::from(".pre-commit-config.yaml"),
proposed_checks: vec![ProposedCheck {
name: "pre-commit".to_string(),
triggers: vec![TriggerKind::Commit],
timeout_secs: 120,
source: ProposedCheckSource::PreCommit {
hook_stage: None,
config_path: None,
},
}],
chain_support: ChainSupport::ManualOnly,
manual_chain_instructions: None,
warnings: vec![],
}
}
fn plain_hook_gate() -> DetectedGate {
DetectedGate {
gate_type: GateType::PlainGitHook {
hook: HookStage::PrePush,
},
source_path: PathBuf::from(".git/hooks/pre-push"),
proposed_checks: vec![],
chain_support: ChainSupport::Unsafe,
manual_chain_instructions: Some(
"Append `klasp gate` calls to .git/hooks/pre-push manually.".to_string(),
),
warnings: vec![
"klasp will not overwrite this hook".to_string(),
"run with --mode chain to append a managed block, or mirror the command manually"
.to_string(),
],
}
}
#[test]
fn empty_plan_renders_no_gates_message() {
let plan = AdoptionPlan::default();
let rendered = render_plan(&plan);
assert!(rendered.contains("No existing gates detected"));
assert!(!rendered.contains("Next:"));
}
#[test]
fn pre_commit_gate_renders_ok_label() {
let plan = AdoptionPlan {
findings: vec![pre_commit_gate()],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("OK pre-commit framework"));
assert!(rendered.contains(".pre-commit-config.yaml"));
assert!(rendered.contains("mirror: type = \"pre_commit\""));
}
#[test]
fn unsafe_chain_gate_renders_warn_label() {
let plan = AdoptionPlan {
findings: vec![plain_hook_gate()],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("WARN plain git hook (pre-push)"));
assert!(rendered.contains("mirror: (no checks proposed; inspect only)"));
assert!(rendered.contains("klasp will not overwrite this hook"));
}
#[test]
fn next_block_present_for_non_empty_plan() {
let plan = AdoptionPlan {
findings: vec![pre_commit_gate()],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("Next:"));
assert!(rendered.contains("klasp init --adopt --mode mirror"));
assert!(rendered.contains("klasp install --agent all"));
assert!(rendered.contains("klasp doctor"));
}
#[test]
fn shell_source_renders_command() {
let gate = DetectedGate {
gate_type: GateType::LintStaged,
source_path: PathBuf::from("package.json"),
proposed_checks: vec![ProposedCheck {
name: "lint-staged".to_string(),
triggers: vec![TriggerKind::Commit],
timeout_secs: 120,
source: ProposedCheckSource::Shell {
command: "pnpm exec lint-staged".to_string(),
},
}],
chain_support: ChainSupport::ManualOnly,
manual_chain_instructions: None,
warnings: vec![],
};
let plan = AdoptionPlan {
findings: vec![gate],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("mirror: command = \"pnpm exec lint-staged\""));
}
#[test]
fn multiple_gates_all_appear() {
let plan = AdoptionPlan {
findings: vec![pre_commit_gate(), plain_hook_gate()],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("pre-commit framework"));
assert!(rendered.contains("plain git hook (pre-push)"));
assert_eq!(rendered.matches("Next:").count(), 1);
}
#[test]
fn warning_on_gate_forces_warn_label() {
let mut gate = pre_commit_gate();
gate.warnings.push("duplicate execution risk".to_string());
assert_eq!(gate.chain_support, ChainSupport::ManualOnly);
let plan = AdoptionPlan {
findings: vec![gate],
};
let rendered = render_plan(&plan);
assert!(rendered.contains("WARN pre-commit framework"));
assert!(rendered.contains("duplicate execution risk"));
}
}