1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub enum PromptTemplate {
13 TrendFollowing,
15 MeanReversion,
17 Conservative,
19 Custom,
21}
22
23impl PromptTemplate {
24 pub fn default_prompt(&self) -> String {
26 match self {
27 PromptTemplate::TrendFollowing => concat!(
28 "You are a trend-following trading agent. ",
29 "Your goal is to identify and ride sustained price movements. ",
30 "Look for breakouts above resistance, increasing volume, and strong momentum indicators. ",
31 "Use trailing stops to protect profits. ",
32 "Enter positions when trend confirmation signals align across multiple timeframes. ",
33 "Cut losses quickly when the trend reverses."
34 ).to_string(),
35 PromptTemplate::MeanReversion => concat!(
36 "You are a mean-reversion trading agent. ",
37 "Your goal is to identify overextended price moves and trade the reversion to the mean. ",
38 "Look for extreme RSI readings, Bollinger Band violations, and significant deviations from moving averages. ",
39 "Enter positions when price is stretched far from its average with signs of exhaustion. ",
40 "Take profits as price returns toward the mean. ",
41 "Use tight stops in case the trend continues."
42 ).to_string(),
43 PromptTemplate::Conservative => concat!(
44 "You are a conservative analysis-only agent operating in paper-trading mode. ",
45 "Your goal is to analyze market conditions and provide trading recommendations WITHOUT placing any real orders. ",
46 "Provide detailed analysis of market structure, key levels, and potential trade setups. ",
47 "Include entry points, stop losses, and take profit targets in your analysis. ",
48 "Flag any high-risk conditions or unusual market behavior. ",
49 "Never recommend executing trades — only analyze and report."
50 ).to_string(),
51 PromptTemplate::Custom => String::new(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct PromptTemplateSummary {
60 pub id: PromptTemplate,
61 pub name: String,
62 pub description: String,
63 pub default_prompt: String,
64}
65
66pub fn list_prompt_templates() -> Vec<PromptTemplateSummary> {
68 vec![
69 PromptTemplateSummary {
70 id: PromptTemplate::TrendFollowing,
71 name: "Trend Following".to_string(),
72 description: "Ride momentum, buy breakouts, use trailing stops".to_string(),
73 default_prompt: PromptTemplate::TrendFollowing.default_prompt(),
74 },
75 PromptTemplateSummary {
76 id: PromptTemplate::MeanReversion,
77 name: "Mean Reversion".to_string(),
78 description: "Fade extremes, buy dips, sell rallies toward the mean".to_string(),
79 default_prompt: PromptTemplate::MeanReversion.default_prompt(),
80 },
81 PromptTemplateSummary {
82 id: PromptTemplate::Conservative,
83 name: "Conservative (Paper Trading)".to_string(),
84 description: "Analyze only, no real orders — paper trading mode".to_string(),
85 default_prompt: PromptTemplate::Conservative.default_prompt(),
86 },
87 PromptTemplateSummary {
88 id: PromptTemplate::Custom,
89 name: "Custom".to_string(),
90 description: "Define your own system prompt".to_string(),
91 default_prompt: PromptTemplate::Custom.default_prompt(),
92 },
93 ]
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub enum TradingMode {
104 Live,
106 Paper,
108}
109
110impl Default for TradingMode {
111 fn default() -> Self {
112 TradingMode::Paper
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct AgentConfig {
124 pub agent_id: String,
126
127 pub prompt_template: PromptTemplate,
129
130 pub system_prompt: String,
134
135 pub analysis_frequency_minutes: u64,
138
139 pub trading_pairs: Vec<String>,
142
143 pub max_position_size_usd: f64,
145
146 pub enabled: bool,
148
149 #[serde(default)]
151 pub trading_mode: TradingMode,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub composer_profile: Option<String>,
159
160 #[serde(default = "default_max_retries")]
164 pub max_retries: u32,
165
166 #[serde(default = "default_max_tool_turns")]
170 pub max_tool_turns: u32,
171}
172
173fn default_max_retries() -> u32 {
174 3
175}
176
177fn default_max_tool_turns() -> u32 {
178 5
179}
180
181impl AgentConfig {
182 pub fn with_template(agent_id: &str, template: PromptTemplate) -> Self {
184 let system_prompt = template.default_prompt();
185 Self {
186 agent_id: agent_id.to_string(),
187 prompt_template: template,
188 system_prompt,
189 analysis_frequency_minutes: 60,
190 trading_pairs: vec!["BTC-PERP".to_string(), "ETH-PERP".to_string()],
191 max_position_size_usd: 10_000.0,
192 enabled: false,
193 trading_mode: TradingMode::default(),
194 composer_profile: None,
195 max_retries: default_max_retries(),
196 max_tool_turns: default_max_tool_turns(),
197 }
198 }
199}
200
201impl Default for AgentConfig {
202 fn default() -> Self {
203 Self::with_template("default", PromptTemplate::Conservative)
204 }
205}
206
207fn agent_config_dir() -> PathBuf {
212 let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
213 path.push("hyper-agent");
214 path.push("agent-configs");
215 let _ = fs::create_dir_all(&path);
216 path
217}
218
219fn agent_config_path(agent_id: &str) -> PathBuf {
220 agent_config_dir().join(format!("{}.json", agent_id))
221}
222
223pub fn load_agent_config(agent_id: &str) -> Option<AgentConfig> {
225 let path = agent_config_path(agent_id);
226 fs::read_to_string(&path)
227 .ok()
228 .and_then(|data| serde_json::from_str(&data).ok())
229}
230
231pub fn save_agent_config_to_disk(config: &AgentConfig) -> Result<(), String> {
233 let path = agent_config_path(&config.agent_id);
234 let json = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?;
235 fs::write(&path, json).map_err(|e| e.to_string())?;
236 Ok(())
237}
238
239#[allow(dead_code)]
241pub fn delete_agent_config_from_disk(agent_id: &str) -> Result<(), String> {
242 let path = agent_config_path(agent_id);
243 if path.exists() {
244 fs::remove_file(&path).map_err(|e| e.to_string())?;
245 }
246 Ok(())
247}
248
249pub fn validate_agent_config(config: &AgentConfig) -> Result<(), String> {
255 if config.agent_id.is_empty() {
256 return Err("agent_id must not be empty".to_string());
257 }
258 if config.analysis_frequency_minutes == 0 {
259 return Err("analysis_frequency_minutes must be at least 1".to_string());
260 }
261 if config.max_position_size_usd < 0.0 {
262 return Err("max_position_size_usd must not be negative".to_string());
263 }
264 if config.system_prompt.is_empty() && config.prompt_template == PromptTemplate::Custom {
265 return Err("system_prompt must not be empty when using Custom template".to_string());
266 }
267 Ok(())
268}
269
270#[cfg(test)]
275mod tests {
276 use super::*;
277 use std::fs;
278
279 #[test]
282 fn test_trading_mode_serialization() {
283 assert_eq!(
284 serde_json::to_string(&TradingMode::Live).unwrap(),
285 "\"live\""
286 );
287 assert_eq!(
288 serde_json::to_string(&TradingMode::Paper).unwrap(),
289 "\"paper\""
290 );
291 }
292
293 #[test]
294 fn test_trading_mode_deserialization() {
295 let live: TradingMode = serde_json::from_str("\"live\"").unwrap();
296 assert_eq!(live, TradingMode::Live);
297 let paper: TradingMode = serde_json::from_str("\"paper\"").unwrap();
298 assert_eq!(paper, TradingMode::Paper);
299 }
300
301 #[test]
302 fn test_trading_mode_default_is_paper() {
303 assert_eq!(TradingMode::default(), TradingMode::Paper);
304 }
305
306 #[test]
309 fn test_prompt_template_serialization() {
310 let template = PromptTemplate::TrendFollowing;
311 let json = serde_json::to_string(&template).unwrap();
312 assert_eq!(json, "\"trendFollowing\"");
313
314 let template = PromptTemplate::MeanReversion;
315 let json = serde_json::to_string(&template).unwrap();
316 assert_eq!(json, "\"meanReversion\"");
317
318 let template = PromptTemplate::Conservative;
319 let json = serde_json::to_string(&template).unwrap();
320 assert_eq!(json, "\"conservative\"");
321
322 let template = PromptTemplate::Custom;
323 let json = serde_json::to_string(&template).unwrap();
324 assert_eq!(json, "\"custom\"");
325 }
326
327 #[test]
328 fn test_prompt_template_deserialization() {
329 let t: PromptTemplate = serde_json::from_str("\"trendFollowing\"").unwrap();
330 assert_eq!(t, PromptTemplate::TrendFollowing);
331
332 let t: PromptTemplate = serde_json::from_str("\"meanReversion\"").unwrap();
333 assert_eq!(t, PromptTemplate::MeanReversion);
334
335 let t: PromptTemplate = serde_json::from_str("\"conservative\"").unwrap();
336 assert_eq!(t, PromptTemplate::Conservative);
337
338 let t: PromptTemplate = serde_json::from_str("\"custom\"").unwrap();
339 assert_eq!(t, PromptTemplate::Custom);
340 }
341
342 #[test]
343 fn test_prompt_template_default_prompts_non_empty() {
344 assert!(!PromptTemplate::TrendFollowing.default_prompt().is_empty());
345 assert!(!PromptTemplate::MeanReversion.default_prompt().is_empty());
346 assert!(!PromptTemplate::Conservative.default_prompt().is_empty());
347 assert!(PromptTemplate::Custom.default_prompt().is_empty());
349 }
350
351 #[test]
352 fn test_prompt_template_default_prompts_contain_key_terms() {
353 let tf = PromptTemplate::TrendFollowing.default_prompt();
354 assert!(tf.contains("trend"));
355 assert!(tf.contains("breakout"));
356
357 let mr = PromptTemplate::MeanReversion.default_prompt();
358 assert!(mr.contains("mean"));
359 assert!(mr.contains("reversion") || mr.contains("mean"));
360
361 let c = PromptTemplate::Conservative.default_prompt();
362 assert!(c.contains("paper-trading") || c.contains("analysis-only"));
363 assert!(c.contains("Never"));
364 }
365
366 #[test]
369 fn test_list_prompt_templates_returns_all() {
370 let templates = list_prompt_templates();
371 assert_eq!(templates.len(), 4);
372
373 let ids: Vec<_> = templates.iter().map(|t| &t.id).collect();
374 assert!(ids.contains(&&PromptTemplate::TrendFollowing));
375 assert!(ids.contains(&&PromptTemplate::MeanReversion));
376 assert!(ids.contains(&&PromptTemplate::Conservative));
377 assert!(ids.contains(&&PromptTemplate::Custom));
378 }
379
380 #[test]
381 fn test_list_prompt_templates_fields_populated() {
382 let templates = list_prompt_templates();
383 for t in &templates {
384 assert!(!t.name.is_empty());
385 assert!(!t.description.is_empty());
386 if t.id != PromptTemplate::Custom {
388 assert!(!t.default_prompt.is_empty());
389 }
390 }
391 }
392
393 #[test]
396 fn test_agent_config_with_template_trend_following() {
397 let config = AgentConfig::with_template("agent-1", PromptTemplate::TrendFollowing);
398 assert_eq!(config.agent_id, "agent-1");
399 assert_eq!(config.prompt_template, PromptTemplate::TrendFollowing);
400 assert!(!config.system_prompt.is_empty());
401 assert!(config.system_prompt.contains("trend"));
402 assert_eq!(config.analysis_frequency_minutes, 60);
403 assert!(!config.trading_pairs.is_empty());
404 assert!(config.max_position_size_usd > 0.0);
405 assert!(!config.enabled); }
407
408 #[test]
409 fn test_agent_config_with_template_conservative() {
410 let config = AgentConfig::with_template("agent-2", PromptTemplate::Conservative);
411 assert_eq!(config.prompt_template, PromptTemplate::Conservative);
412 assert!(
413 config.system_prompt.contains("paper-trading")
414 || config.system_prompt.contains("analysis-only")
415 );
416 }
417
418 #[test]
419 fn test_agent_config_default() {
420 let config = AgentConfig::default();
421 assert_eq!(config.agent_id, "default");
422 assert_eq!(config.prompt_template, PromptTemplate::Conservative);
423 assert!(!config.enabled);
424 }
425
426 #[test]
427 fn test_agent_config_serialization() {
428 let config = AgentConfig {
429 agent_id: "test-agent".to_string(),
430 prompt_template: PromptTemplate::MeanReversion,
431 system_prompt: "Custom prompt text".to_string(),
432 analysis_frequency_minutes: 240,
433 trading_pairs: vec!["BTC-PERP".to_string(), "SOL-PERP".to_string()],
434 max_position_size_usd: 5_000.0,
435 enabled: true,
436 trading_mode: TradingMode::Live,
437 composer_profile: Some("all_weather".to_string()),
438 max_retries: 5,
439 max_tool_turns: 3,
440 };
441
442 let json = serde_json::to_value(&config).unwrap();
443 assert_eq!(json["agentId"], "test-agent");
444 assert_eq!(json["promptTemplate"], "meanReversion");
445 assert_eq!(json["systemPrompt"], "Custom prompt text");
446 assert_eq!(json["analysisFrequencyMinutes"], 240);
447 assert_eq!(
448 json["tradingPairs"],
449 serde_json::json!(["BTC-PERP", "SOL-PERP"])
450 );
451 assert_eq!(json["maxPositionSizeUsd"], 5_000.0);
452 assert_eq!(json["enabled"], true);
453 assert_eq!(json["tradingMode"], "live");
454 assert_eq!(json["composerProfile"], "all_weather");
455 assert_eq!(json["maxRetries"], 5);
456 }
457
458 #[test]
459 fn test_agent_config_deserialization() {
460 let json = serde_json::json!({
461 "agentId": "deser-agent",
462 "promptTemplate": "trendFollowing",
463 "systemPrompt": "Go long on everything",
464 "analysisFrequencyMinutes": 15,
465 "tradingPairs": ["ETH-PERP"],
466 "maxPositionSizeUsd": 25000.0,
467 "enabled": false,
468 "tradingMode": "live"
469 });
470
471 let config: AgentConfig = serde_json::from_value(json).unwrap();
472 assert_eq!(config.agent_id, "deser-agent");
473 assert_eq!(config.prompt_template, PromptTemplate::TrendFollowing);
474 assert_eq!(config.system_prompt, "Go long on everything");
475 assert_eq!(config.analysis_frequency_minutes, 15);
476 assert_eq!(config.trading_pairs, vec!["ETH-PERP"]);
477 assert_eq!(config.max_position_size_usd, 25_000.0);
478 assert!(!config.enabled);
479 assert_eq!(config.trading_mode, TradingMode::Live);
480 }
481
482 #[test]
483 fn test_agent_config_deserialization_missing_trading_mode_defaults_to_paper() {
484 let json = serde_json::json!({
485 "agentId": "old-agent",
486 "promptTemplate": "conservative",
487 "systemPrompt": "Analyze only",
488 "analysisFrequencyMinutes": 60,
489 "tradingPairs": ["BTC-PERP"],
490 "maxPositionSizeUsd": 10000.0,
491 "enabled": false
492 });
493
494 let config: AgentConfig = serde_json::from_value(json).unwrap();
495 assert_eq!(config.trading_mode, TradingMode::Paper);
496 }
497
498 #[test]
499 fn test_agent_config_roundtrip() {
500 let config = AgentConfig::with_template("roundtrip-agent", PromptTemplate::TrendFollowing);
501 let json = serde_json::to_string(&config).unwrap();
502 let deserialized: AgentConfig = serde_json::from_str(&json).unwrap();
503
504 assert_eq!(config.agent_id, deserialized.agent_id);
505 assert_eq!(config.prompt_template, deserialized.prompt_template);
506 assert_eq!(config.system_prompt, deserialized.system_prompt);
507 assert_eq!(
508 config.analysis_frequency_minutes,
509 deserialized.analysis_frequency_minutes
510 );
511 assert_eq!(config.trading_pairs, deserialized.trading_pairs);
512 assert_eq!(
513 config.max_position_size_usd,
514 deserialized.max_position_size_usd
515 );
516 assert_eq!(config.enabled, deserialized.enabled);
517 }
518
519 #[test]
522 fn test_save_and_load_agent_config() {
523 let config = AgentConfig {
524 agent_id: "persist-test-agent-cfg".to_string(),
525 prompt_template: PromptTemplate::MeanReversion,
526 system_prompt: "Test prompt for persistence".to_string(),
527 analysis_frequency_minutes: 30,
528 trading_pairs: vec!["BTC-PERP".to_string()],
529 max_position_size_usd: 7_500.0,
530 enabled: true,
531 trading_mode: TradingMode::Live,
532 composer_profile: None,
533 max_retries: 3,
534 max_tool_turns: 5,
535 };
536
537 save_agent_config_to_disk(&config).unwrap();
539
540 let loaded = load_agent_config("persist-test-agent-cfg");
542 assert!(loaded.is_some());
543 let loaded = loaded.unwrap();
544 assert_eq!(loaded.agent_id, config.agent_id);
545 assert_eq!(loaded.prompt_template, config.prompt_template);
546 assert_eq!(loaded.system_prompt, config.system_prompt);
547 assert_eq!(
548 loaded.analysis_frequency_minutes,
549 config.analysis_frequency_minutes
550 );
551 assert_eq!(loaded.trading_pairs, config.trading_pairs);
552 assert_eq!(loaded.max_position_size_usd, config.max_position_size_usd);
553 assert_eq!(loaded.enabled, config.enabled);
554
555 let _ = fs::remove_file(agent_config_path("persist-test-agent-cfg"));
557 }
558
559 #[test]
560 fn test_load_nonexistent_config_returns_none() {
561 let loaded = load_agent_config("nonexistent-agent-xyz-12345");
562 assert!(loaded.is_none());
563 }
564
565 #[test]
566 fn test_delete_agent_config() {
567 let config =
568 AgentConfig::with_template("delete-test-agent-cfg", PromptTemplate::Conservative);
569 save_agent_config_to_disk(&config).unwrap();
570
571 assert!(load_agent_config("delete-test-agent-cfg").is_some());
573
574 delete_agent_config_from_disk("delete-test-agent-cfg").unwrap();
576
577 assert!(load_agent_config("delete-test-agent-cfg").is_none());
579 }
580
581 #[test]
582 fn test_delete_nonexistent_config_is_ok() {
583 let result = delete_agent_config_from_disk("never-existed-agent-xyz");
584 assert!(result.is_ok());
585 }
586
587 #[test]
588 fn test_overwrite_config() {
589 let agent_id = "overwrite-test-agent-cfg";
590
591 let config1 = AgentConfig {
592 agent_id: agent_id.to_string(),
593 prompt_template: PromptTemplate::TrendFollowing,
594 system_prompt: "First prompt".to_string(),
595 analysis_frequency_minutes: 15,
596 trading_pairs: vec!["BTC-PERP".to_string()],
597 max_position_size_usd: 1_000.0,
598 enabled: false,
599 trading_mode: TradingMode::Paper,
600 composer_profile: None,
601 max_retries: 3,
602 max_tool_turns: 5,
603 };
604 save_agent_config_to_disk(&config1).unwrap();
605
606 let config2 = AgentConfig {
608 agent_id: agent_id.to_string(),
609 prompt_template: PromptTemplate::MeanReversion,
610 system_prompt: "Updated prompt".to_string(),
611 analysis_frequency_minutes: 240,
612 trading_pairs: vec!["ETH-PERP".to_string(), "SOL-PERP".to_string()],
613 max_position_size_usd: 50_000.0,
614 enabled: true,
615 trading_mode: TradingMode::Live,
616 composer_profile: None,
617 max_retries: 3,
618 max_tool_turns: 5,
619 };
620 save_agent_config_to_disk(&config2).unwrap();
621
622 let loaded = load_agent_config(agent_id).unwrap();
623 assert_eq!(loaded.prompt_template, PromptTemplate::MeanReversion);
624 assert_eq!(loaded.system_prompt, "Updated prompt");
625 assert_eq!(loaded.analysis_frequency_minutes, 240);
626 assert_eq!(loaded.trading_pairs.len(), 2);
627 assert_eq!(loaded.max_position_size_usd, 50_000.0);
628 assert!(loaded.enabled);
629
630 let _ = fs::remove_file(agent_config_path(agent_id));
632 }
633
634 #[test]
637 fn test_validate_config_valid() {
638 let config = AgentConfig::default();
639 assert!(validate_agent_config(&config).is_ok());
642 }
643
644 #[test]
645 fn test_validate_config_empty_agent_id() {
646 let config = AgentConfig {
647 agent_id: String::new(),
648 ..AgentConfig::default()
649 };
650 let result = validate_agent_config(&config);
651 assert!(result.is_err());
652 assert!(result.unwrap_err().contains("agent_id"));
653 }
654
655 #[test]
656 fn test_validate_config_zero_frequency() {
657 let config = AgentConfig {
658 agent_id: "valid-id".to_string(),
659 analysis_frequency_minutes: 0,
660 ..AgentConfig::default()
661 };
662 let result = validate_agent_config(&config);
663 assert!(result.is_err());
664 assert!(result.unwrap_err().contains("frequency"));
665 }
666
667 #[test]
668 fn test_validate_config_negative_position_size() {
669 let config = AgentConfig {
670 agent_id: "valid-id".to_string(),
671 max_position_size_usd: -100.0,
672 ..AgentConfig::default()
673 };
674 let result = validate_agent_config(&config);
675 assert!(result.is_err());
676 assert!(result.unwrap_err().contains("negative"));
677 }
678
679 #[test]
680 fn test_validate_config_custom_empty_prompt() {
681 let config = AgentConfig {
682 agent_id: "valid-id".to_string(),
683 prompt_template: PromptTemplate::Custom,
684 system_prompt: String::new(),
685 analysis_frequency_minutes: 60,
686 trading_pairs: vec![],
687 max_position_size_usd: 1_000.0,
688 enabled: false,
689 trading_mode: TradingMode::Paper,
690 composer_profile: None,
691 max_retries: 3,
692 max_tool_turns: 5,
693 };
694 let result = validate_agent_config(&config);
695 assert!(result.is_err());
696 assert!(result.unwrap_err().contains("system_prompt"));
697 }
698
699 #[test]
702 fn test_agent_config_default_max_retries() {
703 let config = AgentConfig::default();
704 assert_eq!(config.max_retries, 3);
705 }
706
707 #[test]
708 fn test_agent_config_max_retries_serialization() {
709 let config = AgentConfig {
710 max_retries: 5,
711 ..AgentConfig::default()
712 };
713 let json = serde_json::to_value(&config).unwrap();
714 assert_eq!(json["maxRetries"], 5);
715 }
716
717 #[test]
718 fn test_agent_config_max_retries_deserialization_present() {
719 let json = serde_json::json!({
720 "agentId": "retry-agent",
721 "promptTemplate": "conservative",
722 "systemPrompt": "test",
723 "analysisFrequencyMinutes": 60,
724 "tradingPairs": ["BTC-PERP"],
725 "maxPositionSizeUsd": 10000.0,
726 "enabled": false,
727 "maxRetries": 7
728 });
729 let config: AgentConfig = serde_json::from_value(json).unwrap();
730 assert_eq!(config.max_retries, 7);
731 }
732
733 #[test]
734 fn test_agent_config_max_retries_deserialization_missing_defaults_to_3() {
735 let json = serde_json::json!({
736 "agentId": "no-retry-field",
737 "promptTemplate": "conservative",
738 "systemPrompt": "test",
739 "analysisFrequencyMinutes": 60,
740 "tradingPairs": ["BTC-PERP"],
741 "maxPositionSizeUsd": 10000.0,
742 "enabled": false
743 });
744 let config: AgentConfig = serde_json::from_value(json).unwrap();
745 assert_eq!(config.max_retries, 3);
746 }
747
748 #[test]
749 fn test_agent_config_max_retries_zero_disables_retry() {
750 let config = AgentConfig {
751 max_retries: 0,
752 ..AgentConfig::default()
753 };
754 assert_eq!(config.max_retries, 0);
755 let json = serde_json::to_value(&config).unwrap();
756 assert_eq!(json["maxRetries"], 0);
757 }
758}