magi-code 0.82.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[test]
fn workspace_labels_keep_meaningful_words_and_boundaries() {
    for (title, expected) in [
        (
            "Audit the Value of Output Compression",
            Some("audit-value-output-compression"),
        ),
        ("Fix: THE parser / not the API!", Some("fix-parser-not-api")),
        ("The of", Some("the-of")),
        ("---", None),
        ("Réparer le Café", Some("réparer-le-café")),
    ] {
        assert_eq!(workspace_title_slug(title).as_deref(), expected);
    }
    assert_eq!(
        workspace_title_slug(&format!("{} next", "x".repeat(63))).unwrap(),
        "x".repeat(63)
    );
}

struct WorkspaceTransport {
    lines: Mutex<Vec<serde_json::Value>>,
    response: serde_json::Value,
}

impl HerdrTransport for WorkspaceTransport {
    fn write_line(&self, _: &Path, line: &str) -> io::Result<()> {
        self.lines.lock().unwrap().push(serde_json::from_str(line)?);
        Ok(())
    }

    fn request_line(&self, _: &Path, line: &str) -> io::Result<String> {
        let request: serde_json::Value = serde_json::from_str(line)?;
        let mut response = self.response.clone();
        response["id"] = request["id"].clone();
        self.lines.lock().unwrap().push(request);
        Ok(response.to_string())
    }
}

#[test]
fn generated_title_renames_the_callers_resolved_workspace() {
    let transport = Arc::new(WorkspaceTransport {
        lines: Mutex::default(),
        response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
    });
    let owner =
        HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
    let reporter = owner.reporter();
    reporter.report_agent_session("session-a");
    transport.lines.lock().unwrap().clear();
    reporter.rename_workspace_from_generated_title(
        "session-a",
        "Audit the Value of Output Compression",
    );
    let lines = transport.lines.lock().unwrap();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[0]["method"], "pane.current");
    assert_eq!(
        lines[0]["params"],
        serde_json::json!({"caller_pane_id": "pane-caller"})
    );
    assert_eq!(lines[1]["method"], "workspace.rename");
    assert_eq!(
        lines[1]["params"],
        serde_json::json!({"workspace_id": "wMoved", "label": "audit-value-output-compression"})
    );
}

#[test]
fn unresolved_workspace_never_falls_back_to_global_focus() {
    for response in [
        serde_json::json!({"result": {"pane": null}}),
        serde_json::json!({"error": {"message": "missing pane"}}),
        serde_json::json!({"result": {"pane": {"workspace_id": ""}}}),
    ] {
        let transport = Arc::new(WorkspaceTransport {
            lines: Mutex::default(),
            response,
        });
        let owner = HerdrOwner::new_with_transport(
            "pane-caller".into(),
            "/unused".into(),
            transport.clone(),
        );
        let reporter = owner.reporter();
        reporter.report_agent_session("session-a");
        transport.lines.lock().unwrap().clear();
        reporter.rename_workspace_from_generated_title("session-a", "Fix parser");
        assert_eq!(transport.lines.lock().unwrap().len(), 1);
    }
}

#[test]
fn released_owner_prevents_workspace_rename() {
    let transport = Arc::new(WorkspaceTransport {
        lines: Mutex::default(),
        response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
    });
    let owner =
        HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
    let reporter = owner.reporter();
    reporter.report_agent_session("session-a");
    owner.release();
    transport.lines.lock().unwrap().clear();
    reporter.rename_workspace_from_generated_title("session-a", "Fix parser");
    assert!(transport.lines.lock().unwrap().is_empty());
}

#[test]
fn delayed_title_for_previous_session_skips_workspace_lookup() {
    let transport = Arc::new(WorkspaceTransport {
        lines: Mutex::default(),
        response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
    });
    let owner =
        HerdrOwner::new_with_transport("pane-caller".into(), "/unused".into(), transport.clone());
    let reporter = owner.reporter();
    reporter.report_agent_session_with_source("session-a", HerdrSessionStartSource::Startup);
    reporter.report_agent_session_with_source("session-b", HerdrSessionStartSource::Select);
    transport.lines.lock().unwrap().clear();
    reporter.rename_workspace_from_generated_title("session-a", "Old title");
    assert!(transport.lines.lock().unwrap().is_empty());
    reporter.rename_workspace_from_generated_title("session-b", "Current title");
    let lines = transport.lines.lock().unwrap();
    assert_eq!(lines.len(), 2);
    assert_eq!(lines[1]["params"]["label"], "current-title");
}

