tlc: lock-in tests for C8, C11, C12 (verified-already-done)

Adds regression tests for Sprint 3 items that were verified
already-done during recon but had no dedicated test coverage:

  - viewer::tests::hex_edit_apply_nibble_at_eof_is_safe_noop
    Locks in the C8 contract: apply_nibble at cursor past EOF
    is a safe no-op (no panic, no spurious modified flag).

  - viewer::tests::move_cursor_clamps_to_size
    Locks in the C8 cursor bounds invariant at move_cursor
    level: positive deltas clamp to file size, negative deltas
    clamp to 0 with no underflow.

  - filemanager::panel::tests::history_dedups_consecutive_entries
    Locks in the C11 contract: refreshing the same directory
    N times grows the history by 1 entry (consecutive dedup),
    not N.

  - filemanager::panel::tests::sort_field_mtime_round_trips_through_config
    Locks in the C12 contract: 'mtime' and 'time' config
    strings both resolve to the Mtime sort field, and the
    Panel reports the human-readable name 'Mtime'.

Tests (4 new, total 1299 passing):
  +hex_edit_apply_nibble_at_eof_is_safe_noop
  +move_cursor_clamps_to_size
  +history_dedups_consecutive_entries
  +sort_field_mtime_round_trips_through_config
This commit is contained in:
kellito
2026-07-05 19:58:29 +03:00
parent a9a3507daf
commit 2db8636f8b
2 changed files with 84 additions and 0 deletions
@@ -1490,4 +1490,49 @@ mod tests {
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn history_dedups_consecutive_entries() {
// C11 — refreshing the same directory (or reading it twice
// without leaving) must NOT add a duplicate entry to the
// navigation history.
let dir = std::env::temp_dir().join("tlc-panel-dedup-test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let mut p = Panel::new(&dir, &empty_cfg()).unwrap();
let baseline = p.history_paths().len();
p.refresh().unwrap();
let after_one_refresh = p.history_paths().len();
p.refresh().unwrap();
p.refresh().unwrap();
let after_three_refreshes = p.history_paths().len();
assert_eq!(
after_one_refresh, baseline,
"first refresh should add 1 entry"
);
assert_eq!(
after_three_refreshes, after_one_refresh,
"subsequent refreshes in same dir must not duplicate history"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn sort_field_mtime_round_trips_through_config() {
// C12 — the Mtime sort field must be parseable from the
// config string "mtime" / "time" and the Panel's
// sort_field_name() must report it as "Mtime" (human-readable).
let dir = std::env::temp_dir().join("tlc-sort-mtime-test");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let mut cfg = empty_cfg();
cfg.sort_field = "mtime".to_string();
let mut p = Panel::new(&dir, &cfg).unwrap();
assert_eq!(p.sort_field_name(), "Mtime");
// Also verify the "time" alias parses to Mtime.
cfg.sort_field = "time".to_string();
let p2 = Panel::new(&dir, &cfg).unwrap();
assert_eq!(p2.sort_field_name(), "Mtime");
let _ = fs::remove_dir_all(&dir);
}
}
@@ -1544,4 +1544,43 @@ mod tests {
assert_eq!(v.path.file_name().unwrap(), "first.txt");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn hex_edit_apply_nibble_at_eof_is_safe_noop() {
// C8 — apply_nibble at cursor >= size must be a no-op
// (no panic, no spurious modification flag). This guards
// against future regressions in the cursor bounds check.
use crate::viewer::source::FileSource;
let bytes = vec![0xAB, 0xCD];
let mut v = Viewer::from_source(
std::path::PathBuf::from("(memory)/hex-bounds-test.bin"),
FileSource::Inline { bytes: bytes.clone() },
);
v.mode = ViewMode::HexEdit;
v.cursor = 100; // past EOF (size = 2)
v.apply_nibble(0xf);
// Cursor unchanged.
assert_eq!(v.cursor, 100);
// Buffer unchanged.
assert!(!v.is_modified(), "no modification should be flagged for EOF nibble");
}
#[test]
fn move_cursor_clamps_to_size() {
// move_cursor with a large positive delta clamps to the
// file size, never overflows. Verifies the C8 cursor
// bounds invariant at the cursor level.
use crate::viewer::source::FileSource;
let bytes = vec![0u8; 10];
let mut v = Viewer::from_source(
std::path::PathBuf::from("(memory)/cursor-clamp-test.bin"),
FileSource::Inline { bytes },
);
v.move_cursor(1000);
assert_eq!(v.cursor, 10);
v.move_cursor(2);
assert_eq!(v.cursor, 10); // still clamped
v.move_cursor(-50);
assert_eq!(v.cursor, 0); // clamps at start, no underflow
}
}