kaish-kernel 0.16.0

Core kernel for kaish: lexer, parser, interpreter, and runtime
Documentation
//! Kernel-routed tests: the `glob` builtin must preserve the leading `/` of
//! an absolute (anchored) pattern in its output.
//!
//! The bug: `glob '/tmp/x/*.txt'` reported `tmp/x/z.txt` — the leading `/`
//! silently dropped — while a bare glob in argv position (`echo /tmp/x/*.txt`)
//! reported the correct absolute path. Because the wrong value is never
//! flagged as an error, it flows downstream: `cat $(glob '/tmp/x/*.txt')`
//! reports "not found" for a file that exists. `--json` output carried the
//! same wrong value.
//!
//! Root cause: `glob.rs` computed `report_root = ctx.resolve_path("/")` for
//! an anchored pattern (i.e. `/`), then did
//! `p.strip_prefix(&report_root)` unconditionally — stripping the leading
//! `/` off every matched absolute path.

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

mod common;

use std::fs;
use tempfile::tempdir;

use common::{kernel_at, run};

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

/// An absolute pattern must yield absolute results, leading `/` intact.
#[tokio::test]
async fn absolute_pattern_preserves_leading_slash() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());

    let abs_pattern = format!("{}/*.txt", dir.path().display());
    let expected = format!("{}/z.txt", dir.path().display());

    let (out, code) = run(&kernel, &format!("glob '{abs_pattern}'")).await;
    assert_eq!(code, 0, "glob should succeed: {out:?}");
    assert!(
        out.contains(&expected),
        "expected absolute path {expected:?} in output, got {out:?}"
    );
    // The specific defect: the leading slash silently dropped.
    let stripped = expected.trim_start_matches('/');
    assert!(
        !out.split_whitespace().any(|line| line == stripped),
        "output must not contain the path with its leading slash stripped: {out:?}"
    );
}

/// Control: a relative pattern must still yield relative results, unchanged
/// by the absolute-path fix.
#[tokio::test]
async fn relative_pattern_stays_relative() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "glob '*.txt'").await;
    assert_eq!(code, 0, "glob should succeed: {out:?}");
    assert_eq!(out, "z.txt", "relative pattern must report a bare relative name: {out:?}");
}

/// `--json` must carry the same corrected absolute value as text output.
#[tokio::test]
async fn absolute_pattern_json_preserves_leading_slash() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());

    let abs_pattern = format!("{}/*.txt", dir.path().display());
    let expected_json_string = format!("\"{}/z.txt\"", dir.path().display());

    let (out, code) = run(&kernel, &format!("glob --json '{abs_pattern}'")).await;
    assert_eq!(code, 0, "glob --json should succeed: {out:?}");
    assert!(
        out.contains(&expected_json_string),
        "expected {expected_json_string:?} in --json output, got {out:?}"
    );
}

/// An absolute pattern is unaffected by the caller's cwd: after `cd`
/// elsewhere, the same absolute pattern must still report the same absolute
/// path (the per-context cwd only governs *relative* resolution).
#[tokio::test]
async fn absolute_pattern_unaffected_by_cd() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    fs::create_dir_all(dir.path().join("elsewhere")).unwrap();
    let kernel = kernel_at(dir.path());

    let abs_pattern = format!("{}/*.txt", dir.path().display());
    let expected = format!("{}/z.txt", dir.path().display());

    let (out, code) = run(
        &kernel,
        &format!("cd elsewhere; glob '{abs_pattern}'"),
    )
    .await;
    assert_eq!(code, 0, "glob should succeed after cd: {out:?}");
    assert!(
        out.contains(&expected),
        "expected absolute path {expected:?} in output after cd, got {out:?}"
    );
}

/// A relative pattern after `cd` into a subdirectory reports names relative
/// to the NEW cwd, with no leading slash and no path prefix at all.
#[tokio::test]
async fn relative_pattern_after_cd_reports_subdir_relative_names() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("sub")).unwrap();
    touch(dir.path(), "sub/inner.txt");
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "cd sub; glob '*.txt'").await;
    assert_eq!(code, 0, "glob should succeed after cd: {out:?}");
    assert_eq!(
        out, "inner.txt",
        "relative pattern after cd must report a bare name relative to the new cwd: {out:?}"
    );
}

/// The same degenerate strip lived in `ExecContext::expand_paths`, which
/// every path-taking builtin uses (`cat`, `head`, `tail`, `wc`, `ls`, `file`,
/// `checksum`, `base64`, `tac`, `xxd`). It stripped the cwd from each match
/// unconditionally, which is a no-op only while cwd is a real prefix — when
/// cwd is `/` the strip removes the leading separator itself.
///
/// `/` is the DEFAULT cwd for an isolated kernel, so this is the ordinary
/// case for an embedder rather than a corner. The pattern is quoted so the
/// kernel does not expand it in argv and the builtin's own expansion runs.
#[tokio::test]
async fn absolute_pattern_through_expand_paths_keeps_leading_slash() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());
    let pattern = format!("{}/*.txt", dir.path().display());
    let expected = format!("{}/z.txt", dir.path().display());

    // cd / is what makes the old strip destructive.
    let (out, code) = run(&kernel, &format!("cd /; ls '{pattern}'")).await;
    assert_eq!(code, 0, "ls should succeed: {out:?}");
    assert_eq!(
        out, expected,
        "an absolute pattern must keep its leading slash with cwd=/: {out:?}"
    );
}

