tokf 0.2.33

Config-driven CLI tool that compresses command output before it reaches an LLM context
Documentation
// Types live in tokf-common; re-export for backward compatibility.
pub use tokf_common::config::types::*;

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::literal_string_with_formatting_args
)]
mod tests {
    use super::*;

    fn load_filter(name: &str) -> FilterConfig {
        let path = format!("{}/filters/{name}", env!("CARGO_MANIFEST_DIR"));
        let content = std::fs::read_to_string(&path).unwrap();
        toml::from_str(&content).unwrap()
    }

    // --- CommandPattern deserialization ---

    #[test]
    fn test_command_pattern_single() {
        let cfg: FilterConfig = toml::from_str(r#"command = "git push""#).unwrap();
        assert_eq!(cfg.command, CommandPattern::Single("git push".to_string()));
        assert_eq!(cfg.command.first(), "git push");
        assert_eq!(cfg.command.patterns(), &["git push".to_string()]);
    }

    #[test]
    fn test_command_pattern_multiple() {
        let cfg: FilterConfig = toml::from_str(r#"command = ["pnpm test", "npm test"]"#).unwrap();
        assert_eq!(
            cfg.command,
            CommandPattern::Multiple(vec!["pnpm test".to_string(), "npm test".to_string()])
        );
        assert_eq!(cfg.command.first(), "pnpm test");
        assert_eq!(
            cfg.command.patterns(),
            &["pnpm test".to_string(), "npm test".to_string()]
        );
    }

    #[test]
    fn test_command_pattern_wildcard() {
        let cfg: FilterConfig = toml::from_str(r#"command = "npm run *""#).unwrap();
        assert_eq!(cfg.command.first(), "npm run *");
    }

    // --- Stdlib filter deserialization ---

    #[test]
    fn test_deserialize_git_push() {
        let cfg = load_filter("git/push.toml");

        assert_eq!(cfg.command.first(), "git push");
        assert_eq!(cfg.match_output.len(), 2);
        assert_eq!(
            cfg.match_output[0].contains.as_deref().unwrap(),
            "Everything up-to-date"
        );
        assert_eq!(
            cfg.match_output[1].contains.as_deref().unwrap(),
            "non-fast-forward"
        );

        let success = cfg.on_success.unwrap();
        assert_eq!(success.skip.len(), 8);
        assert!(success.skip[0].starts_with("^Enumerating"));

        let extract = success.extract.unwrap();
        assert!(extract.pattern.contains("->"));
        assert_eq!(extract.output, "ok \u{2713} {2}");

        let failure = cfg.on_failure.unwrap();
        assert_eq!(failure.skip.len(), 4);
        assert_eq!(failure.tail, Some(10));
    }

    #[test]
    fn test_deserialize_git_status() {
        let cfg = load_filter("git/status.toml");

        assert_eq!(cfg.command.first(), "git status");
        let run = cfg.run.unwrap();
        assert_eq!(run, "git status --porcelain -b");

        // match_output: not-a-git-repo case
        assert_eq!(cfg.match_output.len(), 1);
        assert_eq!(
            cfg.match_output[0].contains.as_deref().unwrap(),
            "not a git repository"
        );
        assert_eq!(cfg.match_output[0].output, "Not a git repository");

        // replace rules: branch line transformations
        assert_eq!(cfg.replace.len(), 3);

        // Detached HEAD rule
        assert_eq!(cfg.replace[0].pattern, "^## HEAD \\(no branch\\).*$");
        assert_eq!(cfg.replace[0].output, "HEAD (detached)");

        // Branch with ahead/behind info
        assert!(cfg.replace[1].pattern.contains("\\["));
        assert_eq!(cfg.replace[1].output, "{1} [{2}]");

        // Plain branch (no ahead/behind)
        assert!(cfg.replace[2].pattern.contains("(?:"));
        assert_eq!(cfg.replace[2].output, "{1}");

        // history hint is off — files are shown directly
        assert!(!cfg.show_history_hint);

        // no parse or output sections
        assert!(cfg.parse.is_none());
        assert!(cfg.output.is_none());
    }

    #[test]
    fn test_deserialize_parse_config_from_toml() {
        // ParseConfig and OutputConfig have no stdlib filter using them anymore;
        // verify their TOML deserialization paths still work correctly.
        let cfg: FilterConfig = toml::from_str(
            r#"
command = "legacy cmd"

[parse]
branch = { line = 1, pattern = '^## (\S+)', output = "{1}" }

[parse.group]
key = { pattern = '^(.{2}) ', output = "{1}" }

[output]
format = "{branch}\n{group_counts}"
group_counts_format = "  {label}: {count}"
empty = "nothing to show"
"#,
        )
        .unwrap();

        let parse = cfg.parse.unwrap();
        let branch = parse.branch.unwrap();
        assert_eq!(branch.line, 1);
        assert_eq!(branch.output, "{1}");

        let group = parse.group.unwrap();
        assert_eq!(group.key.output, "{1}");

        let output = cfg.output.unwrap();
        assert!(output.format.unwrap().contains("{branch}"));
        assert_eq!(
            output.group_counts_format.as_deref(),
            Some("  {label}: {count}")
        );
        assert_eq!(output.empty.as_deref(), Some("nothing to show"));
    }

    #[test]
    fn test_deserialize_cargo_test() {
        let cfg = load_filter("cargo/test.toml");

        assert_eq!(cfg.command.first(), "cargo test");
        assert!(!cfg.skip.is_empty());
        assert!(cfg.skip.iter().any(|s| s.contains("Compiling")));

        assert_eq!(cfg.section.len(), 3);
        assert_eq!(cfg.section[0].name.as_deref(), Some("failures"));
        assert_eq!(cfg.section[0].collect_as.as_deref(), Some("failure_blocks"));
        assert_eq!(cfg.section[1].name.as_deref(), Some("failure_names"));
        assert_eq!(cfg.section[2].name.as_deref(), Some("summary"));

        let success = cfg.on_success.unwrap();
        assert!(success.aggregate.is_none(), "singular aggregate removed");
        assert_eq!(success.aggregates.len(), 3);
        assert_eq!(success.aggregates[0].from, "summary_lines");
        assert_eq!(success.aggregates[0].sum.as_deref(), Some("passed"));
        assert_eq!(success.aggregates[0].count_as.as_deref(), Some("suites"));
        assert!(success.output.unwrap().contains("{passed}"));

        assert!(!cfg.chunk.is_empty(), "chunk config present");
        assert_eq!(cfg.chunk[0].collect_as, "suites_detail");

        let failure = cfg.on_failure.unwrap();
        assert!(failure.output.unwrap().contains("FAILURES"));

        let fallback = cfg.fallback.unwrap();
        assert_eq!(fallback.tail, Some(5));
    }

    #[test]
    fn test_deserialize_git_add() {
        let cfg = load_filter("git/add.toml");

        assert_eq!(cfg.command.first(), "git add");
        assert_eq!(cfg.match_output.len(), 1);
        assert_eq!(cfg.match_output[0].contains.as_deref().unwrap(), "fatal:");

        let success = cfg.on_success.unwrap();
        assert_eq!(success.output.as_deref(), Some("ok \u{2713}"));

        let failure = cfg.on_failure.unwrap();
        assert_eq!(failure.tail, Some(5));
    }

    #[test]
    fn test_deserialize_git_commit() {
        let cfg = load_filter("git/commit.toml");

        assert_eq!(cfg.command.first(), "git commit");

        let success = cfg.on_success.unwrap();
        let extract = success.extract.unwrap();
        assert!(extract.pattern.contains("\\w+"));
        assert_eq!(extract.output, "ok \u{2713} {2}");

        let failure = cfg.on_failure.unwrap();
        assert_eq!(failure.tail, Some(80));
    }

    #[test]
    fn test_deserialize_git_log() {
        let cfg = load_filter("git/log.toml");

        assert_eq!(cfg.command.first(), "git log");

        let run = cfg.run.unwrap();
        assert!(run.contains("{args}"));
        assert!(run.contains("--oneline"));

        let success = cfg.on_success.unwrap();
        assert_eq!(success.output.as_deref(), Some("{output}"));
    }

    #[test]
    fn test_deserialize_git_diff() {
        let cfg = load_filter("git/diff.toml");

        assert_eq!(cfg.command.first(), "git diff");

        let run = cfg.run.unwrap();
        assert!(run.contains("--stat"));
        assert!(run.contains("{args}"));

        assert_eq!(cfg.match_output.len(), 1);
        assert_eq!(cfg.match_output[0].contains.as_deref().unwrap(), "fatal:");

        let success = cfg.on_success.unwrap();
        assert_eq!(success.output.as_deref(), Some("{output}"));

        let failure = cfg.on_failure.unwrap();
        assert_eq!(failure.tail, Some(5));
    }

    // --- Minimal / defaults ---

    #[test]
    fn test_minimal_config_only_command() {
        let cfg: FilterConfig = toml::from_str(r#"command = "echo""#).unwrap();

        assert_eq!(cfg.command.first(), "echo");
        assert_eq!(cfg.run, None);
        assert!(cfg.skip.is_empty());
        assert!(cfg.keep.is_empty());
        assert!(cfg.step.is_empty());
        assert_eq!(cfg.extract, None);
        assert!(cfg.match_output.is_empty());
        assert!(cfg.section.is_empty());
        assert_eq!(cfg.on_success, None);
        assert_eq!(cfg.on_failure, None);
        assert_eq!(cfg.parse, None);
        assert_eq!(cfg.output, None);
        assert_eq!(cfg.fallback, None);
        assert!(cfg.replace.is_empty());
        assert!(!cfg.dedup);
        assert_eq!(cfg.dedup_window, None);
        assert!(!cfg.strip_ansi);
        assert!(!cfg.trim_lines);
        assert!(!cfg.strip_empty_lines);
        assert!(!cfg.collapse_empty_lines);
        assert_eq!(cfg.lua_script, None);
        assert!(cfg.variant.is_empty());
    }

    // --- Variant deserialization ---

    #[test]
    fn test_variant_with_file_detection() {
        let cfg: FilterConfig = toml::from_str(
            r#"
command = ["npm test", "pnpm test"]

[[variant]]
name = "vitest"
detect.files = ["vitest.config.ts", "vitest.config.js"]
filter = "npm/test-vitest"
"#,
        )
        .unwrap();

        assert_eq!(cfg.variant.len(), 1);
        assert_eq!(cfg.variant[0].name, "vitest");
        assert_eq!(
            cfg.variant[0].detect.files,
            vec!["vitest.config.ts", "vitest.config.js"]
        );
        assert_eq!(cfg.variant[0].detect.output_pattern, None);
        assert_eq!(cfg.variant[0].filter, "npm/test-vitest");
    }

    #[test]
    fn test_variant_with_output_pattern() {
        let cfg: FilterConfig = toml::from_str(
            r#"
command = "npm test"

[[variant]]
name = "mocha"
detect.output_pattern = "passing|failing|pending"
filter = "npm/test-mocha"
"#,
        )
        .unwrap();

        assert_eq!(cfg.variant.len(), 1);
        assert_eq!(cfg.variant[0].name, "mocha");
        assert!(cfg.variant[0].detect.files.is_empty());
        assert_eq!(
            cfg.variant[0].detect.output_pattern.as_deref(),
            Some("passing|failing|pending")
        );
        assert_eq!(cfg.variant[0].filter, "npm/test-mocha");
    }

    #[test]
    fn test_multiple_variants() {
        let cfg: FilterConfig = toml::from_str(
            r#"
command = "npm test"

[[variant]]
name = "vitest"
detect.files = ["vitest.config.ts"]
filter = "npm/test-vitest"

[[variant]]
name = "jest"
detect.files = ["jest.config.js"]
filter = "npm/test-jest"

[[variant]]
name = "mocha"
detect.output_pattern = "passing|failing"
filter = "npm/test-mocha"
"#,
        )
        .unwrap();

        assert_eq!(cfg.variant.len(), 3);
        assert_eq!(cfg.variant[0].name, "vitest");
        assert_eq!(cfg.variant[1].name, "jest");
        assert_eq!(cfg.variant[2].name, "mocha");
    }

    // --- Negative tests ---

    #[test]
    fn test_missing_command_field_fails() {
        let result: Result<FilterConfig, _> = toml::from_str(r#"run = "echo hello""#);
        assert!(result.is_err());
    }

    #[test]
    fn test_wrong_type_for_skip_fails() {
        let result: Result<FilterConfig, _> = toml::from_str(
            r#"command = "echo"
skip = "not-an-array""#,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_wrong_type_for_tail_fails() {
        let result: Result<FilterConfig, _> = toml::from_str(
            r#"command = "echo"
[on_success]
tail = "five""#,
        );
        assert!(result.is_err());
    }

    #[test]
    fn test_malformed_toml_fails() {
        let result: Result<FilterConfig, _> = toml::from_str("command = [unterminated");
        assert!(result.is_err());
    }

    #[test]
    fn test_empty_toml_fails() {
        let result: Result<FilterConfig, _> = toml::from_str("");
        assert!(result.is_err());
    }
}