Skip to content

Commit ab95cc9

Browse files
committed
test(dategroup): fix midnight boundary flakiness in multiple_sessions_same_category_grouped
Anchor both sessions to noon today instead of Utc::now() - 5min. Running the test between 00:00 and 00:04 local time caused the earlier session to fall into Yesterday, producing 2 groups instead of 1.
1 parent b9b8d27 commit ab95cc9

3 files changed

Lines changed: 73 additions & 16 deletions

File tree

specs/02-file-watcher.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ sequenceDiagram
125125
WT ->> WT: watch each parent\n(Recursive)
126126
127127
loop file events
128-
WT ->> WT: filter .jsonl files
128+
WT ->> WT: filter: Create events (any)\nOR Modify/Remove of .jsonl files
129129
WT ->> WT: debounce 1 000 ms
130130
WT -->> TK: signal
131131
TK -->> CL: emit "picker-refresh"\n(empty payload)

src-tauri/src/parser/dategroup.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub fn group_sessions_by_date(sessions: &[SessionInfo]) -> Vec<DateGroup> {
7474
#[cfg(test)]
7575
mod tests {
7676
use super::*;
77-
use chrono::{Duration, Utc};
77+
use chrono::{Duration, Local, TimeZone, Utc};
7878

7979
fn make_session(mod_time: DateTime<Utc>) -> SessionInfo {
8080
SessionInfo {
@@ -153,9 +153,14 @@ mod tests {
153153

154154
#[test]
155155
fn multiple_sessions_same_category_grouped() {
156-
let now = Utc::now();
157-
let also_now = now - Duration::minutes(5);
158-
let sessions = vec![make_session(now), make_session(also_now)];
156+
// Anchor to noon today to avoid midnight boundary flakiness
157+
let noon_utc = Local::now()
158+
.date_naive()
159+
.and_hms_opt(12, 0, 0)
160+
.map(|dt| Local.from_local_datetime(&dt).unwrap().with_timezone(&Utc))
161+
.unwrap_or_else(Utc::now);
162+
let also_noon = noon_utc - Duration::minutes(5);
163+
let sessions = vec![make_session(noon_utc), make_session(also_noon)];
159164
let groups = group_sessions_by_date(&sessions);
160165
assert_eq!(groups.len(), 1);
161166
assert_eq!(groups[0].category, DateCategory::Today);

src-tauri/src/watcher.rs

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,24 @@ pub fn start_session_watcher(
264264
}
265265
}
266266

267+
/// Filter for picker watcher events.
268+
/// Triggers on any Create event (new project directory or new session file) and
269+
/// on Modify/Remove events that target a `.jsonl` file.
270+
/// Keeping Create broad is intentional: when Claude Code starts a session in a
271+
/// brand-new project, the OS may deliver a single directory-creation event
272+
/// before the JSONL file appears, and we must not miss that signal.
273+
fn picker_event_filter(event: &notify::Event) -> bool {
274+
if matches!(event.kind, notify::EventKind::Create(_)) {
275+
return true;
276+
}
277+
event.paths.iter().any(|p| {
278+
p.file_name()
279+
.and_then(|n| n.to_str())
280+
.map(|n| n.ends_with(".jsonl"))
281+
.unwrap_or(false)
282+
})
283+
}
284+
267285
/// Start watching project directories for new/changed sessions.
268286
/// When changes are detected the watcher broadcasts a lightweight `picker-refresh`
269287
/// signal with no payload. Clients are responsible for fetching the updated
@@ -312,17 +330,7 @@ pub fn start_picker_watcher(
312330
}
313331
}
314332

315-
run_debounce_loop(
316-
rx,
317-
|event| {
318-
event.paths.iter().any(|p| {
319-
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
320-
name.ends_with(".jsonl")
321-
})
322-
},
323-
signal_tx,
324-
thread_stop_rx,
325-
);
333+
run_debounce_loop(rx, picker_event_filter, signal_tx, thread_stop_rx);
326334
// watcher dropped here → OS watcher fd released
327335
});
328336

@@ -394,6 +402,50 @@ mod tests {
394402
.expect("debounce thread should exit when stop sender is dropped");
395403
}
396404

405+
/// picker_event_filter triggers on Create events regardless of path extension.
406+
#[test]
407+
fn picker_filter_triggers_on_create_events() {
408+
// New project directory (no .jsonl extension)
409+
let mut event =
410+
notify::Event::new(notify::EventKind::Create(notify::event::CreateKind::Folder));
411+
event.paths = vec![std::path::PathBuf::from(
412+
"/home/.claude/projects/-Users-yang-new-project",
413+
)];
414+
assert!(picker_event_filter(&event), "Create(Folder) must trigger");
415+
416+
// Any create — some platforms report CreateKind::Any
417+
let mut event =
418+
notify::Event::new(notify::EventKind::Create(notify::event::CreateKind::Any));
419+
event.paths = vec![std::path::PathBuf::from(
420+
"/home/.claude/projects/-Users-yang-new-project",
421+
)];
422+
assert!(picker_event_filter(&event), "Create(Any) must trigger");
423+
}
424+
425+
/// picker_event_filter triggers on Modify events only for .jsonl files.
426+
#[test]
427+
fn picker_filter_triggers_on_jsonl_modify() {
428+
let mut event = notify::Event::new(notify::EventKind::Modify(
429+
notify::event::ModifyKind::Data(notify::event::DataChange::Any),
430+
));
431+
event.paths = vec![std::path::PathBuf::from(
432+
"/home/.claude/projects/proj/session.jsonl",
433+
)];
434+
assert!(picker_event_filter(&event));
435+
}
436+
437+
/// picker_event_filter ignores Modify events on non-.jsonl files.
438+
#[test]
439+
fn picker_filter_ignores_non_jsonl_modify() {
440+
let mut event = notify::Event::new(notify::EventKind::Modify(
441+
notify::event::ModifyKind::Data(notify::event::DataChange::Any),
442+
));
443+
event.paths = vec![std::path::PathBuf::from(
444+
"/home/.claude/projects/proj/settings.json",
445+
)];
446+
assert!(!picker_event_filter(&event));
447+
}
448+
397449
/// WatcherHandle::stop() must not panic when called multiple times on a closed channel.
398450
#[test]
399451
fn watcher_handle_stop_is_idempotent() {

0 commit comments

Comments
 (0)