gshell 1.0.1

gshell is a shell for people who live in the terminal. It pairs familiar Unix behavior with a tighter core, fast interaction, and an interface built to stay out of the way.
Documentation
use std::sync::{Arc, Mutex};
use std::{fs, time::Duration};

use gshell::{
    parser::Parser,
    runtime::{BootstrapExecutor, Executor},
    shell::{ExitCode, ShellAction, ShellState},
};

#[tokio::test]
async fn function_definition_and_invocation_work() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("greet() { echo hello; }; greet")
        .expect("parse should succeed");
    let result = executor
        .execute(state.clone(), &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "hello\n");
            assert!(state.read().await.functions().get("greet").is_some());
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn function_body_updates_environment_for_later_commands() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("setname() { NAME=gencho; }; setname; echo $NAME")
        .expect("parse should succeed");
    let result = executor
        .execute(state.clone(), &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "gencho\n");
            assert_eq!(state.read().await.env_var("NAME"), Some("gencho"));
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn assignment_prefix_is_scoped_to_function_invocation() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("show() { echo $NAME; }; NAME=gencho show; echo $NAME")
        .expect("parse should succeed");
    let result = executor
        .execute(state.clone(), &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "gencho\n\n");
            assert_eq!(state.read().await.env_var("NAME"), None);
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn function_body_sees_updated_last_exit_status() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("show_status() { false; echo $?; }; show_status")
        .expect("parse should succeed");
    let result = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "1\n");
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn function_recursion_is_rejected() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("loop() { loop; }; loop")
        .expect("parse should succeed");
    let result = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::FAILURE);
            assert!(output.stderr.contains("function recursion detected: loop"));
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn alias_takes_precedence_over_function_lookup() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");
    state.write().await.aliases_mut().set("run", "echo alias");

    let parsed = parser
        .parse("run() { echo function; }; run")
        .expect("parse should succeed");
    let result = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "alias\n");
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn alias_can_expand_to_function_name() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");

    let parsed = parser
        .parse("greet() { echo function; }; alias say=greet; say")
        .expect("parse should succeed");
    let result = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert_eq!(output.stdout, "function\n");
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }
}

#[tokio::test]
async fn function_sequence_streams_intermediate_output_when_sink_is_installed() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");
    let captured = Arc::new(Mutex::new(Vec::<String>::new()));
    let sink = captured.clone();
    state
        .write()
        .await
        .runtime_services_mut()
        .set_output_sink(Some(Arc::new(move |output| {
            if !output.stdout.is_empty() {
                sink.lock()
                    .expect("sink lock should not be poisoned")
                    .push(output.stdout.clone());
            }
        })));

    let parsed = parser
        .parse("greet() { echo hi; sleep 0.2; echo bye; }; greet")
        .expect("parse should succeed");

    let state_for_task = state.clone();
    let task = tokio::spawn(async move {
        executor
            .execute(state_for_task, &parsed)
            .await
            .expect("execution should succeed")
    });

    tokio::time::sleep(Duration::from_millis(50)).await;

    assert_eq!(
        captured
            .lock()
            .expect("sink lock should not be poisoned")
            .as_slice(),
        ["hi\n"]
    );
    assert!(
        !task.is_finished(),
        "function should still be waiting on sleep"
    );

    let result = task.await.expect("task should join successfully");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert!(output.stdout.is_empty());
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }

    assert_eq!(
        captured
            .lock()
            .expect("sink lock should not be poisoned")
            .as_slice(),
        ["hi\n", "bye\n"]
    );
}

#[tokio::test]
async fn function_call_redirection_captures_external_output() {
    let parser = Parser::default();
    let executor = BootstrapExecutor;
    let state = ShellState::shared().await.expect("state should initialize");
    let dir = tempfile::tempdir().expect("temp dir should be created");
    let out = dir.path().join("out.txt");

    let parsed = parser
        .parse(&format!(
            "greet() {{ printf 'hi\\n'; sleep 0.05; printf 'bye\\n'; }}; greet > {}",
            out.display()
        ))
        .expect("parse should succeed");
    let result = executor
        .execute(state, &parsed)
        .await
        .expect("execution should succeed");

    match result {
        ShellAction::Continue(output) => {
            assert_eq!(output.exit_code, ExitCode::SUCCESS);
            assert!(output.stdout.is_empty());
            assert!(output.stderr.is_empty());
        }
        ShellAction::Exit(_) => panic!("function execution should not exit"),
    }

    let content = fs::read_to_string(out).expect("output file should be readable");
    assert_eq!(content, "hi\nbye\n");
}