Skip to main content

agent_trace/commands/
connect.rs

1use crate::observability::CliOutput;
2use crate::session::{self, AgentSession};
3use anyhow::Result;
4use std::path::Path;
5
6pub fn run_connect(root: &Path, name: &str, output: &dyn CliOutput) -> Result<()> {
7    if let Some(existing) = session::load_session(root) {
8        if !existing.is_stale() {
9            anyhow::bail!(
10                "Already connected as '{}'. Run 'agent-trace disconnect' first.",
11                existing.name
12            );
13        }
14    }
15
16    let started = session::start_session(root, name, "cli")?;
17    print_connect_message(&started, output)?;
18    Ok(())
19}
20
21pub fn run_disconnect(root: &Path, output: &dyn CliOutput) -> Result<()> {
22    if let Some(existing) = session::load_session(root) {
23        session::remove_session(root)?;
24        output.line(&format!(
25            "Disconnected '{}'. Actor reverts to User.",
26            existing.name
27        ))?;
28    } else {
29        output.line("Not connected (no agent session active).")?;
30    }
31    Ok(())
32}
33
34fn print_connect_message(s: &AgentSession, output: &dyn CliOutput) -> Result<()> {
35    output.line(&format!(
36        "Connected as '{}'. Session {} (transport: {}).",
37        s.name, s.session_id, s.transport
38    ))?;
39    output.line("Agent writes will be permission-checked and trace-linked.")?;
40    output.line("Running summary: agent-trace resume show")?;
41    output.line("MCP agents: call get_resume_context on reconnect")?;
42    Ok(())
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::observability::NoopOutput;
49    use crate::session::{AgentState, LOCK_FILE};
50    use crate::types::Actor;
51    use tempfile::TempDir;
52
53    fn setup(tmp: &TempDir) -> std::path::PathBuf {
54        let root = tmp.path().to_path_buf();
55        std::fs::create_dir_all(root.join(".agent-trace/locks")).unwrap();
56        root
57    }
58
59    #[test]
60    fn test_connect_writes_lock_file() {
61        let tmp = TempDir::new().unwrap();
62        let root = setup(&tmp);
63        run_connect(&root, "claude", &NoopOutput).unwrap();
64        let lock = root.join(LOCK_FILE);
65        assert!(lock.exists(), "lock file should exist after connect");
66        let content = std::fs::read_to_string(&lock).unwrap();
67        assert!(
68            content.contains("claude"),
69            "lock file should contain agent name"
70        );
71        assert!(
72            !content.contains("pid"),
73            "new lock file should not contain pid"
74        );
75    }
76
77    #[test]
78    fn test_connect_error_if_already_connected() {
79        let tmp = TempDir::new().unwrap();
80        let root = setup(&tmp);
81        run_connect(&root, "claude", &NoopOutput).unwrap();
82        let result = run_connect(&root, "another", &NoopOutput);
83        assert!(result.is_err(), "double connect should fail");
84        let msg = result.unwrap_err().to_string();
85        assert!(
86            msg.contains("claude"),
87            "error should name the existing agent"
88        );
89    }
90
91    #[test]
92    fn test_disconnect_removes_lock_file() {
93        let tmp = TempDir::new().unwrap();
94        let root = setup(&tmp);
95        run_connect(&root, "claude", &NoopOutput).unwrap();
96        run_disconnect(&root, &NoopOutput).unwrap();
97        assert!(
98            !root.join(LOCK_FILE).exists(),
99            "lock file should be gone after disconnect"
100        );
101    }
102
103    #[test]
104    fn test_disconnect_noop_if_not_connected() {
105        let tmp = TempDir::new().unwrap();
106        let root = setup(&tmp);
107        // Should not panic or error
108        run_disconnect(&root, &NoopOutput).unwrap();
109    }
110
111    #[test]
112    fn test_agent_state_reads_connect_lock() {
113        let tmp = TempDir::new().unwrap();
114        let root = setup(&tmp);
115        run_connect(&root, "my-agent", &NoopOutput).unwrap();
116        let state = AgentState::new(None);
117        assert_eq!(
118            state.current_actor(&root),
119            Actor::Agent {
120                name: "my-agent".into()
121            }
122        );
123    }
124
125    #[test]
126    fn test_agent_state_reverts_to_user_after_disconnect() {
127        let tmp = TempDir::new().unwrap();
128        let root = setup(&tmp);
129        run_connect(&root, "my-agent", &NoopOutput).unwrap();
130        run_disconnect(&root, &NoopOutput).unwrap();
131        let state = AgentState::new(None);
132        assert_eq!(state.current_actor(&root), Actor::User);
133    }
134}