Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/desktop/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,45 @@ pub fn open_url(_url: String) -> Result<(), String> {
// renderer; this is a stub kept for future server-side validation.
Ok(())
}

// ── Serde contract ─────────────────────────────────────────────────────
// AppInfo + SessionMeta are read by tauri-api.ts using snake_case keys
// (home_dir, size_bytes, updated_at_secs). They intentionally do NOT use
// rename_all="camelCase" (unlike the tool output structs in tools.rs). Lock
// that so a stray rename_all can't silently break the renderer. See HANDOFF §8a.
#[cfg(test)]
mod contract_tests {
use super::*;

fn keys(v: &serde_json::Value) -> Vec<String> {
v.as_object().unwrap().keys().cloned().collect()
}

#[test]
fn app_info_serializes_snake_case() {
let v = serde_json::to_value(AppInfo {
version: "1.0.0".into(),
platform: "darwin".into(),
home_dir: Some(std::path::PathBuf::from("/Users/x")),
})
.unwrap();
let k = keys(&v);
assert!(k.contains(&"home_dir".to_string()), "got {k:?}");
assert!(!k.contains(&"homeDir".to_string()), "camelCase leaked: {k:?}");
}

#[test]
fn session_meta_serializes_snake_case() {
let v = serde_json::to_value(SessionMeta {
id: "s1".into(),
path: std::path::PathBuf::from("/tmp/s1.jsonl"),
size_bytes: 42,
updated_at_secs: 1700,
})
.unwrap();
let k = keys(&v);
assert!(k.contains(&"size_bytes".to_string()), "got {k:?}");
assert!(k.contains(&"updated_at_secs".to_string()), "got {k:?}");
assert!(!k.contains(&"sizeBytes".to_string()), "camelCase leaked: {k:?}");
}
}
30 changes: 30 additions & 0 deletions apps/desktop/src-tauri/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,33 @@ pub fn write(creds: &Credentials) -> Result<(), String> {
}
Ok(())
}

// ── Serde contract ─────────────────────────────────────────────────────
// tauri-api.ts#readCredentials reads `api_key`/`auth_token`/`base_url` (snake)
// and maps them to camelCase itself. Lock that shape + the skip-if-None omission
// the TS side relies on (missing field → undefined). See HANDOFF §8a.
#[cfg(test)]
mod contract_tests {
use super::*;

#[test]
fn serializes_snake_case_keys() {
let v = serde_json::to_value(Credentials {
api_key: Some("sk".into()),
auth_token: Some("tok".into()),
base_url: Some("https://h/v1".into()),
})
.unwrap();
let keys: Vec<String> = v.as_object().unwrap().keys().cloned().collect();
assert!(keys.contains(&"api_key".to_string()), "got {keys:?}");
assert!(keys.contains(&"auth_token".to_string()), "got {keys:?}");
assert!(keys.contains(&"base_url".to_string()), "got {keys:?}");
assert!(!keys.contains(&"apiKey".to_string()), "camelCase leaked: {keys:?}");
}

#[test]
fn omits_none_fields() {
let v = serde_json::to_value(Credentials::default()).unwrap();
assert_eq!(v.as_object().unwrap().len(), 0, "None fields must be skipped: {v}");
}
}
Loading