//! Real-world builtin command tests
//!
//! These tests are derived from actual Claude Code session transcripts
//! (2024-2026) to ensure kaish builtins match real-world usage patterns.
//!
//! ## Methodology
//!
//! Tests were generated by mining command patterns from Claude Code sessions:
//!
//! ```bash
//! # Extract all Bash tool calls from session transcripts
//! for f in ~/.claude/projects/*/*.jsonl; do
//!   jq -r 'select(.type == "assistant") | .message.content[]?
//!          | select(.type == "tool_use" and .name == "Bash")
//!          | .input.command' "$f" 2>/dev/null
//! done > /tmp/all-bash-commands.txt
//!
//! # Count: 31,463 commands across all projects
//! # Top commands: cargo, git, grep (1036), ls (614), cat (298), echo (271),
//! #               find (260), curl (195), tail (134), wc (71)
//! ```
//!
//! ## Anonymization
//!
//! Raw patterns contain real file paths (e.g., `/home/atobey/src/project/...`).
//! Tests use **synthetic fixtures** (`src/main.rs`, `tmp/app.log`) that capture
//! the **command structure** without exposing actual project files.
//!
//! ## Kernel-routed (ported 2026-06-11)
//!
//! Originally these hand-built `ToolArgs` and called `tool.execute()` directly
//! — bypassing lex → parse → validate → glob pre-expansion → `build_args_async`,
//! the exact layers the clap migration reworked. Now every test runs its
//! real-world command string through `kernel.execute()` over a tempdir root
//! (the `common::kernel_at`/`run` harness), so the mined patterns exercise the
//! same path an agent's keystrokes do. Pipeline patterns that were previously
//! simulated by manually shuttling stdout → stdin are now actual `|` pipelines.
//!
//! ## Bugs Found
//!
//! This test suite initially found 2 bugs (now fixed):
//!
//! 1. **ls single file** - `ls file.txt` failed with "not a directory"
//!    Real ls outputs the filename; we now check if path is a file first.
//!
//! 2. **wc multiple files** - `wc -l file1 file2` only output one line
//!    Real wc outputs per-file counts plus a total; now implemented.
//!
//! Source: ~/.claude/projects/*/*.jsonl (31,000+ bash commands analyzed)

// Test-fixture code: unwrap/expect on known-good setup is the idiom here.
#![allow(clippy::unwrap_used, clippy::expect_used)]
// KernelConfig::repl() mounts the real filesystem.
#![cfg(feature = "localfs")]

mod common;

use std::fs;

use common::{kernel_at, run};
use kaish_kernel::Kernel;
use tempfile::TempDir;

fn touch(dir: &std::path::Path, name: &str, contents: &str) {
    let path = dir.join(name);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent dirs");
    }
    fs::write(path, contents).expect("write file");
}

/// Build the synthetic project tree the mined commands run against.
fn fixture() -> (TempDir, Kernel) {
    let dir = tempfile::tempdir().expect("tempdir");
    let root = dir.path();

    // Rust source files
    touch(
        root,
        "src/main.rs",
        "fn main() {\n    println!(\"Hello, world!\");\n    // TODO: add features\n}\n",
    );
    touch(
        root,
        "src/lib.rs",
        "//! Library crate\npub mod utils;\npub mod api;\n\n/// Main entry point\npub fn init() {\n    // TODO: implement\n}\n",
    );
    touch(
        root,
        "src/lib/utils.rs",
        "pub fn helper() -> String {\n    \"helper\".to_string()\n}\n\npub fn format_output(s: &str) -> String {\n    format!(\"[{}]\", s)\n}\n",
    );
    touch(
        root,
        "src/api.rs",
        "use std::sync::Arc;\n\npub struct Api {\n    pub name: String,\n}\n\nimpl Api {\n    pub fn new(name: &str) -> Self {\n        Self { name: name.to_string() }\n    }\n\n    pub async fn execute(&self) -> Result<(), Error> {\n        // TODO: implement\n        Ok(())\n    }\n}\n",
    );

    // Documentation
    touch(
        root,
        "README.md",
        "# Project\n\nA sample project for testing.\n\n## Features\n\n- Feature one\n- Feature two\n\n## TODO\n\n- Add more features\n",
    );
    touch(
        root,
        "docs/API.md",
        "# API Documentation\n\n## Functions\n\n### init()\n\nInitializes the system.\n\n### helper()\n\nReturns a helper string.\n",
    );
    touch(
        root,
        "CLAUDE.md",
        "# Claude Instructions\n\nThis project uses kaish shell.\n\n## Build\n\ncargo build\ncargo test\n",
    );

    // Config/data files
    touch(
        root,
        "Cargo.toml",
        "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ntokio = \"1.0\"\n",
    );

    // JSON files (for jq tests and cat tests)
    touch(
        root,
        "tmp/data.json",
        r#"{"name": "Alice", "age": 30, "active": true}"#,
    );
    touch(
        root,
        "tmp/array.json",
        r#"[{"id": 1, "name": "one"}, {"id": 2, "name": "two"}, {"id": 3, "name": "three"}]"#,
    );
    touch(
        root,
        "tmp/settings.json",
        r#"{"mcpServers": {"holler": {"url": "http://localhost:8080"}, "sshwarma": {"url": "http://localhost:3000"}}}"#,
    );

    // Log file (for tail/head tests)
    touch(
        root,
        "tmp/app.log",
        "2026-01-25T10:00:00 INFO Starting application\n\
         2026-01-25T10:00:01 DEBUG Loading config\n\
         2026-01-25T10:00:02 INFO Config loaded successfully\n\
         2026-01-25T10:00:03 WARN Deprecated feature used\n\
         2026-01-25T10:00:04 ERROR Connection failed\n\
         2026-01-25T10:00:05 INFO Retrying connection\n\
         2026-01-25T10:00:06 INFO Connected\n\
         2026-01-25T10:00:07 DEBUG Processing request\n\
         2026-01-25T10:00:08 INFO Request completed\n\
         2026-01-25T10:00:09 INFO Shutting down\n",
    );

    let kernel = kernel_at(root);
    (dir, kernel)
}