struct DelayedWorkspaceTransport {
    recording: WorkspaceTransport,
    lookup_started: crossbeam_channel::Sender<()>,
    resume_lookup: crossbeam_channel::Receiver<()>,
}

impl HerdrTransport for DelayedWorkspaceTransport {
    fn write_line(&self, path: &Path, line: &str) -> io::Result<()> {
        self.recording.write_line(path, line)
    }

    fn request_line(&self, path: &Path, line: &str) -> io::Result<String> {
        self.lookup_started.send(()).unwrap();
        self.resume_lookup
            .recv_timeout(std::time::Duration::from_secs(5))
            .unwrap();
        self.recording.request_line(path, line)
    }
}

#[test]
fn session_switch_during_workspace_lookup_rejects_stale_rename() {
    // Returning to A still invalidates the lookup started before selecting B.
    for return_to_a in [false, true] {
        let (lookup_started, started) = crossbeam_channel::bounded(1);
        let (resume, resume_lookup) = crossbeam_channel::bounded(1);
        let transport = Arc::new(DelayedWorkspaceTransport {
            recording: WorkspaceTransport {
                lines: Mutex::default(),
                response: serde_json::json!({"result": {"pane": {"workspace_id": "wMoved"}}}),
            },
            lookup_started,
            resume_lookup,
        });
        let owner = HerdrOwner::new_with_transport(
            "pane-caller".into(),
            "/unused".into(),
            transport.clone(),
        );
        let reporter = owner.reporter();
        reporter.report_agent_session("session-a");
        let worker_reporter = reporter.clone();
        let worker = std::thread::spawn(move || {
            worker_reporter.rename_workspace_from_generated_title("session-a", "Old title");
        });
        started
            .recv_timeout(std::time::Duration::from_secs(5))
            .unwrap();
        reporter.report_agent_session_with_source("session-b", HerdrSessionStartSource::Select);
        if return_to_a {
            reporter.report_agent_session_with_source("session-a", HerdrSessionStartSource::Select);
        }
        transport.recording.lines.lock().unwrap().clear();
        resume.send(()).unwrap();
        worker.join().unwrap();
        let lines = transport.recording.lines.lock().unwrap();
        assert_eq!(lines.len(), 1);
        assert_eq!(lines[0]["method"], "pane.current");
    }
}

#[cfg(unix)]
#[test]
fn workspace_lookup_reads_split_response_and_times_out_without_newline() {
    use std::io::{BufRead, BufReader, Write};
    use std::os::unix::net::UnixListener;
    for complete in [true, false] {
        let directory = tempfile::TempDir::new().unwrap();
        let socket = directory.path().join("herdr.sock");
        let listener = UnixListener::bind(&socket).unwrap();
        let server = std::thread::spawn(move || {
            let (mut stream, _) = listener.accept().unwrap();
            let mut request = String::new();
            BufReader::new(&mut stream).read_line(&mut request).unwrap();
            assert_eq!(request, "lookup\n");
            stream.write_all(b"{\"result\":").unwrap();
            if complete {
                stream.write_all(b"null}\nignored").unwrap();
            } else {
                std::thread::sleep(Duration::from_millis(250));
            }
        });
        let started = std::time::Instant::now();
        let result = UnixSocketTransport.request_line(&socket, "lookup\n");
        if complete {
            assert_eq!(result.unwrap(), "{\"result\":null}");
        } else {
            assert_eq!(result.unwrap_err().kind(), io::ErrorKind::TimedOut);
        }
        assert!(started.elapsed() < Duration::from_secs(1));
        server.join().unwrap();
    }
}