Skip to main content

apm/cmd/
instructions.rs

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}