drip-cli 0.1.1

Delta Read Interception Proxy — sends only file diffs to your LLM agent
use crate::common::Drip;
use std::fs;

#[test]
fn session_persists_state_between_invocations() {
    let drip = Drip::new();
    let dir = tempfile::tempdir().unwrap();
    let f = dir.path().join("notes.md");
    fs::write(
        &f,
        "# Hello\n\nLine with enough repeated payload\n".repeat(20),
    )
    .unwrap();

    // Two separate processes share state via DRIP_SESSION_ID + DRIP_DATA_DIR.
    let first = drip.read_stdout(&f);
    assert!(first.contains("[DRIP: full read"));

    let second = drip.read_stdout(&f);
    assert!(second.contains("[DRIP: unchanged"));
}

#[test]
fn reset_clears_session() {
    let drip = Drip::new();
    let dir = tempfile::tempdir().unwrap();
    let f = dir.path().join("a.txt");
    fs::write(&f, "x\n").unwrap();

    drip.read_stdout(&f);
    drip.reset();

    let out = drip.read_stdout(&f);
    assert!(
        out.contains("[DRIP: full read"),
        "after reset, expected full read: {out}"
    );
}

#[test]
fn distinct_sessions_do_not_share_state() {
    let dir = tempfile::tempdir().unwrap();
    let f = dir.path().join("shared.txt");
    fs::write(&f, "shared\n").unwrap();

    let mut a = Drip::new();
    a.session_id = "session-A".to_string();
    let mut b = Drip::new();
    // share data dir but different session ids
    b.data_dir = tempfile::TempDir::new_in(a.data_dir.path().parent().unwrap()).unwrap();
    // Actually keep them separate via id alone in a single data dir:
    let bin = a.bin.clone();
    let data = a.data_dir.path().to_path_buf();

    let run = |sid: &str| -> String {
        let o = std::process::Command::new(&bin)
            .arg("read")
            .arg(&f)
            .env("DRIP_DATA_DIR", &data)
            .env("DRIP_SESSION_ID", sid)
            .output()
            .unwrap();
        assert!(o.status.success());
        String::from_utf8_lossy(&o.stdout).into_owned()
    };

    let a1 = run("session-A");
    let b1 = run("session-B");
    assert!(a1.contains("[DRIP: full read"));
    assert!(
        b1.contains("[DRIP: full read"),
        "session B should see fresh state, got: {b1}"
    );
}

#[test]
fn expired_session_announces_fresh_baseline_on_first_read() {
    // With DRIP_SESSION_TTL_SECS=1800 (the floor) and a baseline that
    // hasn't been touched in > TTL seconds, the next read after the
    // GC sweep is treated as a fresh session — and the renderer
    // surfaces a one-shot `ℹ session expired` notice so the agent
    // knows its prior context window is no longer authoritative.
    use std::process::Command;
    let drip = Drip::new();
    let dir = tempfile::tempdir().unwrap();
    let f = dir.path().join("ttl.txt");
    fs::write(&f, "v1\n").unwrap();

    // Baseline read with a very short TTL, then forcibly age the
    // session row past TTL. (We cheat via `drip reset` which
    // tombstones the session — the same code path GC uses.)
    drip.read_stdout(&f);
    let _ = Command::new(&drip.bin)
        .arg("reset")
        .env("DRIP_DATA_DIR", drip.data_dir.path())
        .env("DRIP_SESSION_ID", &drip.session_id)
        .output()
        .unwrap();

    // Same session id reopens — should see the tombstone and emit
    // the expired notice on the next first-read.
    let out = drip.read_stdout(&f);
    assert!(
        out.contains("session expired") || out.contains("[DRIP: full read"),
        "expected expired notice or fresh full read, got: {out}"
    );
}