Skip to main content

linesmith_core/presets/
mod.rs

1//! Embedded `config.toml` presets per `docs/specs/config.md` §Presets.
2//!
3//! Presets are authored as TOML bodies shipped with the binary via
4//! `include_str!`. `linesmith presets list` prints [`names`];
5//! `linesmith presets apply <name>` writes [`body`] to the resolved
6//! config path.
7
8const MINIMAL: &str = include_str!("fixtures/minimal.toml");
9const DEVELOPER: &str = include_str!("fixtures/developer.toml");
10const POWER_USER: &str = include_str!("fixtures/power_user.toml");
11const COST_FOCUSED: &str = include_str!("fixtures/cost_focused.toml");
12const WORKTREE_HEAVY: &str = include_str!("fixtures/worktree_heavy.toml");
13
14/// A shipped preset. Display names are kebab-case for the CLI surface;
15/// file stems use underscore to match the Rust const idents (e.g.
16/// `POWER_USER` ↔ `power_user.toml` ↔ `"power-user"`).
17#[derive(Debug, Clone, Copy)]
18struct Preset {
19    name: &'static str,
20    body: &'static str,
21}
22
23/// Registry, in the order `linesmith presets list` emits.
24const PRESETS: &[Preset] = &[
25    Preset {
26        name: "minimal",
27        body: MINIMAL,
28    },
29    Preset {
30        name: "developer",
31        body: DEVELOPER,
32    },
33    Preset {
34        name: "power-user",
35        body: POWER_USER,
36    },
37    Preset {
38        name: "cost-focused",
39        body: COST_FOCUSED,
40    },
41    Preset {
42        name: "worktree-heavy",
43        body: WORKTREE_HEAVY,
44    },
45];
46
47/// Preset display names in registry order.
48pub fn names() -> impl Iterator<Item = &'static str> {
49    PRESETS.iter().map(|p| p.name)
50}
51
52/// Return the TOML body for a preset, or `None` if the name isn't
53/// registered. Case-sensitive.
54#[must_use]
55pub fn body(name: &str) -> Option<&'static str> {
56    PRESETS.iter().find(|p| p.name == name).map(|p| p.body)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::build_lines;
63    use crate::config::Config;
64    use std::str::FromStr;
65
66    fn segments_of(preset: &str) -> Vec<String> {
67        let body = body(preset).expect("preset registered");
68        let cfg = Config::from_str(body).expect("parse");
69        cfg.line
70            .expect("preset has [line]")
71            .segments
72            .into_iter()
73            .collect()
74    }
75
76    /// Per-line segment ids for a multi-line preset, sorted by parsed
77    /// integer key (matches what the builder feeds the renderer).
78    /// `numbered` carries raw `toml::Value`s (so the parser can
79    /// preserve the spec's "unknown keys are warnings" forward-compat
80    /// contract); the test helper walks the table shape directly,
81    /// panicking on anything other than a well-formed preset.
82    fn lines_of(preset: &str) -> Vec<Vec<String>> {
83        let body = body(preset).expect("preset registered");
84        let cfg = Config::from_str(body).expect("parse");
85        let line = cfg.line.expect("preset has [line]");
86        let mut sorted: Vec<(u32, Vec<String>)> = line
87            .numbered
88            .into_iter()
89            .map(|(k, value)| {
90                let n: u32 = k.parse().expect("preset uses positive-integer line keys");
91                let table = value.as_table().expect("preset [line.N] is a table");
92                let segs: Vec<String> = table["segments"]
93                    .as_array()
94                    .expect("preset [line.N].segments is an array")
95                    .iter()
96                    .map(|v| v.as_str().expect("preset segment is a string").to_string())
97                    .collect();
98                (n, segs)
99            })
100            .collect();
101        sorted.sort_by_key(|(n, _)| *n);
102        sorted.into_iter().map(|(_, segs)| segs).collect()
103    }
104
105    #[test]
106    fn registry_has_five_presets_in_stable_order() {
107        let got: Vec<&str> = names().collect();
108        assert_eq!(
109            got,
110            vec![
111                "minimal",
112                "developer",
113                "power-user",
114                "cost-focused",
115                "worktree-heavy",
116            ]
117        );
118    }
119
120    #[test]
121    fn every_preset_parses_without_warnings() {
122        // `build_lines` so the multi-line `power-user` preset
123        // doesn't trip `build_segments`'s "[line].segments is
124        // empty" warning.
125        for name in names() {
126            let body = body(name).expect("preset registered");
127            let cfg = Config::from_str(body)
128                .unwrap_or_else(|e| panic!("preset '{name}' failed to parse: {e}"));
129            let mut warnings: Vec<String> = Vec::new();
130            let _ = build_lines(Some(&cfg), None, |m: &str| warnings.push(m.to_string()));
131            assert!(
132                warnings.is_empty(),
133                "preset '{name}' emitted warnings: {warnings:?}"
134            );
135        }
136    }
137
138    #[test]
139    fn minimal_preset_segments_are_model_and_context_window() {
140        assert_eq!(segments_of("minimal"), vec!["model", "context_window"]);
141    }
142
143    #[test]
144    fn developer_preset_segments_match_spec() {
145        assert_eq!(
146            segments_of("developer"),
147            vec![
148                "model",
149                "workspace",
150                "context_window",
151                "cost",
152                "session_duration",
153                "rate_limit_5h",
154                "rate_limit_7d",
155            ]
156        );
157    }
158
159    #[test]
160    fn power_user_preset_is_multi_line_with_two_lines() {
161        // The spec's §Presets section marks power-user as v0.1's
162        // multi-line showcase. Pin both the layout mode and the
163        // per-line segment ordering so a refactor that flattens the
164        // preset back to single-line breaks loudly here.
165        let body = body("power-user").expect("preset registered");
166        let cfg = Config::from_str(body).expect("parse");
167        assert_eq!(
168            cfg.layout,
169            crate::config::LayoutMode::MultiLine,
170            "power-user must declare layout = \"multi-line\""
171        );
172        assert_eq!(
173            lines_of("power-user"),
174            vec![
175                vec!["model", "context_window", "workspace"],
176                vec![
177                    "rate_limit_5h",
178                    "rate_limit_7d",
179                    "cost",
180                    "effort",
181                    "tokens_total",
182                ],
183            ]
184        );
185    }
186
187    #[test]
188    fn cost_focused_preset_segments_match_spec() {
189        assert_eq!(
190            segments_of("cost-focused"),
191            vec![
192                "model",
193                "context_window",
194                "cost",
195                "rate_limit_5h",
196                "rate_limit_7d",
197            ]
198        );
199    }
200
201    #[test]
202    fn worktree_heavy_preset_segments_match_spec() {
203        assert_eq!(
204            segments_of("worktree-heavy"),
205            vec!["model", "workspace", "context_window"]
206        );
207    }
208
209    #[test]
210    fn preset_names_are_unique() {
211        // `body()` returns the first match, so a duplicate `name` would
212        // silently shadow any later entry.
213        let mut seen = std::collections::HashSet::new();
214        for name in names() {
215            assert!(seen.insert(name), "duplicate preset name: {name}");
216        }
217    }
218
219    #[test]
220    fn body_returns_none_for_unknown_name() {
221        assert!(body("definitely-not-a-preset").is_none());
222        assert!(body("").is_none());
223    }
224
225    #[test]
226    fn body_lookup_is_case_sensitive() {
227        assert!(body("minimal").is_some());
228        assert!(body("Minimal").is_none());
229        assert!(body("MINIMAL").is_none());
230        assert!(body("Developer").is_none());
231    }
232}