o7 0.1.1

O7 workflow DSL runner
Documentation
use assert_cmd::Command;
use o7::state::types::{
    CallStackEntry, HarnessEvent, HarnessEventKind, HarnessEventStream, PersistedRunState,
    RunStatus,
};
use predicates::prelude::*;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

/// Write a minimal valid .7 workflow file into a temp directory, returning the file path.
fn write_test_workflow(dir: &TempDir) -> PathBuf {
    let file = dir.path().join("test.7");
    fs::write(
        &file,
        "version 1\nworkflow main\n  exec\n    harness: h\n    prompt: \"hi\"\n",
    )
    .unwrap();
    file
}

#[test]
fn test_version_flag() {
    Command::cargo_bin("o7")
        .unwrap()
        .arg("--version")
        .assert()
        .success()
        .stdout(predicate::str::contains("o7"));
}

#[test]
fn test_help_flag() {
    Command::cargo_bin("o7")
        .unwrap()
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("workflow DSL runner"));
}

#[test]
fn test_help_run_subcommand() {
    Command::cargo_bin("o7")
        .unwrap()
        .args(["run", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Run a workflow"));
}

#[test]
fn test_lint_valid_file() {
    let dir = TempDir::new().unwrap();
    let file = write_test_workflow(&dir);

    Command::cargo_bin("o7")
        .unwrap()
        .args(["lint", file.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("No errors found"));
}

#[test]
fn test_lint_invalid_file() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("test.7");
    fs::write(&file, "workflow main\n").unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args(["lint", file.to_str().unwrap()])
        .assert()
        .failure();
}

#[test]
fn test_lint_nonexistent_file() {
    Command::cargo_bin("o7")
        .unwrap()
        .args(["lint", "/nonexistent/test.7"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("file not found"));
}

#[test]
fn test_parse_valid_file() {
    let dir = TempDir::new().unwrap();
    let file = write_test_workflow(&dir);

    Command::cargo_bin("o7")
        .unwrap()
        .args(["parse", file.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("OK"));
}

#[test]
fn test_parse_valid_file_shows_workflow_names() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("test.7");
    fs::write(
        &file,
        "version 1\nworkflow main\n  exec\n    harness: h\n    prompt: \"hi\"\nworkflow deploy\n  exec\n    harness: h\n    prompt: \"go\"\n",
    ).unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args(["parse", file.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("2 workflow(s)"))
        .stdout(predicate::str::contains("main"))
        .stdout(predicate::str::contains("deploy"));
}

#[test]
fn test_parse_invalid_file() {
    let dir = TempDir::new().unwrap();
    let file = dir.path().join("test.7");
    fs::write(&file, "workflow main\n").unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args(["parse", file.to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains("Parse errors"));
}

#[test]
fn test_tui_fails_without_terminal() {
    // The TUI requires a real terminal; in CI/test it fails with an OS error.
    let dir = TempDir::new().unwrap();
    let file = write_test_workflow(&dir);

    Command::cargo_bin("o7")
        .unwrap()
        .args(["tui", file.to_str().unwrap()])
        .assert()
        .failure()
        .stderr(predicate::str::contains("TUI error:"));
}

#[test]
fn test_inspect_no_runs() {
    let dir = TempDir::new().unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args(["inspect", "--project", dir.path().to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("No runs found"));
}

#[test]
fn test_inspect_nonexistent_run() {
    let dir = TempDir::new().unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args([
            "inspect",
            "nonexistent-run",
            "--project",
            dir.path().to_str().unwrap(),
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("Run not found"));
}

#[test]
fn test_inspect_shows_harness_event_summary() {
    let dir = TempDir::new().unwrap();
    let state = PersistedRunState {
        schema_version: 1,
        run_id: "run-harness-inspect".to_string(),
        timestamp: "2026-04-10T00:00:00Z".to_string(),
        root_workflow: "main".to_string(),
        status: RunStatus::Paused,
        call_stack: vec![CallStackEntry {
            workflow: "main".to_string(),
            step_index: 0,
        }],
        steps: std::collections::HashMap::new(),
        branches: None,
        collected_outputs: None,
        harness_event_log: Some(vec![HarnessEvent {
            sequence: 0,
            exec_ordinal: 0,
            stream: HarnessEventStream::Stdout,
            kind: HarnessEventKind::Json,
            raw: "{\"type\":\"message\"}".to_string(),
            parsed: Some(serde_json::json!({"type": "message"})),
            step_path: Some(vec!["main".to_string(), "exec:mock".to_string()]),
            boundary_index: Some(0),
            timestamp: None,
        }]),
        safe_boundaries: Vec::new(),
        current_boundary_index: 0,
        event_log: Some(Vec::new()),
    };
    o7::state::persistence::save_state(&state, dir.path().to_str().unwrap()).unwrap();

    Command::cargo_bin("o7")
        .unwrap()
        .args([
            "inspect",
            "run-harness-inspect",
            "--project",
            dir.path().to_str().unwrap(),
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("Harness events: 1"))
        .stdout(predicate::str::contains(
            "[stdout/json] #0 main > exec:mock message",
        ));
}

#[test]
fn test_no_args_shows_help() {
    Command::cargo_bin("o7")
        .unwrap()
        .assert()
        .success()
        .stdout(predicate::str::contains("Usage"));
}

#[test]
fn test_wiz_appears_in_main_help() {
    Command::cargo_bin("o7")
        .unwrap()
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("wiz"));
}

#[test]
fn test_wiz_subcommand_help() {
    Command::cargo_bin("o7")
        .unwrap()
        .args(["wiz", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Interactively configure"));
}

#[test]
fn test_run_no_args_no_o7md_fails_with_helpful_error() {
    let dir = TempDir::new().unwrap();
    // Directory has no o7.md — should fail with a message mentioning o7.md
    Command::cargo_bin("o7")
        .unwrap()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .failure()
        .stderr(predicate::str::contains("o7.md"));
}

#[test]
fn test_run_no_args_finds_o7md_in_cwd() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("o7.md"),
        "version 1\nworkflow main\n  exec\n    harness: h\n    prompt: \"hi\"\n",
    )
    .unwrap();
    // o7.md exists — run will fail (no harness config), but NOT with an "o7.md" error message
    Command::cargo_bin("o7")
        .unwrap()
        .current_dir(dir.path())
        .arg("run")
        .assert()
        .failure()
        .stderr(predicate::str::contains("no o7.md found").not())
        .stderr(predicate::str::contains("no file specified").not());
}

#[test]
fn test_tui_no_args_no_o7md_fails_with_helpful_error() {
    let dir = TempDir::new().unwrap();
    Command::cargo_bin("o7")
        .unwrap()
        .current_dir(dir.path())
        .arg("tui")
        .assert()
        .failure()
        .stderr(predicate::str::contains("o7.md"));
}