// ============================================================================
// GREP Tests - Real-world patterns from Claude Code sessions
// ============================================================================

mod grep_realworld {
    use super::*;

    // Pattern: grep -A 10 "pattern" file.rs
    // Source: Frequently used to find function definitions with context
    #[tokio::test]
    async fn test_grep_after_context_function_definition() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"grep -A 5 "pub fn new" src/api.rs"#).await;
        assert_eq!(code, 0, "grep -A failed: {out:?}");
        assert!(out.contains("pub fn new"));
        assert!(out.contains("name.to_string()")); // Context line
    }

    // Pattern: grep -E "pattern|pattern2" file
    // Source: Common for finding multiple related patterns
    #[tokio::test]
    async fn test_grep_alternation_pattern() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"grep -E "TODO|FIXME" src/main.rs"#).await;
        assert_eq!(code, 0, "grep -E alternation failed: {out:?}");
        assert!(out.contains("TODO"));
    }

    // Pattern: grep -r "pattern" directory/
    // Source: Used extensively for codebase searches
    #[tokio::test]
    async fn test_grep_recursive_in_src() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -r TODO src").await;
        assert_eq!(code, 0, "grep -r failed: {out:?}");
        // Should find TODO in multiple files
        assert!(
            out.contains("main.rs") || out.contains("lib.rs") || out.contains("api.rs"),
            "expected a source filename in: {out:?}"
        );
    }

    // Pattern: grep -rl "pattern" dir
    // Source: Finding which files contain a pattern
    #[tokio::test]
    async fn test_grep_files_with_matches() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -rl TODO src").await;
        assert_eq!(code, 0, "grep -rl failed: {out:?}");
        assert!(!out.is_empty(), "expected at least one matching file");
        // Output should be filenames only
        for line in out.lines() {
            assert!(line.ends_with(".rs"), "Expected filename, got: {line}");
        }
    }

    // Pattern: grep -n "pattern" file
    // Source: Finding line numbers for navigation
    #[tokio::test]
    async fn test_grep_with_line_numbers() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -n impl src/api.rs").await;
        assert_eq!(code, 0, "grep -n failed: {out:?}");
        // Should have line number prefix and the pattern
        assert!(out.contains(':'), "missing line-number prefix: {out:?}");
        assert!(out.contains("impl"));
    }

    // Pattern: grep -i "error" logfile
    // Source: Case-insensitive log searching
    #[tokio::test]
    async fn test_grep_case_insensitive_log_search() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -i error tmp/app.log").await;
        assert_eq!(code, 0, "grep -i failed: {out:?}");
        assert!(out.contains("ERROR")); // Finds ERROR even with lowercase pattern
    }

    // Pattern: grep -c "pattern" file
    // Source: Counting occurrences
    #[tokio::test]
    async fn test_grep_count_matches() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -c INFO tmp/app.log").await;
        assert_eq!(code, 0, "grep -c failed: {out:?}");
        let count: i32 = out.trim().parse().unwrap_or(-1);
        assert!(count > 0, "Expected positive count, got: {out:?}");
    }

    // Pattern: grep -v "pattern" file
    // Source: Filtering out lines (inverse match)
    #[tokio::test]
    async fn test_grep_invert_match() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep -v DEBUG tmp/app.log").await;
        assert_eq!(code, 0, "grep -v failed: {out:?}");
        // Should NOT contain DEBUG lines, but should contain others
        assert!(!out.contains("DEBUG"));
        assert!(out.contains("INFO") || out.contains("ERROR"));
    }

    // Pattern: cmd | grep "pattern"
    // Source: grep over piped data
    #[tokio::test]
    async fn test_grep_from_stdin() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(
            &kernel,
            r#"printf 'line one\nline two\nline three\nfour\nfive\n' | grep line"#,
        )
        .await;
        assert_eq!(code, 0, "grep stdin failed: {out:?}");
        assert_eq!(out.lines().count(), 3); // three lines contain "line"
    }
}

