kanbus 0.14.0

High-performance CLI and web console for the Kanbus issue tracker. Includes kanbus (CLI) and kanbus-console (web UI server).
Documentation
use std::fs;

use cucumber::{gherkin::Step, given, then, when};

use kanbus::cli::run_from_args_with_output;
use kanbus::content_validation::{validate_code_blocks, CodeBlock};
use kanbus::error::KanbusError;

use crate::step_definitions::initialization_steps::KanbusWorld;

#[given(expr = "external validator {string} is not available")]
fn given_external_validator_not_available(_world: &mut KanbusWorld, tool: String) {
    std::env::set_var("KANBUS_TEST_EXTERNAL_TOOL_MISSING", tool);
}

#[given(expr = "external validator {string} is available and returns success")]
fn given_external_validator_available_success(world: &mut KanbusWorld, tool: String) {
    ensure_tool_stub(world, &tool, "exit 0\n");
}

#[given(expr = "external validator {string} is available and returns error {string}")]
fn given_external_validator_available_error(
    world: &mut KanbusWorld,
    tool: String,
    message: String,
) {
    let script = format!("echo \"{message}\" 1>&2\nexit 1\n");
    ensure_tool_stub(world, &tool, &script);
}

#[given(expr = "external validator {string} times out")]
fn given_external_validator_times_out(world: &mut KanbusWorld, tool: String) {
    std::env::set_var("KANBUS_TEST_EXTERNAL_TIMEOUT_MS", "50");
    ensure_tool_stub(world, &tool, "sleep 1\n");
}

#[when("I create an issue with description containing:")]
fn when_create_with_description(world: &mut KanbusWorld, step: &Step) {
    let description = step.docstring().expect("docstring").trim().to_string();
    let args_with_desc: Vec<String> = vec![
        "kanbus".to_string(),
        "create".to_string(),
        "Test".to_string(),
        "Issue".to_string(),
        "--description".to_string(),
        description,
    ];

    let cwd = world
        .working_directory
        .as_ref()
        .expect("working directory not set");

    match run_from_args_with_output(args_with_desc, cwd.as_path()) {
        Ok(output) => {
            world.exit_code = Some(0);
            world.stdout = Some(output.stdout);
            world.stderr = Some(output.stderr);
        }
        Err(error) => {
            world.exit_code = Some(1);
            world.stdout = Some(String::new());
            world.stderr = Some(error.to_string());
        }
    }
}

#[when("I create an issue with --no-validate and description containing:")]
fn when_create_no_validate_with_description(world: &mut KanbusWorld, step: &Step) {
    let description = step.docstring().expect("docstring").trim().to_string();
    let args_with_desc: Vec<String> = vec![
        "kanbus".to_string(),
        "create".to_string(),
        "Test".to_string(),
        "Issue".to_string(),
        "--no-validate".to_string(),
        "--description".to_string(),
        description,
    ];

    let cwd = world
        .working_directory
        .as_ref()
        .expect("working directory not set");

    match run_from_args_with_output(args_with_desc, cwd.as_path()) {
        Ok(output) => {
            world.exit_code = Some(0);
            world.stdout = Some(output.stdout);
            world.stderr = Some(String::new());
        }
        Err(error) => {
            world.exit_code = Some(1);
            world.stdout = Some(String::new());
            world.stderr = Some(error.to_string());
        }
    }
}

#[when(expr = "I comment on {string} with text containing:")]
fn when_comment_with_text(world: &mut KanbusWorld, step: &Step, identifier: String) {
    let text = step.docstring().expect("docstring").trim().to_string();
    std::env::set_var("KANBUS_USER", "dev@example.com");
    let args: Vec<String> = vec![
        "kanbus".to_string(),
        "comment".to_string(),
        identifier,
        text,
    ];

    let cwd = world
        .working_directory
        .as_ref()
        .expect("working directory not set");

    match run_from_args_with_output(args, cwd.as_path()) {
        Ok(output) => {
            world.exit_code = Some(0);
            world.stdout = Some(output.stdout);
            world.stderr = Some(String::new());
        }
        Err(error) => {
            world.exit_code = Some(1);
            world.stdout = Some(String::new());
            world.stderr = Some(error.to_string());
        }
    }
}

