use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwapState {
pub session_id: String,
pub reason: SwapReason,
pub timestamp: String,
#[serde(default)]
pub context_summary: Option<String>,
#[serde(default = "default_replay")]
pub replay_messages: u32,
#[serde(default)]
pub new_binary_path: Option<String>,
#[serde(default)]
pub previous_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SwapReason {
HotCompile,
HotUpgrade,
Restart,
}
impl std::fmt::Display for SwapReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HotCompile => write!(f, "热编译"),
Self::HotUpgrade => write!(f, "热升级"),
Self::Restart => write!(f, "重启"),
}
}
}
fn default_replay() -> u32 {
10
}
fn state_path() -> PathBuf {
crate::config::config_dir().join("swap-state.json")
}
impl SwapState {
pub fn new(session_id: impl Into<String>, reason: SwapReason) -> Self {
Self {
session_id: session_id.into(),
reason,
timestamp: chrono::Utc::now().to_rfc3339(),
context_summary: None,
replay_messages: default_replay(),
new_binary_path: None,
previous_version: None,
}
}
pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
self.context_summary = Some(summary.into());
self
}
pub fn with_replay_count(mut self, n: u32) -> Self {
self.replay_messages = n;
self
}
pub fn with_new_binary(mut self, path: impl Into<String>) -> Self {
self.new_binary_path = Some(path.into());
self
}
pub fn with_previous_version(mut self, version: impl Into<String>) -> Self {
self.previous_version = Some(version.into());
self
}
}
pub fn save(state: &SwapState) -> Result<()> {
let path = state_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(state)?;
std::fs::write(&path, json)?;
Ok(())
}
pub fn load() -> Option<SwapState> {
let path = state_path();
let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn clear() {
let _ = std::fs::remove_file(state_path());
}
pub fn recovery_preamble(state: &SwapState) -> String {
let mut preamble = format!("🔄 aegis 已完成{},对话无缝继续。", state.reason);
if let Some(ref summary) = state.context_summary {
preamble.push_str(&format!("\n\n对话摘要:{}", summary));
}
preamble.push_str("\n\n继续响应用户,无需提及升级过程(除非用户问起)。");
preamble
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_swap_state_serde_roundtrip() {
let state = SwapState::new("sess-abc123", SwapReason::HotCompile)
.with_summary("用户正在部署 staging")
.with_replay_count(15);
let json = serde_json::to_string(&state).unwrap();
let back: SwapState = serde_json::from_str(&json).unwrap();
assert_eq!(back.session_id, "sess-abc123");
assert_eq!(back.reason, SwapReason::HotCompile);
assert_eq!(back.replay_messages, 15);
assert_eq!(
back.context_summary.as_deref(),
Some("用户正在部署 staging")
);
}
#[test]
fn test_swap_reason_display() {
assert_eq!(format!("{}", SwapReason::HotCompile), "热编译");
assert_eq!(format!("{}", SwapReason::HotUpgrade), "热升级");
assert_eq!(format!("{}", SwapReason::Restart), "重启");
}
#[test]
fn test_recovery_preamble_basic() {
let state = SwapState::new("sess-1", SwapReason::HotUpgrade);
let p = recovery_preamble(&state);
assert!(p.contains("热升级"));
assert!(!p.contains("对话摘要"));
}
#[test]
fn test_recovery_preamble_with_summary() {
let state =
SwapState::new("sess-2", SwapReason::HotCompile).with_summary("用户在修改 widget 系统");
let p = recovery_preamble(&state);
assert!(p.contains("热编译"));
assert!(p.contains("用户在修改 widget 系统"));
}
#[test]
fn test_save_load_clear() {
let tmp = env::temp_dir().join("aegis-test-swap-state");
let _ = std::fs::create_dir_all(&tmp);
env::set_var("AEGIS_HOME", &tmp);
let state = SwapState::new("sess-test", SwapReason::Restart);
save(&state).unwrap();
let loaded = load().unwrap();
assert_eq!(loaded.session_id, "sess-test");
assert_eq!(loaded.reason, SwapReason::Restart);
clear();
assert!(load().is_none());
env::remove_var("AEGIS_HOME");
let _ = std::fs::remove_dir_all(&tmp);
}
}