atproto-devtool 0.1.1

A multitool for the atproto developer ecosystem
Documentation
//! CLI tests for the labeler conformance suite: --help output, --no-color, --verbose, and
//! invalid-argument error codes.

use assert_cmd::Command;

/// `test labeler --help` lists every user-facing flag and the target argument.
#[test]
fn help_lists_all_flags() {
    let mut cmd = Command::cargo_bin("atproto-devtool").expect("bin not found");
    let output = cmd
        .arg("test")
        .arg("labeler")
        .arg("--help")
        .output()
        .expect("failed to run atproto-devtool");

    assert!(
        output.status.success(),
        "--help should exit successfully, got {:?}",
        output.status,
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    for needle in [
        "--did",
        "--subscribe-timeout",
        "--verbose",
        "--no-color",
        "--commit-report",
        "--force-self-mint",
        "--self-mint-curve",
        "--report-subject-did",
        "<TARGET>",
    ] {
        assert!(
            stdout.contains(needle),
            "--help stdout missing expected token `{needle}`, got:\n{stdout}",
        );
    }
}

/// Invalid CLI args (missing required argument) exit with code 2.
#[test]
fn bootstrap_failure_exits_two() {
    let mut cmd = Command::cargo_bin("atproto-devtool").expect("bin not found");
    let assert = cmd.arg("test").arg("labeler").assert();

    // Exit code should be 2 for bootstrap/CLI parsing errors.
    // This test invokes the labeler without a required target argument.
    assert.code(2);
}

/// `--no-color` causes the rendered report to be emitted without ANSI escape
/// sequences. Exercises a real pipeline run against an invalid `did:web` target
/// that fails fast on DNS so the command reaches report rendering.
#[test]
fn no_color_renders_without_ansi() {
    let mut cmd = Command::cargo_bin("atproto-devtool").expect("bin not found");
    cmd.env_remove("NO_COLOR");
    cmd.arg("test")
        .arg("labeler")
        .arg("--no-color")
        .arg("did:web:nonexistent.invalid");

    let output = cmd.output().expect("failed to run atproto-devtool");
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        !stdout.contains("\x1b["),
        "Stdout must not contain ANSI escape sequences with --no-color, got:\n{stdout}",
    );
    assert!(
        stdout.contains("== Identity =="),
        "Stdout should contain a rendered identity section, got:\n{stdout}",
    );
    assert!(
        stdout.contains("[FAIL]") || stdout.contains("[NET]"),
        "Stdout should contain at least one failure or network-error check glyph, got:\n{stdout}",
    );
}

/// `--verbose` emits `DEBUG`-level tracing to stderr. Exercises a real pipeline
/// run so that at least one `tracing::debug!` call site fires.
#[test]
fn verbose_flag_emits_debug_tracing_to_stderr() {
    let mut cmd = Command::cargo_bin("atproto-devtool").expect("bin not found");
    cmd.arg("test")
        .arg("labeler")
        .arg("--verbose")
        .arg("--no-color")
        .arg("did:web:nonexistent.invalid");

    let output = cmd.output().expect("failed to run atproto-devtool");
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        stderr.contains("DEBUG"),
        "Stderr should contain DEBUG tracing output with --verbose, got:\n{stderr}",
    );
}

/// AC8.1: `--handle` without `--app-password` produces a parse error.
#[test]
fn ac8_1_handle_without_app_password_fails() {
    let output = Command::cargo_bin("atproto-devtool")
        .expect("bin")
        .args([
            "test",
            "labeler",
            "alice.bsky.social",
            "--handle",
            "alice.bsky.social",
        ])
        .output()
        .expect("run");
    assert!(
        !output.status.success(),
        "expected parse failure when --handle supplied without --app-password"
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("--app-password") || stderr.contains("app_password"),
        "stderr should mention missing --app-password, got: {stderr}"
    );
}

/// AC8.1: `--app-password` without `--handle` produces a parse error.
#[test]
fn ac8_1_app_password_without_handle_fails() {
    let output = Command::cargo_bin("atproto-devtool")
        .expect("bin")
        .args([
            "test",
            "labeler",
            "alice.bsky.social",
            "--app-password",
            "xxxx-xxxx-xxxx-xxxx",
        ])
        .output()
        .expect("run");
    assert!(
        !output.status.success(),
        "expected parse failure when --app-password supplied without --handle"
    );
}

/// AC8.4: Exit code is non-zero when endpoint is unreachable (NetworkError).
#[test]
fn ac8_4_unreachable_endpoint_nonzero_exit() {
    let output = Command::cargo_bin("atproto-devtool")
        .expect("bin")
        .args(["test", "labeler", "https://doesnt-exist.example.test"])
        .output()
        .expect("run");
    assert_ne!(
        output.status.code(),
        Some(0),
        "expected non-zero exit code for unreachable endpoint"
    );
}