This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-daemon/tests/rename.rs
T
Miguel PalhasandClaude Opus 4.6 88afd0df3f
ci / web (push) Failing after 6s
ci / rust (push) Failing after 51s
ci / flake (push) Successful in 2m41s
feat(maestro): auto-approve ACP permissions via preset config
Structured sessions ignored the --dangerously-skip-permissions flags in
preset commands because the daemon discards CLI flags when spawning the
ACP adapter binary. This adds an `auto_approve` bool to SpawnPreset and
CreateSessionRequest so the operator can declare it in config.toml:

  { id = "claude", command = "claude", auto_approve = true }

When set, the daemon's permission pump immediately responds AllowAlways
to every ACP permission request without surfacing it to clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-07 11:19:24 +01:00

206 lines
6.6 KiB
Rust

//! Renaming a live session (#125): `SessionManager::rename` semantics —
//! what lands in `metadata.name`, what reaches the sidecar, and what goes out
//! on the event stream.
mod common;
use std::sync::Arc;
use common::TmuxFixture;
use maestro_daemon::sidecar::SidecarStore;
use maestro_daemon::worktree::WorktreeManager;
use maestro_daemon::SessionManager;
use maestro_protocol::{
display, CatalogEvent, CreateSessionRequest, NameSource, MAX_SESSION_NAME_LEN,
};
use tempfile::TempDir;
use uuid::Uuid;
struct Fixture {
manager: SessionManager,
sidecar: SidecarStore,
_tmux: TmuxFixture,
_state_dir: TempDir,
}
fn fixture() -> Fixture {
let tmux = TmuxFixture::new();
let state_dir = TempDir::new().unwrap();
let sidecar = SidecarStore::new(state_dir.path().join("sessions"));
let manager = SessionManager::new(
Arc::new(tmux.tmux.clone()),
sidecar.clone(),
WorktreeManager::new(None),
state_dir.path().join("fifos"),
);
Fixture {
manager,
sidecar,
_tmux: tmux,
_state_dir: state_dir,
}
}
async fn spawn_named(manager: &SessionManager, name: Option<&str>) -> Uuid {
let metadata = match name {
Some(n) => std::collections::HashMap::from([(
"name".to_string(),
serde_json::Value::String(n.to_string()),
)]),
None => Default::default(),
};
manager
.create(CreateSessionRequest {
view: Default::default(),
cwd: "/tmp".into(),
command: "sleep 60".into(),
env: Default::default(),
metadata,
worktree: None,
auto_approve: false,
})
.await
.unwrap()
.id
}
fn name_of(session: &maestro_protocol::Session) -> Option<&str> {
session.metadata.get("name").and_then(|v| v.as_str())
}
#[tokio::test]
async fn rename_on_unknown_session_is_not_found() {
let fx = fixture();
let err = fx
.manager
.rename(Uuid::new_v4(), "whatever")
.await
.unwrap_err();
assert!(matches!(err, maestro_daemon::SessionError::NotFound(_)));
}
#[tokio::test]
async fn rename_sets_the_name_in_memory_and_on_disk() {
let fx = fixture();
let id = spawn_named(&fx.manager, Some("amber-falcon")).await;
let renamed = fx.manager.rename(id, " crimson-otter ").await.unwrap();
assert_eq!(name_of(&renamed), Some("crimson-otter"));
let snapshot = fx.manager.snapshot().await;
let live = snapshot.iter().find(|s| s.id == id).unwrap();
assert_eq!(name_of(live), Some("crimson-otter"));
// A rename a daemon restart would undo is worse than one that failed.
let persisted = fx.sidecar.read(id).unwrap().unwrap();
assert_eq!(name_of(&persisted), Some("crimson-otter"));
}
#[tokio::test]
async fn a_blank_rename_clears_the_name() {
let fx = fixture();
let id = spawn_named(&fx.manager, Some("amber-falcon")).await;
let renamed = fx.manager.rename(id, " ").await.unwrap();
assert_eq!(name_of(&renamed), None);
// Line 1 falls back to the command, per docs/SIDEBAR.md.
assert_eq!(renamed.command, "sleep 60");
assert_eq!(name_of(&fx.sidecar.read(id).unwrap().unwrap()), None);
}
#[tokio::test]
async fn rename_caps_a_long_name() {
let fx = fixture();
let id = spawn_named(&fx.manager, None).await;
let long = "x".repeat(MAX_SESSION_NAME_LEN + 20);
let renamed = fx.manager.rename(id, &long).await.unwrap();
assert_eq!(
name_of(&renamed).unwrap().chars().count(),
MAX_SESSION_NAME_LEN
);
}
#[tokio::test]
async fn rename_broadcasts_a_session_update() {
let fx = fixture();
let id = spawn_named(&fx.manager, Some("amber-falcon")).await;
let mut events = fx.manager.events();
fx.manager.rename(id, "crimson-otter").await.unwrap();
let event = tokio::time::timeout(std::time::Duration::from_secs(5), events.recv())
.await
.expect("an update within the timeout")
.expect("the event channel stays open");
match event {
CatalogEvent::SessionUpdated { session } => {
assert_eq!(session.id, id);
assert_eq!(name_of(&session), Some("crimson-otter"));
}
other => panic!("expected SessionUpdated, got {other:?}"),
}
}
/// #131: a spawn that carried no name gets one of the daemon's own handles,
/// marked provisional so a first prompt may replace it.
#[tokio::test]
async fn a_nameless_spawn_is_given_a_provisional_handle() {
let fx = fixture();
let id = spawn_named(&fx.manager, None).await;
let snapshot = fx.manager.snapshot().await;
let live = snapshot.iter().find(|s| s.id == id).unwrap();
assert!(name_of(live).is_some(), "the daemon names it");
assert_eq!(display::name_source(live), NameSource::Generated);
assert!(display::is_provisional_name(live));
// It survives a restart the same way the rest of the metadata does.
let persisted = fx.sidecar.read(id).unwrap().unwrap();
assert_eq!(name_of(&persisted), name_of(live));
assert_eq!(display::name_source(&persisted), NameSource::Generated);
}
/// A name the spawn request carried is a person's choice: settled, never
/// dimmed, never autonamed over (#131).
#[tokio::test]
async fn a_requested_name_is_a_user_name() {
let fx = fixture();
let id = spawn_named(&fx.manager, Some(" amber-falcon ")).await;
let snapshot = fx.manager.snapshot().await;
let live = snapshot.iter().find(|s| s.id == id).unwrap();
assert_eq!(name_of(live), Some("amber-falcon"));
assert_eq!(display::name_source(live), NameSource::User);
assert!(!display::is_provisional_name(live));
}
/// Renaming a provisional name pins it (#131).
#[tokio::test]
async fn a_rename_settles_a_generated_name() {
let fx = fixture();
let id = spawn_named(&fx.manager, None).await;
let renamed = fx.manager.rename(id, "crimson-otter").await.unwrap();
assert_eq!(display::name_source(&renamed), NameSource::User);
assert!(!display::is_provisional_name(&renamed));
assert_eq!(
display::name_source(&fx.sidecar.read(id).unwrap().unwrap()),
NameSource::User
);
}
/// Clearing the name drops the source with it, so the card falling back to
/// its command carries no stale provisional marker (#131).
#[tokio::test]
async fn clearing_the_name_drops_its_source() {
let fx = fixture();
let id = spawn_named(&fx.manager, None).await;
let cleared = fx.manager.rename(id, " ").await.unwrap();
assert_eq!(name_of(&cleared), None);
assert!(!cleared
.metadata
.contains_key(maestro_protocol::NAME_SOURCE_KEY));
assert!(!display::is_provisional_name(&cleared));
}