/// The control for the case above: with a cwd that really is a prefix, the
/// old code produced the right answer by accident. It must still be right.
#[tokio::test]
async fn expand_paths_absolute_pattern_unaffected_by_cwd() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());
    let pattern = format!("{}/*.txt", dir.path().display());
    let expected = format!("{}/z.txt", dir.path().display());

    let (out, code) = run(&kernel, &format!("ls '{pattern}'")).await;
    assert_eq!(code, 0, "ls should succeed: {out:?}");
    assert_eq!(out, expected, "absolute stays absolute from any cwd: {out:?}");
}

/// A relative pattern through the same door still reports a bare relative
/// name — fixing absolute must not make everything absolute.
#[tokio::test]
async fn relative_pattern_through_expand_paths_stays_relative() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "z.txt");
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "ls '*.txt'").await;
    assert_eq!(code, 0, "ls should succeed: {out:?}");
    assert_eq!(out, "z.txt", "relative pattern must stay relative: {out:?}");
}

// ── grep -r shares the same "an absolute operand comes back relative" defect ──

/// `grep -r PATTERN /abs/dir` reported bare names, so a match could not be
/// used as a path: `for f in $(grep -rl …); do cat "$f"; done` looked for a
/// file that was never there. GNU keeps the operand it was given, and so does
/// kaish now.
#[tokio::test]
async fn grep_recursive_absolute_operand_reports_absolute_paths() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "a.txt");
    fs::write(dir.path().join("a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());
    let expected = format!("{}/a.txt", dir.path().display());

    let (out, code) = run(&kernel, &format!("grep -rl HIT {}", dir.path().display())).await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, expected, "an absolute operand keeps absolute results: {out:?}");
}

/// The same, from a cwd of `/`. Stripping `/` as a prefix removes the leading
/// separator rather than relativizing, and `/` is the default cwd for an
/// isolated kernel.
#[tokio::test]
async fn grep_recursive_absolute_operand_is_unaffected_by_cwd() {
    let dir = tempdir().unwrap();
    touch(dir.path(), "a.txt");
    fs::write(dir.path().join("a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());
    let expected = format!("{}/a.txt", dir.path().display());

    let (out, code) = run(
        &kernel,
        &format!("cd /; grep -rl HIT {}", dir.path().display()),
    )
    .await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, expected, "cwd must not change an absolute answer: {out:?}");
}

/// A relative operand now prefixes results with the operand as written, GNU
/// style: `grep -r p d` reports `d/a.txt`, not the bare `a.txt` kaish used
/// to strip down to.
#[tokio::test]
async fn grep_recursive_relative_operand_matches_gnu_prefix() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("d")).unwrap();
    fs::write(dir.path().join("d/a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "grep -rl HIT d").await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, "d/a.txt", "a relative operand is prefixed like GNU: {out:?}");
}

/// A relative operand spelled with a leading `./` keeps that spelling in the
/// prefix, matching GNU byte-for-byte.
#[tokio::test]
async fn grep_recursive_dot_slash_operand_keeps_its_spelling() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("d")).unwrap();
    fs::write(dir.path().join("d/a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "grep -rl HIT ./d").await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, "./d/a.txt", "a ./-spelled operand keeps its ./ prefix: {out:?}");
}

/// The `.` operand is the one exception both GNU and kaish carve out: no
/// GNU does NOT special-case `.`: it joins the operand like any other, so
/// `grep -r p .` reports `./d/a.txt`. Verified against `/usr/bin/grep` 3.12,
/// not against the `grep` on PATH — this machine's shell shadows `grep` with
/// `ugrep`, which DOES strip the `./` and would have confirmed the wrong
/// answer. The first version of this test pinned that wrong answer.
#[tokio::test]
async fn grep_recursive_dot_operand_keeps_gnu_dot_slash() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("d")).unwrap();
    fs::write(dir.path().join("d/a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "grep -rl HIT .").await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, "./d/a.txt", "an explicit `.` is joined, as GNU does: {out:?}");
}

/// The complement, and the reason `.` cannot simply be prefixed always: a
/// DEFAULTED operand — no path written at all — reports bare names in GNU.
#[tokio::test]
async fn grep_recursive_defaulted_operand_has_no_prefix() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("d")).unwrap();
    fs::write(dir.path().join("d/a.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "grep -rl HIT").await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    assert_eq!(out, "d/a.txt", "a defaulted operand stays bare: {out:?}");
}

/// Several directory operands already showed each match under its own
/// cwd-relative subpath (`d/a.txt`, `d2/b.txt`) before this change, and must
/// keep doing so — the new prefix rule applies only to a sole directory
/// operand.
#[tokio::test]
async fn grep_recursive_multiple_dir_operands_display_is_unchanged() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("d")).unwrap();
    fs::create_dir_all(dir.path().join("d2")).unwrap();
    fs::write(dir.path().join("d/a.txt"), b"HIT\n").unwrap();
    fs::write(dir.path().join("d2/b.txt"), b"HIT\n").unwrap();
    let kernel = kernel_at(dir.path());

    let (out, code) = run(&kernel, "grep -rl HIT d d2").await;
    assert_eq!(code, 0, "grep should match: {out:?}");
    let mut lines: Vec<&str> = out.lines().collect();
    lines.sort_unstable();
    assert_eq!(lines, vec!["d/a.txt", "d2/b.txt"]);
}