index-cli 1.0.0

Command-line prototype for Index.
//! Integration command-surface coverage tests for the `index` binary.

use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn unique_temp_dir(label: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0_u128, |value| value.as_nanos());
    std::env::temp_dir().join(format!("index-cli-command-surface-{label}-{nanos}"))
}

fn index_command_with_base(base: &Path) -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_index"));
    command
        .env("XDG_CONFIG_HOME", base.join("config").as_os_str())
        .env("XDG_CACHE_HOME", base.join("cache").as_os_str())
        .env("XDG_STATE_HOME", base.join("state").as_os_str());
    command
}

fn output_text(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).to_string()
}

fn write_pack_file(
    path: &Path,
    id: &str,
    selector: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    fs::write(
        path,
        format!(
            r#"{{
  "version": "index.pack/v1",
  "id": "{id}",
  "rules": [
    {{
      "host": "example.org",
      "path_prefix": "/docs",
      "manifest": {{
        "version": "index.idx/v1",
        "scope": "/docs",
        "content": {{ "main_selector": "{selector}" }}
      }}
    }}
  ]
}}"#
        ),
    )?;
    Ok(())
}

#[test]
fn compatibility_pack_install_list_and_remove_cycle() -> Result<(), Box<dyn std::error::Error>> {
    let base = unique_temp_dir("pack-cycle");
    fs::create_dir_all(&base)?;
    let pack_path = base.join("cycle.pack.json");
    write_pack_file(&pack_path, "cycle-pack", "main article")?;

    let install = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("install"),
            pack_path.clone().into_os_string(),
            OsString::from("--user"),
        ])
        .output()?;
    assert!(install.status.success());
    assert!(output_text(&install.stdout).contains("index-compat-pack-install-v1"));

    let list = index_command_with_base(&base)
        .args([OsString::from("compatibility-pack"), OsString::from("list")])
        .output()?;
    assert!(list.status.success());
    let list_stdout = output_text(&list.stdout);
    assert!(list_stdout.contains("index-compat-pack-list-v1"));
    assert!(list_stdout.contains("count: 1"));
    assert!(list_stdout.contains("user\t"));

    let remove = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("remove"),
            OsString::from("cycle-pack"),
        ])
        .output()?;
    assert!(remove.status.success());
    assert!(output_text(&remove.stdout).contains("removed_files: 1"));

    let remove_again = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("remove"),
            OsString::from("cycle-pack"),
        ])
        .output()?;
    assert!(remove_again.status.success());
    assert!(output_text(&remove_again.stdout).contains("removed_files: 0"));
    Ok(())
}

#[test]
fn compatibility_pack_update_mismatch_and_verify_failure() -> Result<(), Box<dyn std::error::Error>>
{
    let base = unique_temp_dir("pack-update-mismatch");
    fs::create_dir_all(&base)?;
    let pack_path = base.join("mismatch.pack.json");
    write_pack_file(&pack_path, "actual-pack", "main article")?;

    let mismatch = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("update"),
            OsString::from("expected-pack"),
            pack_path.clone().into_os_string(),
        ])
        .output()?;
    assert!(!mismatch.status.success());
    assert!(output_text(&mismatch.stderr).contains("pack id mismatch"));

    let verify = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("verify"),
            pack_path.clone().into_os_string(),
            OsString::from("key-1"),
            OsString::from("secret-1"),
            OsString::from("deadbeef"),
        ])
        .output()?;
    assert!(!verify.status.success());
    assert!(output_text(&verify.stderr).contains("signature mismatch"));
    Ok(())
}

#[test]
fn compatibility_pack_rollback_and_runtime_inspect_paths() -> Result<(), Box<dyn std::error::Error>>
{
    let base = unique_temp_dir("pack-rollback-inspect");
    fs::create_dir_all(&base)?;
    let first = base.join("first.pack.json");
    let second = base.join("second.pack.json");
    write_pack_file(&first, "roll-pack", "main first")?;
    write_pack_file(&second, "roll-pack", "main second")?;

    let install_first = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("install"),
            first.clone().into_os_string(),
        ])
        .output()?;
    assert!(install_first.status.success());

    let install_second = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("install"),
            second.clone().into_os_string(),
        ])
        .output()?;
    assert!(install_second.status.success());

    let inspect_enabled = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("inspect"),
            OsString::from("https://example.org/docs/page"),
        ])
        .output()?;
    assert!(inspect_enabled.status.success());
    let inspect_enabled_stdout = output_text(&inspect_enabled.stdout);
    assert!(inspect_enabled_stdout.contains("index-compat-pack-runtime-v1"));
    assert!(inspect_enabled_stdout.contains("selected: user:roll-pack"));

    let rollback = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("rollback"),
            OsString::from("roll-pack"),
        ])
        .output()?;
    assert!(rollback.status.success());
    assert!(output_text(&rollback.stdout).contains("index-compat-pack-rollback-v1"));

    let policy_path = base
        .join("config")
        .join("index")
        .join("compat-pack-runtime.conf");
    if let Some(parent) = policy_path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(
        &policy_path,
        "enabled=false\nallow_user=true\nallow_trusted=true\n",
    )?;

    let inspect_disabled = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("inspect"),
            OsString::from("https://example.org/docs/page"),
        ])
        .output()?;
    assert!(inspect_disabled.status.success());
    let inspect_disabled_stdout = output_text(&inspect_disabled.stdout);
    assert!(inspect_disabled_stdout.contains("policy: enabled=false"));
    assert!(inspect_disabled_stdout.contains("selected: none"));
    Ok(())
}

