face 0.1.0

Fold And Cluster Entries — a Unix-style CLI for grouping, paging, and summarizing structured command output.
Documentation
//! Mode-flag coverage: `--explain`, `--schema`, `--verbose`.
//!
//! Acceptance: docs/design.md §3 MODES, §4.6 (provenance / explain /
//! schema), §11.1 (verbose emits skip warnings + provenance).

mod common;

use common::drive;

mod explain {
    use super::drive;

    /// Acceptance: docs/design.md §4.6 — `--explain` dumps the full
    /// reasoning broken into `input:`, `strategy:`, `output:` sections
    /// without the JSON envelope.
    #[test]
    fn explain_prints_input_strategy_output_sections() {
        let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
        let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        // Soft-match: the developer may write the explain output to
        // stdout (default) or stderr; check both for the section names.
        let combined = format!("{stdout}{stderr}");
        assert!(
            combined.contains("input"),
            "explain has `input` section: {combined:?}",
        );
        assert!(
            combined.contains("strategy"),
            "explain has `strategy` section: {combined:?}",
        );
        assert!(
            combined.contains("output"),
            "explain has `output` section: {combined:?}",
        );
        // Explain mode does not emit the JSON envelope: stdout should
        // not parse as a face envelope. (If the developer chooses to
        // emit explain on stderr and still emit human format on stdout,
        // this assertion still passes — human format starts with
        // `face: …` not `{`.)
        assert!(
            !stdout.trim_start().starts_with("{\"result\""),
            "explain mode does not emit envelope JSON: {stdout:?}",
        );
    }

    /// Acceptance: docs/design.md §4.6 — explain output names the
    /// detected input format, e.g. `jsonl`.
    #[test]
    fn explain_includes_format_jsonl() {
        let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
        let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        let combined = format!("{stdout}{stderr}");
        assert!(
            combined.contains("jsonl"),
            "explain reports the detected jsonl format: {combined:?}",
        );
    }

    /// Acceptance: docs/design.md §4.6 — explain output reports the
    /// detected items path, not a hard-coded `.`.
    #[test]
    fn explain_includes_detected_items_path() {
        let stdin = "{\"items\":[{\"kind\":\"bug\"},{\"kind\":\"feat\"}],\"query\":\"TODO\"}";
        let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        let combined = format!("{stdout}{stderr}");
        assert!(
            combined.contains(".items"),
            "explain reports detected items path: {combined:?}",
        );
    }
}

mod schema {
    use super::drive;

