use std::collections::HashMap;
use serde_json::json;
use crate::quill::FileTreeNode;
use crate::{Document, Quill, SeedOverlay, Severity};
fn overlay(value: serde_json::Value) -> SeedOverlay {
SeedOverlay::from_json(&value).expect("overlay json must be an object")
}
fn quill_from_yaml(yaml: &str) -> Quill {
let mut files = HashMap::new();
files.insert(
"Quill.yaml".to_string(),
FileTreeNode::File {
contents: yaml.as_bytes().to_vec(),
},
);
let root = FileTreeNode::Directory { files };
Quill::from_tree(root).expect("quill_from_yaml: Quill::from_tree failed")
}
const QUILL: &str = r#"
quill:
name: seed_test
version: "1.0"
backend: typst
description: Seed test
main:
body:
example: "Main body text."
fields:
title:
type: string
example: FIRSTNAME LASTNAME
status:
type: string
default: draft
notes:
type: string
card_kinds:
note:
fields:
author:
type: string
example: A. Author
tag:
type: string
"#;
#[test]
fn seed_main_commits_only_example_fields() {
let quill = quill_from_yaml(QUILL);
let card = quill.seed_main();
let payload = card.payload();
assert_eq!(
payload.get("title").and_then(|v| v.as_str()),
Some("FIRSTNAME LASTNAME"),
);
assert!(
payload.get("status").is_none(),
"default-only field must be absent (interpolated at render)"
);
assert!(payload.get("notes").is_none());
let reference = card.quill().expect("main card must carry $quill");
assert_eq!(reference.name, "seed_test");
assert_eq!(
card.kind(),
Some("main"),
"main card must carry $kind: main"
);
assert_eq!(card.body(), "Main body text.");
}
#[test]
fn seeded_document_round_trips_through_markdown() {
let quill = quill_from_yaml(QUILL);
let doc = quill.seed_document();
let markdown = doc.to_markdown();
let reparsed = crate::Document::from_markdown(&markdown)
.expect("seeded document must re-parse from its own markdown");
assert_eq!(
reparsed.main().quill().map(|r| r.name.as_str()),
Some("seed_test")
);
assert_eq!(reparsed.main().kind(), Some("main"));
assert_eq!(
reparsed
.main()
.payload()
.get("title")
.and_then(|v| v.as_str()),
Some("FIRSTNAME LASTNAME"),
);
assert_eq!(reparsed.main().body(), "Main body text.\n");
assert_eq!(reparsed.cards().len(), 1);
assert_eq!(reparsed.cards()[0].kind(), Some("note"));
assert_eq!(
reparsed.cards()[0]
.payload()
.get("author")
.and_then(|v| v.as_str()),
Some("A. Author"),
);
}
#[test]
fn seed_document_emits_one_seeded_card_per_kind() {
let quill = quill_from_yaml(QUILL);
let doc = quill.seed_document();
assert_eq!(
doc.main().payload().get("title").and_then(|v| v.as_str()),
Some("FIRSTNAME LASTNAME"),
);
assert_eq!(doc.cards().len(), 1);
let note = &doc.cards()[0];
assert_eq!(note.kind(), Some("note"));
assert!(
note.quill().is_none(),
"composable card must not carry $quill"
);
assert_eq!(
note.payload().get("author").and_then(|v| v.as_str()),
Some("A. Author"),
);
assert!(note.payload().get("tag").is_none());
}
#[test]
fn seeded_document_compiles_with_default_then_zero_for_absent_fields() {
let quill = quill_from_yaml(QUILL);
let doc = quill.seed_document();
let data = quill
.compile_data(&doc)
.expect("seeded document must compile");
assert_eq!(
data.get("title").and_then(|v| v.as_str()),
Some("FIRSTNAME LASTNAME"),
);
assert_eq!(data.get("status").and_then(|v| v.as_str()), Some("draft"));
assert_eq!(data.get("notes").and_then(|v| v.as_str()), Some(""));
}
#[test]
fn seed_card_for_known_and_unknown_kind() {
let quill = quill_from_yaml(QUILL);
let note = quill.seed_card("note", None).expect("known kind");
assert_eq!(note.kind(), Some("note"));
assert_eq!(
note.payload().get("author").and_then(|v| v.as_str()),
Some("A. Author"),
);
assert!(
quill.seed_card("missing", None).is_none(),
"unknown kind must return None"
);
}
#[test]
fn overlay_overrides_example_and_falls_through_for_untouched_fields() {
let quill = quill_from_yaml(QUILL);
let ov = overlay(json!({ "author": "Custom Author" }));
let card = quill.seed_card("note", Some(&ov)).expect("known kind");
assert_eq!(
card.payload().get("author").and_then(|v| v.as_str()),
Some("Custom Author"),
"overlay value wins over the example",
);
}
#[test]
fn overlay_adds_a_field_the_base_omits() {
let quill = quill_from_yaml(QUILL);
assert!(quill
.seed_card("note", None)
.unwrap()
.payload()
.get("tag")
.is_none());
let ov = overlay(json!({ "tag": "pinned" }));
let card = quill.seed_card("note", Some(&ov)).expect("known kind");
assert_eq!(
card.payload().get("tag").and_then(|v| v.as_str()),
Some("pinned")
);
assert_eq!(
card.payload().get("author").and_then(|v| v.as_str()),
Some("A. Author"),
);
}
#[test]
fn overlay_fields_are_ordered_by_ui_order() {
let quill = quill_from_yaml(QUILL);
let ov = overlay(json!({ "tag": "pinned", "author": "Custom" }));
let card = quill.seed_card("note", Some(&ov)).expect("known kind");
let keys: Vec<&str> = card.payload().keys().map(String::as_str).collect();
assert_eq!(keys, vec!["author", "tag"]);
}
#[test]
fn overlay_body_overrides_and_non_schema_keys_are_ignored() {
let quill = quill_from_yaml(QUILL);
assert_eq!(quill.seed_card("note", None).unwrap().body(), "");
let ov = overlay(json!({ "author": "X", "$body": "Overlay body.", "bogus": "drop me" }));
let card = quill.seed_card("note", Some(&ov)).expect("known kind");
assert_eq!(card.body(), "Overlay body.");
assert!(
card.payload().get("bogus").is_none(),
"a key naming no schema field must not land on the card",
);
}
#[test]
fn seed_omits_body_when_body_disabled() {
let quill = quill_from_yaml(
r#"
quill:
name: bodyless
version: "1.0"
backend: typst
description: Bodyless card test
main:
fields:
title:
type: string
example: T
card_kinds:
data:
body:
enabled: false
fields:
value:
type: string
example: V
"#,
);
let card = quill.seed_card("data", None).expect("known kind");
assert_eq!(
card.body(),
"",
"body must be empty when body.enabled is false"
);
assert_eq!(
card.payload().get("value").and_then(|v| v.as_str()),
Some("V"),
);
}
fn doc_with_seed(seed_block: &str) -> Document {
let md = format!("~~~card-yaml\n$quill: seed_test@1.0\n$kind: main\n{seed_block}~~~\n");
Document::from_markdown(&md).expect("doc should parse")
}
#[test]
fn seed_overlay_type_mismatch_is_advisory_and_does_not_gate_render() {
let quill = quill_from_yaml(QUILL);
let doc = doc_with_seed("$seed:\n note:\n author: 123\n");
let diags = quill.validate(&doc);
let seed_diag = diags
.iter()
.find(|d| d.path.as_deref() == Some("$seed.note.author"))
.expect("a diagnostic rooted at the seed field");
assert_eq!(
seed_diag.severity,
Severity::Warning,
"seed diagnostics are advisory, not errors",
);
assert!(
quill.compile_data(&doc).is_ok(),
"compile_data must ignore $seed"
);
assert!(quill.dry_run(&doc).is_ok(), "dry_run must ignore $seed");
}
#[test]
fn seed_overlay_unknown_kind_is_flagged_but_renders() {
let quill = quill_from_yaml(QUILL);
let doc = doc_with_seed("$seed:\n bogus_kind:\n x: 1\n");
let diags = quill.validate(&doc);
let d = diags
.iter()
.find(|d| d.code.as_deref() == Some("validation::seed_unknown_kind"))
.expect("unknown-kind advisory");
assert_eq!(d.path.as_deref(), Some("$seed.bogus_kind"));
assert_eq!(d.severity, Severity::Warning);
assert!(quill.compile_data(&doc).is_ok());
}
#[test]
fn well_formed_seed_overlay_yields_no_seed_diagnostics() {
let quill = quill_from_yaml(QUILL);
let doc = doc_with_seed("$seed:\n note:\n author: Custom\n");
let diags = quill.validate(&doc);
assert!(
!diags
.iter()
.any(|d| d.path.as_deref().is_some_and(|p| p.starts_with("$seed"))),
"a well-formed overlay should produce no seed diagnostics: {diags:?}",
);
}