use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::forge::{PrId, PrMetadata, RemoteComment};
use crate::model::DiffFile;
use crate::vcs::CommitInfo;
pub const CACHE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize)]
pub struct PrCache {
pub version: u32,
pub head_sha: String,
pub metadata: PrMetadata,
pub diff_files: Vec<DiffFile>,
pub commits: Vec<CommitInfo>,
pub comments: Vec<RemoteComment>,
}
fn cache_path(id: &PrId, host: &str) -> PathBuf {
let tmp = std::env::temp_dir();
tmp.join("travelagent")
.join(host)
.join(&id.owner)
.join(&id.repo)
.join(format!("pr-{}.json", id.number))
}
#[must_use]
pub fn read_cache(id: &PrId, host: &str, head_sha: &str) -> Option<PrCache> {
let path = cache_path(id, host);
let contents = std::fs::read_to_string(&path).ok()?;
let cache: PrCache = serde_json::from_str(&contents).ok()?;
if cache.version != CACHE_VERSION || cache.head_sha != head_sha {
return None;
}
Some(cache)
}
pub fn write_cache(id: &PrId, host: &str, cache: &PrCache) -> std::io::Result<()> {
let path = cache_path(id, host);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string(cache).map_err(|e| std::io::Error::other(e.to_string()))?;
std::fs::write(&path, json)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forge::{MergeableStatus, PrState};
use chrono::Utc;
#[allow(dead_code)]
fn test_pr_id() -> PrId {
PrId {
owner: "test-owner".into(),
repo: "test-repo".into(),
number: 42,
}
}
fn test_metadata() -> PrMetadata {
PrMetadata {
title: "Test PR".into(),
body: "Description".into(),
author: "alice".into(),
state: PrState::Open,
base_branch: "main".into(),
head_branch: "feature".into(),
head_sha: "abc123def456".into(),
created_at: Utc::now(),
mergeable: Some(MergeableStatus::Clean),
is_draft: false,
}
}
fn test_cache(head_sha: &str) -> PrCache {
PrCache {
version: CACHE_VERSION,
head_sha: head_sha.to_string(),
metadata: test_metadata(),
diff_files: vec![],
commits: vec![],
comments: vec![],
}
}
#[test]
fn read_cache_returns_none_for_missing_file() {
let id = PrId {
owner: "nonexistent-owner-xyz".into(),
repo: "nonexistent-repo-xyz".into(),
number: 99999,
};
assert!(read_cache(&id, "example.com", "abc123").is_none());
}
#[test]
fn write_and_read_cache_roundtrip() {
let id = PrId {
owner: "test-owner".into(),
repo: "test-repo".into(),
number: 10001,
};
let cache = test_cache("sha-abc123");
write_cache(&id, "github.com", &cache).unwrap();
let loaded = read_cache(&id, "github.com", "sha-abc123");
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.head_sha, "sha-abc123");
assert_eq!(loaded.metadata.title, "Test PR");
assert_eq!(loaded.version, CACHE_VERSION);
let _ = std::fs::remove_file(cache_path(&id, "github.com"));
}
#[test]
fn cache_miss_on_different_head_sha() {
let id = PrId {
owner: "test-owner".into(),
repo: "test-repo".into(),
number: 10002,
};
let cache = test_cache("sha-old");
write_cache(&id, "github.com", &cache).unwrap();
let loaded = read_cache(&id, "github.com", "sha-new");
assert!(loaded.is_none());
let _ = std::fs::remove_file(cache_path(&id, "github.com"));
}
#[test]
fn cache_miss_on_different_version() {
let id = PrId {
owner: "test-owner".into(),
repo: "test-repo".into(),
number: 10003,
};
let cache = PrCache {
version: CACHE_VERSION + 1,
head_sha: "sha-abc".to_string(),
metadata: test_metadata(),
diff_files: vec![],
commits: vec![],
comments: vec![],
};
write_cache(&id, "github.com", &cache).unwrap();
let loaded = read_cache(&id, "github.com", "sha-abc");
assert!(loaded.is_none());
let _ = std::fs::remove_file(cache_path(&id, "github.com"));
}
}