lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
mod common;

use common::TestFixture;

const PARENT_CONTENT: &str = "\
---
title: \"Multi Doc\"
type: rfc
status: draft
author: \"test\"
date: 2026-01-01
tags: []
---
";

const APPENDIX_CONTENT: &str = "\
---
title: \"Appendix\"
type: rfc
status: draft
author: \"test\"
date: 2026-01-01
tags: []
---
";

const GLOSSARY_CONTENT: &str = "\
---
title: \"Glossary\"
type: rfc
status: draft
author: \"test\"
date: 2026-01-01
tags: []
---
";

fn setup_parent_with_children() -> TestFixture {
    let fixture = TestFixture::new();
    fixture.write_subfolder_doc("docs/rfcs/RFC-003-multi", PARENT_CONTENT);
    fixture.write_child_doc("docs/rfcs/RFC-003-multi", "appendix.md", APPENDIX_CONTENT);
    fixture.write_child_doc("docs/rfcs/RFC-003-multi", "glossary.md", GLOSSARY_CONTENT);
    fixture
}

#[test]
fn show_parent_json_includes_children() {
    let fixture = setup_parent_with_children();
    let store = fixture.store();

    let output = lazyspec::cli::show::run_json(
        &store,
        "RFC-003",
        false,
        25,
        &lazyspec::engine::fs::RealFileSystem,
    )
    .unwrap();
    let json: serde_json::Value = serde_json::from_str(&output).unwrap();

    let children = json["children"].as_array().expect("children array missing");
    assert_eq!(children.len(), 2);

    let titles: Vec<&str> = children
        .iter()
        .map(|c| c["title"].as_str().unwrap())
        .collect();
    assert!(titles.contains(&"Appendix"));
    assert!(titles.contains(&"Glossary"));

    for child in children {
        assert!(child["path"].as_str().unwrap().contains("RFC-003-multi"));
    }
}

#[test]
fn show_child_json_includes_parent() {
    let fixture = setup_parent_with_children();
    let store = fixture.store();

    let output = lazyspec::cli::show::run_json(
        &store,
        "RFC-003/appendix",
        false,
        25,
        &lazyspec::engine::fs::RealFileSystem,
    )
    .unwrap();
    let json: serde_json::Value = serde_json::from_str(&output).unwrap();

    let parent = json["parent"].as_object().expect("parent object missing");
    assert_eq!(parent["title"].as_str().unwrap(), "Multi Doc");
    assert!(parent["path"].as_str().unwrap().contains("index.md"));
}

#[test]
fn list_includes_child_documents() {
    let fixture = setup_parent_with_children();
    let store = fixture.store();

    let output = lazyspec::cli::list::run_json(&store, None, None);
    let json: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();

    let titles: Vec<&str> = json.iter().map(|d| d["title"].as_str().unwrap()).collect();
    assert!(titles.contains(&"Multi Doc"));
    assert!(titles.contains(&"Appendix"));
    assert!(titles.contains(&"Glossary"));
}

#[test]
fn list_json_includes_family_metadata() {
    let fixture = setup_parent_with_children();
    let store = fixture.store();

    let output = lazyspec::cli::list::run_json(&store, None, None);
    let json: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();

    let parent = json.iter().find(|d| d["title"] == "Multi Doc").unwrap();
    let children = parent["children"]
        .as_array()
        .expect("parent should have children array");
    assert_eq!(children.len(), 2);

    let appendix = json.iter().find(|d| d["title"] == "Appendix").unwrap();
    let parent_ref = appendix["parent"]
        .as_object()
        .expect("child should have parent object");
    assert_eq!(parent_ref["title"].as_str().unwrap(), "Multi Doc");
}

#[test]
fn show_parent_json_no_children_field_when_none() {
    let fixture = TestFixture::new();
    fixture.write_rfc("RFC-010-flat.md", "Flat RFC", "draft");
    let store = fixture.store();

    let output = lazyspec::cli::show::run_json(
        &store,
        "RFC-010",
        false,
        25,
        &lazyspec::engine::fs::RealFileSystem,
    )
    .unwrap();
    let json: serde_json::Value = serde_json::from_str(&output).unwrap();

    assert!(
        json.get("children").is_none(),
        "children field should be absent for docs without children"
    );
}

#[test]
fn list_json_virtual_doc_flag() {
    let fixture = TestFixture::new();
    fixture.write_child_doc(
        "docs/rfcs/RFC-004-virtual",
        "notes.md",
        "---\ntitle: \"Notes\"\ntype: rfc\nstatus: draft\nauthor: \"test\"\ndate: 2026-01-01\ntags: []\n---\n",
    );
    fixture.write_child_doc(
        "docs/rfcs/RFC-004-virtual",
        "design.md",
        "---\ntitle: \"Design\"\ntype: rfc\nstatus: draft\nauthor: \"test\"\ndate: 2026-01-01\ntags: []\n---\n",
    );
    let store = fixture.store();

    let output = lazyspec::cli::list::run_json(&store, None, None);
    let json: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();

    let virtual_parent = json
        .iter()
        .find(|d| d["title"] == "Virtual")
        .expect("virtual parent should appear in list");
    assert_eq!(
        virtual_parent["virtual_doc"].as_bool(),
        Some(true),
        "virtual_doc should be true"
    );
    let children = virtual_parent["children"]
        .as_array()
        .expect("virtual parent should have children");
    assert_eq!(children.len(), 2);
}