1use anyhow::Result;
2use std::path::Path;
3
4pub fn run(cli_cmd: clap::Command, root: &Path, role: Option<&str>) -> Result<()> {
5 let commands = extract_commands(&cli_cmd);
6 let text = apm_core::instructions::generate(root, role, &commands)?;
7 print!("{}", text);
8 Ok(())
9}
10
11fn extract_commands(cli_cmd: &clap::Command) -> Vec<(String, String)> {
12 let mut cmds: Vec<&clap::Command> = cli_cmd
13 .get_subcommands()
14 .filter(|c| !c.is_hide_set())
15 .collect();
16 cmds.sort_by_key(|c| c.get_name());
17 cmds.iter()
18 .map(|c| {
19 let name = c.get_name().to_string();
20 let about = c.get_about().map(|a| a.to_string()).unwrap_or_default();
21 (name, about)
22 })
23 .collect()
24}
25
26#[cfg(test)]
27mod tests {
28 use super::*;
29
30 fn make_test_cmd() -> clap::Command {
31 clap::Command::new("testapp")
32 .subcommand(
33 clap::Command::new("show")
34 .about("Show a ticket"),
35 )
36 .subcommand(
37 clap::Command::new("start")
38 .about("Claim a ticket"),
39 )
40 .subcommand(
41 clap::Command::new("state")
42 .about("Transition state"),
43 )
44 .subcommand(
45 clap::Command::new("spec")
46 .about("Read or write spec sections"),
47 )
48 .subcommand(
49 clap::Command::new("new")
50 .about("Create a new ticket"),
51 )
52 .subcommand(
53 clap::Command::new("sync")
54 .about("Sync with remote"),
55 )
56 .subcommand(
57 clap::Command::new("list")
58 .about("List tickets"),
59 )
60 .subcommand(
61 clap::Command::new("next")
62 .about("Return next actionable ticket"),
63 )
64 .subcommand(
65 clap::Command::new("set")
66 .about("Set a field"),
67 )
68 .subcommand(clap::Command::new("_hook").about("Hidden").hide(true))
69 }
70
71 #[test]
72 fn run_no_role_returns_ok() {
73 let tmp = tempfile::tempdir().unwrap();
74 let result = run(make_test_cmd(), tmp.path(), None);
75 assert!(result.is_ok());
76 }
77
78 #[test]
79 fn run_with_role_returns_ok() {
80 let tmp = tempfile::tempdir().unwrap();
81 let result = run(make_test_cmd(), tmp.path(), Some("worker"));
82 assert!(result.is_ok());
83 }
84
85 #[test]
86 fn extract_commands_excludes_hidden() {
87 let commands = extract_commands(&make_test_cmd());
88 let names: Vec<&str> = commands.iter().map(|(n, _)| n.as_str()).collect();
89 assert!(!names.contains(&"_hook"), "hidden command should be excluded");
90 assert!(names.contains(&"show"), "show should be included");
91 }
92
93 #[test]
94 fn extract_commands_sorted() {
95 let commands = extract_commands(&make_test_cmd());
96 let names: Vec<&str> = commands.iter().map(|(n, _)| n.as_str()).collect();
97 let mut sorted = names.clone();
98 sorted.sort();
99 assert_eq!(names, sorted, "commands should be sorted alphabetically");
100 }
101
102 #[test]
103 fn generate_no_ansi_via_run() {
104 let tmp = tempfile::tempdir().unwrap();
105 let commands = extract_commands(&make_test_cmd());
106 let out = apm_core::instructions::generate(tmp.path(), None, &commands).unwrap();
107 assert!(!out.contains('\x1b'), "ANSI escape code found in output");
108 }
109
110 #[test]
111 fn generate_contains_all_sections() {
112 let tmp = tempfile::tempdir().unwrap();
113 let commands = extract_commands(&make_test_cmd());
114 let out = apm_core::instructions::generate(tmp.path(), None, &commands).unwrap();
115 assert!(out.contains("## State Machine"));
116 assert!(out.contains("## Ticket Format"));
117 assert!(out.contains("## Shell Discipline"));
118 assert!(out.contains("## Session Identity"));
119 assert!(out.contains("## Command Reference"));
120 }
121
122 #[test]
123 fn worker_role_includes_start() {
124 let tmp = tempfile::tempdir().unwrap();
125 let commands = extract_commands(&make_test_cmd());
126 let out = apm_core::instructions::generate(tmp.path(), Some("worker"), &commands).unwrap();
127 let cr_pos = out.find("## Command Reference").unwrap();
128 assert!(
129 out[cr_pos..].contains("apm start"),
130 "apm start not found in worker command reference"
131 );
132 }
133
134 #[test]
135 fn worker_role_excludes_set() {
136 let tmp = tempfile::tempdir().unwrap();
137 let commands = extract_commands(&make_test_cmd());
138 let out = apm_core::instructions::generate(tmp.path(), Some("worker"), &commands).unwrap();
139 let cr_pos = out.find("## Command Reference").unwrap();
140 assert!(
141 !out[cr_pos..].contains("apm set"),
142 "apm set found in worker command reference but should be excluded"
143 );
144 }
145}