    /// Acceptance: docs/design.md §4.6 — `--schema` describes input
    /// shape (records, fields, items_path) without grouping.
    #[test]
    fn schema_lists_fields_and_records() {
        let stdin = "{\"items\":[{\"kind\":\"bug\",\"score\":0.5}],\"query\":\"TODO\"}";
        let (code, stdout, stderr) = drive(&["face", "--schema"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        let combined = format!("{stdout}{stderr}");
        // Soft-match: the §4.6 example uses these labels but the
        // developer may format slightly differently. Check for the
        // canonical key names that name the structural facts.
        assert!(
            combined.contains("records"),
            "schema mentions `records`: {combined:?}",
        );
        assert!(
            combined.contains("fields"),
            "schema mentions `fields`: {combined:?}",
        );
        assert!(
            combined.contains("items_path"),
            "schema mentions `items_path`: {combined:?}",
        );
        assert!(
            combined.contains(".items"),
            "schema reports detected items path: {combined:?}",
        );
        // The two field names from the input must appear somewhere.
        assert!(
            combined.contains("kind"),
            "schema lists field `kind`: {combined:?}",
        );
        assert!(
            combined.contains("score"),
            "schema lists field `score`: {combined:?}",
        );
    }

    /// Acceptance: docs/design.md §4.6 — schema exposes nested JSON
    /// leaf paths so users can copy the same path into `--by`.
    #[test]
    fn schema_lists_nested_leaf_paths() {
        let stdin = "{\"type\":\"match\",\"data\":{\"path\":{\"text\":\"src/lib.rs\"}}}\n\
                     {\"type\":\"summary\",\"data\":{\"elapsed_total\":{\"secs\":0}}}\n";
        let (code, stdout, stderr) = drive(&["face", "--schema"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        let combined = format!("{stdout}{stderr}");
        assert!(
            combined.contains("data.path.text"),
            "schema lists nested path field: {combined:?}",
        );
    }
}

mod interactive {
    use super::drive;

    /// Acceptance: docs/design.md §10.4 — when no TTY is available,
    /// `--interactive` silently falls back to normal non-interactive
    /// rendering so scripts can pass it conditionally.
    #[test]
    fn non_tty_falls_back_to_normal_output() {
        let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
        let (code, stdout, stderr) = drive(&["face", "--interactive"], stdin);
        assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
        assert!(
            stdout.contains("bug"),
            "fallback renders clusters: {stdout:?}"
        );
        assert!(
            stdout.contains("feat"),
            "fallback renders clusters: {stdout:?}"
        );
    }
}

mod stderr_skips {
    //! §11.1 — every skipped record (parser-side or clusterer-side)
    //! produces one stderr line. The clusterer-side path is exercised
    //! by feeding a record with a value at the axis field that the
    //! clusterer cannot coerce (an array at an `exact` axis).

    use super::drive;

    /// Acceptance: docs/design.md §11.1 — clusterer-side skips
    /// (collected via the `Diagnostics` sink) are surfaced to stderr
    /// alongside parser-side skips. A record whose `exact_key` field
    /// is an array trips a `WrongType` skip in the exact clusterer.
    #[test]
    fn tree_skips_emit_warning_lines() {
        // Two valid records (good for picking `kind` as the grouping
        // axis) plus one with an array at `kind` that the exact
        // clusterer cannot coerce. Pin the grouping axis explicitly
        // so auto-detection's heuristics don't reject the dataset.
        let stdin = "{\"kind\":\"bug\"}\n\
                     {\"kind\":[1,2]}\n\
                     {\"kind\":\"feat\"}\n";
        let (code, stdout, stderr) = drive(&["face", "--by=kind", "--verbose"], stdin);
        assert_eq!(
            code, 0,
            "tree skip is not fatal: stdout={stdout:?} stderr={stderr:?}"
        );
        assert!(
            stderr.contains("face: skipped record"),
            "tree-side skip must produce a `face: skipped record` line on stderr; \
             stderr={stderr:?}",
        );
    }
}

mod verbose {
    use super::drive;

    /// Acceptance: docs/design.md §11.1 — malformed records are quiet
    /// by default so command composition does not get stderr noise.
    #[test]
    fn default_silences_stderr_warnings() {
        let stdin = "{\"kind\":\"bug\"}\n\
                     {malformed json line\n\
                     {\"kind\":\"feat\"}\n";

        let (code, _stdout, stderr) = drive(&["face"], stdin);
        assert_eq!(
            code, 0,
            "malformed record is skipped, not fatal: stderr={stderr:?}",
        );
        assert!(
            stderr.is_empty(),
            "stderr is quiet by default: stderr={stderr:?}",
        );
    }

    /// Acceptance: docs/design.md §11.1 — `--verbose` emits skipped
    /// record warnings on stderr.
    #[test]
    fn verbose_emits_stderr_warnings() {
        let stdin = "{\"kind\":\"bug\"}\n\
                     {malformed json line\n\
                     {\"kind\":\"feat\"}\n";

        let (code, _stdout, stderr) = drive(&["face", "--verbose"], stdin);
        assert_eq!(code, 0, "verbose mode is still successful");
        assert!(
            stderr.contains("face: skipped record"),
            "stderr should warn about the skip under --verbose: stderr={stderr:?}",
        );
    }

    /// Acceptance: docs/design.md §4.6 — `--verbose` emits provenance
    /// on stderr.
    #[test]
    fn verbose_emits_provenance() {
        let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
        let (code, _stdout, stderr) = drive(&["face", "--verbose"], stdin);
        assert_eq!(code, 0, "verbose success: stderr={stderr:?}");
        assert!(
            stderr.contains("items by"),
            "stderr contains provenance under --verbose: {stderr:?}",
        );
    }
}