agent_trace/commands/
connect.rs1use 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 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}