tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! The startup tips (tix #1632): the embedded tip list, the `tips_on_startup`
//! setting, and what `tix init` generates.

use std::fs;

use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;

use tix::config::Config;

fn tix() -> Command {
    Command::cargo_bin("tix").unwrap()
}

fn read_config(dir: &std::path::Path) -> Config {
    Config::load_with_warning(&dir.join("tix.toml"), |_| {}).unwrap()
}

/// `tix init` writes the key so the setting is discoverable where the rest of the
/// configuration lives, and the file it writes loads back with tips on.
#[test]
fn init_writes_tips_on_startup_true() {
    let tmp = TempDir::new().unwrap();
    let dir = tmp.path();
    tix().current_dir(dir).arg("init").assert().success();

    let raw = fs::read_to_string(dir.join("tix.toml")).unwrap();
    assert!(
        raw.contains("tips_on_startup = true"),
        "the generated file must name the setting:\n{raw}"
    );
    // It must land before the first table header: a bare key after a table is read
    // as a member of that table, not as a top-level setting.
    let key = raw.find("tips_on_startup = true").unwrap();
    let first_table = raw.find("\n[").unwrap();
    assert!(
        key < first_table,
        "the setting is a top-level key, not a table member:\n{raw}"
    );

    assert!(
        read_config(dir).tips_on_startup,
        "the generated file must load with tips on"
    );
}

/// A `tix.toml` that never mentions the setting keeps the default (on) and gains
/// no key on save, so an existing repository's file is never rewritten because of
/// this feature.
#[test]
fn a_legacy_config_defaults_to_on_and_gains_no_key_on_save() {
    let tmp = TempDir::new().unwrap();
    let dir = tmp.path();
    let legacy = "issues_dir = \"docs/issues\"\n\n\
        [kanban.legacy]\n\
        columns = [\n    \"backlog\",\n    \"doing\",\n    \"review\",\n    \"shipped\",\n]\n\
        done_column = \"shipped\"\n";
    fs::write(dir.join("tix.toml"), legacy).unwrap();

    let config = read_config(dir);
    assert!(config.tips_on_startup, "tips are on when the key is absent");
    config.save(&dir.join("tix.toml")).unwrap();
    assert_eq!(
        fs::read_to_string(dir.join("tix.toml")).unwrap(),
        legacy,
        "an absent key must not be written back"
    );
}

/// Turning the tips off is a real, persisted setting: `false` is written and read
/// back, which is what the modal's `d` key relies on.
#[test]
fn an_explicit_false_persists_and_reads_back() {
    let tmp = TempDir::new().unwrap();
    let dir = tmp.path();
    tix().current_dir(dir).arg("init").assert().success();

    let mut config = read_config(dir);
    config.tips_on_startup = false;
    config.save(&dir.join("tix.toml")).unwrap();

    let raw = fs::read_to_string(dir.join("tix.toml")).unwrap();
    assert!(
        raw.contains("tips_on_startup = false"),
        "the off state must be written:\n{raw}"
    );
    assert!(!read_config(dir).tips_on_startup);
}

/// The embedded list is the one the modal shows, and it is substantial.
#[test]
fn the_embedded_tips_are_categorized_and_plentiful() {
    let tips = tix::tips::all();
    assert!(
        tips.len() >= 100,
        "tix #1632 asks for at least 100 tips, found {}",
        tips.len()
    );
    let categories: std::collections::BTreeSet<&str> =
        tips.iter().map(|tip| tip.category.as_str()).collect();
    assert!(
        categories.len() >= 10,
        "the tips must be categorized, found {}",
        categories.len()
    );
}

/// Outside a repository the bare `tix` launch still fails with the contract's
/// message when it cannot ask (a piped stdin cannot consent), so a script never
/// gets a repository written into its working directory.
#[test]
fn a_launch_that_cannot_be_asked_keeps_the_contract_error() {
    let tmp = TempDir::new().unwrap();
    let dir = tmp.path();
    tix()
        .current_dir(dir)
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::str::contains(
            "no tix.toml found — run \"tix init\" first",
        ));
    assert!(
        !dir.join("tix.toml").exists(),
        "no configuration may be created without consent"
    );
}