codewhale-tui 0.8.63

Terminal UI for open-source and open-weight coding models
//! Gherkin acceptance coverage for visible core command surfaces.

use cucumber::{World as _, given, then, when, writer::Stats as _};
use tempfile::TempDir;

use crate::commands::{self, CommandResult};
use crate::config::{ApiProvider, Config};
use crate::test_support::{EnvVarGuard, lock_test_env};
use crate::tui::app::{App, TuiOptions};
use crate::tui::history::HistoryCell;

const FEATURE_NAME: &str = "Core command visible surfaces";
const FEATURE_PATH: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/tests/features/core_command_surfaces.feature"
);
const INFORMATIONAL_SCENARIO: &str =
    "Core informational commands write visible transcript messages";
const STATE_SCENARIO: &str = "Core state commands report visible changes";
const CLEAR_SCENARIO: &str = "Clear replaces prior transcript with visible confirmation";
const PERSISTENT_WORK_SCENARIO: &str = "Persistent work commands report visible dispatch requests";

#[derive(Default, cucumber::World)]
struct CoreCommandWorld {
    tmpdir: Option<TempDir>,
    app: Option<Box<App>>,
    home_path: Option<std::path::PathBuf>,
    last_message: Option<String>,
    last_result_is_error: Option<bool>,
}

impl std::fmt::Debug for CoreCommandWorld {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CoreCommandWorld")
            .field("has_tmpdir", &self.tmpdir.is_some())
            .field("has_app", &self.app.is_some())
            .field("home_path", &self.home_path)
            .field("last_message", &self.last_message)
            .field("last_result_is_error", &self.last_result_is_error)
            .finish()
    }
}

#[given("a CodeWhale core command workspace")]
fn core_command_workspace(world: &mut CoreCommandWorld) {
    let tmpdir = TempDir::new().expect("core command TempDir");
    let mut app = create_test_app_with_tmpdir(&tmpdir);
    app.ui_locale = crate::localization::Locale::En;
    app.api_provider = ApiProvider::Deepseek;
    app.model = "deepseek-v4-pro".to_string();
    app.auto_model = false;
    app.model_ids_passthrough = false;

    world.home_path = Some(tmpdir.path().join("home"));
    world.app = Some(Box::new(app));
    world.tmpdir = Some(tmpdir);
}

#[given("a CodeWhale core command workspace with one visible user message")]
fn core_command_workspace_with_one_visible_user_message(world: &mut CoreCommandWorld) {
    core_command_workspace(world);
    let app = world.app.as_deref_mut().expect("app should exist");
    app.add_message(HistoryCell::User {
        content: "Remember the whale migration".to_string(),
    });
}

#[when(regex = r#"^the user runs the core command "([^"]+)"$"#)]
fn user_runs_core_command(world: &mut CoreCommandWorld, command: String) {
    let result = execute_isolated(world, &command);
    record_visible_result(world, result);
}

#[then(regex = r#"^the message window should include "([^"]+)"$"#)]
fn message_window_should_include(world: &mut CoreCommandWorld, expected: String) {
    let visible = visible_message_window(world);

    assert!(
        visible.contains(&expected),
        "message window should include {expected:?}\nvisible transcript:\n{visible}"
    );
}

#[then(regex = r#"^the message window should not include "([^"]+)"$"#)]
fn message_window_should_not_include(world: &mut CoreCommandWorld, forbidden: String) {
    let visible = visible_message_window(world);

    assert!(
        !visible.contains(&forbidden),
        "message window should not include {forbidden:?}\nvisible transcript:\n{visible}"
    );
}

#[tokio::test(flavor = "current_thread")]
async fn core_informational_commands_write_visible_transcript_messages() {
    run_scenario(INFORMATIONAL_SCENARIO, 11).await;
}

#[tokio::test(flavor = "current_thread")]
async fn core_state_commands_report_visible_changes() {
    run_scenario(STATE_SCENARIO, 8).await;
}

#[tokio::test(flavor = "current_thread")]
async fn clear_replaces_prior_transcript_with_visible_confirmation() {
    run_scenario(CLEAR_SCENARIO, 4).await;
}

#[tokio::test(flavor = "current_thread")]
async fn persistent_work_commands_report_visible_dispatch_requests() {
    run_scenario(PERSISTENT_WORK_SCENARIO, 7).await;
}

async fn run_scenario(name: &'static str, expected_steps: usize) {
    let writer = CoreCommandWorld::cucumber()
        .fail_on_skipped()
        .with_default_cli()
        .filter_run(FEATURE_PATH, move |feature, _, scenario| {
            feature.name == FEATURE_NAME && scenario.name == name
        })
        .await;
    assert_eq!(writer.failed_steps(), 0, "scenario failed: {name}");
    assert_eq!(writer.skipped_steps(), 0, "scenario skipped steps: {name}");
    assert_eq!(
        writer.passed_steps(),
        expected_steps,
        "scenario did not run: {name}"
    );
}

fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
    let options = TuiOptions {
        model: "deepseek-v4-pro".to_string(),
        workspace: tmpdir.path().to_path_buf(),
        config_path: None,
        config_profile: None,
        allow_shell: false,
        use_alt_screen: true,
        use_mouse_capture: false,
        use_bracketed_paste: true,
        max_subagents: 1,
        skills_dir: tmpdir.path().join("skills"),
        memory_path: tmpdir.path().join("memory.md"),
        notes_path: tmpdir.path().join("notes.txt"),
        mcp_config_path: tmpdir.path().join("mcp.json"),
        use_memory: false,
        start_in_agent_mode: false,
        skip_onboarding: true,
        yolo: false,
        resume_session_id: None,
        initial_input: None,
    };
    App::new(options, &Config::default())
}

fn execute_isolated(world: &mut CoreCommandWorld, command: &str) -> CommandResult {
    let home = world
        .home_path
        .as_ref()
        .expect("test home should exist")
        .clone();
    std::fs::create_dir_all(&home).expect("create isolated test home");

    let _lock = lock_test_env();
    let _home = EnvVarGuard::set("HOME", &home);
    let _codewhale_home = EnvVarGuard::set("CODEWHALE_HOME", home.join(".codewhale"));

    let app = world.app.as_deref_mut().expect("app should exist");
    commands::user_registry::reload(Some(&app.workspace));
    commands::execute(command, app)
}

fn record_visible_result(world: &mut CoreCommandWorld, result: CommandResult) {
    world.last_result_is_error = Some(result.is_error);
    world.last_message = result.message.clone();

    if let Some(message) = result.message {
        let app = world.app.as_deref_mut().expect("app should exist");
        app.add_message(HistoryCell::System { content: message });
    }
}

fn visible_message_window(world: &CoreCommandWorld) -> String {
    let app = world.app.as_deref().expect("app should exist");
    app.history
        .iter()
        .filter_map(|cell| match cell {
            HistoryCell::User { content }
            | HistoryCell::Assistant { content, .. }
            | HistoryCell::System { content }
            | HistoryCell::Thinking { content, .. } => Some(content.as_str()),
            HistoryCell::Error { message, .. } => Some(message.as_str()),
            HistoryCell::ArchivedContext { summary, .. } => Some(summary.as_str()),
            HistoryCell::Tool(_) | HistoryCell::SubAgent(_) => None,
        })
        .collect::<Vec<_>>()
        .join("\n")
}