use serde::Serialize;
#[derive(Serialize)]
pub struct AddResponse {
pub status: String,
pub id: String,
}
#[derive(Serialize)]
pub struct SearchResponse {
pub results: Vec<SearchResultItem>,
}
#[derive(Serialize)]
pub struct SearchResultItem {
pub id: String,
pub content: String,
pub similarity: f64,
pub created_at: String,
pub retrieval_count: i64,
pub last_retrieved_at: Option<String>,
pub memory_type: String,
pub status: String,
pub importance: String,
}
#[derive(Serialize)]
pub struct GetResponse {
pub id: String,
pub content: String,
pub project_id: String,
pub metadata: Option<String>,
pub created_at: String,
pub updated_at: String,
pub retrieval_count: i64,
pub last_retrieved_at: Option<String>,
pub memory_type: String,
pub status: String,
pub importance: String,
}
#[derive(Serialize)]
pub struct ListResponse {
pub memories: Vec<ListItem>,
}
#[derive(Serialize)]
pub struct ListItem {
pub id: String,
pub content: String,
pub created_at: String,
pub retrieval_count: i64,
pub last_retrieved_at: Option<String>,
pub memory_type: String,
pub status: String,
pub importance: String,
}
#[derive(Serialize)]
pub struct DeleteResponse {
pub status: String,
pub id: String,
}
#[derive(Serialize)]
pub struct UpdateResponse {
pub status: String,
pub id: String,
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Serialize)]
pub struct ConflictsResponse {
pub status: String,
pub proposed: String,
pub conflicts: Vec<ConflictMemoryResponse>,
}
#[derive(Serialize)]
pub struct ConflictMemoryResponse {
pub id: String,
pub content: String,
pub similarity: f64,
}
#[derive(Serialize)]
pub struct ValidateResponse {
pub token_count: usize,
pub max_tokens: usize,
pub within_limit: bool,
}
#[derive(Serialize)]
pub struct DoctorResponse {
pub project_id: String,
pub total_rows: usize,
pub real_rows: usize,
pub mock_rows: usize,
pub unknown_rows: usize,
}
#[derive(Serialize)]
pub struct ReindexResponse {
pub project_id: String,
pub reindexed: usize,
pub skipped: usize,
pub failed: Vec<ReindexFailure>,
}
#[derive(Serialize, Clone)]
pub struct ReindexFailure {
pub id: String,
pub error: String,
}
#[derive(Serialize)]
pub struct DoctorProjectsSuspectedSplit {
pub pair: [String; 2],
pub row_counts: [usize; 2],
}
#[derive(Serialize)]
pub struct DoctorProjectsResponse {
pub suspected_splits: Vec<DoctorProjectsSuspectedSplit>,
}
#[derive(Serialize)]
pub struct DoctorFtsProject {
pub project_id: String,
pub memory_rows: usize,
pub missing_from_fts: usize,
}
#[derive(Serialize)]
pub struct DoctorFtsResponse {
pub in_sync: bool,
pub underpopulated_by_project: Vec<DoctorFtsProject>,
pub orphan_rows: usize,
pub total_desynced: usize,
pub repaired: bool,
pub actions: usize,
}
#[derive(Serialize)]
pub struct BackupResponse {
pub source: String,
pub destination: String,
pub rows: usize,
pub bytes: u64,
}
#[derive(Serialize)]
pub struct MergeResponse {
pub from: String,
pub to: String,
pub rows_moved: usize,
}
#[derive(Serialize)]
#[allow(dead_code)] pub struct ExportResponse {
pub rows: usize,
pub path: String,
}
#[derive(Serialize, Clone, Debug)]
#[allow(dead_code)] pub struct ExportRowLine {
pub id: String,
pub project_id: String,
pub content: String,
pub metadata: Option<String>,
pub embedding: String,
pub created_at: String,
pub updated_at: String,
pub memory_type: String,
pub status: String,
pub superseded_by: Option<String>,
pub retrieval_count: i64,
pub last_retrieved_at: Option<String>,
}
#[derive(Serialize)]
#[allow(dead_code)] pub struct ImportResponse {
pub inserted: usize,
pub skipped: usize,
pub rows_total: usize,
}
pub fn print_json<T: Serialize>(value: &T) {
match serde_json::to_string_pretty(value) {
Ok(json) => println!("{}", json),
Err(e) => {
eprintln!("Failed to serialize JSON: {}", e);
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_add_response() {
let response = AddResponse {
status: "added".to_string(),
id: "test-id".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"status\":\"added\""));
assert!(json.contains("\"id\":\"test-id\""));
}
#[test]
fn test_serialize_search_response() {
let response = SearchResponse {
results: vec![SearchResultItem {
id: "test-id".to_string(),
content: "test content".to_string(),
similarity: 0.95,
created_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 3,
last_retrieved_at: Some("2024-01-02T00:00:00Z".to_string()),
memory_type: "guard".to_string(),
status: "active".to_string(),
importance: "high".to_string(),
}],
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"results\""));
assert!(json.contains("\"similarity\":0.95"));
assert!(json.contains("\"retrieval_count\":3"));
assert!(json.contains("\"last_retrieved_at\":\"2024-01-02T00:00:00Z\""));
assert!(json.contains("\"memory_type\":\"guard\""));
assert!(json.contains("\"status\":\"active\""));
assert!(json.contains("\"importance\":\"high\""));
}
#[test]
fn test_serialize_search_result_null_last_retrieved() {
let response = SearchResponse {
results: vec![SearchResultItem {
id: "test-id".to_string(),
content: "test content".to_string(),
similarity: 0.95,
created_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 0,
last_retrieved_at: None,
memory_type: "fact".to_string(),
status: "active".to_string(),
importance: "medium".to_string(),
}],
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"retrieval_count\":0"));
assert!(json.contains("\"last_retrieved_at\":null"));
}
#[test]
fn test_serialize_list_item_with_retrieval_fields() {
let item = ListItem {
id: "test-id".to_string(),
content: "test content".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 5,
last_retrieved_at: Some("2024-01-02T12:00:00Z".to_string()),
memory_type: "fact".to_string(),
status: "active".to_string(),
importance: "medium".to_string(),
};
let json = serde_json::to_string(&item).unwrap();
assert!(json.contains("\"retrieval_count\":5"));
assert!(json.contains("\"last_retrieved_at\":\"2024-01-02T12:00:00Z\""));
assert!(json.contains("\"importance\":\"medium\""));
let item_none = ListItem {
id: "test-id".to_string(),
content: "test content".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 0,
last_retrieved_at: None,
memory_type: "fact".to_string(),
status: "active".to_string(),
importance: "low".to_string(),
};
let json_none = serde_json::to_string(&item_none).unwrap();
assert!(json_none.contains("\"retrieval_count\":0"));
assert!(json_none.contains("\"last_retrieved_at\":null"));
}
#[test]
fn test_serialize_backup_response() {
let response = BackupResponse {
source: "/tmp/memories.db".to_string(),
destination: "/tmp/memories-backup.db".to_string(),
rows: 42,
bytes: 2048,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"source\":\"/tmp/memories.db\""));
assert!(json.contains("\"destination\":\"/tmp/memories-backup.db\""));
assert!(json.contains("\"rows\":42"));
assert!(json.contains("\"bytes\":2048"));
}
#[test]
fn test_serialize_export_row_line_pins_memory_type_not_type() {
let row = ExportRowLine {
id: "id-1".to_string(),
project_id: "proj".to_string(),
content: "c".to_string(),
metadata: None,
embedding: "AA==".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
memory_type: "guard".to_string(),
status: "active".to_string(),
superseded_by: None,
retrieval_count: 3,
last_retrieved_at: Some("2024-01-02T00:00:00Z".to_string()),
};
let json = serde_json::to_string(&row).unwrap();
assert!(json.contains("\"memory_type\":\"guard\""));
assert!(json.contains("\"id\":\"id-1\""));
assert!(json.contains("\"project_id\":\"proj\""));
assert!(json.contains("\"embedding\":\"AA==\""));
assert!(json.contains("\"retrieval_count\":3"));
assert!(json.contains("\"superseded_by\":null"));
assert!(json.contains("\"last_retrieved_at\":\"2024-01-02T00:00:00Z\""));
assert!(
!json.contains("\"type\":"),
"must not have bare \"type\" key, got: {}",
json
);
let row2 = ExportRowLine {
superseded_by: Some("x".to_string()),
..row
};
let j2 = serde_json::to_string(&row2).unwrap();
assert!(j2.contains("\"superseded_by\":\"x\""));
}
#[test]
fn test_serialize_export_import_backup_responses() {
let e = ExportResponse {
rows: 42,
path: "p.jsonl".to_string(),
};
let ej = serde_json::to_string(&e).unwrap();
assert!(ej.contains("\"rows\":42"));
assert!(ej.contains("\"path\":\"p.jsonl\""));
let i = ImportResponse {
inserted: 10,
skipped: 5,
rows_total: 15,
};
let ij = serde_json::to_string(&i).unwrap();
assert!(ij.contains("\"inserted\":10"));
assert!(ij.contains("\"skipped\":5"));
assert!(ij.contains("\"rows_total\":15"));
let b = BackupResponse {
source: "b.db".to_string(),
destination: "b-backup.db".to_string(),
rows: 0,
bytes: 2048,
};
let bj = serde_json::to_string(&b).unwrap();
assert!(bj.contains("\"bytes\":2048"));
assert!(bj.contains("\"source\":\"b.db\""));
}
#[test]
fn test_serialize_get_and_list_items_carry_type_and_status() {
let get = GetResponse {
id: "test-id".to_string(),
content: "test content".to_string(),
project_id: "proj".to_string(),
metadata: None,
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 2,
last_retrieved_at: Some("2024-01-02T00:00:00Z".to_string()),
memory_type: "procedure".to_string(),
status: "candidate".to_string(),
importance: "critical".to_string(),
};
let get_json = serde_json::to_string(&get).unwrap();
assert!(get_json.contains("\"retrieval_count\":2"));
assert!(get_json.contains("\"memory_type\":\"procedure\""));
assert!(get_json.contains("\"status\":\"candidate\""));
assert!(get_json.contains("\"importance\":\"critical\""));
let list = ListResponse {
memories: vec![ListItem {
id: "test-id".to_string(),
content: "test content".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
retrieval_count: 0,
last_retrieved_at: None,
memory_type: "observation".to_string(),
status: "deprecated".to_string(),
importance: "medium".to_string(),
}],
};
let list_json = serde_json::to_string(&list).unwrap();
assert!(list_json.contains("\"memory_type\":\"observation\""));
assert!(list_json.contains("\"status\":\"deprecated\""));
assert!(list_json.contains("\"importance\":\"medium\""));
}
}