trip-test 0.1.2

Contract testing & regression safety for MCP servers
Documentation
//! Snapshot file format and I/O.
//!
//! Snapshots capture a baseline of an MCP server's contract (tool schemas)
//! and recorded exchanges (tool calls + responses) for regression testing.

use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fs;
use chrono::Utc;

/// Metadata about a snapshot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMeta {
    pub name: String,
    pub recorded_at: String,
    pub server_name: String,
    pub server_version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
    pub name: String,
    pub description: String,
    #[serde(rename = "inputSchema")]
    pub input_schema: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Exchange {
    pub id: String,
    pub tool: String,
    pub input: Value,
    pub match_mode: String,
    pub expected: ExchangeResult,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeResult {
    pub is_error: bool,
    pub content: Vec<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
    pub format_version: u32,
    pub meta: SnapshotMeta,
    pub tool_catalog: Vec<ToolInfo>,
    pub exchanges: Vec<Exchange>,
}

impl Snapshot {
    pub fn new(server_name: String, server_version: String) -> Self {
        Snapshot {
            format_version: 1,
            meta: SnapshotMeta {
                name: format!("{}-baseline", server_name),
                recorded_at: Utc::now().to_rfc3339(),
                server_name,
                server_version,
            },
            tool_catalog: Vec::new(),
            exchanges: Vec::new(),
        }
    }

    pub fn add_tool(&mut self, tool: ToolInfo) {
        self.tool_catalog.push(tool);
    }

    pub fn add_exchange(&mut self, tool: String, input: Value, result: ExchangeResult) {
        let id = format!("ex-{:03}", self.exchanges.len() + 1);
        self.exchanges.push(Exchange {
            id,
            tool,
            input,
            match_mode: "structural".to_string(),
            expected: result,
        });
    }

    pub fn save(&self, path: &str) -> Result<()> {
        let json = serde_json::to_string_pretty(&self)?;
        fs::write(path, json)?;
        println!("Snapshot saved to: {}", path);
        Ok(())
    }

    pub fn load(path: &str) -> Result<Self> {
        let content = fs::read_to_string(path)?;
        let snapshot = serde_json::from_str(&content)?;
        Ok(snapshot)
    }
}