#[path = "helpers/mod.rs"]
mod helpers;
use helpers::{count_backlinks, enqueue_curate_job, has_backlink_to, test_dispatcher_with_index};
use chrono::Utc;
use gradatum_core::frontmatter::{ExtraFields, Frontmatter};
use gradatum_core::scope::VaultId;
use gradatum_core::section::Section;
use gradatum_core::status::NoteStatus;
async fn seed_target_note(fixture: &helpers::DispatcherFixture, title: &str, body: &str) -> String {
let frontmatter = Frontmatter {
schema_version: 1,
vault_id: VaultId::new("main"),
locus: None,
section: Section::Reference,
status: NoteStatus::Live,
status_reason: None,
status_changed: None,
tags: Default::default(),
author: None,
created: Utc::now(),
updated: None,
extra: ExtraFields::empty(),
};
let body_full = format!("# {title}\n{body}");
let note = fixture
.vault
.write_note(frontmatter, body_full)
.await
.expect("vault.write_note seed cible");
fixture
.index
.upsert_note_title(¬e.id, title)
.await
.expect("upsert_note_title seed cible");
note.id.to_string()
}
#[tokio::test]
async fn wikilinks_b5_parallel_persists_all_links() {
let fixture = test_dispatcher_with_index().await;
let targets = [
("Note Parallèle A", "Contenu A"),
("Note Parallèle B", "Contenu B"),
("Note Parallèle C", "Contenu C"),
("Note Parallèle D", "Contenu D"),
("Note Parallèle E", "Contenu E"),
];
let mut target_ids = Vec::new();
for (title, body) in &targets {
let id = seed_target_note(&fixture, title, body).await;
target_ids.push(id);
}
let body = "# [DECISIONS] Note parallèle multi-liens\n\n\
Voir [[Note Parallèle A]], [[Note Parallèle B]], [[Note Parallèle C]], \
[[Note Parallèle D]] et [[Note Parallèle E]] pour les références.";
enqueue_curate_job(&fixture, "[DECISIONS] Note parallèle multi-liens", body).await;
let processed = fixture.dispatcher.run_once().await.unwrap();
assert!(processed, "le dispatcher doit traiter le job");
for (idx_pos, target_id) in target_ids.iter().enumerate() {
let count = count_backlinks(&fixture.index, target_id).await;
assert_eq!(
count, 1,
"C9 : note cible #{idx_pos} ({target_id}) doit avoir exactement 1 backlink, count={count}"
);
assert!(
has_backlink_to(&fixture.index, target_id).await,
"C9 : has_backlink_to doit être true pour note cible #{idx_pos} ({target_id})"
);
}
}
#[tokio::test]
async fn wikilinks_b5_parallel_unresolved_wikilink_does_not_block() {
let fixture = test_dispatcher_with_index().await;
let known_title = "Note Connue Parallèle";
let known_id = seed_target_note(&fixture, known_title, "Contenu connu.").await;
let body = "# [DECISIONS] Note mixte lien connu/inconnu\n\n\
Voir [[Note Connue Parallèle]] et [[Titre Totalement Inexistant XYZ123]] pour le contexte.";
enqueue_curate_job(&fixture, "[DECISIONS] Note mixte lien connu/inconnu", body).await;
let result = fixture.dispatcher.run_once().await;
assert!(
result.is_ok(),
"C9 : curate ne doit pas échouer sur wikilink non résolu — err={result:?}"
);
assert!(result.unwrap(), "un job doit avoir été traité");
assert!(
has_backlink_to(&fixture.index, &known_id).await,
"C9 : lien résolu vers '{known_title}' ({known_id}) doit être persisté"
);
let phantom_count = count_backlinks(&fixture.index, "Titre Totalement Inexistant XYZ123").await;
assert_eq!(
phantom_count, 0,
"C9 : lien non résolu ne doit pas créer d'arête fantôme"
);
}
#[tokio::test]
async fn wikilinks_b5_parallel_empty_returns_immediately() {
let fixture = test_dispatcher_with_index().await;
let body = "# [DECISIONS] Note sans wikilinks\n\nContenu simple sans liens.";
enqueue_curate_job(&fixture, "[DECISIONS] Note sans wikilinks", body).await;
let result = fixture.dispatcher.run_once().await;
assert!(
result.is_ok(),
"C9 : curate sans wikilinks ne doit pas échouer — err={result:?}"
);
assert!(result.unwrap(), "un job doit avoir été traité");
let count = count_backlinks(&fixture.index, "00000000000000000000000000").await;
assert_eq!(count, 0, "C9 : aucun lien fantôme sans wikilinks");
}