- Add MIT LICENSE file - Expand .gitignore with standard Rust/Linux entries - Remove dangling symlinks (breadmancli, breadpadcli) and dev scratchpad (svgs.txt) from git tracking - Replace unsafe unwrap() calls with expect() in breadman CLI (guarded by prior filter)
236 lines
7.7 KiB
Rust
236 lines
7.7 KiB
Rust
// End-to-end pipeline tests: classify → save → reload
|
|
//
|
|
// These mirror what breadpad (capture) and breadman (display) do in production.
|
|
// Both apps share the same Store path; we prove here that a note typed in the
|
|
// popup survives the classify+save step and is visible to a fresh store handle,
|
|
// exactly as breadman would see it on startup.
|
|
|
|
use breadpad_shared::classifier::Classifier;
|
|
use breadpad_shared::store::Store;
|
|
use breadpad_shared::types::{Note, NoteType};
|
|
use chrono::Timelike;
|
|
use tempfile::TempDir;
|
|
|
|
// Mirrors commit_note() in breadpad/src/main.rs.
|
|
// `user_type` is the type the user selected in the chip row (default = NoteType::Note).
|
|
fn capture(store: &Store, text: &str, user_type: NoteType) -> Note {
|
|
let mut classifier = Classifier::load("08:00");
|
|
let result = classifier.classify(text);
|
|
|
|
let mut note = Note::new(text.into(), user_type.clone(), None);
|
|
|
|
// When the user left the type at the default, let the classifier override it.
|
|
if user_type == NoteType::from_str("note") {
|
|
note.note_type = result.note_type;
|
|
}
|
|
note.time = result.time;
|
|
note.rrule = result.rrule;
|
|
note.body = result.body;
|
|
|
|
store.save_note(¬e).unwrap();
|
|
note
|
|
}
|
|
|
|
fn setup() -> (TempDir, Store) {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = Store::from_dir(dir.path()).unwrap();
|
|
(dir, store)
|
|
}
|
|
|
|
// Open a second Store handle pointing at the same directory — this simulates
|
|
// breadman reading from the path that breadpad wrote to.
|
|
fn breadman_store(dir: &TempDir) -> Store {
|
|
Store::from_dir(dir.path()).unwrap()
|
|
}
|
|
|
|
// ---- basic round-trip ----
|
|
|
|
#[test]
|
|
fn todo_note_appears_in_store() {
|
|
let (dir, store) = setup();
|
|
let saved = capture(&store, "buy groceries", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len(), 1);
|
|
assert_eq!(notes[0].id, saved.id);
|
|
assert_eq!(notes[0].note_type, NoteType::Todo);
|
|
assert_eq!(notes[0].body, "buy groceries");
|
|
assert!(!notes[0].done);
|
|
}
|
|
|
|
#[test]
|
|
fn idea_note_appears_in_store() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "what if we added dark mode", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len(), 1);
|
|
assert_eq!(notes[0].note_type, NoteType::Idea);
|
|
}
|
|
|
|
#[test]
|
|
fn question_note_appears_in_store() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "why does the cache miss on cold start?", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len(), 1);
|
|
assert_eq!(notes[0].note_type, NoteType::Question);
|
|
}
|
|
|
|
#[test]
|
|
fn plain_note_appears_in_store() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "retro went well today", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len(), 1);
|
|
assert_eq!(notes[0].note_type, NoteType::Note);
|
|
}
|
|
|
|
// ---- reminder with time ----
|
|
|
|
#[test]
|
|
fn reminder_has_time_set() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "call mum at 6pm", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Reminder);
|
|
assert!(notes[0].time.is_some(), "reminder should have a scheduled time");
|
|
let local: chrono::DateTime<chrono::Local> = notes[0].time.unwrap().into();
|
|
assert_eq!(local.hour(), 18);
|
|
}
|
|
|
|
#[test]
|
|
fn reminder_body_has_time_stripped() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "call mum at 6pm", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert!(!notes[0].body.contains("6pm"), "time phrase should be removed from body");
|
|
assert!(notes[0].body.contains("call mum"));
|
|
}
|
|
|
|
#[test]
|
|
fn in_duration_reminder_has_time() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "check on the build in 30 minutes", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Reminder);
|
|
assert!(notes[0].time.is_some());
|
|
}
|
|
|
|
// ---- recurring reminder ----
|
|
|
|
#[test]
|
|
fn recurring_reminder_has_rrule() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "standup every monday at 9am", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Reminder);
|
|
let rrule = notes[0].rrule.as_ref().expect("should have rrule");
|
|
assert!(rrule.as_str().contains("FREQ=WEEKLY"));
|
|
assert!(rrule.as_str().contains("BYDAY=MO"));
|
|
}
|
|
|
|
#[test]
|
|
fn daily_reminder_has_rrule() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "drink water every day at 8am", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Reminder);
|
|
assert!(notes[0].rrule.as_ref().unwrap().as_str().contains("FREQ=DAILY"));
|
|
}
|
|
|
|
// ---- user-forced type is respected ----
|
|
|
|
#[test]
|
|
fn user_selected_type_overrides_classifier() {
|
|
let (dir, store) = setup();
|
|
// Text would classify as Todo, but user explicitly chose Idea
|
|
capture(&store, "fix the login bug", NoteType::Idea);
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Idea, "user chip selection should win over classifier");
|
|
}
|
|
|
|
#[test]
|
|
fn user_selected_reminder_overrides_classifier() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "team meeting notes from today", NoteType::Reminder);
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].note_type, NoteType::Reminder);
|
|
}
|
|
|
|
// ---- multiple notes all appear ----
|
|
|
|
#[test]
|
|
fn three_notes_all_visible_to_breadman() {
|
|
let (dir, store) = setup();
|
|
capture(&store, "buy milk", NoteType::from_str("note"));
|
|
capture(&store, "what if we rewrote in Zig", NoteType::from_str("note"));
|
|
capture(&store, "team standup went well", NoteType::from_str("note"));
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len(), 3);
|
|
|
|
let types: Vec<NoteType> = notes.iter().map(|n| n.note_type.clone()).collect();
|
|
assert!(types.contains(&NoteType::Todo));
|
|
assert!(types.contains(&NoteType::Idea));
|
|
assert!(types.contains(&NoteType::Note));
|
|
}
|
|
|
|
#[test]
|
|
fn notes_written_sequentially_all_survive() {
|
|
let (dir, store) = setup();
|
|
let n = 10u32;
|
|
for i in 0..n {
|
|
capture(&store, &format!("note number {}", i), NoteType::Note);
|
|
}
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes.len() as u32, n);
|
|
}
|
|
|
|
// ---- note fields are fully preserved ----
|
|
|
|
#[test]
|
|
fn note_id_is_stable_after_reload() {
|
|
let (dir, store) = setup();
|
|
let saved = capture(&store, "check the logs", NoteType::Todo);
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
assert_eq!(notes[0].id, saved.id);
|
|
}
|
|
|
|
#[test]
|
|
fn note_created_timestamp_preserved() {
|
|
let (dir, store) = setup();
|
|
let saved = capture(&store, "morning standup", NoteType::Note);
|
|
|
|
let notes = breadman_store(&dir).load_all().unwrap();
|
|
// Timestamps should be equal within 1 second (serde round-trips subsecond precision)
|
|
let diff = (notes[0].created - saved.created).num_seconds().abs();
|
|
assert!(diff <= 1, "created timestamp drifted by {}s", diff);
|
|
}
|
|
|
|
// ---- store isolation: two separate runs don't bleed ----
|
|
|
|
#[test]
|
|
fn separate_store_dirs_are_isolated() {
|
|
let (dir_a, store_a) = setup();
|
|
let (dir_b, store_b) = setup();
|
|
capture(&store_a, "note for session A", NoteType::Note);
|
|
capture(&store_b, "note for session B", NoteType::Note);
|
|
|
|
let notes_a = breadman_store(&dir_a).load_all().unwrap();
|
|
let notes_b = breadman_store(&dir_b).load_all().unwrap();
|
|
assert_eq!(notes_a.len(), 1);
|
|
assert_eq!(notes_b.len(), 1);
|
|
assert_ne!(notes_a[0].id, notes_b[0].id);
|
|
}
|