osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use super::{GuideEntry, GuideSection, GuideSectionKind, GuideView};
use crate::core::command_def::{ArgDef, CommandDef, FlagDef};
use crate::core::output_model::{OutputDocument, OutputDocumentKind, OutputItems, OutputResult};
use crate::guide::HelpLevel;
use serde_json::Value;
use serde_json::json;

#[test]
fn filtered_for_help_level_hides_verbose_sections_until_requested_unit() {
    let mut view = GuideView::from_text(
        "Usage: osp [COMMAND]\n\nCommands:\n  help  Show help\n\nCommon Invocation Options:\n  --json  Render as JSON\n",
    );
    view.sections
        .push(GuideSection::new("Notes", GuideSectionKind::Notes).paragraph("extra note"));

    let tiny = view.filtered_for_help_level(HelpLevel::Tiny);
    let normal = view.filtered_for_help_level(HelpLevel::Normal);
    let verbose = view.filtered_for_help_level(HelpLevel::Verbose);

    assert!(!tiny.usage.is_empty());
    assert!(tiny.commands.is_empty());
    assert!(normal.common_invocation_options.is_empty());
    assert!(!normal.commands.is_empty());
    assert!(!normal.sections.is_empty());
    assert!(!verbose.common_invocation_options.is_empty());
}

#[test]
fn guide_view_from_command_def_builds_usage_arguments_and_options_unit() {
    let view = GuideView::from_command_def(
        &CommandDef::new("theme")
            .about("Inspect and apply themes")
            .flag(FlagDef::new("raw").long("raw").help("Show raw values"))
            .arg(ArgDef::new("name").value_name("name"))
            .subcommand(CommandDef::new("list").about("List themes")),
    );

    assert_eq!(view.usage.len(), 1);
    assert_eq!(view.commands.len(), 1);
    assert_eq!(view.arguments.len(), 1);
    assert_eq!(view.options.len(), 1);
}

#[test]
fn guide_output_restore_round_trips_canonical_and_authored_ordered_shapes_unit() {
    let cases = vec![
        (
            GuideView::from_text("Usage: osp history <COMMAND>\n\nCommands:\n  help  Print help\n"),
            vec![],
            Some(vec!["osp history <COMMAND>".to_string()]),
            Some(vec!["help".to_string()]),
            false,
        ),
        (
            GuideView {
                sections: vec![
                    GuideSection::new("OSP", GuideSectionKind::Custom).paragraph("Welcome"),
                    GuideSection::new("Usage", GuideSectionKind::Usage)
                        .paragraph("[INVOCATION_OPTIONS] COMMAND [ARGS]..."),
                    GuideSection::new("Commands", GuideSectionKind::Commands)
                        .entry("doctor", "Run diagnostics checks"),
                ],
                ..GuideView::default()
            },
            vec![
                "OSP".to_string(),
                "Usage".to_string(),
                "Commands".to_string(),
            ],
            Some(vec!["[INVOCATION_OPTIONS] COMMAND [ARGS]...".to_string()]),
            Some(vec!["doctor".to_string()]),
            true,
        ),
        (
            GuideView {
                sections: vec![
                    GuideSection::new("Usage", GuideSectionKind::Usage)
                        .paragraph("[INVOCATION_OPTIONS] COMMAND [ARGS]..."),
                    GuideSection::new("Commands", GuideSectionKind::Commands)
                        .entry("help", "Show this command overview."),
                ],
                ..GuideView::default()
            },
            vec![],
            Some(vec!["[INVOCATION_OPTIONS] COMMAND [ARGS]...".to_string()]),
            Some(vec!["help".to_string()]),
            false,
        ),
    ];

    for (view, expected_sections, expected_usage, expected_commands, ordered_sections) in cases {
        let output = view.to_output_result();
        assert!(matches!(
            output.document,
            Some(OutputDocument {
                kind: OutputDocumentKind::Guide,
                value: Value::Object(_),
            })
        ));

        let rebuilt = GuideView::try_from_output_result(&output).expect("guide output");
        assert_eq!(
            rebuilt
                .sections
                .iter()
                .map(|section| section.title.clone())
                .collect::<Vec<_>>(),
            expected_sections
        );
        if let Some(expected_usage) = expected_usage {
            assert_eq!(rebuilt.usage, expected_usage);
        }
        if let Some(expected_commands) = expected_commands {
            assert_eq!(
                rebuilt
                    .commands
                    .iter()
                    .map(|entry| entry.name.clone())
                    .collect::<Vec<_>>(),
                expected_commands
            );
        }

        let json = rebuilt.to_json_value();
        if ordered_sections {
            assert!(json.get("usage").is_none());
            assert!(json.get("commands").is_none());
            assert_eq!(
                json["sections"]
                    .as_array()
                    .expect("ordered sections array")
                    .iter()
                    .map(|section| section["title"].as_str().unwrap_or_default().to_string())
                    .collect::<Vec<_>>(),
                expected_sections
            );
        } else {
            assert_eq!(json["usage"][0], rebuilt.usage[0]);
            assert_eq!(json["commands"][0]["name"], rebuilt.commands[0].name);
        }
    }
}

