i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
//! End-to-end integration tests for the `share` subcommand.
//!
//! These spawn the actual `i-self` binary as a subprocess (via the
//! `CARGO_BIN_EXE_i-self` env var that `cargo test` injects) and exercise
//! the user-facing CLI surface: list → export → import → re-list.
//!
//! Each test seeds a fixture session in a temp dir, points the relevant
//! `ISELF_*_DIR` env var at it, and verifies the cycle works end-to-end.
//! Internal tests in `src/share/*` cover the parser/importer logic in
//! isolation; this file is the only place the actual CLI dispatch is
//! exercised.

use std::path::Path;
use std::process::Command;

fn bin() -> Command {
    let mut cmd = Command::new(env!("CARGO_BIN_EXE_i-self"));
    // Mute the tracing output so test stderr stays readable; the CLI's
    // success/failure is determined by exit code + stdout payload.
    cmd.env("RUST_LOG", "error");
    cmd
}

fn write(path: &Path, content: &str) {
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(path, content).unwrap();
}

#[test]
fn share_ls_finds_seeded_goose_session() {
    let tmp = tempfile::tempdir().unwrap();
    write(
        &tmp.path().join("test-session.jsonl"),
        r#"{"role":"user","created":1715000000,"content":[{"type":"text","text":"refactor auth"}]}
{"role":"assistant","content":[{"type":"text","text":"on it"}]}
"#,
    );

    let out = bin()
        .args(["share", "ls", "--provider", "goose"])
        .env("ISELF_GOOSE_DIR", tmp.path())
        // Empty out the other providers' dirs so they don't list real
        // sessions from the user's actual home and pollute the output.
        .env("ISELF_CODEX_DIR", tmp.path().join("__empty__"))
        .env("ISELF_CONTINUE_DIR", tmp.path().join("__empty__"))
        .env("ISELF_OPENCODE_DIR", tmp.path().join("__empty__"))
        .env("HOME", tmp.path()) // hide ~/.claude, ~/.aider history etc.
        .output()
        .expect("spawn");

    assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("test-session"), "got: {}", stdout);
    assert!(stdout.contains("[goose"), "should label provider: {}", stdout);
    assert!(stdout.contains("refactor auth"), "should show title hint: {}", stdout);
}

#[test]
fn share_export_to_json_then_import_to_aider_round_trips() {
    let src_dir = tempfile::tempdir().unwrap();
    let dst_dir = tempfile::tempdir().unwrap();
    let work_dir = tempfile::tempdir().unwrap();
    let aider_project = work_dir.path().join("aider-target");
    std::fs::create_dir_all(&aider_project).unwrap();

    // Seed a Goose session and export it via the CLI.
    write(
        &src_dir.path().join("src-session.jsonl"),
        r#"{"role":"user","created":1715000000,"content":[{"type":"text","text":"first user turn"}]}
{"role":"assistant","content":[{"type":"text","text":"first assistant reply"}]}
"#,
    );

    let exported = work_dir.path().join("exported.json");
    let export_out = bin()
        .args([
            "share",
            "export",
            "src-session",
            "--format",
            "json",
            "--output",
            exported.to_str().unwrap(),
        ])
        .env("ISELF_GOOSE_DIR", src_dir.path())
        .env("HOME", dst_dir.path()) // keep claude/aider lookups isolated
        .output()
        .expect("spawn export");
    assert!(
        export_out.status.success(),
        "export failed: {}",
        String::from_utf8_lossy(&export_out.stderr)
    );
    assert!(exported.exists(), "export should have written {:?}", exported);
    let json = std::fs::read_to_string(&exported).unwrap();
    assert!(json.contains("first user turn"));
    assert!(json.contains("first assistant reply"));

    // Import that JSON into the Aider target.
    let import_out = bin()
        .args([
            "share",
            "import",
            exported.to_str().unwrap(),
            "--target",
            "aider",
            "--project",
            aider_project.to_str().unwrap(),
        ])
        .output()
        .expect("spawn import");
    assert!(
        import_out.status.success(),
        "import failed: {}",
        String::from_utf8_lossy(&import_out.stderr)
    );
    let history = aider_project.join(".aider.chat.history.md");
    assert!(history.exists(), "aider history should have been created");
    let body = std::fs::read_to_string(&history).unwrap();
    assert!(body.contains("[i-self import]"), "provenance line missing");
    assert!(body.contains("> first user turn"), "user turn missing");
    assert!(body.contains("first assistant reply"), "assistant reply missing");
}

#[test]
fn share_import_unknown_target_fails_cleanly() {
    let tmp = tempfile::tempdir().unwrap();
    let session = tmp.path().join("session.json");
    // Minimal valid SharedSession JSON.
    std::fs::write(
        &session,
        r#"{"provider":"x","id":"y","project_path":null,"started_at":null,"messages":[]}"#,
    )
    .unwrap();

    let out = bin()
        .args([
            "share",
            "import",
            session.to_str().unwrap(),
            "--target",
            "definitely-not-a-real-target",
        ])
        .output()
        .expect("spawn");
    assert!(!out.status.success(), "expected failure for bad target");
    let stderr = String::from_utf8_lossy(&out.stderr);
    let stdout = String::from_utf8_lossy(&out.stdout);
    let combined = format!("{}{}", stderr, stdout);
    assert!(combined.contains("unknown target"), "expected helpful error, got: {}", combined);
}

#[test]
fn share_export_to_html_produces_self_contained_file() {
    let src_dir = tempfile::tempdir().unwrap();
    let work_dir = tempfile::tempdir().unwrap();
    write(
        &src_dir.path().join("html-test.jsonl"),
        r#"{"role":"user","content":"Hello <world>"}
{"role":"assistant","content":"```rust\nfn main() {}\n```"}
"#,
    );
    let out_path = work_dir.path().join("session.html");
    let out = bin()
        .args([
            "share",
            "export",
            "html-test",
            "--format",
            "html",
            "--output",
            out_path.to_str().unwrap(),
        ])
        .env("ISELF_GOOSE_DIR", src_dir.path())
        .env("HOME", work_dir.path())
        .output()
        .expect("spawn");
    assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
    let html = std::fs::read_to_string(&out_path).unwrap();
    assert!(html.starts_with("<!DOCTYPE html>"));
    // No external assets — embedded styling, no scripts.
    assert!(html.contains("<style>"));
    assert!(!html.contains("<script"));
    // User content was escaped (<world> can't survive as a real tag).
    assert!(!html.contains("Hello <world>"));
    assert!(html.contains("&lt;world&gt;"));
    // Code fence got promoted to <pre>.
    assert!(html.contains("data-lang=\"rust\""));
}

#[test]
fn vuln_check_known_safe_package_returns_zero_vulns() {
    // Sanity check: the OSV path works and exit code 0 for a clean package.
    // We use a recent stable version of `serde` which has no advisories.
    // Marked #[ignore] because it hits the network. To run:
    //   cargo test --test share_cycle -- --ignored vuln_check_known_safe
    if std::env::var("ISELF_RUN_NETWORK_TESTS").is_err() {
        eprintln!("skipping network-touching integration test (set ISELF_RUN_NETWORK_TESTS=1)");
        return;
    }
    let out = bin()
        .args(["vuln", "check", "serde", "1.0.200", "--ecosystem", "cargo"])
        .output()
        .expect("spawn");
    // Even if vulns existed, exit code 0 is fine — `check` doesn't gate on
    // findings; only `scan` exits 2 on partial scans.
    assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
}