use std::fmt::Write as _;
use std::path::Path;
use trusty_mpm::core::policy_labels::CONVENTION_LABEL;
use trusty_mpm::core::trusty_tools_config::ResolvedTicketing;
use super::config::StateModel;
use super::standard_components::render_component_labels;
use super::standard_live::render_filing_targets;
use crate::commands::ticket::runner::CommandRunner;
pub(crate) fn print_standard(
ticketing: &ResolvedTicketing,
model: &StateModel,
runner: &dyn CommandRunner,
start_dir: Option<&Path>,
) {
print!("{}", render_standard(ticketing, model, runner, start_dir));
}
pub(crate) fn render_standard(
ticketing: &ResolvedTicketing,
model: &StateModel,
runner: &dyn CommandRunner,
start_dir: Option<&Path>,
) -> String {
let session = crate::commands::tmux_attach::current_tmux_session_name();
let mut out = String::new();
out.push_str(
"ticketing standard (agents.ticketing in ~/.trusty-tools/trusty-mpm/config.yaml)\n",
);
out.push_str("\nfixed — configuration cannot change these:\n");
let _ = writeln!(
out,
" pr_link_keyword: {} #N (a merge must not auto-close before live verification;\n\
\x20 a one-off Closes is `tm pr open --closes`, never config)",
ticketing.pr_link_keyword
);
let _ = writeln!(
out,
" {CONVENTION_LABEL}: component label only, never a lifecycle label"
);
out.push_str("\nsettings:\n");
let _ = writeln!(out, " default_assignee: {}", ticketing.default_assignee);
let _ = writeln!(
out,
" ensure_labels: {} (launch-time; `tm issue seed-labels` always seeds)",
ticketing.ensure_labels
);
let _ = writeln!(out, " claim_comment: {}", ticketing.claim_comment);
let _ = writeln!(
out,
" close_requires_note: {}",
ticketing.close_requires_note
);
let _ = writeln!(
out,
" lifecycle_model: {}",
ticketing
.lifecycle_model
.as_ref()
.map_or_else(|| "(discovered)".to_string(), |p| p.display().to_string())
);
let _ = writeln!(
out,
" default_project: {}",
ticketing
.default_project
.as_deref()
.unwrap_or("(none — pick one from the live list below)")
);
let _ = writeln!(
out,
" milestone_required: {} (exactly one; unset needs a `no-milestone: <reason>` comment)",
ticketing.milestone_required
);
let _ = writeln!(
out,
" project_required: {} (at least one)",
ticketing.project_required
);
out.push_str(&render_filing_targets(runner));
out.push_str(&render_component_labels(
ticketing,
session.as_deref(),
start_dir,
runner,
));
let lifecycle: Vec<&super::config::StateLabel> = model
.states
.iter()
.filter_map(|s| s.label.as_ref())
.collect();
let _ = writeln!(
out,
"\nlifecycle labels ({}) — from issue-state.yaml, never from this block:",
lifecycle.len()
);
for label in lifecycle {
let _ = writeln!(
out,
" {} #{} {}",
label.name, label.color, label.description
);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::issue::config::DEFAULT_MODEL_YAML;
use crate::commands::ticket::runner::CommandOutput;
use std::cell::RefCell;
fn model() -> StateModel {
serde_yaml::from_str(DEFAULT_MODEL_YAML).expect("embedded default parses")
}
struct FakeRunner(RefCell<Vec<CommandOutput>>);
impl CommandRunner for FakeRunner {
fn run(&self, _program: &str, _args: &[&str]) -> anyhow::Result<CommandOutput> {
let mut outs = self.0.borrow_mut();
if outs.is_empty() {
anyhow::bail!("FakeRunner exhausted")
}
Ok(outs.remove(0))
}
}
fn out(success: bool, stdout: &str, stderr: &str) -> CommandOutput {
CommandOutput {
success,
stdout: stdout.to_string(),
stderr: stderr.to_string(),
}
}
fn healthy_gh() -> FakeRunner {
FakeRunner(RefCell::new(vec![
out(true, "Backlog · mpm/core\n", ""),
out(true, "bobmatnyc", ""),
out(
true,
r#"{"projects":[{"number":3,"title":"trusty-mpm"}]}"#,
"",
),
out(
true,
r#"[{"name":"trusty-mpm","color":"BFD4F2","description":"trusty-mpm platform and related work"}]"#,
"",
),
]))
}
fn broken_gh() -> FakeRunner {
FakeRunner(RefCell::new(vec![
out(false, "", "gh auth login required"),
out(false, "", "gh auth login required"),
]))
}
#[test]
fn standard_reports_the_builtin_defaults() {
let text = render_standard(&ResolvedTicketing::default(), &model(), &healthy_gh(), None);
assert!(text.contains("default_assignee: @me"), "{text}");
assert!(text.contains("ensure_labels: true"), "{text}");
assert!(text.contains("lifecycle_model: (discovered)"), "{text}");
assert!(text.contains("trusty-mpm"), "{text}");
}
#[test]
fn standard_states_the_two_fixed_rules() {
let text = render_standard(&ResolvedTicketing::default(), &model(), &healthy_gh(), None);
assert!(text.contains("pr_link_keyword: Refs #N"), "{text}");
assert!(
text.contains("component label only, never a lifecycle label"),
"{text}"
);
}
#[test]
fn standard_reports_configured_extra_labels() {
let cfg = ResolvedTicketing::default()
.with_extra_labels(vec![trusty_mpm::core::policy_labels::PolicyLabel::new(
"area/cli",
"0E8A16",
"CLI surface",
)])
.with_default_assignee("bobmatnyc");
let text = render_standard(&cfg, &model(), &healthy_gh(), None);
assert!(text.contains("area/cli #0E8A16 CLI surface"), "{text}");
assert!(text.contains("default_assignee: bobmatnyc"), "{text}");
}
#[test]
fn standard_lists_the_model_lifecycle_labels() {
let m = model();
let text = render_standard(&ResolvedTicketing::default(), &m, &healthy_gh(), None);
let first = m
.states
.iter()
.find_map(|s| s.label.as_ref())
.expect("the factory model labels its states");
assert!(text.contains(&first.name), "{text}");
assert!(
text.contains("from issue-state.yaml, never from this block"),
"{text}"
);
}
#[test]
fn standard_reports_the_filing_targets() {
let cfg = ResolvedTicketing::default().with_default_project("trusty-mpm");
let text = render_standard(&cfg, &model(), &healthy_gh(), None);
assert!(text.contains("default_project: trusty-mpm"), "{text}");
assert!(text.contains("milestone_required: true"), "{text}");
assert!(text.contains("project_required: true"), "{text}");
assert!(text.contains("open milestones (1):"), "{text}");
assert!(text.contains("Backlog · mpm/core"), "{text}");
assert!(text.contains("#3 trusty-mpm"), "{text}");
}
#[test]
fn standard_still_requires_a_milestone_when_the_fetch_fails() {
let text = render_standard(&ResolvedTicketing::default(), &model(), &broken_gh(), None);
assert!(text.contains("milestones: unavailable ("), "{text}");
assert!(text.contains("gh auth login required"), "{text}");
assert!(text.contains("milestone_required: true"), "{text}");
assert!(text.contains("project_required: true"), "{text}");
assert!(!text.contains("open milestones ("), "{text}");
}
#[test]
fn standard_lists_every_workspace_crate_as_a_component() {
let tmp = tempfile::TempDir::new().unwrap();
let root = tmp.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n",
)
.unwrap();
for (dir, package) in [
("trusty-mpm", "trusty-mpm"),
("trusty-search", "trusty-search"),
] {
std::fs::create_dir_all(root.join("crates").join(dir)).unwrap();
std::fs::write(
root.join("crates").join(dir).join("Cargo.toml"),
format!("[package]\nname = \"{package}\"\nversion = \"0.1.0\"\n"),
)
.unwrap();
}
let text = render_standard(
&ResolvedTicketing::default(),
&model(),
&healthy_gh(),
Some(root),
);
assert!(text.contains("trusty-search"), "{text}");
assert!(
text.contains("MISSING — `gh label create`"),
"a crate the repo has no label for is flagged: {text}"
);
}
#[test]
fn a_disabled_requirement_still_prints() {
let cfg = ResolvedTicketing::default().with_filing_requirements(false, false);
let text = render_standard(&cfg, &model(), &healthy_gh(), None);
assert!(text.contains("milestone_required: false"), "{text}");
assert!(text.contains("project_required: false"), "{text}");
assert!(
text.contains("default_project: (none"),
"an unset default names itself: {text}"
);
}
}