Ensure planner writes GIT COMMIT entry before invoking git commit. Keep history entry even when git commit fails, matching summary text. Document invariant in code comment above write_git_commit call. Add lightweight test to assert history write precedes git::commit using test doubles instead of a real git repository. Investigate git history to find regression and its prior fix, and record a short root-cause summary outside the codebase. Reference completed_requirements_2025-12-10_16-55-05.md for details. Reference completed_todo_2025-12-10_16-55-05.md for task tracking.
241 lines
10 KiB
Rust
241 lines
10 KiB
Rust
//! Tests for the critical invariant: planner_history.txt must be written BEFORE git commit
|
|
//!
|
|
//! This test suite ensures that the ordering of history write and git commit operations
|
|
//! is maintained correctly. This is essential for audit trail purposes and post-mortem
|
|
//! analysis when commits fail.
|
|
|
|
use anyhow::Result;
|
|
use std::fs;
|
|
use std::process::Command;
|
|
use tempfile::TempDir;
|
|
|
|
/// Helper to create a test git repository
|
|
fn setup_test_git_repo() -> Result<TempDir> {
|
|
let temp_dir = TempDir::new()?;
|
|
let repo_path = temp_dir.path();
|
|
|
|
// Initialize git repo
|
|
Command::new("git")
|
|
.args(["init"])
|
|
.current_dir(repo_path)
|
|
.output()?;
|
|
|
|
// Configure git user (required for commits)
|
|
Command::new("git")
|
|
.args(["config", "user.name", "Test User"])
|
|
.current_dir(repo_path)
|
|
.output()?;
|
|
|
|
Command::new("git")
|
|
.args(["config", "user.email", "test@example.com"])
|
|
.current_dir(repo_path)
|
|
.output()?;
|
|
|
|
// Create g3-plan directory
|
|
let plan_dir = repo_path.join("g3-plan");
|
|
fs::create_dir_all(&plan_dir)?;
|
|
|
|
// Create planner_history.txt
|
|
fs::write(plan_dir.join("planner_history.txt"), "")?;
|
|
|
|
Ok(temp_dir)
|
|
}
|
|
|
|
/// Test that history entry is written even when git commit fails due to missing files
|
|
#[test]
|
|
fn test_history_written_before_commit_on_empty_staging() {
|
|
let temp_dir = setup_test_git_repo().expect("Failed to setup test repo");
|
|
let repo_path = temp_dir.path();
|
|
let plan_dir = repo_path.join("g3-plan");
|
|
|
|
// Import necessary types
|
|
use g3_planner::planner::PlannerConfig;
|
|
use g3_planner::history;
|
|
|
|
// Create a config
|
|
let config = PlannerConfig {
|
|
codepath: repo_path.to_path_buf(),
|
|
no_git: false,
|
|
max_turns: 5,
|
|
quiet: true,
|
|
config_path: None,
|
|
};
|
|
|
|
// Write a history entry as would happen in stage_and_commit
|
|
let summary = "Test commit message";
|
|
history::write_git_commit(&plan_dir, summary).expect("Failed to write history");
|
|
|
|
// Read history file to verify entry was written
|
|
let history_content = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
|
|
// Verify the history entry exists
|
|
assert!(history_content.contains("GIT COMMIT"), "History should contain GIT COMMIT entry");
|
|
assert!(history_content.contains("Test commit message"), "History should contain the commit message");
|
|
|
|
// Now attempt a commit (which will fail because nothing is staged)
|
|
// This simulates the scenario where history is written but commit fails
|
|
let commit_result = g3_planner::git::commit(&config.codepath, summary, "Test description");
|
|
|
|
// The commit should fail (nothing staged)
|
|
assert!(commit_result.is_err(), "Commit should fail with nothing staged");
|
|
|
|
// But history entry should still be present
|
|
let history_after = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file after commit");
|
|
|
|
assert!(history_after.contains("GIT COMMIT"), "History should still contain GIT COMMIT entry after failed commit");
|
|
assert!(history_after.contains("Test commit message"), "History should still contain the message after failed commit");
|
|
}
|
|
|
|
/// Test successful commit flow with history written first
|
|
#[test]
|
|
fn test_history_written_before_successful_commit() {
|
|
let temp_dir = setup_test_git_repo().expect("Failed to setup test repo");
|
|
let repo_path = temp_dir.path();
|
|
let plan_dir = repo_path.join("g3-plan");
|
|
|
|
use g3_planner::history;
|
|
|
|
// Create a file to commit
|
|
let test_file = repo_path.join("test.txt");
|
|
fs::write(&test_file, "test content").expect("Failed to create test file");
|
|
|
|
// Stage the file
|
|
Command::new("git")
|
|
.args(["add", "test.txt"])
|
|
.current_dir(repo_path)
|
|
.output()
|
|
.expect("Failed to stage file");
|
|
|
|
// Write history entry BEFORE commit
|
|
let summary = "Add test file";
|
|
history::write_git_commit(&plan_dir, summary).expect("Failed to write history");
|
|
|
|
// Verify history was written
|
|
let history_before = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
assert!(history_before.contains("GIT COMMIT"), "History should contain GIT COMMIT before commit");
|
|
assert!(history_before.contains("Add test file"), "History should contain message before commit");
|
|
|
|
// Now make the commit
|
|
let commit_result = g3_planner::git::commit(repo_path, summary, "Test description");
|
|
assert!(commit_result.is_ok(), "Commit should succeed with staged file");
|
|
|
|
// Verify history is still there after successful commit
|
|
let history_after = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file after commit");
|
|
assert!(history_after.contains("GIT COMMIT"), "History should contain GIT COMMIT after commit");
|
|
assert!(history_after.contains("Add test file"), "History should contain message after commit");
|
|
}
|
|
|
|
/// Test the ordering invariant: history must be written before attempting the commit
|
|
/// This ensures that if the commit operation is interrupted or fails, the history entry exists
|
|
#[test]
|
|
fn test_history_ordering_invariant() {
|
|
let temp_dir = setup_test_git_repo().expect("Failed to setup test repo");
|
|
let repo_path = temp_dir.path();
|
|
let plan_dir = repo_path.join("g3-plan");
|
|
|
|
use g3_planner::history;
|
|
|
|
// Test 1: Verify history is written first, even before staging
|
|
let summary1 = "First history entry";
|
|
|
|
// Record initial history state
|
|
let history_initial = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
|
|
// Write history entry
|
|
history::write_git_commit(&plan_dir, summary1).expect("Failed to write history");
|
|
|
|
// Write history entry BEFORE attempting commit
|
|
let history_after_write = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
|
|
// Verify the history entry exists and is different from initial state
|
|
assert_ne!(history_initial, history_after_write, "History should have changed after write");
|
|
assert!(history_after_write.contains("GIT COMMIT"), "History should contain GIT COMMIT entry");
|
|
assert!(history_after_write.contains("First history entry"), "History should contain the commit message");
|
|
|
|
// This demonstrates the ordering: history is written and persisted to disk
|
|
// BEFORE any git operations are attempted. If git::commit() were to fail
|
|
// at this point (e.g., due to missing staged files, git config errors, etc.),
|
|
// the history entry would already be on disk and available for audit.
|
|
|
|
// The other tests (test_history_written_before_commit_on_empty_staging and
|
|
// test_multiple_history_entries_with_failures) verify behavior with actual failures.
|
|
|
|
// This test focuses on the invariant itself: write happens first.
|
|
}
|
|
|
|
/// Test multiple history entries with mixed success/failure
|
|
#[test]
|
|
fn test_multiple_history_entries_with_failures() {
|
|
let temp_dir = setup_test_git_repo().expect("Failed to setup test repo");
|
|
let repo_path = temp_dir.path();
|
|
let plan_dir = repo_path.join("g3-plan");
|
|
|
|
use g3_planner::history;
|
|
|
|
// First entry - will fail (nothing staged)
|
|
history::write_git_commit(&plan_dir, "Commit 1 - will fail").expect("Failed to write history");
|
|
let _ = g3_planner::git::commit(repo_path, "Commit 1 - will fail", "Desc 1");
|
|
|
|
// Second entry - will succeed
|
|
let test_file = repo_path.join("file1.txt");
|
|
fs::write(&test_file, "content 1").expect("Failed to create file");
|
|
Command::new("git")
|
|
.args(["add", "file1.txt"])
|
|
.current_dir(repo_path)
|
|
.output()
|
|
.expect("Failed to stage file");
|
|
|
|
history::write_git_commit(&plan_dir, "Commit 2 - will succeed").expect("Failed to write history");
|
|
let _ = g3_planner::git::commit(repo_path, "Commit 2 - will succeed", "Desc 2");
|
|
|
|
// Third entry - will fail (nothing staged)
|
|
history::write_git_commit(&plan_dir, "Commit 3 - will fail").expect("Failed to write history");
|
|
let _ = g3_planner::git::commit(repo_path, "Commit 3 - will fail", "Desc 3");
|
|
|
|
// Read history and verify all entries are present
|
|
let history_content = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
|
|
// All three attempts should be recorded, regardless of success/failure
|
|
assert!(history_content.contains("Commit 1 - will fail"), "First commit attempt should be in history");
|
|
assert!(history_content.contains("Commit 2 - will succeed"), "Second commit attempt should be in history");
|
|
assert!(history_content.contains("Commit 3 - will fail"), "Third commit attempt should be in history");
|
|
|
|
// Count the number of GIT COMMIT entries
|
|
let commit_count = history_content.matches("GIT COMMIT").count();
|
|
assert_eq!(commit_count, 3, "Should have exactly 3 GIT COMMIT entries");
|
|
}
|
|
|
|
/// Test that history entries have consistent format and timestamps
|
|
#[test]
|
|
fn test_history_entry_format() {
|
|
let temp_dir = setup_test_git_repo().expect("Failed to setup test repo");
|
|
let plan_dir = temp_dir.path().join("g3-plan");
|
|
|
|
use g3_planner::history;
|
|
|
|
// Write a history entry
|
|
let summary = "Test formatting";
|
|
history::write_git_commit(&plan_dir, summary).expect("Failed to write history");
|
|
|
|
// Read and verify format
|
|
let history_content = fs::read_to_string(plan_dir.join("planner_history.txt"))
|
|
.expect("Failed to read history file");
|
|
|
|
// Should contain timestamp (YYYY-MM-DD HH:MM:SS format)
|
|
assert!(history_content.contains("-"), "Should contain date separators");
|
|
assert!(history_content.contains(":"), "Should contain time separators");
|
|
|
|
// Should contain the entry type
|
|
assert!(history_content.contains("GIT COMMIT"), "Should contain entry type");
|
|
|
|
// Should contain the message in parentheses
|
|
assert!(history_content.contains("(Test formatting)"), "Should contain message in parentheses");
|
|
}
|