Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions crates/lash-tui/tests/activity_bar_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Integration tests for the status-bar activity sections.
//!
//! These cover the two cases `tasks/tasks.status-bar-activity.md` left
//! deferred for want of a TUI harness that did not exist yet: the ordering
//! `seed_from_db` uses to pick between several in-progress tasks, and a
//! toggle driven through the real key path rather than through
//! `ActivityState::record_transition` directly.

use lash_tui::activity::ActivityState;
use lash_tui::testing::{keys, TestAppBuilder};
use std::path::PathBuf;
use std::time::Instant;
use tempfile::TempDir;

/// Build a project containing `body` and index it, returning the temp dir
/// (which must outlive the test), the db path, and the task file path.
fn setup_project(body: &str) -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path().to_path_buf();
std::fs::create_dir(project_root.join(".lash")).unwrap();
let db_path = project_root.join(".lash").join("lash.db");

let conn = lash_db::init_database(&db_path).unwrap();
std::fs::write(project_root.join("tasks.md"), body).unwrap();

use lash_db::{Indexer, IndexerConfig};
let parser_config = lash_types::LashConfig::default();
let indexer_config = IndexerConfig::new(project_root).with_progress(false);
let mut indexer = Indexer::new(&conn, indexer_config, &parser_config);
indexer.index_project().unwrap();

(temp_dir, db_path)
}

#[test]
fn seeding_picks_the_smaller_full_id_when_several_tasks_are_in_progress() {
// Two in-progress tasks, written in the order that would give the wrong
// answer if seeding took whichever the DB happened to return first:
// `zulu` is above `alpha` in the file, so insertion order and full_id
// order disagree.
let (_temp_dir, db_path) = setup_project(
r"# Sample

@id: sample

## Tasks

- [>] Zulu task
@id: zulu
- [>] Alpha task
@id: alpha
",
);
let conn = lash_db::open_database(&db_path).unwrap();

let mut activity = ActivityState::default();
activity.seed_from_db(&conn, Instant::now());

let seeded = activity
.in_progress
.as_ref()
.expect("an in-progress task should have been seeded");
assert_eq!(
seeded.full_id, "sample#alpha",
"seeding must be deterministic on full_id, not on row order"
);
}

#[test]
fn seeding_leaves_in_progress_empty_when_nothing_is_in_progress() {
let (_temp_dir, db_path) = setup_project(
r"# Sample

@id: sample

## Tasks

- [ ] Open task
- [x] Done task
",
);
let conn = lash_db::open_database(&db_path).unwrap();

let mut activity = ActivityState::default();
activity.seed_from_db(&conn, Instant::now());

assert!(activity.in_progress.is_none());
}

/// Drive the app to the Tasks pane with the first task selected, then press
/// Space `toggles` times. Returns the app so the caller can inspect state.
fn app_after_toggles(db_path: &PathBuf, toggles: usize) -> lash_tui::testing::TestTuiApp {
// Tab twice to move focus Navigation -> Description -> Detail (the tasks
// pane), which is where Space toggles rather than warning.
let mut events = vec![keys::tab(), keys::tab()];
for _ in 0..toggles {
events.push(keys::char(' '));
}

let mut app = TestAppBuilder::new()
.with_db(db_path)
.with_size(80, 24)
.with_events(events)
.build()
.unwrap();

// One tick per queued event, plus the initial render.
for _ in 0..=(2 + toggles) {
app.tick().unwrap();
}
app
}

#[test]
fn toggling_open_to_in_progress_fills_the_in_progress_slot() {
let (_temp_dir, db_path) = setup_project(
r"# Sample

@id: sample

## Tasks

- [ ] Alpha task
@id: alpha
",
);

let app = app_after_toggles(&db_path, 1);

let in_progress = app
.state()
.activity
.in_progress
.as_ref()
.expect("toggling Open -> InProgress should fill the in-progress slot");
assert_eq!(in_progress.title, "Alpha task");
assert!(
app.state().activity.recently_completed.is_empty(),
"nothing has completed yet"
);
}

#[test]
fn toggling_in_progress_to_done_clears_the_slot_and_records_the_completion() {
let (_temp_dir, db_path) = setup_project(
r"# Sample

@id: sample

## Tasks

- [ ] Alpha task
@id: alpha
",
);

// Open -> InProgress -> Done.
let app = app_after_toggles(&db_path, 2);

assert!(
app.state().activity.in_progress.is_none(),
"completing the in-progress task must clear the slot"
);
let recent = &app.state().activity.recently_completed;
assert_eq!(recent.len(), 1, "the completion should have been recorded");
assert_eq!(recent[0].title, "Alpha task");
}
4 changes: 2 additions & 2 deletions tasks/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ Tasks are organized by major module/component. Each task file contains:
### User Interfaces
- [x] [tasks.tui.md](tasks.tui.md) - Terminal UI implementation ✅
- [x] [tasks.task-creation-ui.md](tasks.task-creation-ui.md) - Task creation via CLI (`lash add`) and TUI form modal (Tasks 1-12 complete) ✅
- [ ] [tasks.status-bar-activity.md](tasks.status-bar-activity.md) - Status bar in-progress + recently-completed activity sections
- [ ] [tasks.live-updates.md](tasks.live-updates.md) - Live TUI updates (Store actor, file watcher, external-change reload)
- [x] [tasks.status-bar-activity.md](tasks.status-bar-activity.md) - Status bar in-progress + recently-completed activity sections ✅
- [x] [tasks.live-updates.md](tasks.live-updates.md) - Live TUI updates (Store actor, file watcher, external-change reload) ✅

### Advanced Features
- [x] [tasks.agent-integration.md](tasks.agent-integration.md) - Agent prompts, token minimization (Tasks 1-6 complete) ✅
Expand Down
4 changes: 2 additions & 2 deletions tasks/tasks.status-bar-activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ completion times.

- [x] Seed via existing `TaskRepository::find_by_status(InProgress)` (already ordered by `full_id` — no new query needed)
- [x] Seed `state.activity.in_progress` in `TuiApp::new_with_scheme`
- [ ] Test: DB with two in-progress tasks → smaller full_id wins *(deferred — relies on TUI integration harness; ordering itself is covered by existing repository tests)*
- [x] Test: DB with two in-progress tasks → smaller full_id wins *(`crates/lash-tui/tests/activity_bar_tests.rs`; the fixture lists `zulu` above `alpha` so row order and full_id order disagree)*

---

Expand All @@ -93,7 +93,7 @@ for its transient duration.
- [x] Insert `record_transition` call after `update_markdown_task_status` succeeds (primary toggle path)
- [x] Hook the three cascading handlers (`handle_confirm_cascading_complete`, `handle_confirm_linked_file_complete`, `handle_confirm_cascading_incomplete`) — initiating task only, not cascade results
- [x] Existing tests for `handle_toggle_status` still pass
- [ ] New end-to-end test: toggling Open→InProgress sets `activity.in_progress`; InProgress→Done clears it and pushes to recent *(deferred to Phase C when a more general TUI integration harness exists; covered indirectly by unit tests on `ActivityState::record_transition`)*
- [x] New end-to-end test: toggling Open→InProgress sets `activity.in_progress`; InProgress→Done clears it and pushes to recent *(`crates/lash-tui/tests/activity_bar_tests.rs`, driven through `TestAppBuilder` and real Space key events)*

---

Expand Down
Loading