From 2db8636f8be234e26f83c19893efa5d35e34fe71 Mon Sep 17 00:00:00 2001 From: kellito Date: Sun, 5 Jul 2026 19:58:29 +0300 Subject: [PATCH] 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 --- .../tui/tlc/source/src/filemanager/panel.rs | 45 +++++++++++++++++++ .../recipes/tui/tlc/source/src/viewer/mod.rs | 39 ++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/local/recipes/tui/tlc/source/src/filemanager/panel.rs b/local/recipes/tui/tlc/source/src/filemanager/panel.rs index caaff20f0f..28f993d55e 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/panel.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/panel.rs @@ -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); + } } diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index 123e6fec00..433c095369 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -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 + } }