feat(M12): complete milestone with plugins, checkpointing, and rewind

Implements the remaining M12 features from AGENTS.md:

**Plugin System (crates/platform/plugins)**
- Plugin manifest schema with plugin.json support
- Plugin loader for commands, agents, skills, hooks, and MCP servers
- Discovers plugins from ~/.config/owlen/plugins and .owlen/plugins
- Includes comprehensive tests (4 passing)

**Session Checkpointing (crates/core/agent)**
- Checkpoint struct capturing session state and file diffs
- CheckpointManager with snapshot, diff, save, load, and rewind capabilities
- File diff tracking with before/after content
- Checkpoint persistence to .owlen/checkpoints/
- Includes comprehensive tests (6 passing)

**REPL Commands (crates/app/cli)**
- /checkpoint - Save current session with file diffs
- /checkpoints - List all saved checkpoints
- /rewind <id> - Restore session and files from checkpoint
- Updated /help documentation

M12 milestone now fully complete:
 /permissions, /status, /cost (previously implemented)
 Checkpointing and /rewind
 Plugin loader with manifest schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-01 21:59:08 +01:00
parent 04a7085007
commit 5caf502009
8 changed files with 852 additions and 1 deletions

View File

@@ -0,0 +1,210 @@
use agent_core::{Checkpoint, CheckpointManager, FileDiff, SessionHistory, SessionStats};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_checkpoint_save_and_load() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().to_path_buf();
let stats = SessionStats::new();
let mut history = SessionHistory::new();
history.add_user_message("Hello".to_string());
history.add_assistant_message("Hi there!".to_string());
let file_diffs = vec![FileDiff::new(
PathBuf::from("test.txt"),
"before".to_string(),
"after".to_string(),
)];
let checkpoint = Checkpoint::new(
"test-checkpoint".to_string(),
stats.clone(),
&history,
file_diffs,
);
// Save checkpoint
checkpoint.save(&checkpoint_dir).unwrap();
// Load checkpoint
let loaded = Checkpoint::load(&checkpoint_dir, "test-checkpoint").unwrap();
assert_eq!(loaded.id, "test-checkpoint");
assert_eq!(loaded.user_prompts, vec!["Hello"]);
assert_eq!(loaded.assistant_responses, vec!["Hi there!"]);
assert_eq!(loaded.file_diffs.len(), 1);
assert_eq!(loaded.file_diffs[0].path, PathBuf::from("test.txt"));
assert_eq!(loaded.file_diffs[0].before, "before");
assert_eq!(loaded.file_diffs[0].after, "after");
}
#[test]
fn test_checkpoint_list() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().to_path_buf();
// Create a few checkpoints
for i in 1..=3 {
let checkpoint = Checkpoint::new(
format!("checkpoint-{}", i),
SessionStats::new(),
&SessionHistory::new(),
vec![],
);
checkpoint.save(&checkpoint_dir).unwrap();
}
let checkpoints = Checkpoint::list(&checkpoint_dir).unwrap();
assert_eq!(checkpoints.len(), 3);
assert!(checkpoints.contains(&"checkpoint-1".to_string()));
assert!(checkpoints.contains(&"checkpoint-2".to_string()));
assert!(checkpoints.contains(&"checkpoint-3".to_string()));
}
#[test]
fn test_checkpoint_manager_snapshot_and_diff() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().join("checkpoints");
let test_file = temp_dir.path().join("test.txt");
// Create initial file content
fs::write(&test_file, "initial content").unwrap();
let mut manager = CheckpointManager::new(checkpoint_dir.clone());
// Snapshot the file
manager.snapshot_file(&test_file).unwrap();
// Modify the file
fs::write(&test_file, "modified content").unwrap();
// Create a diff
let diff = manager.create_diff(&test_file).unwrap();
assert!(diff.is_some());
let diff = diff.unwrap();
assert_eq!(diff.path, test_file);
assert_eq!(diff.before, "initial content");
assert_eq!(diff.after, "modified content");
}
#[test]
fn test_checkpoint_manager_save_and_restore() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().join("checkpoints");
let test_file = temp_dir.path().join("test.txt");
// Create initial file content
fs::write(&test_file, "initial content").unwrap();
let mut manager = CheckpointManager::new(checkpoint_dir.clone());
// Snapshot the file
manager.snapshot_file(&test_file).unwrap();
// Modify the file
fs::write(&test_file, "modified content").unwrap();
// Save checkpoint
let mut history = SessionHistory::new();
history.add_user_message("test".to_string());
let checkpoint = manager
.save_checkpoint("test-checkpoint".to_string(), SessionStats::new(), &history)
.unwrap();
assert_eq!(checkpoint.file_diffs.len(), 1);
assert_eq!(checkpoint.file_diffs[0].before, "initial content");
assert_eq!(checkpoint.file_diffs[0].after, "modified content");
// Modify file again
fs::write(&test_file, "final content").unwrap();
assert_eq!(fs::read_to_string(&test_file).unwrap(), "final content");
// Rewind to checkpoint
let restored_files = manager.rewind_to("test-checkpoint").unwrap();
assert_eq!(restored_files.len(), 1);
assert_eq!(restored_files[0], test_file);
// File should be reverted to initial content (before the checkpoint)
assert_eq!(fs::read_to_string(&test_file).unwrap(), "initial content");
}
#[test]
fn test_checkpoint_manager_multiple_files() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().join("checkpoints");
let test_file1 = temp_dir.path().join("file1.txt");
let test_file2 = temp_dir.path().join("file2.txt");
// Create initial files
fs::write(&test_file1, "file1 initial").unwrap();
fs::write(&test_file2, "file2 initial").unwrap();
let mut manager = CheckpointManager::new(checkpoint_dir.clone());
// Snapshot both files
manager.snapshot_file(&test_file1).unwrap();
manager.snapshot_file(&test_file2).unwrap();
// Modify both files
fs::write(&test_file1, "file1 modified").unwrap();
fs::write(&test_file2, "file2 modified").unwrap();
// Save checkpoint
let checkpoint = manager
.save_checkpoint(
"multi-file-checkpoint".to_string(),
SessionStats::new(),
&SessionHistory::new(),
)
.unwrap();
assert_eq!(checkpoint.file_diffs.len(), 2);
// Modify files again
fs::write(&test_file1, "file1 final").unwrap();
fs::write(&test_file2, "file2 final").unwrap();
// Rewind
let restored_files = manager.rewind_to("multi-file-checkpoint").unwrap();
assert_eq!(restored_files.len(), 2);
// Both files should be reverted
assert_eq!(fs::read_to_string(&test_file1).unwrap(), "file1 initial");
assert_eq!(fs::read_to_string(&test_file2).unwrap(), "file2 initial");
}
#[test]
fn test_checkpoint_no_changes() {
let temp_dir = TempDir::new().unwrap();
let checkpoint_dir = temp_dir.path().join("checkpoints");
let test_file = temp_dir.path().join("test.txt");
// Create file
fs::write(&test_file, "content").unwrap();
let mut manager = CheckpointManager::new(checkpoint_dir.clone());
// Snapshot the file
manager.snapshot_file(&test_file).unwrap();
// Don't modify the file
// Create diff - should be None because nothing changed
let diff = manager.create_diff(&test_file).unwrap();
assert!(diff.is_none());
// Save checkpoint - should have no diffs
let checkpoint = manager
.save_checkpoint(
"no-change-checkpoint".to_string(),
SessionStats::new(),
&SessionHistory::new(),
)
.unwrap();
assert_eq!(checkpoint.file_diffs.len(), 0);
}