// ============================================================================
// CAT Tests - Real-world patterns
// ============================================================================

mod cat_realworld {
    use super::*;

    // Pattern: cat file.json
    // Source: Reading JSON config files
    #[tokio::test]
    async fn test_cat_json_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cat tmp/settings.json").await;
        assert_eq!(code, 0, "cat json failed: {out:?}");
        assert!(out.contains("mcpServers"));
        assert!(out.contains("holler"));
    }

    // Pattern: cat ~/.config/file.toml
    // Source: Reading TOML config
    #[tokio::test]
    async fn test_cat_toml_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cat Cargo.toml").await;
        assert_eq!(code, 0, "cat toml failed: {out:?}");
        assert!(out.contains("[package]"));
        assert!(out.contains("[dependencies]"));
    }

    // Pattern: cat file1 file2 file3
    // Source: Concatenating multiple files
    #[tokio::test]
    async fn test_cat_multiple_files() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cat src/main.rs src/lib.rs").await;
        assert_eq!(code, 0, "cat multiple failed: {out:?}");
        assert!(out.contains("fn main")); // from main.rs
        assert!(out.contains("pub mod utils")); // from lib.rs
    }

    // Pattern: cat -n file.rs
    // Source: Viewing with line numbers for reference
    #[tokio::test]
    async fn test_cat_with_line_numbers() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cat -n src/main.rs").await;
        assert_eq!(code, 0, "cat -n failed: {out:?}");
        // First content line is numbered
        let first = out.lines().next().expect("at least one line");
        assert!(first.contains('1'), "expected a line number: {first:?}");
        assert!(first.contains("fn main"), "expected content: {first:?}");
    }

    // Pattern: cat nonexistent 2>/dev/null || echo "Not found"
    // Test: cat on nonexistent file should fail
    #[tokio::test]
    async fn test_cat_nonexistent_file() {
        let (_dir, kernel) = fixture();
        let result = kernel
            .execute("cat nonexistent/file.txt")
            .await
            .expect("kernel execute");
        assert!(!result.ok(), "cat of a missing file must fail");
        assert!(
            result.err.contains("file.txt") || result.err.contains("nonexistent"),
            "error should name the path: {}",
            result.err
        );
    }
}

// ============================================================================
// HEAD Tests - Real-world patterns
// ============================================================================

mod head_realworld {
    use super::*;

    // Pattern: head --lines 5 file.log (long form)
    // Source: Looking at start of log files
    #[tokio::test]
    async fn test_head_log_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "head --lines 5 tmp/app.log").await;
        assert_eq!(code, 0, "head failed: {out:?}");
        assert_eq!(out.lines().count(), 5);
        assert!(out.contains("Starting application")); // First line
    }

    // Pattern: head -n 3 src/file.rs
    // Source: Quick file preview
    #[tokio::test]
    async fn test_head_source_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "head -n 3 src/api.rs").await;
        assert_eq!(code, 0, "head src failed: {out:?}");
        assert_eq!(out.lines().count(), 3);
        assert!(out.contains("use std::sync::Arc"));
    }

    // Pattern: head -n 3 file (short flag with value)
    #[tokio::test]
    async fn test_head_with_n_flag() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "head -n 3 tmp/app.log").await;
        assert_eq!(code, 0, "head -n failed: {out:?}");
        assert!(out.lines().count() <= 3);
    }

    // Default: head file (should show 10 lines)
    #[tokio::test]
    async fn test_head_default_lines() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "head tmp/app.log").await;
        assert_eq!(code, 0, "head default failed: {out:?}");
        // Log has 10 lines, default should show all or 10
        assert!(out.lines().count() <= 10);
    }
}

// ============================================================================
// TAIL Tests - Real-world patterns
// ============================================================================

