Skip to content

Commit 2c84649

Browse files
Caio-Zeclaude
andcommitted
postprod_rules: Add test coverage for NoteStore and file scanning (Phase 8)
13 tests covering NoteStore CRUD, query filtering, assignment round-trip, and file scanning with symlink detection. Promotes scan_md_files/PromptFileEntry/PickerSection to pub(crate) for test access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aeb078a commit 2c84649

4 files changed

Lines changed: 419 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/postprod_rules/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,7 @@ util.workspace = true
3939
uuid.workspace = true
4040
workspace.workspace = true
4141
zed_actions.workspace = true
42+
43+
[dev-dependencies]
44+
gpui = { workspace = true, features = ["test-support"] }
45+
tempfile.workspace = true

crates/postprod_rules/src/note_store.rs

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,355 @@ impl NoteStore {
322322
})
323323
}
324324
}
325+
326+
#[cfg(test)]
327+
mod tests {
328+
use super::*;
329+
use gpui::{Entity, TestAppContext};
330+
use rope::Rope;
331+
332+
async fn create_test_store(cx: &mut TestAppContext) -> Entity<NoteStore> {
333+
cx.executor().allow_parking();
334+
let temp_dir = tempfile::tempdir().unwrap();
335+
let db_path = temp_dir.path().join("notes-db");
336+
let store = cx.update(|cx| NoteStore::new(db_path, cx)).await.unwrap();
337+
// Leak temp_dir so the database directory outlives the store
338+
std::mem::forget(temp_dir);
339+
cx.new(|_cx| store)
340+
}
341+
342+
// -----------------------------------------------------------------------
343+
// 8.1 — CRUD tests
344+
// -----------------------------------------------------------------------
345+
346+
#[gpui::test]
347+
async fn create_and_load(cx: &mut TestAppContext) {
348+
let store = create_test_store(cx).await;
349+
350+
let id = NoteId::new();
351+
store
352+
.update(cx, |s, cx| {
353+
s.save(
354+
id,
355+
Some("Test Note".into()),
356+
false,
357+
vec![],
358+
Rope::from("Hello, world!"),
359+
cx,
360+
)
361+
})
362+
.await
363+
.unwrap();
364+
365+
let body = store.update(cx, |s, _cx| s.load_body_sync(id)).unwrap();
366+
assert_eq!(body, "Hello, world!");
367+
368+
let meta = store.update(cx, |s, _cx| s.metadata(id)).unwrap();
369+
assert_eq!(meta.title, Some(SharedString::from("Test Note")));
370+
assert!(!meta.default);
371+
assert!(meta.assigned_automations.is_empty());
372+
}
373+
374+
#[gpui::test]
375+
async fn update_title_and_body(cx: &mut TestAppContext) {
376+
let store = create_test_store(cx).await;
377+
378+
let id = NoteId::new();
379+
store
380+
.update(cx, |s, cx| {
381+
s.save(
382+
id,
383+
Some("Original".into()),
384+
false,
385+
vec![],
386+
Rope::from("original body"),
387+
cx,
388+
)
389+
})
390+
.await
391+
.unwrap();
392+
393+
store
394+
.update(cx, |s, cx| {
395+
s.save(
396+
id,
397+
Some("Updated".into()),
398+
false,
399+
vec![],
400+
Rope::from("updated body"),
401+
cx,
402+
)
403+
})
404+
.await
405+
.unwrap();
406+
407+
let meta = store.update(cx, |s, _cx| s.metadata(id)).unwrap();
408+
assert_eq!(meta.title, Some(SharedString::from("Updated")));
409+
410+
let body = store.update(cx, |s, _cx| s.load_body_sync(id)).unwrap();
411+
assert_eq!(body, "updated body");
412+
}
413+
414+
#[gpui::test]
415+
async fn delete_removes(cx: &mut TestAppContext) {
416+
let store = create_test_store(cx).await;
417+
418+
let id = NoteId::new();
419+
store
420+
.update(cx, |s, cx| {
421+
s.save(
422+
id,
423+
Some("Doomed".into()),
424+
false,
425+
vec![],
426+
Rope::from("bye"),
427+
cx,
428+
)
429+
})
430+
.await
431+
.unwrap();
432+
433+
store
434+
.update(cx, |s, cx| s.delete(id, cx))
435+
.await
436+
.unwrap();
437+
438+
let meta = store.update(cx, |s, _cx| s.metadata(id));
439+
assert!(meta.is_none());
440+
441+
let body = store.update(cx, |s, _cx| s.load_body_sync(id));
442+
assert!(body.is_err());
443+
}
444+
445+
#[gpui::test]
446+
async fn all_metadata_returns_all(cx: &mut TestAppContext) {
447+
let store = create_test_store(cx).await;
448+
449+
for i in 0..3 {
450+
store
451+
.update(cx, |s, cx| {
452+
s.save(
453+
NoteId::new(),
454+
Some(SharedString::from(format!("Note {i}"))),
455+
false,
456+
vec![],
457+
Rope::from("body"),
458+
cx,
459+
)
460+
})
461+
.await
462+
.unwrap();
463+
}
464+
465+
let all = store.update(cx, |s, _cx| s.all_metadata());
466+
assert_eq!(all.len(), 3);
467+
}
468+
469+
#[gpui::test]
470+
async fn first_returns_alphabetically(cx: &mut TestAppContext) {
471+
let store = create_test_store(cx).await;
472+
473+
store
474+
.update(cx, |s, cx| {
475+
s.save(
476+
NoteId::new(),
477+
Some("Zebra".into()),
478+
false,
479+
vec![],
480+
Rope::from("z"),
481+
cx,
482+
)
483+
})
484+
.await
485+
.unwrap();
486+
487+
store
488+
.update(cx, |s, cx| {
489+
s.save(
490+
NoteId::new(),
491+
Some("Alpha".into()),
492+
false,
493+
vec![],
494+
Rope::from("a"),
495+
cx,
496+
)
497+
})
498+
.await
499+
.unwrap();
500+
501+
let first = store.update(cx, |s, _cx| s.first()).unwrap();
502+
assert_eq!(first.title, Some(SharedString::from("Alpha")));
503+
}
504+
505+
// -----------------------------------------------------------------------
506+
// 8.2 — Query tests
507+
// -----------------------------------------------------------------------
508+
509+
#[gpui::test]
510+
async fn default_metadata_filters(cx: &mut TestAppContext) {
511+
let store = create_test_store(cx).await;
512+
513+
store
514+
.update(cx, |s, cx| {
515+
s.save(
516+
NoteId::new(),
517+
Some("Default note".into()),
518+
true,
519+
vec![],
520+
Rope::from("d"),
521+
cx,
522+
)
523+
})
524+
.await
525+
.unwrap();
526+
527+
store
528+
.update(cx, |s, cx| {
529+
s.save(
530+
NoteId::new(),
531+
Some("Regular note".into()),
532+
false,
533+
vec![],
534+
Rope::from("r"),
535+
cx,
536+
)
537+
})
538+
.await
539+
.unwrap();
540+
541+
let defaults = store.update(cx, |s, _cx| s.default_metadata());
542+
assert_eq!(defaults.len(), 1);
543+
assert_eq!(defaults[0].title, Some(SharedString::from("Default note")));
544+
}
545+
546+
#[gpui::test]
547+
async fn notes_for_automation_filters(cx: &mut TestAppContext) {
548+
let store = create_test_store(cx).await;
549+
550+
// A default note (applies to all)
551+
store
552+
.update(cx, |s, cx| {
553+
s.save(
554+
NoteId::new(),
555+
Some("Default".into()),
556+
true,
557+
vec![],
558+
Rope::from("d"),
559+
cx,
560+
)
561+
})
562+
.await
563+
.unwrap();
564+
565+
// Assigned to "build-report"
566+
store
567+
.update(cx, |s, cx| {
568+
s.save(
569+
NoteId::new(),
570+
Some("Build rules".into()),
571+
false,
572+
vec!["build-report".into()],
573+
Rope::from("b"),
574+
cx,
575+
)
576+
})
577+
.await
578+
.unwrap();
579+
580+
// Assigned to "mix-check"
581+
store
582+
.update(cx, |s, cx| {
583+
s.save(
584+
NoteId::new(),
585+
Some("Mix rules".into()),
586+
false,
587+
vec!["mix-check".into()],
588+
Rope::from("m"),
589+
cx,
590+
)
591+
})
592+
.await
593+
.unwrap();
594+
595+
let build_notes =
596+
store.update(cx, |s, _cx| s.notes_for_automation("build-report"));
597+
assert_eq!(build_notes.len(), 2); // default + assigned
598+
599+
let unknown_notes =
600+
store.update(cx, |s, _cx| s.notes_for_automation("unknown"));
601+
assert_eq!(unknown_notes.len(), 1); // default only
602+
}
603+
604+
// -----------------------------------------------------------------------
605+
// 8.3 — Assignment round-trip tests
606+
// -----------------------------------------------------------------------
607+
608+
#[gpui::test]
609+
async fn save_metadata_updates_assignments(cx: &mut TestAppContext) {
610+
let store = create_test_store(cx).await;
611+
612+
let id = NoteId::new();
613+
store
614+
.update(cx, |s, cx| {
615+
s.save(
616+
id,
617+
Some("Note".into()),
618+
false,
619+
vec!["a".into(), "b".into()],
620+
Rope::from("body"),
621+
cx,
622+
)
623+
})
624+
.await
625+
.unwrap();
626+
627+
store
628+
.update(cx, |s, cx| {
629+
s.save_metadata(
630+
id,
631+
Some("Note".into()),
632+
false,
633+
vec!["b".into(), "c".into()],
634+
cx,
635+
)
636+
})
637+
.await
638+
.unwrap();
639+
640+
let meta = store.update(cx, |s, _cx| s.metadata(id)).unwrap();
641+
assert_eq!(meta.assigned_automations, vec!["b", "c"]);
642+
}
643+
644+
#[gpui::test]
645+
async fn clear_assignments(cx: &mut TestAppContext) {
646+
let store = create_test_store(cx).await;
647+
648+
let id = NoteId::new();
649+
store
650+
.update(cx, |s, cx| {
651+
s.save(
652+
id,
653+
Some("Note".into()),
654+
false,
655+
vec!["a".into()],
656+
Rope::from("body"),
657+
cx,
658+
)
659+
})
660+
.await
661+
.unwrap();
662+
663+
store
664+
.update(cx, |s, cx| {
665+
s.save_metadata(id, Some("Note".into()), false, vec![], cx)
666+
})
667+
.await
668+
.unwrap();
669+
670+
let notes = store.update(cx, |s, _cx| s.notes_for_automation("a"));
671+
assert!(
672+
notes.iter().all(|n| n.id != id),
673+
"cleared note should not appear for automation 'a'"
674+
);
675+
}
676+
}

0 commit comments

Comments
 (0)