use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Watermark {
pub ts_ms: i64,
pub client: String,
}
fn watermark_path(project_hash: &str) -> PathBuf {
difflore_core::db::project_index_dir(project_hash).join("last-session-start.json")
}
pub fn read_watermark(project_hash: &str) -> Option<Watermark> {
read_watermark_at(&watermark_path(project_hash))
}
pub fn write_watermark(project_hash: &str, wm: &Watermark) -> Result<(), String> {
write_watermark_at(&watermark_path(project_hash), wm)
}
fn read_watermark_at(path: &Path) -> Option<Watermark> {
let raw = std::fs::read_to_string(path).ok()?;
serde_json::from_str::<Watermark>(&raw).ok()
}
fn write_watermark_at(path: &Path, wm: &Watermark) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("create dir: {e}"))?;
}
let body = serde_json::to_string(wm).map_err(|e| format!("serialize: {e}"))?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, body).map_err(|e| format!("write tmp: {e}"))?;
std::fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_then_read_returns_same_value() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("last-session-start.json");
let wm = Watermark {
ts_ms: 1_700_000_000_000,
client: "claude-code".to_owned(),
};
write_watermark_at(&path, &wm).expect("write ok");
let back = read_watermark_at(&path).expect("read ok");
assert_eq!(back.ts_ms, wm.ts_ms);
assert_eq!(back.client, wm.client);
}
#[test]
fn read_missing_returns_none() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("never-written.json");
assert!(read_watermark_at(&path).is_none());
}
#[test]
fn read_garbage_json_returns_none() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("garbage.json");
std::fs::write(&path, "not json at all").expect("write");
assert!(read_watermark_at(&path).is_none());
}
#[test]
fn write_creates_missing_parent_dirs() {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp
.path()
.join("projects")
.join("abc123")
.join("last-session-start.json");
assert!(!path.parent().expect("parent").exists(), "precondition");
let wm = Watermark {
ts_ms: 1,
client: "cursor".to_owned(),
};
write_watermark_at(&path, &wm).expect("write ok");
assert!(path.exists(), "watermark file missing post-write");
}
}