mod tail_realworld {
    use super::*;

    // Pattern: tail -n 5 file.log
    // Source: Looking at recent log entries
    #[tokio::test]
    async fn test_tail_log_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "tail -n 5 tmp/app.log").await;
        assert_eq!(code, 0, "tail failed: {out:?}");
        assert_eq!(out.lines().count(), 5);
        assert!(out.contains("Shutting down")); // Last line
    }

    // Pattern: tail -n 3 file
    // Source: Checking the end of a source file
    #[tokio::test]
    async fn test_tail_source_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "tail -n 3 src/api.rs").await;
        assert_eq!(code, 0, "tail src failed: {out:?}");
        assert_eq!(out.lines().count(), 3);
    }

    // Default: tail file (should show 10 lines)
    #[tokio::test]
    async fn test_tail_default_lines() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "tail tmp/app.log").await;
        assert_eq!(code, 0, "tail default failed: {out:?}");
        assert!(out.lines().count() <= 10);
    }
}

// ============================================================================
// WC Tests - Real-world patterns
// ============================================================================

mod wc_realworld {
    use super::*;

    // Pattern: wc -l file.rs
    // Source: Checking file sizes
    #[tokio::test]
    async fn test_wc_lines_single_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "wc -l src/api.rs").await;
        assert_eq!(code, 0, "wc -l failed: {out:?}");
        // Format is `<count> <filename>` — the count is the first whitespace token.
        let count: i32 = out
            .split_whitespace()
            .next()
            .unwrap_or("0")
            .parse()
            .unwrap_or(-1);
        assert!(count > 0, "Expected positive line count, got: {out:?}");
    }

    // Pattern: wc -l file1 file2 file3
    // Source: Comparing file sizes
    #[tokio::test]
    async fn test_wc_lines_multiple_files() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "wc -l src/main.rs src/lib.rs").await;
        assert_eq!(code, 0, "wc -l multiple failed: {out:?}");
        // Should have output for each file
        assert!(out.lines().count() >= 2, "Expected output for multiple files: {out:?}");
    }

    // Pattern: wc -l docs/*.md README.md CLAUDE.md
    // Glob + explicit operands mixed — the kernel pre-expands the glob
    #[tokio::test]
    async fn test_wc_markdown_files() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "wc -l docs/*.md README.md CLAUDE.md").await;
        assert_eq!(code, 0, "wc -l markdown failed: {out:?}");
        assert!(out.contains("API.md"), "glob operand missing: {out:?}");
        assert!(out.contains("README.md"), "explicit operand missing: {out:?}");
    }

    // Pattern: wc file (no flags) - should show lines, words, bytes
    #[tokio::test]
    async fn test_wc_default_output() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "wc README.md").await;
        assert_eq!(code, 0, "wc default failed: {out:?}");
        // Default output should have multiple numbers
        let parts: Vec<&str> = out.split_whitespace().collect();
        assert!(parts.len() >= 3, "Expected lines, words, bytes: {out:?}");
    }
}

// ============================================================================
// LS Tests - Real-world patterns
// ============================================================================

mod ls_realworld {
    use super::*;

    // Pattern: ls src/
    // Source: Exploring directory structure
    #[tokio::test]
    async fn test_ls_directory() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "ls src").await;
        assert_eq!(code, 0, "ls failed: {out:?}");
        assert!(out.contains("main.rs"));
        assert!(out.contains("lib.rs"));
    }

    // Pattern: ls -la directory/
    // Source: Detailed file listing
    #[tokio::test]
    async fn test_ls_long_all() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "ls -la src").await;
        assert_eq!(code, 0, "ls -la failed: {out:?}");
        assert!(out.contains("main.rs"));
    }

    // Pattern: ls file (single file shows its name, not "not a directory")
    #[tokio::test]
    async fn test_ls_single_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "ls README.md").await;
        assert_eq!(code, 0, "ls file failed: {out:?}");
        assert!(out.contains("README.md"));
    }

    // Pattern: cd dir && ls (current directory)
    #[tokio::test]
    async fn test_ls_cwd() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cd src && ls").await;
        assert_eq!(code, 0, "ls cwd failed: {out:?}");
        assert!(out.contains("main.rs") || out.contains("lib.rs"));
    }
}

// ============================================================================
// ECHO Tests - Real-world patterns
// ============================================================================

mod echo_realworld {
    use super::*;

