Skip to main content

trip_test/
snapshot.rs

1//! Snapshot file format and I/O.
2//!
3//! Snapshots capture a baseline of an MCP server's contract (tool schemas)
4//! and recorded exchanges (tool calls + responses) for regression testing.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::fs;
10use chrono::Utc;
11
12/// Metadata about a snapshot.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct SnapshotMeta {
15    pub name: String,
16    pub recorded_at: String,
17    pub server_name: String,
18    pub server_version: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ToolInfo {
23    pub name: String,
24    pub description: String,
25    #[serde(rename = "inputSchema")]
26    pub input_schema: Option<Value>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct Exchange {
32    pub id: String,
33    pub tool: String,
34    pub input: Value,
35    pub match_mode: String,
36    pub expected: ExchangeResult,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ExchangeResult {
41    pub is_error: bool,
42    pub content: Vec<Value>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct Snapshot {
47    pub format_version: u32,
48    pub meta: SnapshotMeta,
49    pub tool_catalog: Vec<ToolInfo>,
50    pub exchanges: Vec<Exchange>,
51}
52
53impl Snapshot {
54    pub fn new(server_name: String, server_version: String) -> Self {
55        Snapshot {
56            format_version: 1,
57            meta: SnapshotMeta {
58                name: format!("{}-baseline", server_name),
59                recorded_at: Utc::now().to_rfc3339(),
60                server_name,
61                server_version,
62            },
63            tool_catalog: Vec::new(),
64            exchanges: Vec::new(),
65        }
66    }
67
68    pub fn add_tool(&mut self, tool: ToolInfo) {
69        self.tool_catalog.push(tool);
70    }
71
72    pub fn add_exchange(&mut self, tool: String, input: Value, result: ExchangeResult) {
73        let id = format!("ex-{:03}", self.exchanges.len() + 1);
74        self.exchanges.push(Exchange {
75            id,
76            tool,
77            input,
78            match_mode: "structural".to_string(),
79            expected: result,
80        });
81    }
82
83    pub fn save(&self, path: &str) -> Result<()> {
84        let json = serde_json::to_string_pretty(&self)?;
85        fs::write(path, json)?;
86        println!("Snapshot saved to: {}", path);
87        Ok(())
88    }
89
90    pub fn load(path: &str) -> Result<Self> {
91        let content = fs::read_to_string(path)?;
92        let snapshot = serde_json::from_str(&content)?;
93        Ok(snapshot)
94    }
95}