Skip to main content

vtcode_config/types/
mod.rs

1//! Common types and interfaces used throughout the application
2
3use crate::constants::reasoning;
4use crate::core::PromptCachingConfig;
5use hashbrown::HashMap;
6use serde::{Deserialize, Deserializer, Serialize};
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::fmt;
10use std::path::PathBuf;
11
12/// Supported reasoning effort levels configured via vtcode.toml
13/// These map to different provider-specific parameters:
14/// - For Gemini 3 Pro: Maps to thinking_level (low, high) - medium coming soon
15/// - For other models: Maps to provider-specific reasoning parameters
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "lowercase")]
19#[derive(Default)]
20pub enum ReasoningEffortLevel {
21    /// No reasoning configuration - for models that don't support configurable reasoning
22    None,
23    /// Minimal reasoning effort - maps to low thinking level for Gemini 3 Pro
24    Minimal,
25    /// Low reasoning effort - maps to low thinking level for Gemini 3 Pro
26    Low,
27    /// Medium reasoning effort - Note: Not fully available for Gemini 3 Pro yet, defaults to high
28    #[default]
29    Medium,
30    /// High reasoning effort - maps to high thinking level for Gemini 3 Pro
31    High,
32    /// Extra high reasoning effort - for gpt-5.3-codex+ long-running tasks
33    XHigh,
34}
35
36impl ReasoningEffortLevel {
37    /// Return the textual representation expected by downstream APIs
38    pub fn as_str(self) -> &'static str {
39        match self {
40            Self::None => "none",
41            Self::Minimal => "minimal",
42            Self::Low => reasoning::LOW,
43            Self::Medium => reasoning::MEDIUM,
44            Self::High => reasoning::HIGH,
45            Self::XHigh => "xhigh",
46        }
47    }
48
49    /// Attempt to parse an effort level from user configuration input
50    pub fn parse(value: &str) -> Option<Self> {
51        let normalized = value.trim();
52        if normalized.eq_ignore_ascii_case("none") {
53            Some(Self::None)
54        } else if normalized.eq_ignore_ascii_case("minimal") {
55            Some(Self::Minimal)
56        } else if normalized.eq_ignore_ascii_case(reasoning::LOW) {
57            Some(Self::Low)
58        } else if normalized.eq_ignore_ascii_case(reasoning::MEDIUM) {
59            Some(Self::Medium)
60        } else if normalized.eq_ignore_ascii_case(reasoning::HIGH) {
61            Some(Self::High)
62        } else if normalized.eq_ignore_ascii_case("xhigh") {
63            Some(Self::XHigh)
64        } else {
65            None
66        }
67    }
68
69    /// Enumerate the allowed configuration values for validation and messaging
70    pub fn allowed_values() -> &'static [&'static str] {
71        reasoning::ALLOWED_LEVELS
72    }
73}
74
75/// System prompt mode (inspired by pi-coding-agent philosophy)
76/// Controls verbosity and complexity of system prompts sent to models
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "lowercase")]
80#[derive(Default)]
81pub enum SystemPromptMode {
82    /// Minimal prompt (~500-800 tokens) - Pi-inspired, modern models need less guidance
83    /// Best for: Power users, token-constrained contexts, fast responses
84    Minimal,
85    /// Lightweight prompt (~1-2k tokens) - Essential guidance only
86    /// Best for: Resource-constrained operations, simple tasks
87    Lightweight,
88    /// Default prompt (~6-7k tokens) - Full guidance with all features
89    /// Best for: General usage, comprehensive error handling
90    #[default]
91    Default,
92    /// Specialized prompt (~7-8k tokens) - Complex refactoring and analysis
93    /// Best for: Multi-file changes, sophisticated code analysis
94    Specialized,
95}
96
97impl SystemPromptMode {
98    /// Return the textual representation for configuration
99    pub fn as_str(self) -> &'static str {
100        match self {
101            Self::Minimal => "minimal",
102            Self::Lightweight => "lightweight",
103            Self::Default => "default",
104            Self::Specialized => "specialized",
105        }
106    }
107
108    /// Parse system prompt mode from user configuration
109    pub fn parse(value: &str) -> Option<Self> {
110        let normalized = value.trim();
111        if normalized.eq_ignore_ascii_case("minimal") {
112            Some(Self::Minimal)
113        } else if normalized.eq_ignore_ascii_case("lightweight") {
114            Some(Self::Lightweight)
115        } else if normalized.eq_ignore_ascii_case("default") {
116            Some(Self::Default)
117        } else if normalized.eq_ignore_ascii_case("specialized") {
118            Some(Self::Specialized)
119        } else {
120            None
121        }
122    }
123
124    /// Allowed configuration values for validation
125    pub fn allowed_values() -> &'static [&'static str] {
126        &["minimal", "lightweight", "default", "specialized"]
127    }
128}
129
130impl fmt::Display for SystemPromptMode {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(self.as_str())
133    }
134}
135
136impl<'de> Deserialize<'de> for SystemPromptMode {
137    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138    where
139        D: Deserializer<'de>,
140    {
141        let raw = String::deserialize(deserializer)?;
142        if let Some(parsed) = Self::parse(&raw) {
143            Ok(parsed)
144        } else {
145            Ok(Self::default())
146        }
147    }
148}
149
150/// Tool documentation mode (inspired by pi-coding-agent progressive disclosure)
151/// Controls how much tool documentation is loaded upfront vs on-demand
152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
154#[serde(rename_all = "lowercase")]
155#[derive(Default)]
156pub enum ToolDocumentationMode {
157    /// Minimal signatures only (~800 tokens total) - Pi-style, power users
158    /// Best for: Maximum efficiency, experienced users, token-constrained contexts
159    Minimal,
160    /// Signatures + common parameters (~1,200 tokens total) - Smart hints
161    /// Best for: General usage, balances overhead vs guidance (recommended)
162    #[default]
163    Progressive,
164    /// Full documentation upfront (~3,000 tokens total) - Current behavior
165    /// Best for: Maximum hand-holding, comprehensive parameter documentation
166    Full,
167}
168
169impl ToolDocumentationMode {
170    /// Return the textual representation for configuration
171    pub fn as_str(self) -> &'static str {
172        match self {
173            Self::Minimal => "minimal",
174            Self::Progressive => "progressive",
175            Self::Full => "full",
176        }
177    }
178
179    /// Parse tool documentation mode from user configuration
180    pub fn parse(value: &str) -> Option<Self> {
181        let normalized = value.trim();
182        if normalized.eq_ignore_ascii_case("minimal") {
183            Some(Self::Minimal)
184        } else if normalized.eq_ignore_ascii_case("progressive") {
185            Some(Self::Progressive)
186        } else if normalized.eq_ignore_ascii_case("full") {
187            Some(Self::Full)
188        } else {
189            None
190        }
191    }
192
193    /// Allowed configuration values for validation
194    pub fn allowed_values() -> &'static [&'static str] {
195        &["minimal", "progressive", "full"]
196    }
197}
198
199impl fmt::Display for ToolDocumentationMode {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str(self.as_str())
202    }
203}
204
205impl<'de> Deserialize<'de> for ToolDocumentationMode {
206    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
207    where
208        D: Deserializer<'de>,
209    {
210        let raw = String::deserialize(deserializer)?;
211        if let Some(parsed) = Self::parse(&raw) {
212            Ok(parsed)
213        } else {
214            Ok(Self::default())
215        }
216    }
217}
218
219/// Verbosity level for model output (GPT-5.1 and compatible models)
220#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
222#[serde(rename_all = "lowercase")]
223#[derive(Default)]
224pub enum VerbosityLevel {
225    Low,
226    #[default]
227    Medium,
228    High,
229}
230
231impl VerbosityLevel {
232    /// Return the textual representation expected by downstream APIs
233    pub fn as_str(self) -> &'static str {
234        match self {
235            Self::Low => "low",
236            Self::Medium => "medium",
237            Self::High => "high",
238        }
239    }
240
241    /// Attempt to parse a verbosity level from user configuration input
242    pub fn parse(value: &str) -> Option<Self> {
243        let normalized = value.trim();
244        if normalized.eq_ignore_ascii_case("low") {
245            Some(Self::Low)
246        } else if normalized.eq_ignore_ascii_case("medium") {
247            Some(Self::Medium)
248        } else if normalized.eq_ignore_ascii_case("high") {
249            Some(Self::High)
250        } else {
251            None
252        }
253    }
254
255    /// Enumerate the allowed configuration values
256    pub fn allowed_values() -> &'static [&'static str] {
257        &["low", "medium", "high"]
258    }
259}
260
261impl fmt::Display for VerbosityLevel {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        f.write_str(self.as_str())
264    }
265}
266
267impl<'de> Deserialize<'de> for VerbosityLevel {
268    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
269    where
270        D: Deserializer<'de>,
271    {
272        let raw = String::deserialize(deserializer)?;
273        if let Some(parsed) = Self::parse(&raw) {
274            Ok(parsed)
275        } else {
276            Ok(Self::default())
277        }
278    }
279}
280
281impl fmt::Display for ReasoningEffortLevel {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        f.write_str(self.as_str())
284    }
285}
286
287impl<'de> Deserialize<'de> for ReasoningEffortLevel {
288    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
289    where
290        D: Deserializer<'de>,
291    {
292        let raw = String::deserialize(deserializer)?;
293        if let Some(parsed) = Self::parse(&raw) {
294            Ok(parsed)
295        } else {
296            Ok(Self::default())
297        }
298    }
299}
300
301/// Preferred rendering surface for the interactive chat UI
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
304#[serde(rename_all = "lowercase")]
305#[derive(Default)]
306pub enum UiSurfacePreference {
307    #[default]
308    Auto,
309    Alternate,
310    Inline,
311}
312
313impl UiSurfacePreference {
314    /// String representation used in configuration and logging
315    pub fn as_str(self) -> &'static str {
316        match self {
317            Self::Auto => "auto",
318            Self::Alternate => "alternate",
319            Self::Inline => "inline",
320        }
321    }
322
323    /// Parse a surface preference from configuration input
324    pub fn parse(value: &str) -> Option<Self> {
325        let normalized = value.trim();
326        if normalized.eq_ignore_ascii_case("auto") {
327            Some(Self::Auto)
328        } else if normalized.eq_ignore_ascii_case("alternate")
329            || normalized.eq_ignore_ascii_case("alt")
330        {
331            Some(Self::Alternate)
332        } else if normalized.eq_ignore_ascii_case("inline") {
333            Some(Self::Inline)
334        } else {
335            None
336        }
337    }
338
339    /// Enumerate the accepted configuration values for validation messaging
340    pub fn allowed_values() -> &'static [&'static str] {
341        &["auto", "alternate", "inline"]
342    }
343}
344
345impl fmt::Display for UiSurfacePreference {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        f.write_str(self.as_str())
348    }
349}
350
351impl<'de> Deserialize<'de> for UiSurfacePreference {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: Deserializer<'de>,
355    {
356        let raw = String::deserialize(deserializer)?;
357        if let Some(parsed) = Self::parse(&raw) {
358            Ok(parsed)
359        } else {
360            Ok(Self::default())
361        }
362    }
363}
364
365/// Source describing how the active model was selected
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
367pub enum ModelSelectionSource {
368    /// Model provided by workspace configuration
369    #[default]
370    WorkspaceConfig,
371    /// Model provided by CLI override
372    CliOverride,
373}
374
375/// Default editing mode for agent startup (Codex-inspired workflow)
376///
377/// Controls the initial mode when a session starts. This is a **configuration**
378/// enum for `default_editing_mode` in vtcode.toml. At runtime, the mode can be
379/// cycled (Edit → Plan → Edit) via Shift+Tab or /plan command.
380///
381/// Inspired by OpenAI Codex's emphasis on structured planning before execution,
382/// but provider-agnostic (works with Gemini, Anthropic, OpenAI, xAI, DeepSeek, etc.)
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
385#[serde(rename_all = "lowercase")]
386#[derive(Default)]
387pub enum EditingMode {
388    /// Full tool access - can read, write, execute commands (default)
389    /// Use for: Implementation, bug fixes, feature development
390    #[default]
391    Edit,
392    /// Read-only exploration - mutating tools blocked
393    /// Use for: Planning, research, architecture analysis
394    /// Agent can write plans to `.vtcode/plans/` but not modify code
395    Plan,
396}
397
398impl EditingMode {
399    /// Return the textual representation for configuration and display
400    pub fn as_str(self) -> &'static str {
401        match self {
402            Self::Edit => "edit",
403            Self::Plan => "plan",
404        }
405    }
406
407    /// Parse editing mode from user configuration input
408    pub fn parse(value: &str) -> Option<Self> {
409        let normalized = value.trim();
410        if normalized.eq_ignore_ascii_case("edit") {
411            Some(Self::Edit)
412        } else if normalized.eq_ignore_ascii_case("plan") {
413            Some(Self::Plan)
414        } else {
415            None
416        }
417    }
418
419    /// Enumerate the allowed configuration values for validation
420    pub fn allowed_values() -> &'static [&'static str] {
421        &["edit", "plan"]
422    }
423
424    /// Check if this mode allows file modifications
425    pub fn can_modify_files(self) -> bool {
426        matches!(self, Self::Edit)
427    }
428
429    /// Check if this mode allows command execution
430    pub fn can_execute_commands(self) -> bool {
431        matches!(self, Self::Edit)
432    }
433
434    /// Check if this is read-only planning mode
435    pub fn is_read_only(self) -> bool {
436        matches!(self, Self::Plan)
437    }
438}
439
440impl fmt::Display for EditingMode {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        f.write_str(self.as_str())
443    }
444}
445
446impl<'de> Deserialize<'de> for EditingMode {
447    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
448    where
449        D: Deserializer<'de>,
450    {
451        let raw = String::deserialize(deserializer)?;
452        if let Some(parsed) = Self::parse(&raw) {
453            Ok(parsed)
454        } else {
455            Ok(Self::default())
456        }
457    }
458}
459
460/// Configuration for the agent
461#[derive(Debug, Clone)]
462pub struct AgentConfig {
463    pub model: String,
464    pub api_key: String,
465    pub provider: String,
466    pub api_key_env: String,
467    pub workspace: PathBuf,
468    pub verbose: bool,
469    pub quiet: bool,
470    pub theme: String,
471    pub reasoning_effort: ReasoningEffortLevel,
472    pub ui_surface: UiSurfacePreference,
473    pub prompt_cache: PromptCachingConfig,
474    pub model_source: ModelSelectionSource,
475    pub custom_api_keys: BTreeMap<String, String>,
476    pub checkpointing_enabled: bool,
477    pub checkpointing_storage_dir: Option<PathBuf>,
478    pub checkpointing_max_snapshots: usize,
479    pub checkpointing_max_age_days: Option<u64>,
480    pub max_conversation_turns: usize,
481    pub model_behavior: Option<crate::core::ModelConfig>,
482}
483
484/// Workshop agent capability levels
485#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
486pub enum CapabilityLevel {
487    /// Basic chat only
488    Basic,
489    /// Can read files
490    FileReading,
491    /// Can read files and list directories
492    FileListing,
493    /// Can read files, list directories, and run bash commands
494    Bash,
495    /// Can read files, list directories, run bash commands, and edit files
496    Editing,
497    /// Full capabilities including code search
498    CodeSearch,
499}
500
501/// Session information
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct SessionInfo {
504    pub session_id: String,
505    pub start_time: u64,
506    pub total_turns: usize,
507    pub total_decisions: usize,
508    pub error_count: usize,
509}
510
511/// Conversation turn information
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct ConversationTurn {
514    pub turn_number: usize,
515    pub timestamp: u64,
516    pub user_input: Option<String>,
517    pub agent_response: Option<String>,
518    pub tool_calls: Vec<ToolCallInfo>,
519    pub decision: Option<DecisionInfo>,
520}
521
522/// Tool call information
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct ToolCallInfo {
525    pub name: String,
526    pub args: Value,
527    pub result: Option<Value>,
528    pub error: Option<String>,
529    pub execution_time_ms: Option<u64>,
530}
531
532/// Decision information for tracking
533#[derive(Debug, Clone, Serialize, Deserialize)]
534pub struct DecisionInfo {
535    pub turn_number: usize,
536    pub action_type: String,
537    pub description: String,
538    pub reasoning: String,
539    pub outcome: Option<String>,
540    pub confidence_score: Option<f64>,
541    pub timestamp: u64,
542}
543
544/// Error information for tracking
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct ErrorInfo {
547    pub error_type: String,
548    pub message: String,
549    pub turn_number: usize,
550    pub recoverable: bool,
551    pub timestamp: u64,
552}
553
554/// Task information for project workflows
555#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct TaskInfo {
557    pub task_type: String,
558    pub description: String,
559    pub completed: bool,
560    pub success: bool,
561    pub duration_seconds: Option<u64>,
562    pub tools_used: Vec<String>,
563    pub dependencies: Vec<String>,
564}
565
566/// Project creation specification
567#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct ProjectSpec {
569    pub name: String,
570    pub features: Vec<String>,
571    pub template: Option<String>,
572    pub dependencies: HashMap<String, String>,
573}
574
575/// Workspace analysis result
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct WorkspaceAnalysis {
578    pub root_path: String,
579    pub project_type: Option<String>,
580    pub languages: Vec<String>,
581    pub frameworks: Vec<String>,
582    pub config_files: Vec<String>,
583    pub source_files: Vec<String>,
584    pub test_files: Vec<String>,
585    pub documentation_files: Vec<String>,
586    pub total_files: usize,
587    pub total_size_bytes: u64,
588}
589
590/// Command execution result
591#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct CommandResult {
593    pub command: String,
594    pub success: bool,
595    pub stdout: String,
596    pub stderr: String,
597    pub exit_code: Option<i32>,
598    pub execution_time_ms: u64,
599}
600
601/// File operation result
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct FileOperationResult {
604    pub operation: String,
605    pub path: String,
606    pub success: bool,
607    pub details: HashMap<String, Value>,
608    pub error: Option<String>,
609}
610
611/// Performance metrics
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct PerformanceMetrics {
614    pub session_duration_seconds: u64,
615    pub total_api_calls: usize,
616    pub total_tokens_used: Option<usize>,
617    pub average_response_time_ms: f64,
618    pub tool_execution_count: usize,
619    pub error_count: usize,
620    pub recovery_success_rate: f64,
621}
622
623/// Quality metrics for agent actions
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct QualityMetrics {
626    pub decision_confidence_avg: f64,
627    pub tool_success_rate: f64,
628    pub error_recovery_rate: f64,
629    pub context_preservation_rate: f64,
630    pub user_satisfaction_score: Option<f64>,
631}
632
633/// Configuration for tool behavior
634#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct ToolConfig {
636    pub enable_validation: bool,
637    pub max_execution_time_seconds: u64,
638    pub allow_file_creation: bool,
639    pub allow_file_deletion: bool,
640    pub working_directory: Option<String>,
641}
642
643/// Context management settings
644#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct ContextConfig {
646    pub max_context_length: usize,
647    pub compression_threshold: usize,
648    pub summarization_interval: usize,
649    pub preservation_priority: Vec<String>,
650}
651
652/// Logging configuration
653#[derive(Debug, Clone, Serialize, Deserialize)]
654pub struct LoggingConfig {
655    pub level: String,
656    pub file_logging: bool,
657    pub log_directory: Option<String>,
658    pub max_log_files: usize,
659    pub max_log_size_mb: usize,
660}
661
662/// Analysis depth for workspace analysis
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub enum AnalysisDepth {
665    Basic,
666    Standard,
667    Deep,
668}
669
670/// Output format for commands
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub enum OutputFormat {
673    Text,
674    Json,
675    Html,
676}
677
678/// Compression level for context compression
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub enum CompressionLevel {
681    Light,
682    Medium,
683    Aggressive,
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn test_editing_mode_parse() {
692        assert_eq!(EditingMode::parse("edit"), Some(EditingMode::Edit));
693        assert_eq!(EditingMode::parse("EDIT"), Some(EditingMode::Edit));
694        assert_eq!(EditingMode::parse("Edit"), Some(EditingMode::Edit));
695        assert_eq!(EditingMode::parse("plan"), Some(EditingMode::Plan));
696        assert_eq!(EditingMode::parse("PLAN"), Some(EditingMode::Plan));
697        assert_eq!(EditingMode::parse("Plan"), Some(EditingMode::Plan));
698        assert_eq!(EditingMode::parse("agent"), None);
699        assert_eq!(EditingMode::parse("invalid"), None);
700        assert_eq!(EditingMode::parse(""), None);
701    }
702
703    #[test]
704    fn test_editing_mode_as_str() {
705        assert_eq!(EditingMode::Edit.as_str(), "edit");
706        assert_eq!(EditingMode::Plan.as_str(), "plan");
707    }
708
709    #[test]
710    fn test_editing_mode_capabilities() {
711        // Edit mode: full access
712        assert!(EditingMode::Edit.can_modify_files());
713        assert!(EditingMode::Edit.can_execute_commands());
714        assert!(!EditingMode::Edit.is_read_only());
715
716        // Plan mode: read-only
717        assert!(!EditingMode::Plan.can_modify_files());
718        assert!(!EditingMode::Plan.can_execute_commands());
719        assert!(EditingMode::Plan.is_read_only());
720    }
721
722    #[test]
723    fn test_editing_mode_default() {
724        assert_eq!(EditingMode::default(), EditingMode::Edit);
725    }
726
727    #[test]
728    fn test_editing_mode_display() {
729        assert_eq!(format!("{}", EditingMode::Edit), "edit");
730        assert_eq!(format!("{}", EditingMode::Plan), "plan");
731    }
732
733    #[test]
734    fn test_editing_mode_allowed_values() {
735        let values = EditingMode::allowed_values();
736        assert_eq!(values, &["edit", "plan"]);
737    }
738}