    // Pattern: echo "message"
    #[tokio::test]
    async fn test_echo_simple_string() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"echo "Hello, world!""#).await;
        assert_eq!(code, 0, "echo failed: {out:?}");
        assert_eq!(out, "Hello, world!");
    }

    // Pattern: echo "---"
    // Source: Common separator in output
    #[tokio::test]
    async fn test_echo_separator() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"echo "---""#).await;
        assert_eq!(code, 0, "echo separator failed: {out:?}");
        assert_eq!(out, "---");
    }

    // Pattern: echo "" (empty string)
    #[tokio::test]
    async fn test_echo_empty() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"echo """#).await;
        assert_eq!(code, 0, "echo empty failed: {out:?}");
        assert_eq!(out, "");
    }

    // Pattern: echo "multiple" "arguments"
    #[tokio::test]
    async fn test_echo_multiple_args() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, r#"echo "Hello" "World""#).await;
        assert_eq!(code, 0, "echo multiple failed: {out:?}");
        assert_eq!(out, "Hello World");
    }

    // Pattern: echo -n "no newline"
    #[tokio::test]
    async fn test_echo_no_newline() {
        let (_dir, kernel) = fixture();
        // run() trims, so inspect the raw result for the trailing-newline check.
        let result = kernel
            .execute(r#"echo -n "no newline""#)
            .await
            .expect("kernel execute");
        assert!(result.ok(), "echo -n failed: {}", result.err);
        assert_eq!(result.text_out(), "no newline");
        assert!(!result.text_out().ends_with('\n'));
    }
}

// ============================================================================
// JQ Tests - Real-world patterns (extends jq_native tests)
// ============================================================================

mod jq_realworld {
    use super::*;

    // Pattern: jq '.field' file.json (positional file - the bug we fixed!)
    #[tokio::test]
    async fn test_jq_positional_file_basic() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "jq '.name' tmp/data.json").await;
        assert_eq!(code, 0, "jq positional failed: {out:?}");
        assert!(out.contains("Alice"));
    }

    // Pattern: jq '.[] | select(.id == 2)' file.json
    #[tokio::test]
    async fn test_jq_select_from_array() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "jq '.[] | select(.id == 2)' tmp/array.json").await;
        assert_eq!(code, 0, "jq select failed: {out:?}");
        assert!(out.contains("two"));
    }

    // Pattern: jq -r '.field' file.json
    #[tokio::test]
    async fn test_jq_raw_output_positional_file() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "jq -r '.name' tmp/data.json").await;
        assert_eq!(code, 0, "jq -r failed: {out:?}");
        assert_eq!(out, "Alice"); // No quotes with -r
    }

    // Pattern: jq 'length' file.json
    #[tokio::test]
    async fn test_jq_length_function() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "jq 'length' tmp/array.json").await;
        assert_eq!(code, 0, "jq length failed: {out:?}");
        assert_eq!(out, "3");
    }

    // Pattern: jq '.mcpServers | keys' settings.json
    #[tokio::test]
    async fn test_jq_keys_function() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "jq '.mcpServers | keys' tmp/settings.json").await;
        assert_eq!(code, 0, "jq keys failed: {out:?}");
        assert!(out.contains("holler"));
        assert!(out.contains("sshwarma"));
    }

    // Pattern: echo '{...}' | jq '.field' (stdin)
    #[tokio::test]
    async fn test_jq_from_stdin() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(
            &kernel,
            r#"echo '{"name": "Bob", "value": 42}' | jq '.value'"#,
        )
        .await;
        assert_eq!(code, 0, "jq stdin failed: {out:?}");
        assert_eq!(out, "42");
    }
}

// ============================================================================
// Pipeline Pattern Tests — real `|` pipelines through the kernel
// ============================================================================

mod pipeline_patterns {
    use super::*;

    // Pattern: grep "pattern" file | wc -l
    #[tokio::test]
    async fn test_grep_pipe_wc() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "grep INFO tmp/app.log | wc -l").await;
        assert_eq!(code, 0, "grep | wc failed: {out:?}");
        let count: i32 = out.trim().parse().unwrap_or(0);
        assert!(count > 0, "Expected INFO lines, got: {out:?}");
    }

    // Pattern: cat file | grep "pattern"
    #[tokio::test]
    async fn test_cat_pipe_grep() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "cat tmp/app.log | grep ERROR").await;
        assert_eq!(code, 0, "cat | grep failed: {out:?}");
        assert!(out.contains("Connection failed"));
    }

    // Pattern: head -50 file | grep "pattern"
    #[tokio::test]
    async fn test_head_pipe_grep() {
        let (_dir, kernel) = fixture();
        let (out, code) = run(&kernel, "head -n 5 tmp/app.log | grep DEBUG").await;
        assert_eq!(code, 0, "head | grep failed: {out:?}");
        assert!(out.contains("Loading config"));
    }
}
