From f181a7fac3b87bd2882bcfe0ec3c8bb32f47dca9 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 1/6] docs(tdd): remove plan.md from workflow Signed-off-by: Bruce D'Arcus --- .agent/workflows/tdd.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.agent/workflows/tdd.md b/.agent/workflows/tdd.md index aac0c74..1a0a946 100644 --- a/.agent/workflows/tdd.md +++ b/.agent/workflows/tdd.md @@ -29,5 +29,3 @@ description: Test-Driven Development workflow with high safety and auditable res - **Git Note**: Attach a summary of the task using `git notes add -m "" `. - **Note Content**: Task name, changes, files modified, and "why". -## 6. Documentation Updates -- Update `PLAN.md` or track plans with the commit SHA and status `[x]`. From f7714930a776ef0491361d476950a5b63256544d Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 2/6] docs(plan): add test coverage extension plan Signed-off-by: Bruce D'Arcus --- docs/TEST_PLAN.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/TEST_PLAN.md diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md new file mode 100644 index 0000000..4121860 --- /dev/null +++ b/docs/TEST_PLAN.md @@ -0,0 +1,41 @@ +# Test Coverage Extension Plan + +This document outlines a strategy to significantly increase test coverage across the `csln` workspace, focusing on the core processing logic and data extraction. + +## 1. Processor Component Logic (`processor/src/values.rs`) + +The extraction of values from bibliographic references into template components is currently the most complex and least tested area. + +### Priorities: +- [ ] **Field Extraction**: Implement unit tests for all `Variables` and `Numbers` variants across all `InputReference` types (Monograph, SerialComponent, etc.). +- [ ] **Contributor Formatting**: Expand tests for `role_to_string` and `TemplateContributor::values` to cover all forms (Long, Short, Verb) and role substitution logic. +- [ ] **Date Processing**: Add comprehensive tests for `TemplateDate` including EDTF parsing edge cases and disambiguation suffixes (`int_to_letter`). + +## 2. Rendering & Formatting (`processor/src/render.rs`) + +Ensure the final string output matches expected citation standards. + +### Priorities: +- [ ] **Display Trait**: Add tests for `ProcTemplateComponent`'s `Display` implementation, specifically verifying prefix/suffix handling and `WrapPunctuation` (Parentheses, Brackets). +- [ ] **Collection Rendering**: Test `refs_to_string` with multiple references to ensure proper separators and final punctuation. + +## 3. Top-Level Processor Features (`processor/src/processor.rs`) + +Verify the orchestration of styles, bibliographies, and citations. + +### Priorities: +- [ ] **Sorting**: Test `sort_references` with complex `SortKey` arrays (e.g., Author -> Year -> Title). +- [ ] **Disambiguation**: Verify hint calculation and how it affects output (e.g., adding 'a', 'b' to years). +- [ ] **Error Handling**: Test `process_citations` with missing references or invalid style configurations. + +## 4. Model Integrity (`csln/src/`) + +### Priorities: +- [ ] **Style Validation**: Add tests for `Style` deserialization from YAML/JSON to ensure complex templates are parsed correctly. +- [ ] **Locale Handling**: Verify locale merging and term lookup safety. + +## Execution Strategy + +1. **Phase 1**: Add unit tests in the same files as the logic (using `#[cfg(test)]`) for fast feedback. +2. **Phase 2**: Create a `tests/` directory in the `processor` crate for high-level integration tests using real styles and bibliographies. +3. **Phase 3**: Integrate coverage reporting into CI to maintain a >80% threshold for new PRs. From 7bd95e67d877d522e8975df3a24d238a0d592bc6 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 3/6] test(proc): add value extraction tests Signed-off-by: Bruce D'Arcus --- processor/src/values.rs | 175 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/processor/src/values.rs b/processor/src/values.rs index 6518410..93fcf2e 100644 --- a/processor/src/values.rs +++ b/processor/src/values.rs @@ -406,3 +406,178 @@ impl ComponentValues for TemplateDate { }) } } +#[cfg(test)] +mod tests { + use super::*; + use crate::types::RenderOptions; + use csln::bibliography::reference::{ + EdtfString, InputReference, Monograph, MonographType, NumOrStr, Serial, + SerialComponent, SerialComponentType, SerialType, Title, + }; + use csln::style::options::Config; + + fn mock_monograph() -> Monograph { + Monograph { + id: None, + r#type: MonographType::Book, + title: Title::Single("Title".to_string()), + author: None, + issued: EdtfString("2023".to_string()), + publisher: None, + url: None, + accessed: None, + note: None, + isbn: None, + doi: None, + edition: None, + translator: None, + } + } + + fn mock_serial_component() -> SerialComponent { + SerialComponent { + id: None, + r#type: SerialComponentType::Article, + title: Some(Title::Single("Article".to_string())), + author: None, + issued: EdtfString("2023".to_string()), + parent: Serial { + r#type: SerialType::AcademicJournal, + title: Title::Single("Journal".to_string()), + }, + url: None, + accessed: None, + note: None, + doi: None, + pages: None, + volume: None, + issue: None, + translator: None, + } + } + + #[test] + fn test_simple_string_values() { + let config = Config::default(); + let locale = Locale::default(); + let options = RenderOptions { global: &config, local: &config, locale: &locale }; + let hints = ProcHints::default(); + + let template_doi = + TemplateSimpleString { variable: Variables::Doi, rendering: None }; + let mut serial = mock_serial_component(); + serial.doi = Some("10.1234/5678".to_string()); + let ref_doi = InputReference::SerialComponent(serial); + let values = template_doi.values(&ref_doi, &hints, &options).unwrap(); + assert_eq!(values.value, "10.1234/5678"); + + let template_isbn = + TemplateSimpleString { variable: Variables::Isbn, rendering: None }; + let mut monograph = mock_monograph(); + monograph.isbn = Some("978-3-16-148410-0".to_string()); + let ref_isbn = InputReference::Monograph(monograph); + let values = template_isbn.values(&ref_isbn, &hints, &options).unwrap(); + assert_eq!(values.value, "978-3-16-148410-0"); + } + + #[test] + fn test_number_values() { + let config = Config::default(); + let locale = Locale::default(); + let options = RenderOptions { global: &config, local: &config, locale: &locale }; + let hints = ProcHints::default(); + + let template_vol = TemplateNumber { + number: Numbers::Volume, + form: None, + rendering: None, + }; + let mut serial = mock_serial_component(); + serial.volume = Some(NumOrStr::Number(42)); + let ref_vol = InputReference::SerialComponent(serial); + let values = template_vol.values(&ref_vol, &hints, &options).unwrap(); + assert_eq!(values.value, "42"); + } + + #[test] + fn test_title_values() { + let config = Config::default(); + let locale = Locale::default(); + let options = RenderOptions { global: &config, local: &config, locale: &locale }; + let hints = ProcHints::default(); + + let template_primary = TemplateTitle { + title: Titles::Primary, + form: None, + rendering: None, + }; + let monograph = mock_monograph(); + let ref_mono = InputReference::Monograph(monograph); + let values = template_primary.values(&ref_mono, &hints, &options).unwrap(); + assert_eq!(values.value, "Title"); + } + + #[test] + fn test_contributor_values() { + let config = Config::default(); + let locale = Locale::default(); + let options = RenderOptions { global: &config, local: &config, locale: &locale }; + let hints = ProcHints::default(); + use csln::bibliography::reference::{Contributor, SimpleName}; + + let template_author = TemplateContributor { + contributor: ContributorRole::Author, + form: ContributorForm::Long, + rendering: None, + }; + let mut monograph = mock_monograph(); + monograph.author = Some(Contributor::SimpleName(SimpleName { + name: "John Smith".to_string(), + location: None, + })); + let ref_mono = InputReference::Monograph(monograph); + let values = template_author.values(&ref_mono, &hints, &options).unwrap(); + assert_eq!(values.value, "John Smith"); + } + + #[test] + fn test_date_disambiguation() { + use csln::style::options::{Disambiguation, Processing, ProcessingCustom}; + + let mut config_inner = Config::default(); + config_inner.processing = Some(Processing::Custom(ProcessingCustom { + disambiguate: Some(Disambiguation { + year_suffix: true, + ..Default::default() + }), + ..Default::default() + })); + + let locale = Locale::default(); + let options = RenderOptions { + global: &config_inner, + local: &config_inner, + locale: &locale, + }; + + let mut hints = ProcHints::default(); + hints.disamb_condition = true; + hints.group_index = 1; // 'a' + + let template_date = TemplateDate { + date: Dates::Issued, + form: DateForm::Year, + rendering: None, + }; + let monograph = mock_monograph(); + let ref_mono = InputReference::Monograph(monograph); + + let values = template_date.values(&ref_mono, &hints, &options).unwrap(); + assert_eq!(values.value, "2023"); + assert_eq!(values.suffix, Some("a".to_string())); + + hints.group_index = 2; // 'b' + let values = template_date.values(&ref_mono, &hints, &options).unwrap(); + assert_eq!(values.suffix, Some("b".to_string())); + } +} From e9acdcf3024ca66f66b3164258ced1c565336c58 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 4/6] test(proc): add reference rendering tests Signed-off-by: Bruce D'Arcus --- processor/src/render.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/processor/src/render.rs b/processor/src/render.rs index 4e50137..f156314 100644 --- a/processor/src/render.rs +++ b/processor/src/render.rs @@ -84,3 +84,39 @@ fn render_proc_template_component() { ); assert_eq!(proc_template_component.to_string(), "(doi: 10/1234 ||)".to_string()); } + +#[test] +fn test_refs_to_string() { + use crate::types::{ProcTemplateComponent, ProcValues}; + use csln::style::template::{TemplateComponent, TemplateSimpleString, Variables}; + + let comp1 = TemplateComponent::SimpleString(TemplateSimpleString { + variable: Variables::Doi, + rendering: None, + }); + let proc1 = ProcTemplateComponent::new( + comp1, + ProcValues { + value: "10.1".to_string(), + prefix: None, + suffix: None, + }, + ); + + let comp2 = TemplateComponent::SimpleString(TemplateSimpleString { + variable: Variables::Isbn, + rendering: None, + }); + let proc2 = ProcTemplateComponent::new( + comp2, + ProcValues { + value: "1234".to_string(), + prefix: None, + suffix: None, + }, + ); + + let template = vec![proc1, proc2]; + let output = refs_to_string(vec![template]); + assert_eq!(output, "10.1. 1234."); +} From 6b4afaca43c2164fedf983e49d7432eb4e9b7fe8 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 5/6] test(proc): add sort and disambiguation tests Signed-off-by: Bruce D'Arcus --- processor/src/processor.rs | 67 ++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/processor/src/processor.rs b/processor/src/processor.rs index fa7c7d0..a7a917d 100644 --- a/processor/src/processor.rs +++ b/processor/src/processor.rs @@ -410,13 +410,66 @@ mod tests { } #[test] - fn make_group_key_defaults() { - // Test default grouping (should be empty or based on default config) + fn test_sort_references() { + use csln::style::options::{Processing, ProcessingCustom, Sort, SortSpec}; + + let mut style = Style::default(); + let config = ProcessingCustom { + sort: Some(Sort { + template: vec![SortSpec { key: SortKey::Author, ascending: true }], + ..Default::default() + }), + ..Default::default() + }; + style.options = Some(Config { + processing: Some(Processing::Custom(config)), + ..Default::default() + }); + + let ref_a = mock_reference("a", "Zzz", "2020"); + let ref_b = mock_reference("b", "Aaa", "2021"); + + let processor = Processor { style, ..Default::default() }; + + let sorted = processor.sort_references(vec![&ref_a, &ref_b]); + assert_eq!(sorted[0].id().unwrap(), "b"); + assert_eq!(sorted[1].id().unwrap(), "a"); + } + + #[test] + fn test_calculate_proc_hints() { + // Two refs with same author and year should trigger disambiguation + let ref_a = mock_reference("a", "Smith", "2020"); + let ref_b = mock_reference("b", "Smith", "2020"); + + let mut bib = Bibliography::new(); + bib.insert("a".to_string(), ref_a); + bib.insert("b".to_string(), ref_b); + + let processor = Processor::new( + Style::default(), + bib, + Citations::default(), + Locale::default(), + ); + let hints = processor.get_proc_hints(); + + let hint_a = hints.get("a").unwrap(); + let hint_b = hints.get("b").unwrap(); + + assert!(hint_a.disamb_condition); + assert!(hint_b.disamb_condition); + assert_ne!(hint_a.group_index, hint_b.group_index); + } + + #[test] + fn test_error_handling() { let processor = Processor::default(); - let reference = mock_reference("ref1", "Smith", "2020"); - let key = processor.make_group_key(&reference); - // Default group key produces "First Last:Year" format or similar depending on implementation - // The failure shows "Given Smith:2020" - assert_eq!(key, "Given Smith:2020"); + let result = processor.get_reference("nonexistent"); + assert!(result.is_err()); + match result { + Err(ProcessorError::ReferenceNotFound(id)) => assert_eq!(id, "nonexistent"), + _ => panic!("Expected ReferenceNotFound error"), + } } } From 10fa748aa793275e4e63fd44928a1be715036b05 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Tue, 27 Jan 2026 07:29:10 -0500 Subject: [PATCH 6/6] test(csln): add style and locale validation Signed-off-by: Bruce D'Arcus --- csln/src/style/locale.rs | 36 ++++++++++++++++++++++++++++++++++++ csln/src/style/mod.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/csln/src/style/locale.rs b/csln/src/style/locale.rs index 90d8ab6..167c903 100644 --- a/csln/src/style/locale.rs +++ b/csln/src/style/locale.rs @@ -269,3 +269,39 @@ pub enum LocalizedTermNameMisc { WorkingPaper, } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_locale_deserialization() { + let json = r#" + { + "locale": "en-US", + "dates": { + "months": { + "long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + "short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + }, + "seasons": ["Spring", "Summer", "Autumn", "Winter"] + }, + "roles": {}, + "terms": { + "and": "and", + "anonymous": { + "long": "anonymous", + "short": "anon" + }, + "circa": { + "long": "circa", + "short": "c." + } + } + } + "#; + let locale: Locale = serde_json::from_str(json).unwrap(); + assert_eq!(locale.locale, "en-US"); + assert_eq!(locale.dates.months.long[0], "January"); + assert_eq!(locale.terms.and.as_ref().unwrap(), "and"); + } +} diff --git a/csln/src/style/mod.rs b/csln/src/style/mod.rs index f0823d1..c7db67c 100644 --- a/csln/src/style/mod.rs +++ b/csln/src/style/mod.rs @@ -90,4 +90,32 @@ mod tests { assert!(style.bibliography.is_none()); assert!(style.citation.is_none()); } + + #[test] + fn test_style_deserialization_complex() { + let json = r#" + { + "info": { + "title": "Complex Style", + "id": "http://example.com/styles/complex" + }, + "bibliography": { + "template": [ + { + "contributor": "author", + "form": "long" + }, + { + "date": "issued", + "form": "year" + } + ] + } + } + "#; + let style: Style = serde_json::from_str(json).unwrap(); + assert_eq!(style.info.title.as_ref().unwrap(), "Complex Style"); + let bib = style.bibliography.unwrap(); + assert_eq!(bib.template.len(), 2); + } }