#[when(expr = "I comment on {string} with --no-validate and text containing:")]
fn when_comment_no_validate_with_text(world: &mut KanbusWorld, step: &Step, identifier: String) {
    let text = step.docstring().expect("docstring").trim().to_string();
    std::env::set_var("KANBUS_USER", "dev@example.com");
    let args: Vec<String> = vec![
        "kanbus".to_string(),
        "comment".to_string(),
        identifier,
        "--no-validate".to_string(),
        text,
    ];

    let cwd = world
        .working_directory
        .as_ref()
        .expect("working directory not set");

    match run_from_args_with_output(args, cwd.as_path()) {
        Ok(output) => {
            world.exit_code = Some(0);
            world.stdout = Some(output.stdout);
            world.stderr = Some(String::new());
        }
        Err(error) => {
            world.exit_code = Some(1);
            world.stdout = Some(String::new());
            world.stderr = Some(error.to_string());
        }
    }
}

#[when(expr = "I update {string} with description containing:")]
fn when_update_with_description(world: &mut KanbusWorld, step: &Step, identifier: String) {
    let description = step.docstring().expect("docstring").trim().to_string();
    let args: Vec<String> = vec![
        "kanbus".to_string(),
        "update".to_string(),
        identifier,
        "--description".to_string(),
        description,
    ];

    let cwd = world
        .working_directory
        .as_ref()
        .expect("working directory not set");

    match run_from_args_with_output(args, cwd.as_path()) {
        Ok(output) => {
            world.exit_code = Some(0);
            world.stdout = Some(output.stdout);
            world.stderr = Some(String::new());
        }
        Err(error) => {
            world.exit_code = Some(1);
            world.stdout = Some(String::new());
            world.stderr = Some(error.to_string());
        }
    }
}

#[when("I validate code blocks directly:")]
fn when_validate_code_blocks_directly(world: &mut KanbusWorld, step: &Step) {
    let text = step.docstring().expect("docstring").trim();
    match validate_code_blocks(text) {
        Ok(()) => world.validation_error = None,
        Err(error) => world.validation_error = Some(error.to_string()),
    }
}

#[when(expr = "I validate external tool {string} directly with content:")]
fn when_validate_external_tool_directly(world: &mut KanbusWorld, tool: String, step: &Step) {
    let content = step.docstring().expect("docstring").trim().to_string();
    let block = CodeBlock {
        language: tool.clone(),
        content,
        start_line: 1,
    };
    let result = validate_external_tool(&block, &tool);
    match result {
        Ok(()) => world.validation_error = None,
        Err(error) => world.validation_error = Some(error.to_string()),
    }
}

#[then("the code block validation should succeed")]
fn then_code_block_validation_succeeds(world: &mut KanbusWorld) {
    assert!(world.validation_error.is_none());
}

#[then(expr = "the code block validation should fail with {string}")]
fn then_code_block_validation_fails(world: &mut KanbusWorld, message: String) {
    let error = world.validation_error.as_deref().unwrap_or("");
    assert!(
        error.contains(&message),
        "expected error to contain '{message}'"
    );
}

#[given("a registered user")]
fn given_registered_user(_world: &mut KanbusWorld) {}

#[when("they log in")]
fn when_they_log_in(_world: &mut KanbusWorld) {}

#[then("they see the dashboard")]
fn then_they_see_dashboard(_world: &mut KanbusWorld) {}

fn ensure_tool_stub(world: &mut KanbusWorld, tool: &str, script: &str) {
    if world.original_path.is_none() {
        world.original_path = Some(std::env::var("PATH").ok());
    }
    if world.external_tool_dir.is_none() {
        world.external_tool_dir =
            Some(tempfile::TempDir::new().expect("create external tool temp dir"));
    }
    let dir = world
        .external_tool_dir
        .as_ref()
        .expect("external tool dir")
        .path();
    let tool_path = dir.join(tool);
    fs::write(&tool_path, format!("#!/bin/sh\n{script}")).expect("write tool stub");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut permissions = fs::metadata(&tool_path)
            .expect("tool metadata")
            .permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&tool_path, permissions).expect("set tool permissions");
    }
    let new_path = match world.original_path.as_ref().and_then(|value| value.clone()) {
        Some(existing) => format!("{}:{}", dir.display(), existing),
        None => dir.display().to_string(),
    };
    std::env::set_var("PATH", new_path);
}

fn validate_external_tool(block: &CodeBlock, tool: &str) -> Result<(), KanbusError> {
    match tool {
        "mmdc" | "plantuml" | "d2" => {
            validate_code_blocks(&format!("```{}\n{}\n```", tool, block.content))
        }
        _ => Ok(()),
    }
}