#[test]
fn guide_restore_prefers_document_and_accepts_legacy_row_shapes_unit() {
    let invalid_document_output = OutputResult {
        items: OutputItems::Rows(vec![
            json!({"commands": [{"name": "list"}]})
                .as_object()
                .cloned()
                .expect("object"),
        ]),
        document: Some(OutputDocument::new(
            OutputDocumentKind::Guide,
            json!([{"commands": [{"name": "list"}]}]),
        )),
        meta: Default::default(),
    };
    assert!(GuideView::try_from_output_result(&invalid_document_output).is_none());

    let legacy_summary_output = OutputResult::from_rows(vec![
        json!({
            "commands": [
                {
                    "name": "list",
                    "summary": "Show"
                }
            ]
        })
        .as_object()
        .cloned()
        .expect("object"),
    ]);
    let rebuilt = GuideView::try_from_output_result(&legacy_summary_output).expect("guide output");
    assert_eq!(rebuilt.commands[0].name, "list");
    assert_eq!(rebuilt.commands[0].short_help, "Show");
}

#[test]
fn guide_markdown_surfaces_sections_and_bounds_entry_rows_unit() {
    let view = GuideView {
        usage: vec!["history <COMMAND>".to_string()],
        commands: vec![
            GuideEntry {
                name: "list".to_string(),
                short_help: "List history entries".to_string(),
                display_indent: None,
                display_gap: None,
            },
            GuideEntry {
                name: "plugins".to_string(),
                short_help: "Inspect and manage plugin providers".to_string(),
                display_indent: None,
                display_gap: None,
            },
            GuideEntry {
                name: "options".to_string(),
                short_help: "per invocation: --format/--json/--table/--value/--md, --mode, --color, --unicode/--ascii, -v/-q/-d, --cache, --plugin-provider".to_string(),
                display_indent: None,
                display_gap: None,
            },
        ],
        options: vec![GuideEntry {
            name: "-h, --help".to_string(),
            short_help: "Print help".to_string(),
            display_indent: None,
            display_gap: None,
        }],
        ..GuideView::default()
    };

    let default_markdown = view.to_markdown();
    assert!(default_markdown.contains("## Usage"));
    assert!(default_markdown.contains("history <COMMAND>"));
    assert!(default_markdown.contains("## Commands"));
    assert!(default_markdown.contains("- `list` List history entries"));
    assert!(default_markdown.contains("## Options"));
    assert!(default_markdown.contains("- `-h, --help` Print help"));
    assert!(!default_markdown.contains("| name"));

    let bounded_markdown = view.to_markdown_with_width(Some(90));
    let lines = bounded_markdown.lines().collect::<Vec<_>>();
    assert!(
        lines.iter().any(|line| line.contains("- `plugins` ")),
        "expected plugins row in:\n{bounded_markdown}"
    );
    assert!(
        lines.iter().any(|line| line.contains("- `options` ")),
        "expected options row in:\n{bounded_markdown}"
    );
}

#[test]
fn guide_value_lines_use_semantic_content_and_preserve_object_value_order_unit() {
    let cases = vec![
        (
            GuideView {
                sections: vec![GuideSection {
                    title: "Commands".to_string(),
                    kind: GuideSectionKind::Commands,
                    paragraphs: Vec::new(),
                    entries: vec![GuideEntry {
                        name: "config".to_string(),
                        short_help: "Inspect and edit runtime config".to_string(),
                        display_indent: None,
                        display_gap: None,
                    }],
                    data: None,
                }],
                ..GuideView::default()
            },
            vec!["Inspect and edit runtime config".to_string()],
        ),
        (
            GuideView {
                sections: vec![GuideSection {
                    title: "Session".to_string(),
                    kind: GuideSectionKind::Custom,
                    paragraphs: Vec::new(),
                    entries: Vec::new(),
                    data: Some(json!({
                        "logged_in_as": "oistes",
                        "theme": "rose-pine-moon",
                        "version": "1.4.9"
                    })),
                }],
                ..GuideView::default()
            },
            vec![
                "oistes".to_string(),
                "rose-pine-moon".to_string(),
                "1.4.9".to_string(),
            ],
        ),
    ];

    for (view, expected) in cases {
        assert_eq!(view.to_value_lines(), expected);
    }
}