#[test]
fn auth_assist_import_export_inspect_and_challenge() -> Result<(), Box<dyn std::error::Error>> {
    let base = unique_temp_dir("auth-assist");
    fs::create_dir_all(&base)?;

    let bundle_path = base.join("cookies.store");
    fs::write(
        &bundle_path,
        "index-cookies-v1\nhttps://news.ycombinator.com\tsid=secret; HttpOnly=true; Secure=true\n",
    )?;

    let imported = index_command_with_base(&base)
        .args([
            OsString::from("auth-assist"),
            OsString::from("import"),
            bundle_path.clone().into_os_string(),
        ])
        .output()?;
    assert!(imported.status.success());
    assert!(output_text(&imported.stdout).contains("index-auth-assist-import-v1"));

    let inspected = index_command_with_base(&base)
        .args([
            OsString::from("auth-assist"),
            OsString::from("inspect"),
            OsString::from("news.ycombinator.com"),
        ])
        .output()?;
    assert!(inspected.status.success());
    let inspected_stdout = output_text(&inspected.stdout);
    assert!(inspected_stdout.contains("index-auth-assist-inspect-v1"));
    assert!(inspected_stdout.contains("origin_cookie_names: sid"));

    let export_path = base.join("cookies.exported.store");
    let exported = index_command_with_base(&base)
        .args([
            OsString::from("auth-assist"),
            OsString::from("export"),
            export_path.clone().into_os_string(),
        ])
        .output()?;
    assert!(exported.status.success());
    assert!(output_text(&exported.stdout).contains("index-auth-assist-export-v1"));
    assert!(fs::read_to_string(export_path)?.contains("index-cookies-v1"));

    let diagnosed = index_command_with_base(&base)
        .args([
            OsString::from("auth-assist"),
            OsString::from("diagnose-submit"),
            OsString::from("example.org/login"),
            OsString::from("419"),
            OsString::from("expired"),
            OsString::from("session"),
        ])
        .output()?;
    assert!(diagnosed.status.success());
    assert!(output_text(&diagnosed.stdout).contains("class: session-expired"));

    let challenge = index_command_with_base(&base)
        .args([
            OsString::from("challenge-diagnose"),
            OsString::from("example.org"),
            OsString::from("country not supported in your region"),
        ])
        .output()?;
    assert!(challenge.status.success());
    assert!(output_text(&challenge.stdout).contains("class: geo-gate"));
    Ok(())
}

#[test]
fn compatibility_pack_argument_error_paths_are_actionable() -> Result<(), Box<dyn std::error::Error>>
{
    let base = unique_temp_dir("pack-arg-errors");
    fs::create_dir_all(&base)?;

    let inspect_extra = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("inspect"),
            OsString::from("https://example.org"),
            OsString::from("extra"),
        ])
        .output()?;
    assert!(!inspect_extra.status.success());
    assert!(output_text(&inspect_extra.stderr).contains("too many arguments"));

    let list_extra = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("list"),
            OsString::from("extra"),
        ])
        .output()?;
    assert!(!list_extra.status.success());
    assert!(output_text(&list_extra.stderr).contains("too many arguments"));

    let rollback_missing = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("rollback"),
            OsString::from("missing-pack"),
        ])
        .output()?;
    assert!(!rollback_missing.status.success());
    assert!(output_text(&rollback_missing.stderr).contains("rollback directory"));

    let unknown = index_command_with_base(&base)
        .args([
            OsString::from("compatibility-pack"),
            OsString::from("unknown"),
        ])
        .output()?;
    assert!(!unknown.status.success());
    assert!(output_text(&unknown.stderr).contains("unsupported compatibility-pack command"));
    Ok(())
}

#[test]
fn challenge_diagnose_classifies_multiple_blocked_flow_types()
-> Result<(), Box<dyn std::error::Error>> {
    let base = unique_temp_dir("challenge-cases");
    fs::create_dir_all(&base)?;

    let captcha = index_command_with_base(&base)
        .args([
            OsString::from("challenge-diagnose"),
            OsString::from("example.org"),
            OsString::from("captcha"),
        ])
        .output()?;
    assert!(captcha.status.success());
    assert!(output_text(&captcha.stdout).contains("class: captcha"));

    let auth_wall = index_command_with_base(&base)
        .args([
            OsString::from("challenge-diagnose"),
            OsString::from("example.org"),
            OsString::from("login required"),
        ])
        .output()?;
    assert!(auth_wall.status.success());
    assert!(output_text(&auth_wall.stdout).contains("class: auth-wall"));

    let policy_block = index_command_with_base(&base)
        .args([
            OsString::from("challenge-diagnose"),
            OsString::from("example.org"),
            OsString::from("access denied"),
        ])
        .output()?;
    assert!(policy_block.status.success());
    assert!(output_text(&policy_block.stdout).contains("class: policy-blocked"));
    Ok(())
}