vtcode_config/types/
mod.rs

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