docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
2026-07-23 16:55:39 -04:00
parent ff0fb56c67
commit a02b555e66
1458 changed files with 10729 additions and 21750 deletions
@@ -84,7 +84,7 @@ impl HunkTrackerActor {
let accepted = matches!(action, HunkAction::Accept);
self.update_session_stats(&hunk.line_info, accepted);
// Remove from turn_index
// Drop from turn_index
self.remove_from_turn_index(hunk_id, &hunk.source);
match action {
@@ -383,7 +383,7 @@ impl HunkTrackerActor {
let accepted = matches!(action, HunkAction::Accept);
self.update_session_stats(&hunk.line_info, accepted);
// Remove from turn_index
// Drop from turn_index
self.remove_from_turn_index(&hunk.id, &hunk.source);
affected_hunk_ids.push(hunk.id.clone());
@@ -190,7 +190,7 @@ mod tests {
assert!(!is_binary(empty));
}
// === TooLarge / bounded read tests (SF-2) ===
// TooLarge / bounded read tests (SF-2)
#[test]
fn test_classify_bytes_too_large() {
@@ -237,7 +237,7 @@ mod tests {
);
}
// === LFS pointer tests ===
// LFS pointer tests
#[test]
fn test_is_lfs_pointer_valid() {
@@ -310,7 +310,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("huge_binary.bin");
let mut data = vec![0xFFu8; MAX_TRACKED_TEXT_BYTES * 10];
data[50] = 0; // null byte in prefix
// null byte in prefix
data[50] = 0;
std::fs::write(&path, &data).unwrap();
let state = read_file_bounded(&path).await;
// Size > limit means TooLarge (bounded read guarantee - no full allocation)
@@ -415,7 +415,8 @@ impl HunkTrackerActor {
}
result.content
}
Err(_) => missing_content(), // spawn_blocking was cancelled or panicked
// spawn_blocking was cancelled or panicked
Err(_) => missing_content(),
}
}
@@ -97,7 +97,8 @@ impl HunkTrackerActor {
if let Some(best_match) = find_matching_old_hunk(new_hunk, &old_hunks) {
// Skip if this old hunk was already claimed by another new hunk
if claimed_old_ids.contains(&best_match.id) {
continue; // new_hunk keeps its new ID
// new_hunk keeps its new ID
continue;
}
claimed_old_ids.insert(best_match.id.clone());
@@ -567,7 +567,7 @@ impl HunkTrackerActor {
}
}
/// Restore a previously snapshotted state, replacing all current file
/// Restore a earlier snapshotted state, replacing all current file
/// states, turn index, and session stats.
/// Preserves the full FileContentState (including Binary/TooLarge).
fn restore_snapshot(&mut self, snapshot: HunkTrackerSnapshot) {
@@ -589,7 +589,7 @@ impl HunkTrackerActor {
self.turn_index = snapshot.turn_index;
self.session_stats = snapshot.session_stats;
// TODO: Re-emit HunkEvent::FileAdded / HunkEvent::HunkAdded for
// TODO: Re-emit HunkEvent::`FileAdded` / HunkEvent::`HunkAdded` for
// all restored files and hunks so that connected clients (TUI, VSCode
// extension) see the restored state without requiring a manual refresh.
// Alternative: emit a single HunkEvent::StateRestored { file_count }
@@ -29,7 +29,7 @@ use super::state::{FileContentState, FileHunkState};
/// of truth for the string.
pub const REFRESH_SCAN_LOG_PREFIX: &str = "refresh_all_baselines: completed in";
/// Log-line prefix for the unchanged-git-state skip path of
/// Log-line prefix for the `unchanged`-git-state skip path of
/// [`HunkTrackerActor::refresh_all_baselines`] (no scan ran).
pub const REFRESH_SKIP_LOG_PREFIX: &str = "refresh_all_baselines: git state unchanged";
@@ -37,7 +37,7 @@ pub const REFRESH_SKIP_LOG_PREFIX: &str = "refresh_all_baselines: git state unch
///
/// Git-stored content typically has exactly one trailing newline appended.
/// We strip only one to avoid falsely treating files with meaningful trailing
/// whitespace as clean. Bare `\r` (classic Mac) is intentionally out of scope.
/// whitespace as clean. Bare `\r` (classic Mac) is deliberately out of scope.
fn strip_single_trailing_newline(content: &str) -> &str {
content
.strip_suffix("\r\n")
@@ -67,7 +67,8 @@ impl HunkTrackerActor {
// Classify current content into FileContentState (single classification, cloned for file_states)
let current_state = classify_string(content.clone());
let current_state_for_hunks = current_state.clone(); // Used by recompute_hunks below
// Used by recompute_hunks below
let current_state_for_hunks = current_state.clone();
// Binary or TooLarge content: still track as an agent file (so
// `get_all_tracked_paths` reports it for worktree replication)
@@ -132,7 +133,7 @@ impl HunkTrackerActor {
},
);
// Emit FileAdded event
// Emit `FileAdded` event
self.send_event(HunkEvent::FileAdded {
path: path.clone(),
is_agent_file: true,
@@ -277,7 +278,8 @@ impl HunkTrackerActor {
path.clone(),
FileHunkState {
baseline,
current_content: missing_content(), // Will be set by recompute_hunks
// Will be set by recompute_hunks
current_content: missing_content(),
hunks: vec![],
is_agent_file: false,
baseline_accepted: false,
@@ -311,7 +313,7 @@ impl HunkTrackerActor {
(FileContentState::Symlink, FileContentState::Symlink) => true,
// Symlink on disk vs Full(target) in HEAD (or vice versa):
// git stores symlinks as plain text blobs, so the types
// differ even when the file is unchanged. Consult dirty cache.
// differ even when the file is `unchanged`. Consult dirty cache.
(FileContentState::Symlink, FileContentState::Full(_))
| (FileContentState::Full(_), FileContentState::Symlink) => {
let rel = path.strip_prefix(&self.working_dir).unwrap_or(&path);
@@ -367,7 +369,8 @@ impl HunkTrackerActor {
// `rm foo.txt` on a committed file).
let baseline = self.read_baseline(&path).await;
if matches!(baseline, FileContentState::Missing) {
return; // Not in HEAD either, nothing to track
// Not in HEAD either, nothing to track
return;
}
// Seed file_states with baseline and Missing current content.
@@ -427,7 +430,7 @@ impl HunkTrackerActor {
// Clear hunks since baseline == current
let old_hunks = std::mem::take(&mut state.hunks);
// Remove from turn_index and emit removed events for all hunks
// Drop from turn_index and emit `Removed` events for all hunks
for hunk in old_hunks {
if let Some(prompt_index) = hunk.source.prompt_index()
&& let Some(set) = self.turn_index.get_mut(&prompt_index)
@@ -455,7 +458,7 @@ impl HunkTrackerActor {
/// - Re-read baseline from the new HEAD
/// - Re-read current content from disk
/// - Recompute hunks
/// - Drop files that are now clean (baseline == current, not agent files)
/// - Drop files that are clean (baseline == current, not agent files)
pub(super) async fn refresh_all_baselines(&mut self) {
self.refresh_all_baselines_except(&HashSet::new()).await;
}
@@ -572,7 +575,7 @@ impl HunkTrackerActor {
state.current_content = new_current;
state.baseline_accepted = false;
// Check if file is now clean (baseline == current).
// Check if file is clean (baseline == current).
// For Full states, compare text (ignoring trailing newline).
// For non-diffable states (Binary/TooLarge/LFS): consult the git
// dirty cache (refreshed above) — if git says the file is clean,
@@ -601,7 +604,7 @@ impl HunkTrackerActor {
}
// Symlink on disk vs Full(target) in HEAD (or vice versa):
// git stores symlinks as plain text blobs, so the types
// differ even when the file is unchanged. Consult dirty cache.
// differ even when the file is `unchanged`. Consult dirty cache.
(FileContentState::Symlink, FileContentState::Full(_))
| (FileContentState::Full(_), FileContentState::Symlink) => {
let rel = path.strip_prefix(&self.working_dir).unwrap_or(&path);
@@ -698,7 +701,7 @@ impl HunkTrackerActor {
for path in non_agent_paths {
if let Some(state) = self.file_states.remove(&path) {
// Remove from turn_index and emit removed events for all hunks
// Drop from turn_index and emit `Removed` events for all hunks
for hunk in state.hunks {
if let Some(prompt_index) = hunk.source.prompt_index()
&& let Some(set) = self.turn_index.get_mut(&prompt_index)
@@ -15,7 +15,6 @@ use crate::types::{
use super::HunkTrackerActor;
impl HunkTrackerActor {
/// Get all hunks.
pub(super) fn get_all_hunks(&self) -> Vec<Arc<Hunk>> {
self.file_states
.values()
@@ -10,7 +10,7 @@ use crate::types::Hunk;
/// Maximum size (in bytes) of file text content to retain in memory.
/// Files larger than this are stored as TooLarge.
/// This is aligned with the diff limit to ensure consistent behavior.
pub(crate) const MAX_TRACKED_TEXT_BYTES: usize = 1024 * 1024; // 1 MB
pub(crate) const MAX_TRACKED_TEXT_BYTES: usize = 1024 * 1024;
/// Explicit state of file content storage.
/// Replaces Option<String> for baseline/current_content to avoid unbounded memory.
@@ -144,7 +144,6 @@ impl TestHarness {
self.working_dir.join(path)
}
/// Get all hunks
async fn get_all_hunks(&self) -> Vec<Arc<Hunk>> {
self.handle.get_all_hunks().await
}
@@ -160,7 +159,6 @@ impl TestHarness {
self.handle.get_file_hunk_data(self.abs_path(path)).await
}
/// Accept a hunk
async fn accept_hunk(&self, hunk_id: &crate::types::HunkId) -> bool {
self.handle
.hunk_action(hunk_id.clone(), HunkAction::Accept)
@@ -211,9 +209,7 @@ impl TestHarness {
}
}
// =========================================================================
// Basic Hunk Tracking Tests
// =========================================================================
#[tokio::test]
async fn test_new_file_creates_single_hunk() {
@@ -349,9 +345,7 @@ async fn test_revert_to_baseline_removes_hunk() {
);
}
// =========================================================================
// Hunk Accept/Reject Tests
// =========================================================================
#[tokio::test]
async fn test_accept_hunk_removes_it() {
@@ -395,9 +389,7 @@ async fn test_reject_hunk_reverts_file() {
assert!(hunks_after.is_empty(), "Rejected hunk should be removed");
}
// =========================================================================
// Event Emission Tests
// =========================================================================
#[tokio::test]
async fn test_hunk_added_event_emitted() {
@@ -426,7 +418,8 @@ async fn test_hunk_removed_event_on_revert() {
harness.write_baseline("foo.rs", "original\n");
harness.agent_write("foo.rs", "modified\n", 0);
harness.settle().await;
harness.drain_events(); // Clear initial events
// Clear initial events
harness.drain_events();
// Revert
harness.agent_write("foo.rs", "original\n", 1);
@@ -439,9 +432,7 @@ async fn test_hunk_removed_event_on_revert() {
assert!(has_removed, "Should emit HunkRemoved event when reverting");
}
// =========================================================================
// Prompt Index Attribution Tests
// =========================================================================
#[tokio::test]
async fn test_hunks_have_prompt_index() {
@@ -558,7 +549,6 @@ line 3
);
harness.settle().await;
// Now create an external edit on the agent file
harness.external_write(
"external_only.rs",
r#"line 1
@@ -787,9 +777,7 @@ line 5
assert_eq!(summary.files_with_pending, 0);
}
// =========================================================================
// Source Attribution Preservation Tests
// =========================================================================
#[tokio::test]
async fn test_external_edit_preserves_agent_hunk_source() {
@@ -909,9 +897,7 @@ line 10
}
}
// =========================================================================
// Binary File Handling Tests
// =========================================================================
#[tokio::test]
async fn test_binary_file_agent_write_ignored() {
@@ -969,9 +955,7 @@ async fn test_text_file_with_valid_utf8_tracked() {
assert_eq!(hunks[0].new_text, "Hello, 世界!\n");
}
// =========================================================================
// Accept/Reject Per-Hunk Tests (Bug Demonstration)
// =========================================================================
// These tests explicitly demonstrate the bug where accept/reject affects
// ALL hunks in a file instead of just the targeted hunk.
//
@@ -1048,11 +1032,9 @@ line 10
"Immediately after accept: 1 hunk in list (state.hunks.retain)"
);
// ============================================================
// BUG: Now trigger a recompute by making a trivial external change
// This will diff baseline vs current, and since baseline == current
// (from the buggy accept), all hunks will disappear!
// ============================================================
// Make a tiny change that doesn't affect the hunks
// This triggers recompute_hunks internally
@@ -1150,10 +1132,8 @@ line 10
assert!(success, "Reject should succeed");
harness.settle().await;
// ============================================================
// BUG: After rejecting ONE hunk, the ENTIRE file is reverted!
// FIX: Now we only revert the specific hunk's lines
// ============================================================
// Read file content from disk
let content = std::fs::read_to_string(harness.working_dir.join("bug_reject.rs")).unwrap();
@@ -1209,7 +1189,6 @@ original line 2
harness.accept_hunk(&hunks[0].id).await;
harness.settle().await;
// Now make a NEW change to a DIFFERENT line
harness.agent_write(
"baseline_bug.rs",
r#"modified line 1
@@ -1221,9 +1200,7 @@ NEW CHANGE
let new_hunks = harness.get_all_hunks().await;
// ============================================================
// This SHOULD work correctly - new change creates new hunk
// ============================================================
assert_eq!(
new_hunks.len(),
1,
@@ -1244,9 +1221,7 @@ NEW CHANGE
);
}
// =========================================================================
// Tests that will PASS after the fix is implemented
// =========================================================================
/// EXPECTED BEHAVIOR: Accept one hunk, other hunks remain
///
@@ -1624,9 +1599,7 @@ line 12
);
}
// =========================================================================
// Per-Turn Attribution Tests (Bug Demonstration)
// =========================================================================
// These tests demonstrate the bug where agent-to-agent overlapping edits
// lose the latest prompt_index attribution.
@@ -1701,13 +1674,11 @@ line 5
"Hunk ID should be preserved for overlapping edit"
);
// ============================================================
// BUG: The hunk should now be attributed to turn 1, but it's still turn 0
// FIX: Now agent-to-agent edits update the prompt_index
// ============================================================
match &hunks_after_turn_1[0].source {
crate::types::HunkSource::AgentEdit { prompt_index } => {
// FIX APPLIED: prompt_index is now 1 (the latest agent turn)
// FIX APPLIED: prompt_index is 1 (the latest agent turn)
assert_eq!(
*prompt_index, 1,
"FIXED: Hunk should be re-attributed to turn 1"
@@ -1775,9 +1746,7 @@ line 5
}
}
// =========================================================================
// Integration Bug Test: record_agent_write vs handle_file_change
// =========================================================================
// This test demonstrates the bug in the CLI shell where tool execution
// only triggers fs_notify (handle_file_change) but never calls record_agent_write.
// This means ALL hunks from agent tools are classified as External, not AgentEdit.
@@ -1811,7 +1780,7 @@ async fn test_bug_fs_notify_path_creates_external_hunks_not_agent_hunks() {
crate::types::HunkSource::External => {
// This is the CURRENT BROKEN BEHAVIOR
// Since forward_to_hunk_tracker only calls handle_file_change,
// and the file wasn't previously tracked as an agent file,
// and the file wasn't already tracked as an agent file,
// the hunk is created as External.
}
crate::types::HunkSource::AgentEdit { prompt_index } => {
@@ -1840,7 +1809,8 @@ async fn test_record_agent_write_creates_agent_edit_hunks() {
harness.write_baseline("tool_correct.rs", "original content\n");
// Simulate what SHOULD happen: tool calls record_agent_write directly
let prompt_index = 5; // Example prompt index
// Example prompt index
let prompt_index = 5;
harness.agent_write("tool_correct.rs", "modified by agent tool\n", prompt_index);
harness.settle().await;
@@ -2232,9 +2202,7 @@ async fn test_repro_turn_action_preserves_other_turns() {
);
}
// =========================================================================
// Worktree Diff Bug: previous_content as fallback baseline
// =========================================================================
// When a session runs in a worktree created from dirty state, files that
// exist on disk but are not committed to git should use previous_content
// as the baseline, not None. Otherwise the diff shows the entire file
@@ -2262,7 +2230,8 @@ async fn test_worktree_previous_content_used_as_baseline_when_not_in_git() {
"hi.txt",
"hello world\nanother line\n",
0,
Some("hello world\n"), // previous_content from the tool
// previous_content from the tool
Some("hello world\n"),
);
harness.settle().await;
@@ -2297,7 +2266,8 @@ async fn test_new_file_without_previous_content_shows_all_lines() {
"brand_new.txt",
"line 1\nline 2\n",
0,
None, // No previous content — truly new file
// No previous content — truly new file
None,
);
harness.settle().await;
@@ -2333,7 +2303,8 @@ async fn test_git_baseline_takes_precedence_over_previous_content() {
"committed.txt",
"original line 1\nmodified line 2\n",
0,
Some("some other content\n"), // previous_content differs from git HEAD
// previous_content differs from git HEAD
Some("some other content\n"),
);
harness.settle().await;
@@ -2356,9 +2327,7 @@ async fn test_git_baseline_takes_precedence_over_previous_content() {
);
}
// =========================================================================
// Baseline refresh after accept + git restore
// =========================================================================
/// Reproduces the bug where accepting all hunks then running `git restore .`
/// leaves the hunk tracker with a stale baseline, producing a giant backwards
@@ -2504,12 +2473,10 @@ async fn test_binary_file_survives_baseline_refresh() {
);
}
// =========================================================================
// HunkContentChanged event tests
// =========================================================================
// `HunkContentChanged` event tests
/// When the agent edits the same region twice, the hunk tracker must emit
/// HunkContentChanged (not just HunkAdded) so LOC tracking records the update.
/// `HunkContentChanged` (not just `HunkAdded`) so LOC tracking records the update.
#[tokio::test]
async fn test_content_changed_emitted_on_overlapping_agent_edit() {
let mut harness = TestHarness::new();
@@ -2520,7 +2487,8 @@ async fn test_content_changed_emitted_on_overlapping_agent_edit() {
// Agent modifies lines 2-3 (prompt 0)
harness.agent_write("content.rs", "line1\nchanged2\nchanged3\nline4\nline5\n", 0);
harness.settle().await;
harness.drain_events(); // consume initial events
// consume initial events
harness.drain_events();
// Agent edits the same region again, expanding it (prompt 1)
harness.agent_write(
@@ -2532,7 +2500,7 @@ async fn test_content_changed_emitted_on_overlapping_agent_edit() {
let events = harness.drain_events();
// Must contain at least one HunkContentChanged event
// Must contain at least one `HunkContentChanged` event
let content_changed_events: Vec<_> = events
.iter()
.filter(|e| matches!(e, HunkEvent::HunkContentChanged { .. }))
@@ -2568,7 +2536,7 @@ async fn test_content_changed_emitted_on_overlapping_agent_edit() {
}
/// When a human externally edits a region that the agent already touched,
/// HunkContentChanged must have trigger_source=ExternalEditOnAgentFile.
/// `HunkContentChanged` must have trigger_source=ExternalEditOnAgentFile.
#[tokio::test]
async fn test_content_changed_external_edit_on_agent_hunk() {
let mut harness = TestHarness::new();
@@ -2625,12 +2593,12 @@ async fn test_content_changed_external_edit_on_agent_hunk() {
/// different locations). Then agent writes a new version that merges
/// both regions into one contiguous change. The diff engine produces
/// one merged hunk. `find_matching_old_hunk` matches one old hunk and
/// claims its ID. The other old hunk is now "orphaned" — but the merged
/// claims its ID. The other old hunk is "orphaned" — but the merged
/// new hunk still overlaps with it.
///
/// For HunkContentChanged, the prev lookup should find the matched old
/// For `HunkContentChanged`, the prev lookup should find the matched old
/// hunk by ID (primary path). We separately verify that non-ID-matched
/// hunks that overlap still get a HunkRemoved event (the overlap fallback
/// hunks that overlap still get a `HunkRemoved` event (the overlap fallback
/// for prev is only used when a NEW hunk gets a fresh ID but has overlap).
///
/// To test the actual fallback: we need a case where `find_matching_old_hunk`
@@ -2649,8 +2617,8 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
// Agent writes: change line 3 and line 17 (two separate hunks far apart)
let mut v1: Vec<String> = (1..=20).map(|i| format!("line{i}\n")).collect();
v1[2] = "CHANGED3\n".to_string(); // line 3
v1[16] = "CHANGED17\n".to_string(); // line 17
v1[2] = "CHANGED3\n".to_string();
v1[16] = "CHANGED17\n".to_string();
harness.agent_write("overlap.rs", &v1.join(""), 0);
harness.settle().await;
@@ -2662,20 +2630,23 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
hunks_v1.iter().map(|h| &h.line_info).collect::<Vec<_>>()
);
let old_hunk_ids: Vec<_> = hunks_v1.iter().map(|h| h.id.clone()).collect();
harness.drain_events(); // consume v1 events
// consume v1 events
harness.drain_events();
// Agent writes again: change line 3 AND line 4 (expanding the first hunk
// so it's different content). Also change line 17 differently.
let mut v2: Vec<String> = (1..=20).map(|i| format!("line{i}\n")).collect();
v2[2] = "CHANGED3_V2\n".to_string();
v2[3] = "CHANGED4_V2\n".to_string(); // expand first hunk
v2[16] = "CHANGED17_V2\n".to_string(); // change second hunk
// expand first hunk
v2[3] = "CHANGED4_V2\n".to_string();
// change second hunk
v2[16] = "CHANGED17_V2\n".to_string();
harness.agent_write("overlap.rs", &v2.join(""), 1);
harness.settle().await;
let events = harness.drain_events();
// We should see HunkContentChanged events with prev_lines_added > 0.
// We should see `HunkContentChanged` events with `prev_lines_added` > 0.
// At least one of them should have come from the overlap fallback path
// (the old hunk whose ID was claimed by a different new hunk).
let content_changed: Vec<_> = events
@@ -2691,7 +2662,7 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
})
.collect();
// There should be at least one HunkContentChanged
// There should be at least one `HunkContentChanged`
assert!(
!content_changed.is_empty(),
"Should emit HunkContentChanged for overlapping edits. Events: {:?}",
@@ -2701,7 +2672,7 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
.collect::<Vec<_>>()
);
// Every HunkContentChanged should have prev_lines_added > 0
// Every `HunkContentChanged` should have `prev_lines_added` > 0
// (they all overlap with an old hunk that had lines)
for (hunk_id, prev_added, _prev_removed) in &content_changed {
assert!(
@@ -2713,7 +2684,7 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
);
}
// Verify that at least one HunkContentChanged has a NEW hunk ID
// Verify that at least one `HunkContentChanged` has a NEW hunk ID
// (not matching any old hunk ID) — this proves the overlap fallback
// path was used (the hunk got a fresh ID because the old ID was
// already claimed by another new hunk).
@@ -2723,7 +2694,7 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
// Note: this assertion may not always hold depending on diff engine
// behavior (both hunks might get matched by ID). If it fails, the
// test still validates that prev_lines_added > 0 for all events,
// test still validates that `prev_lines_added` > 0 for all events,
// which is the core correctness property. Log rather than fail.
if !has_new_id {
eprintln!(
@@ -2734,9 +2705,7 @@ async fn test_content_changed_prev_lookup_uses_overlap_fallback() {
}
}
// ============================================================================
// SF-1: State transition tests (Full <-> TooLarge, Full <-> Binary)
// ============================================================================
/// SF-1: Test Full -> TooLarge transition via handle_file_change
/// When a tracked text file grows beyond MAX_TRACKED_TEXT_BYTES, it should
@@ -2761,7 +2730,6 @@ async fn test_transition_full_to_too_large_external_edit() {
let tracked = harness.handle.get_all_tracked_paths().await;
assert!(tracked.contains(&file_path), "File should be tracked");
// Now grow the file beyond MAX_TRACKED_TEXT_BYTES
let large_content = "x".repeat(MAX_TRACKED_TEXT_BYTES + 100);
std::fs::write(&file_path, &large_content).unwrap();
@@ -2946,9 +2914,7 @@ async fn test_too_large_survives_baseline_refresh() {
);
}
// =========================================================================
// get_file_hunk_data() Query API Tests
// =========================================================================
// These tests verify the explicit FileContentStatus contract exposed by
// get_file_hunk_data(), ensuring Missing, Binary, TooLarge, and Full states
// are correctly propagated through the query/API surface.
@@ -3251,9 +3217,7 @@ async fn test_get_file_hunk_data_mixed_states() {
assert!(data.hunks.is_empty());
}
// =========================================================================
// Action-Path Hardening Tests
// =========================================================================
// These tests verify that accept/reject actions are safe under the explicit
// content-state model, and that transitions correctly clear/create hunks.
@@ -3595,9 +3559,7 @@ async fn test_file_creation_hunk_action() {
assert!(!exists, "File should be deleted after rejecting creation");
}
// =========================================================================
// Validation + UI Messaging Smoke Tests
// =========================================================================
// These tests verify that the API correctly exposes file content status
// for clients to display appropriate UI messages (e.g., "file too large").
@@ -3611,7 +3573,8 @@ async fn test_ui_messaging_too_large_file() {
// Create a large file
let file_path = harness.working_dir.join("huge_ui_test.txt");
let large_size = MAX_TRACKED_TEXT_BYTES + 500_000; // ~1.5 MB
// ~1.5 MB
let large_size = MAX_TRACKED_TEXT_BYTES + 500_000;
let large_content = "x".repeat(large_size);
std::fs::write(&file_path, &large_content).unwrap();
@@ -3854,9 +3817,7 @@ async fn test_memory_bounded_multiple_large_files() {
}
}
// =========================================================================
// Issue #2: Deleted committed files visible as deletion hunks
// =========================================================================
/// Deleting a committed file that was never tracked by the hunk tracker
/// should produce a deletion hunk (in AllDirty mode).
@@ -3915,7 +3876,8 @@ async fn test_deleted_committed_file_produces_deletion_hunk() {
/// produce any hunks (we only track agent files).
#[tokio::test]
async fn test_deleted_committed_file_ignored_in_agent_only_mode() {
let mut harness = TestHarness::new(); // AgentOnly mode
// AgentOnly mode
let mut harness = TestHarness::new();
// Commit a file (never tracked by hunk tracker)
harness.write_baseline("foo.txt", "content\n");
@@ -3983,14 +3945,13 @@ async fn test_deleted_file_cleaned_up_after_commit() {
let hunks = harness.get_all_hunks().await;
assert_eq!(hunks.len(), 1, "Should have deletion hunk");
// Now commit the deletion
git(&harness.working_dir, &["add", "cleanup.txt"]);
git(
&harness.working_dir,
&["commit", "-m", "delete cleanup.txt"],
);
// Refresh baselines (simulates git_head_changed)
// Refresh baselines (simulates `git_head_changed`)
harness.handle.refresh_all_baselines();
harness.settle().await;
@@ -4047,11 +4008,9 @@ async fn test_reject_deletion_of_committed_file_restores_it() {
);
}
// =========================================================================
// Issue #1: Staged-only files visible after git reset --soft HEAD^
// =========================================================================
/// After `git reset --soft HEAD^`, a file that was added in the undone
/// After `git reset --soft HEAD^`, a file added by the undone
/// commit should appear as a new file (staged in index, not in HEAD).
#[tokio::test]
async fn test_soft_reset_staged_new_file_visible() {
@@ -4066,7 +4025,7 @@ async fn test_soft_reset_staged_new_file_visible() {
// Soft reset: moves HEAD back but keeps index and worktree
git(&harness.working_dir, &["reset", "--soft", "HEAD^"]);
// Trigger baseline refresh (simulates git_head_changed)
// Trigger baseline refresh (simulates `git_head_changed`)
harness.handle.refresh_all_baselines();
harness.settle().await;
@@ -4265,7 +4224,7 @@ async fn test_mixed_reset_modified_files_visible() {
);
}
/// After `git reset HEAD~1` (mixed reset), newly added files in the undone
/// After `git reset HEAD~1` (mixed reset), newly created files in the undone
/// commit should appear as untracked new files with creation hunks.
#[tokio::test]
async fn test_mixed_reset_new_file_visible() {
@@ -4305,9 +4264,7 @@ async fn test_mixed_reset_new_file_visible() {
);
}
// =========================================================================
// GetAllFileContents Tests
// =========================================================================
/// Empty actor returns no file contents.
#[tokio::test]
@@ -4550,9 +4507,7 @@ async fn test_get_all_file_contents_returns_absolute_paths() {
);
}
// =========================================================================
// refresh_all_baselines: non-diffable file cleanup via git dirty cache
// =========================================================================
/// A committed binary file that is NOT dirty in git status should be removed
/// from tracking after refresh_all_baselines (no longer a phantom).
@@ -4723,9 +4678,7 @@ async fn test_dirty_lfs_file_survives_refresh() {
);
}
// =========================================================================
// Gitignored file filtering tests
// =========================================================================
/// Gitignored files (e.g., cargo build artifacts in `target/`) should NOT
/// be tracked in AllDirty mode. The git dirty cache never contains ignored
@@ -4820,10 +4773,9 @@ async fn test_gitignored_file_cleaned_up_by_refresh_all_baselines() {
let tracked = harness.handle.get_all_tracked_paths().await;
assert!(tracked.contains(&real_file), "Dirty file should be tracked");
// Now restore the file so it's clean
std::fs::write(&real_file, "original\n").unwrap();
// Trigger refresh — the file is now clean (baseline == current), should be removed
// Trigger refresh — the file is clean (baseline == current), should be removed
harness.handle.refresh_all_baselines();
let _ = harness.handle.get_all_hunks().await;
@@ -4998,9 +4950,7 @@ async fn test_untracked_directory_tracks_child_files_not_directory() {
);
}
// =========================================================================
// Command Coalescing Tests
// =========================================================================
use crate::actor::{CoalescedBatch, CoalescedPathAction};
use crate::commands::HunkTrackerCommand;
@@ -5279,7 +5229,7 @@ async fn test_coalescing_delete_then_recreate() {
/// Integration test: refresh_all + file changes are correctly coalesced.
/// When refresh_all is in the batch, tracked-file changes should be skipped
/// (refresh_all handles them), but new files should still be added.
/// (refresh_all handles them), but new files should still be tracked.
#[tokio::test]
async fn test_coalescing_refresh_all_with_file_changes() {
let mut harness = TestHarness::with_mode(TrackingMode::AllDirty);
@@ -5290,7 +5240,6 @@ async fn test_coalescing_refresh_all_with_file_changes() {
harness.agent_write("existing.rs", "modified\n", 0);
harness.settle().await;
// Now queue: file change on existing + refresh_all + new file change
std::fs::write(harness.working_dir.join("existing.rs"), "v2\n").unwrap();
harness
.handle
@@ -5396,9 +5345,7 @@ async fn test_snapshot_turn_delta_is_per_turn() {
assert!(empty.file_states.is_empty() && empty.hunk_ids.is_empty());
}
// =========================================================================
// Baseline refresh during git rebases (scan counting + hunk preservation)
// =========================================================================
/// Count of `BaselineUpdated` events in a drained batch. The real
/// `refresh_all_baselines` scan path emits one per still-tracked file, while
@@ -5412,9 +5359,7 @@ fn baseline_updates(events: &[HunkEvent]) -> usize {
.count()
}
// =========================================================================
// Scoped (pathspec-limited) dirty-cache scans
// =========================================================================
/// Construct an actor directly (not spawned, via the production constructor)
/// so tests can call `pub(super)` methods like `refresh_git_dirty_cache` and
@@ -13,7 +13,7 @@ use crate::types::{
/// Commands sent to the HunkTrackerActor via mpsc channel.
#[derive(Debug)]
pub enum HunkTrackerCommand {
// === Mutation Commands (fire-and-forget) ===
// Mutation Commands (fire-and-forget)
/// Agent tool wrote to a file - record it and compute hunks
RecordAgentWrite {
path: PathBuf,
@@ -40,7 +40,7 @@ pub enum HunkTrackerCommand {
/// Set tracking mode
SetMode { mode: TrackingMode },
// === Action Commands (accept/reject hunks) ===
// Action Commands (accept/reject hunks)
/// Apply action (accept/reject) to a specific hunk
HunkAction {
hunk_id: HunkId,
@@ -68,7 +68,7 @@ pub enum HunkTrackerCommand {
reply: oneshot::Sender<Result<Vec<HunkId>, HunkActionError>>,
},
// === Query Commands (request-response via oneshot) ===
// Query Commands (request-response via oneshot)
/// Get all current hunks
GetAllHunks {
reply: oneshot::Sender<Vec<Arc<Hunk>>>,
@@ -121,7 +121,7 @@ pub enum HunkTrackerCommand {
reply: oneshot::Sender<Vec<FileContentEntry>>,
},
// === Session Summary Commands ===
// Session Summary Commands
/// Get complete session summary (stats + pending turns)
GetSessionSummary {
reply: oneshot::Sender<SessionSummary>,
@@ -140,7 +140,7 @@ pub enum HunkTrackerCommand {
/// content from disk. Used after a git HEAD/index change to reconcile stale state.
RefreshAllBaselines,
// === Snapshot / Restore Commands (for cross-session sync-back) ===
// Snapshot / Restore Commands (for cross-session sync-back)
/// Take a snapshot of all hunk tracker state for preservation across
/// session kill/reload cycles.
SnapshotState {
@@ -153,7 +153,7 @@ pub enum HunkTrackerCommand {
reply: oneshot::Sender<HunkTurnDelta>,
},
/// Restore a previously snapshotted state. Replaces all current file
/// Restore a earlier snapshotted state. Replaces all current file
/// states, turn index, and session stats.
RestoreState(HunkTrackerSnapshot),
}
+19 -10
View File
@@ -17,7 +17,7 @@ const DIFF_TIMEOUT: Duration = Duration::from_secs(10);
/// Maximum file size (in bytes) to attempt diffing.
/// Files larger than this will be skipped to avoid pathological diff behavior.
const MAX_DIFF_FILE_SIZE: usize = 1024 * 1024; // 1 MB
const MAX_DIFF_FILE_SIZE: usize = 1024 * 1024;
/// Generate a unified diff patch string from baseline and current content.
/// This produces a patch that can be parsed by Pierre's `getSingularPatch`.
@@ -112,7 +112,8 @@ pub fn generate_hunk_patch(baseline: &str, current: &str, hunk: &Hunk) -> String
// Hunk header (1-indexed)
let header_old_start = context_before_start + 1;
let header_new_start = context_before_start + 1; // Context is same in both
// Context is same in both
let header_new_start = context_before_start + 1;
let _ = writeln!(
output,
@@ -297,7 +298,8 @@ impl HunkBuilder {
source,
old_text,
new_text,
patch: None, // Patch is generated later when requested
// Patch is generated later when requested
patch: None,
created_at: chrono::Utc::now(),
selected: false,
}
@@ -346,7 +348,8 @@ pub fn patch_lines(
insert_text: &str,
) -> String {
let lines: Vec<&str> = content.lines().collect();
let start_idx = start_line.saturating_sub(1); // Convert to 0-indexed
// Convert to 0-indexed
let start_idx = start_line.saturating_sub(1);
let mut result = Vec::new();
@@ -608,7 +611,8 @@ mod tests {
line_info: HunkLineInfo {
old_start: 10,
old_count: 1,
new_start: 12, // slightly shifted
// slightly shifted
new_start: 12,
new_count: 1,
},
source: agent_source(),
@@ -630,7 +634,8 @@ mod tests {
line_info: HunkLineInfo {
old_start: 100,
old_count: 1,
new_start: 102, // slightly shifted
// slightly shifted
new_start: 102,
new_count: 1,
},
source: agent_source(),
@@ -665,7 +670,8 @@ mod tests {
old_start: 1,
old_count: 1,
new_start: 1,
new_count: 2, // covers new lines 1-2
// covers new lines 1-2
new_count: 2,
},
source: agent_source(),
old_text: Some("old-small\n".to_string()),
@@ -682,7 +688,8 @@ mod tests {
old_start: 3,
old_count: 1,
new_start: 3,
new_count: 4, // covers new lines 3-6
// covers new lines 3-6
new_count: 4,
},
source: agent_source(),
old_text: Some("old-large\n".to_string()),
@@ -692,7 +699,8 @@ mod tests {
selected: false,
});
let old_hunks = vec![old_hunk_small.clone(), old_hunk_large.clone()]; // small first!
// small first!
let old_hunks = vec![old_hunk_small.clone(), old_hunk_large.clone()];
// New hunk overlaps both, but more with large:
// new lines 2-5 (end=6)
@@ -706,7 +714,8 @@ mod tests {
old_start: 2,
old_count: 4,
new_start: 2,
new_count: 4, // lines 2-5
// lines 2-5
new_count: 4,
},
source: agent_source(),
old_text: Some("different-old\n".to_string()),
@@ -6,7 +6,7 @@ use std::sync::Arc;
use crate::types::{Hunk, HunkId, HunkLineInfo, HunkSource};
/// Why a hunk was removed. Used by the LOC sink to decide whether to
/// Why the tracker removed a hunk. Used by the LOC sink to decide whether to
/// negate the hunk's accumulated LOC contribution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -30,7 +30,7 @@ pub enum HunkEvent {
/// A new hunk was created
HunkAdded { path: PathBuf, hunk: Arc<Hunk> },
/// A hunk was removed.
/// A hunk stopped being tracked.
HunkRemoved {
path: PathBuf,
hunk_id: HunkId,
@@ -69,6 +69,6 @@ pub enum HunkEvent {
/// A file stopped being tracked (all hunks gone, not an agent file)
FileRemoved { path: PathBuf },
/// Baseline was updated for a file (after accept or commit)
/// The tracker updated a file's baseline (after accept or commit)
BaselineUpdated { path: PathBuf },
}
@@ -68,7 +68,6 @@ impl HunkTrackerHandle {
.send(HunkTrackerCommand::HandleFileDeleted { path });
}
/// Refresh git dirty cache.
pub fn refresh_git_dirty_cache(&self) {
let _ = self.cmd_tx.send(HunkTrackerCommand::RefreshGitDirtyCache);
}
@@ -140,7 +139,6 @@ impl HunkTrackerHandle {
reply_rx.await.unwrap_or_else(|_| Ok(vec![]))
}
/// Get all hunks.
pub async fn get_all_hunks(&self) -> Vec<Arc<Hunk>> {
let (reply_tx, reply_rx) = oneshot::channel();
let _ = self
@@ -169,7 +167,6 @@ impl HunkTrackerHandle {
reply_rx.await.unwrap_or_default()
}
/// Get hunks by source.
pub async fn get_hunks_by_source(&self, source: HunkSourceFilter) -> Vec<Arc<Hunk>> {
let (reply_tx, reply_rx) = oneshot::channel();
let _ = self.cmd_tx.send(HunkTrackerCommand::GetHunksBySource {
@@ -293,7 +290,7 @@ impl HunkTrackerHandle {
reply_rx.await.ok()
}
/// Restore a previously snapshotted state. Replaces all current file
/// Restore a earlier snapshotted state. Replaces all current file
/// states, turn index, and session stats in the actor.
///
/// This is fire-and-forget — doesn't wait for processing.
@@ -17,9 +17,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------
/// Who authored a change.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -65,7 +63,7 @@ pub enum EventType {
Added,
/// An existing hunk's content changed in place.
Updated,
/// A hunk was removed. `lines_added` / `lines_removed` are negated
/// Removal of an existing hunk. `lines_added` / `lines_removed` are negated
/// so that `SUM` zeroes out the hunk's accumulated contribution.
Removed,
}
@@ -80,9 +78,7 @@ impl std::fmt::Display for EventType {
}
}
// ---------------------------------------------------------------------------
// HunkRecord
// ---------------------------------------------------------------------------
/// A single LOC attribution record derived from a [`Hunk`].
///
@@ -126,7 +122,7 @@ pub struct HunkRecord {
pub source_type: Option<SourceType>,
/// Whether this is a new hunk or an in-place update.
pub event_type: EventType,
/// Why the hunk was removed. Only set for [`EventType::Removed`] records.
/// Reason for the removal. Only set for [`EventType::Removed`] records.
#[serde(skip_serializing_if = "Option::is_none")]
pub removal_reason: Option<HunkRemovalReason>,
}
@@ -201,9 +197,7 @@ impl HunkRecord {
}
}
// ---------------------------------------------------------------------------
// HunkRecordWriter
// ---------------------------------------------------------------------------
/// Trait for persisting [`HunkRecord`]s.
///
@@ -279,9 +273,7 @@ impl HunkRecordWriter for JsonlHunkRecordWriter {
}
}
// ---------------------------------------------------------------------------
// LocAggregate (channel-based bridge to signals)
// ---------------------------------------------------------------------------
/// Lightweight aggregate update emitted by the LOC sink for consumption by
/// an external bridge (e.g., the signals system in `kigi-shell`).
@@ -290,7 +282,7 @@ impl HunkRecordWriter for JsonlHunkRecordWriter {
/// The bridge task translates them into `SignalEvent` variants.
#[derive(Debug, Clone)]
pub enum LocAggregate {
/// Lines were added or changed (from HunkAdded or HunkContentChanged).
/// New or modified lines (from `HunkAdded` or `HunkContentChanged`).
LinesChanged {
author_type: AuthorType,
lines_added: i64,
@@ -305,9 +297,7 @@ pub enum LocAggregate {
},
}
// ---------------------------------------------------------------------------
// Sink configuration
// ---------------------------------------------------------------------------
/// Context passed to the LOC sink at spawn time.
pub struct LocSinkContext {
@@ -322,9 +312,7 @@ pub struct LocSinkContext {
pub aggregate_tx: Option<mpsc::UnboundedSender<LocAggregate>>,
}
// ---------------------------------------------------------------------------
// run_loc_sink
// ---------------------------------------------------------------------------
/// Consume [`HunkEvent`]s and write LOC attribution records.
///
@@ -344,7 +332,7 @@ pub async fn run_loc_sink(
ctx: LocSinkContext,
cancellation_token: tokio_util::sync::CancellationToken,
) {
// Accumulated (lines_added, lines_removed) per hunk_id.
// Accumulated (`lines_added`, `lines_removed`) per hunk_id.
// Used to emit negating records when hunks are rejected/superseded.
let mut acc: HashMap<HunkId, (i64, i64)> = HashMap::new();
@@ -383,7 +371,7 @@ async fn handle_event(
match event {
HunkEvent::HunkAdded { path: _, ref hunk } => {
// For new hunks, the hunk's own source is the correct attribution.
// lines_added/lines_removed are the full counts (no prior state).
// `lines_added`/`lines_removed` are the full counts (no prior state).
let record = HunkRecord::from_hunk(
hunk,
&ctx.session_id,
@@ -9,9 +9,7 @@ use crate::types::{Hunk, HunkId, HunkLineInfo, HunkSource};
use super::*;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn sample_agent_hunk() -> Hunk {
Hunk {
@@ -129,9 +127,7 @@ fn make_ctx() -> LocSinkContext {
}
}
// ---------------------------------------------------------------------------
// Unit tests: HunkRecord::from_hunk
// ---------------------------------------------------------------------------
#[test]
fn from_hunk_agent_edit() {
@@ -148,7 +144,8 @@ fn from_hunk_agent_edit() {
assert_eq!(record.hunk_id, HunkId::from_string("test-hunk-001".into()));
assert_eq!(record.file_path, PathBuf::from("/tmp/foo.rs"));
assert_eq!(record.hunk_start, 10);
assert_eq!(record.hunk_end, 14); // 10 + 5 - 1
// 10 + 5 - 1
assert_eq!(record.hunk_end, 14);
assert_eq!(record.lines_added, 5);
assert_eq!(record.lines_removed, 3);
assert_eq!(record.author_type, Some(AuthorType::Agent));
@@ -191,7 +188,8 @@ fn from_hunk_external() {
assert_eq!(record.prompt_index, None);
assert_eq!(record.source_type, Some(SourceType::External));
assert_eq!(record.hunk_start, 1);
assert_eq!(record.hunk_end, 4); // 1 + 4 - 1
// 1 + 4 - 1
assert_eq!(record.hunk_end, 4);
}
#[test]
@@ -244,7 +242,8 @@ fn from_hunk_pure_deletion() {
// Pure deletion: new_count == 0, so uses old_start/old_count
assert_eq!(record.hunk_start, 5);
assert_eq!(record.hunk_end, 7); // 5 + 3 - 1
// 5 + 3 - 1
assert_eq!(record.hunk_end, 7);
assert_eq!(record.lines_added, 0i64);
assert_eq!(record.lines_removed, 3i64);
}
@@ -252,7 +251,8 @@ fn from_hunk_pure_deletion() {
/// Verify that attribution_source overrides the hunk's preserved source.
#[test]
fn from_hunk_trigger_source_overrides_preserved_source() {
let hunk = sample_agent_hunk(); // hunk.source = AgentEdit
// hunk.source = AgentEdit
let hunk = sample_agent_hunk();
let trigger = HunkSource::ExternalEditOnAgentFile;
let record = HunkRecord::from_hunk(
&hunk,
@@ -273,9 +273,7 @@ fn from_hunk_trigger_source_overrides_preserved_source() {
assert_eq!(record.event_type, EventType::Updated);
}
// ---------------------------------------------------------------------------
// Sink tests
// ---------------------------------------------------------------------------
#[tokio::test]
async fn sink_processes_added_and_content_changed() {
@@ -285,7 +283,8 @@ async fn sink_processes_added_and_content_changed() {
let hunk = sample_agent_hunk();
let mut updated_hunk = sample_agent_hunk();
updated_hunk.line_info.new_count = 8; // grew from 5 to 8 lines
// grew from 5 to 8 lines
updated_hunk.line_info.new_count = 8;
// Send a mix of events — only HunkAdded and HunkContentChanged should produce records
tx.send(HunkEvent::FileAdded {
@@ -302,8 +301,10 @@ async fn sink_processes_added_and_content_changed() {
path: PathBuf::from("/tmp/foo.rs"),
hunk: Arc::new(updated_hunk),
trigger_source: HunkSource::AgentEdit { prompt_index: 2 },
prev_lines_added: 5, // original hunk had 5 lines added
prev_lines_removed: 3, // original hunk had 3 lines removed
// original hunk had 5 lines added
prev_lines_added: 5,
// original hunk had 3 lines removed
prev_lines_removed: 3,
})
.unwrap();
tx.send(HunkEvent::HunkMoved {
@@ -377,7 +378,8 @@ async fn sink_removed_hunk_zeroes_out_accumulated_total() {
let ctx = make_ctx();
let cancel = tokio_util::sync::CancellationToken::new();
let hunk = sample_agent_hunk(); // lines_added=5, lines_removed=3
// lines_added=5, lines_removed=3
let hunk = sample_agent_hunk();
let hunk_id = hunk.id.clone();
let path = hunk.path.clone();
@@ -429,12 +431,14 @@ async fn sink_removed_hunk_after_updates_zeroes_correctly() {
let ctx = make_ctx();
let cancel = tokio_util::sync::CancellationToken::new();
let hunk = sample_agent_hunk(); // lines_added=5, lines_removed=3
// lines_added=5, lines_removed=3
let hunk = sample_agent_hunk();
let hunk_id = hunk.id.clone();
let path = hunk.path.clone();
let mut updated = sample_agent_hunk();
updated.line_info.new_count = 8; // grew from 5 → 8
// grew from 5 → 8
updated.line_info.new_count = 8;
// Add → update → remove
tx.send(HunkEvent::HunkAdded {
@@ -478,7 +482,8 @@ async fn sink_accepted_hunk_preserves_loc() {
let ctx = make_ctx();
let cancel = tokio_util::sync::CancellationToken::new();
let hunk = sample_agent_hunk(); // lines_added=5, lines_removed=3
// lines_added=5, lines_removed=3
let hunk = sample_agent_hunk();
let hunk_id = hunk.id.clone();
let path = hunk.path.clone();
@@ -576,7 +581,8 @@ async fn sink_shrinking_hunk_produces_negative_delta() {
.sum();
assert_eq!(agent_total, 10);
assert_eq!(human_total, -3);
assert_eq!(agent_total + human_total, 7); // net lines in file
// net lines in file
assert_eq!(agent_total + human_total, 7);
}
#[tokio::test]
@@ -615,9 +621,7 @@ async fn sink_drains_on_cancellation() {
assert!(w.flush_count > 0, "Writer should be flushed on shutdown");
}
// ---------------------------------------------------------------------------
// JSONL round-trip test
// ---------------------------------------------------------------------------
#[tokio::test]
async fn jsonl_round_trip() {
@@ -687,9 +691,7 @@ async fn jsonl_writer_appends() {
assert_eq!(lines.len(), 2);
}
// ---------------------------------------------------------------------------
// Deserialization validation
// ---------------------------------------------------------------------------
/// Invalid enum values must be rejected during deserialization.
/// This validates that the serde enum gate works — a typo like "foo"
@@ -728,9 +730,7 @@ fn deserialize_rejects_invalid_source_type() {
assert!(result.is_err(), "Should reject invalid source_type");
}
// ---------------------------------------------------------------------------
// Writer failure resilience
// ---------------------------------------------------------------------------
/// The sink must continue processing events even when the writer fails.
/// This validates the "log warning and drop the record" error policy.
+7 -15
View File
@@ -47,11 +47,11 @@ impl std::fmt::Display for HunkId {
pub struct HunkLineInfo {
/// 1-indexed start line in baseline (old) file
pub old_start: usize,
/// Number of lines from baseline that were changed/deleted
/// Number of baseline lines this hunk changes or deletes
pub old_count: usize,
/// 1-indexed start line in current (new) file
pub new_start: usize,
/// Number of lines in current that were added/modified
/// Number of current-file lines this hunk adds or modifies
pub new_count: usize,
}
@@ -81,7 +81,7 @@ pub enum HunkSource {
prompt_index: usize,
},
/// External edit (by user) to a file the agent has previously touched.
/// External edit (by user) to a file the agent has already touched.
/// These are tracked separately so we know they're "part of agent session"
/// but weren't written by the agent itself.
ExternalEditOnAgentFile,
@@ -253,7 +253,7 @@ pub enum HunkAction {
pub enum HunkUpdate {
/// A new hunk was created
Added(Hunk),
/// A hunk was removed (accepted, rejected, or reverted)
/// A hunk left the pending set (accepted, rejected, or reverted)
Removed { hunk_id: HunkId },
/// A hunk's position changed but content is the same
Moved {
@@ -299,9 +299,7 @@ pub enum TrackingMode {
AllDirty,
}
// ============================================================================
// Session Stats & Summary
// ============================================================================
/// Simple counters for session summary. Reset on baseline reset (commit).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -359,9 +357,7 @@ pub struct SessionSummary {
pub unattributed_pending: usize,
}
// ============================================================================
// Content Status Types (for explicit API responses)
// ============================================================================
/// Status of file content - explicit discrimination for API consumers.
/// This replaces the ambiguous `Option<String>` where `None` could mean
@@ -494,13 +490,13 @@ pub struct FileHunkData {
/// Hunks for this file (each hunk includes its own patch fragment)
pub hunks: Vec<Arc<Hunk>>,
// === Explicit content status (new fields) ===
// Explicit content status (new fields)
/// Baseline content with explicit status (git HEAD)
pub baseline: FileContentView,
/// Current content with explicit status (on disk)
pub current: FileContentView,
// === Legacy fields for backward compatibility ===
// Legacy fields for backward compatibility
// These are populated from FileContentView for existing callers.
// Will be deprecated once all callers migrate to baseline/current views.
/// Baseline content (git HEAD) - legacy, use `baseline.content` instead
@@ -511,9 +507,7 @@ pub struct FileHunkData {
pub current_content: Option<String>,
}
// ============================================================================
// Snapshot / Restore (for cross-session sync-back)
// ============================================================================
// FileContentState is crate-internal (actor::state is pub(crate));
// imported here for snapshot serialization.
@@ -782,7 +776,7 @@ mod snapshot_tests {
assert_eq!(snap.session_stats.accepted_hunks, 5);
}
// === Snapshot preserves Binary/TooLarge (regression test) ===
// Snapshot preserves Binary/TooLarge (regression test)
#[test]
fn snapshot_preserves_binary_state() {
@@ -855,9 +849,7 @@ mod snapshot_tests {
}
}
// ============================================================================
// FileContentView Tests (content status propagation)
// ============================================================================
#[cfg(test)]
mod content_view_tests {