use assert_cmd::Command;
use predicates::prelude::*;
use rototo::docs::{DOC_NAV_SECTIONS, DOCS};
#[test]
fn prints_version() {
Command::cargo_bin("rototo")
.unwrap()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("rototo 0.1.0-alpha.2"));
}
#[test]
fn top_level_help_teaches_docs_entrypoint() {
Command::cargo_bin("rototo")
.unwrap()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"rototo is a control plane for runtime configuration in application code",
))
.stdout(predicate::str::contains("Start:"))
.stdout(predicate::str::contains("rototo docs"))
.stdout(predicate::str::contains(
"List bundled docs. Use this when you are learning rototo.",
))
.stdout(predicate::str::contains("rototo docs -p quickstart"))
.stdout(predicate::str::contains(
"rototo docs -s 'refresh|last-known-good'",
))
.stdout(predicate::str::contains("Core Commands:"))
.stdout(predicate::str::contains(
"rototo inspect <workspace> [-v <variable> ...] [-q <qualifier> ...]",
))
.stdout(predicate::str::contains(
"rototo resolve <workspace> [-v <variable> ...] [-q <qualifier> ...] --env <env> -c <context>",
))
.stdout(predicate::str::contains("Targets:"))
.stdout(predicate::str::contains(
"-q, --qualifier <id> Target a qualifier.",
))
.stdout(predicate::str::contains(
"-c, --context <input> JSON object, @file, or path=value. Repeatable.",
))
.stdout(predicate::str::contains(
"rototo docs -p source-uri-reference",
));
}
#[test]
fn top_level_help_shows_verb_commands() {
Command::cargo_bin("rototo")
.unwrap()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("inspect"))
.stdout(predicate::str::contains("lint"))
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("get"))
.stdout(predicate::str::contains("resolve"))
.stdout(predicate::str::contains("-q, --quiet").not())
.stdout(predicate::str::contains("--quiet"))
.stdout(predicate::str::contains("[aliases: ws]").not())
.stdout(predicate::str::contains("[aliases: qual]").not())
.stdout(predicate::str::contains("[aliases: var]").not())
.stdout(predicate::str::contains("[aliases: diag]").not())
.stdout(predicate::str::contains("[aliases: comp]").not())
.stdout(predicate::str::contains("rototo diagnostics"))
.stdout(predicate::str::contains("rototo completions"));
}
#[test]
fn top_level_help_uses_terminal_palette_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"\u{1b}[1;38;2;227;223;216mUsage\u{1b}[0m:",
))
.stdout(predicate::str::contains(
"\u{1b}[1;38;2;227;223;216mCore Commands\u{1b}[0m:",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230minspect\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;144;139;132m[\u{1b}[0mOPTIONS\u{1b}[38;2;144;139;132m]\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;144;139;132m<COMMAND>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230m<variable>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;232;135;97m<qualifier>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;97;203;124m<env>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;144;139;132m<context>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[1;38;2;227;223;216mTargets\u{1b}[0m:",
))
.stdout(
predicate::str::contains(
"to \u{1b}[38;2;94;182;230mlist\u{1b}[0m the bundled documentation",
)
.not(),
)
.stdout(predicate::str::contains("Print \u{1b}[38;2;94;182;230mhelp\u{1b}[0m").not());
}
#[test]
fn subcommand_help_uses_terminal_palette_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.args(["inspect", "--help"])
.assert()
.success()
.stdout(predicate::str::contains(
"\u{1b}[1;38;2;227;223;216mArguments\u{1b}[0m:",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230m--variable\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230m<ID>\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;232;135;97m<ID>\u{1b}[0m",
));
}
#[test]
fn lint_and_resolve_help_show_verbose_flag() {
Command::cargo_bin("rototo")
.unwrap()
.args(["lint", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--verbose"))
.stdout(predicate::str::contains(
"Show workspace loading and lint target processing",
));
Command::cargo_bin("rototo")
.unwrap()
.args(["resolve", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--verbose"))
.stdout(predicate::str::contains(
"Show env fallback, rule order, qualifier predicates, bucket math, and selected values",
));
}
#[test]
fn no_color_overrides_forced_color_in_help() {
Command::cargo_bin("rototo")
.unwrap()
.env("NO_COLOR", "1")
.env("CLICOLOR_FORCE", "1")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"Usage: rototo [OPTIONS] <COMMAND>",
))
.stdout(predicate::str::contains("\u{1b}[").not());
}
#[test]
fn public_command_aliases_still_execute() {
Command::cargo_bin("rototo")
.unwrap()
.args(["diag", "list"])
.assert()
.success()
.stdout(predicate::str::contains(
"rototo/workspace-toml-file-parse-failed",
));
Command::cargo_bin("rototo")
.unwrap()
.args(["comp", "zsh"])
.assert()
.success()
.stdout(predicate::str::contains("#compdef rototo"));
}
#[test]
fn legacy_noun_commands_are_removed() {
for command in ["workspace", "qualifier", "variable"] {
Command::cargo_bin("rototo")
.unwrap()
.arg(command)
.assert()
.failure()
.stderr(predicate::str::contains(format!(
"unrecognized subcommand '{command}'"
)));
}
}
#[test]
fn verb_commands_execute_with_entity_flags() {
Command::cargo_bin("rototo")
.unwrap()
.args(["inspect", "examples/basic"])
.assert()
.success()
.stdout(predicate::str::contains("qualifier usage:"));
Command::cargo_bin("rototo")
.unwrap()
.args(["list", "examples/basic", "-v"])
.assert()
.success()
.stdout(predicate::str::contains("checkout-redesign"))
.stdout(predicate::str::contains("premium-users").not());
Command::cargo_bin("rototo")
.unwrap()
.args(["get", "examples/basic", "-q", "premium-users"])
.assert()
.success()
.stdout(predicate::str::contains("attribute = \"user.tier\""));
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-v",
"checkout-redesign",
"-q",
"premium-users",
"--env",
"prod",
"-c",
"user.tier=premium",
"--json",
])
.assert()
.success()
.stdout(predicate::str::contains(r#""id": "checkout-redesign""#))
.stdout(predicate::str::contains(r#""id": "premium-users""#))
.stdout(predicate::str::contains(r#""value_key": "premium""#));
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-q",
"premium-users",
"-c",
"user.tier=premium",
])
.assert()
.success()
.stdout(predicate::str::contains("premium-users=true"));
}
#[test]
fn lint_verbose_prints_processing_trace() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"lint",
"examples/basic",
"-v",
"checkout-redesign",
"--verbose",
])
.assert()
.success()
.stdout(predicate::str::contains("lint trace:"))
.stdout(predicate::str::contains("source: examples/basic"))
.stdout(predicate::str::contains(
"staged workspace: examples/basic (local)",
))
.stdout(predicate::str::contains("targets:"))
.stdout(predicate::str::contains("variable checkout-redesign"))
.stdout(predicate::str::contains(
"variable checkout-redesign: ok (0 diagnostics)",
))
.stdout(predicate::str::contains("ok: variable://checkout-redesign"));
}
#[test]
fn resolve_verbose_prints_processing_trace() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-v",
"checkout-redesign",
"-q",
"premium-users",
"--env",
"prod",
"-c",
"user.tier=premium",
"--verbose",
])
.assert()
.success()
.stdout(predicate::str::contains("resolve trace:"))
.stdout(predicate::str::contains("source: examples/basic"))
.stdout(predicate::str::contains("targets:"))
.stdout(predicate::str::contains("variable checkout-redesign"))
.stdout(predicate::str::contains("qualifier premium-users"))
.stdout(predicate::str::contains("environment: prod"))
.stdout(predicate::str::contains("context inputs:"))
.stdout(predicate::str::contains("user.tier=premium"))
.stdout(predicate::str::contains("merged context:"))
.stdout(predicate::str::contains(r#""tier": "premium""#))
.stdout(predicate::str::contains("variable checkout-redesign:"))
.stdout(predicate::str::contains("requested env: prod"))
.stdout(predicate::str::contains("env block: prod"))
.stdout(predicate::str::contains(
"rule 1: premium-users -> true, selects premium",
))
.stdout(predicate::str::contains(
"definition: Premium users get the premium checkout",
))
.stdout(predicate::str::contains(
"definition: Users on the premium plan",
))
.stdout(predicate::str::contains(
r#"predicate 1 user.tier eq "premium" -> true"#,
))
.stdout(predicate::str::contains(r#"context value: "premium""#))
.stdout(predicate::str::contains(
"selected: premium (first matching rule: rule 1 premium-users)",
))
.stdout(predicate::str::contains("qualifier premium-users:"))
.stdout(predicate::str::contains("result: true"))
.stdout(predicate::str::contains("premium-users=true"));
}
#[test]
fn resolve_verbose_shows_default_when_no_rule_matches() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-v",
"checkout-redesign",
"--env",
"prod",
"-c",
"user.tier=standard",
"--verbose",
])
.assert()
.success()
.stdout(predicate::str::contains(
"rule 1: premium-users -> false, skipped",
))
.stdout(predicate::str::contains(r#"context value: "standard""#))
.stdout(predicate::str::contains("default: control"))
.stdout(predicate::str::contains(
"selected: control (environment default)",
))
.stdout(predicate::str::contains("(control)"));
}
#[test]
fn resolve_verbose_shows_bucket_predicate_homework() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-q",
"beta-rollout-bucket",
"-c",
"user.id=agent-42",
"--verbose",
])
.assert()
.success()
.stdout(predicate::str::contains("qualifier beta-rollout-bucket:"))
.stdout(predicate::str::contains(
"user.id bucket salt checkout-redesign-2026-05 range [0,1000)",
))
.stdout(predicate::str::contains(r#"context value: "agent-42""#))
.stdout(predicate::str::contains("bucket:"));
}
#[test]
fn verbose_is_suppressed_for_json_output() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"resolve",
"examples/basic",
"-q",
"premium-users",
"-c",
"user.tier=premium",
"--verbose",
"--json",
])
.assert()
.success()
.stdout(predicate::str::contains(r#""qualifiers": ["#))
.stdout(predicate::str::contains("resolve trace:").not());
}
#[test]
fn quiet_suppresses_successful_lint_output() {
Command::cargo_bin("rototo")
.unwrap()
.args(["--quiet", "lint", "examples/basic"])
.assert()
.success()
.stdout(predicate::eq(""));
}
#[test]
fn quiet_keeps_lint_diagnostics() {
Command::cargo_bin("rototo")
.unwrap()
.args([
"--quiet",
"lint",
"tests/fixtures/workspaces/missing-environments",
])
.assert()
.failure()
.stdout(predicate::str::contains(
"error[rototo/workspace-manifest-schema-failed]",
));
}
#[test]
fn styles_human_output_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.args(["lint", "examples/basic"])
.assert()
.success()
.stdout(predicate::str::contains(
"\u{1b}[38;2;97;203;124mok\u{1b}[0m:",
));
}
#[test]
fn inspection_uses_terminal_palette_hierarchy_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.args(["inspect", "examples/basic"])
.assert()
.success()
.stdout(predicate::str::contains(
"\u{1b}[38;2;227;223;216mqualifier relationships\u{1b}[0m:",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;97;203;124menvironment\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230mvariable\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;232;135;97mqualifier\u{1b}[0m",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;144;139;132mcontext\u{1b}[0m",
))
.stdout(predicate::str::contains("\u{1b}[38;2;97;203;124mprod"))
.stdout(predicate::str::contains(
"\u{1b}[38;2;94;182;230mcheckout-redesign",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;232;135;97mpremium-users",
))
.stdout(predicate::str::contains(
"\u{1b}[38;2;144;139;132muser.tier",
));
}
#[test]
fn no_color_overrides_forced_color() {
Command::cargo_bin("rototo")
.unwrap()
.env("NO_COLOR", "1")
.env("CLICOLOR_FORCE", "1")
.args(["lint", "examples/basic"])
.assert()
.success()
.stdout(predicate::str::contains("ok: "))
.stdout(predicate::str::contains("\u{1b}[").not());
}
#[test]
fn json_output_is_not_styled_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.args(["lint", "examples/basic", "--json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""diagnostics": []"#))
.stdout(predicate::str::contains("\u{1b}[").not());
}
#[test]
fn generates_zsh_completions() {
Command::cargo_bin("rototo")
.unwrap()
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicate::str::contains("#compdef rototo"));
}
#[test]
fn lists_bundled_docs() {
let assert = Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "160")
.arg("docs")
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("Page") && stdout.contains("Title") && stdout.contains(" | "),
"docs command should render as a Page/Title table"
);
assert!(stdout.contains("Use `rototo docs -p <page-prefix>`"));
assert!(stdout.contains("prefixes work"));
assert!(stdout.contains("Use `rototo docs -s <regex> [regex ...]`"));
assert!(stdout.contains("rototo docs -s 'refresh|last-known-good'"));
for section in DOC_NAV_SECTIONS {
assert!(
stdout.contains(section.title),
"docs list did not include section `{}`",
section.title
);
}
for page in DOCS {
assert!(
stdout.contains(&docs_list_page_label(page.id)),
"docs list did not include page label for `{}`",
page.id
);
assert!(
stdout.contains(page.list_title),
"docs list did not include descriptive title for `{}`",
page.id
);
}
}
#[test]
fn docs_list_truncates_page_ids_and_wraps_titles_to_columns() {
let assert = Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "72")
.arg("docs")
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(stdout.contains(" how-to-add-a-new-runtime..."));
assert!(!stdout.contains("how-to-add-a-new-runtime-config-value"));
for line in stdout.lines() {
assert!(
line.len() <= 72,
"docs list line exceeded COLUMNS=72: `{line}`"
);
}
}
#[test]
fn docs_list_styles_table_columns_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.arg("docs")
.assert()
.success()
.stdout(predicate::str::contains("\u{1b}[1;38;2;94;182;230mPage"))
.stdout(predicate::str::contains("\u{1b}[1;38;2;227;223;216mTitle"))
.stdout(predicate::str::contains("\u{1b}[38;2;232;135;97mStart"))
.stdout(predicate::str::contains("\u{1b}[1;38;2;232;135;97mStart").not());
}
#[test]
fn pretty_prints_bundled_docs_page() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "--page", "cli-reference"])
.assert()
.success()
.stdout(predicate::str::contains(
"rototo CLI reference\n====================",
))
.stdout(predicate::str::contains("Global Flags\n------------"))
.stdout(predicate::str::contains(
"- `--json`: emit machine-readable JSON",
))
.stdout(predicate::str::contains("# rototo CLI reference").not());
}
#[test]
fn shows_bundled_docs_with_short_page_flag() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "-p", "cli-reference"])
.assert()
.success()
.stdout(predicate::str::contains(
"rototo CLI reference\n====================",
));
}
#[test]
fn shows_best_matching_bundled_doc() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "--page", "quick"])
.assert()
.success()
.stdout(predicate::str::contains("Quickstart\n=========="));
}
#[test]
fn docs_page_renders_bundled_links_as_cli_commands() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "160")
.args(["docs", "--page", "why-rototo"])
.assert()
.success()
.stdout(predicate::str::contains(
"Read Quickstart (rototo docs -p quickstart)",
))
.stdout(predicate::str::contains(
"Read Production Workflow (rototo docs -p production-workflow)",
))
.stdout(predicate::str::contains("(quickstart.html)").not());
}
#[test]
fn docs_show_lists_options_for_ambiguous_page_query() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "100")
.args(["docs", "--page", "how-to-add"])
.assert()
.failure()
.stderr(predicate::str::contains(
"documentation page `how-to-add` matched multiple pages",
))
.stderr(predicate::str::contains(
"Which documentation page did you want?",
))
.stderr(predicate::str::contains(" how-to-add-a-new-runtime..."))
.stderr(predicate::str::contains(" how-to-add-a-new-context..."));
}
#[test]
fn docs_show_lists_options_for_unknown_page_query() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "120")
.args(["docs", "--page", "missing-page"])
.assert()
.failure()
.stderr(predicate::str::contains(
"no documentation page matched `missing-page`",
))
.stderr(predicate::str::contains(
"Which documentation page did you want?",
))
.stderr(predicate::str::contains("quickstart"));
}
#[test]
fn docs_search_finds_page_ids_with_short_flag() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "120")
.args(["docs", "-s", "source-uri-reference"])
.assert()
.success()
.stdout(predicate::str::contains(
"source-uri-reference: Reference: local, file, Git",
))
.stdout(predicate::str::contains(" page id: source-uri-reference"))
.stdout(predicate::str::contains("Page | Title").not());
}
#[test]
fn docs_search_finds_content_with_long_flag() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "120")
.args(["docs", "--search", "last-known-good"])
.assert()
.success()
.stdout(predicate::str::contains(
"how-to-keep-config-fresh-in-a-running-app: Keep config fresh:",
))
.stdout(predicate::str::contains(
" description: Keep config fresh:",
))
.stdout(predicate::str::contains("last-known-good"));
}
#[test]
fn docs_search_applies_all_regex_terms() {
Command::cargo_bin("rototo")
.unwrap()
.env("COLUMNS", "120")
.args(["docs", "-s", "refresh", "last-known-good"])
.assert()
.success()
.stdout(predicate::str::contains(
"how-to-keep-config-fresh-in-a-running-app: Keep config fresh:",
))
.stdout(predicate::str::contains(
" description: Keep config fresh:",
))
.stdout(predicate::str::contains("quickstart").not());
}
#[test]
fn docs_search_highlights_matching_regex_terms_when_color_is_forced() {
Command::cargo_bin("rototo")
.unwrap()
.env_remove("NO_COLOR")
.env("CLICOLOR_FORCE", "1")
.env("COLUMNS", "120")
.args(["docs", "-s", "refresh"])
.assert()
.success()
.stdout(predicate::str::contains(
"\u{1b}[1;38;2;97;203;124mrefresh\u{1b}[0m",
))
.stdout(predicate::str::contains("Page | Title").not());
}
#[test]
fn docs_search_rejects_invalid_regex() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "--search", "["])
.assert()
.failure()
.stderr(predicate::str::contains("invalid docs search regex"));
}
#[test]
fn docs_rejects_page_and_search_together() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "-p", "cli-reference", "-s", "sdk"])
.assert()
.failure()
.stderr(predicate::str::contains(
"accepts either --page or -s/--search",
));
}
#[test]
fn rejects_removed_bundled_docs_subcommands() {
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "show", "cli", "--format", "html"])
.assert()
.failure();
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "export", "--out", "site"])
.assert()
.failure();
Command::cargo_bin("rototo")
.unwrap()
.args(["docs", "serve"])
.assert()
.failure();
}
fn docs_list_page_label(id: &str) -> String {
let label = format!(" {id}");
if label.len() <= 30 {
label
} else {
let mut label = label[..27].to_owned();
while label.ends_with('-') {
label.pop();
}
format!("{label}...")
}
}