use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use async_trait::async_trait;
use tempfile::TempDir;
use tokio::sync::Mutex;
use super::mock_control::MockSessionControl;
use crate::core::sm::agent::SessionManagerAgent;
use crate::core::sm::agent::mock::{MockChatProvider, MockResolver};
use crate::core::sm::config::SessionManagerConfig;
use crate::core::sm::control::SessionControl;
use crate::core::sm::goals::{GOAL_TAG, GoalMemory, GoalStatus, SmGoalStore};
struct MemPalace {
entries: StdMutex<Vec<(String, String)>>,
}
impl MemPalace {
fn arc() -> Arc<Self> {
Arc::new(Self {
entries: StdMutex::new(Vec::new()),
})
}
}
#[async_trait]
impl GoalMemory for MemPalace {
async fn remember_goal(&self, json: String, tag: &str) -> Result<(), String> {
assert_eq!(tag, GOAL_TAG);
let id = serde_json::from_str::<serde_json::Value>(&json)
.ok()
.and_then(|v| v.get("id").and_then(|x| x.as_str()).map(str::to_string))
.unwrap_or_default();
let mut e = self.entries.lock().expect("lock");
if let Some(slot) = e.iter_mut().find(|(eid, _)| *eid == id) {
slot.1 = json;
} else {
e.push((id, json));
}
Ok(())
}
async fn list_goals(&self, _tag: &str) -> Result<Vec<String>, String> {
Ok(self
.entries
.lock()
.expect("lock")
.iter()
.map(|(_, j)| j.clone())
.collect())
}
}
fn enabled_config() -> SessionManagerConfig {
SessionManagerConfig {
enabled: true,
..SessionManagerConfig::default()
}
}
fn agent_with(decision_json: &str, data_root: &std::path::Path) -> SessionManagerAgent {
let provider = MockChatProvider::new(decision_json, 0.0);
let resolver = Arc::new(MockResolver::with_provider(provider));
#[cfg(feature = "sm-memory")]
{
SessionManagerAgent::with_runtime(enabled_config(), resolver, data_root.to_path_buf(), None)
}
#[cfg(not(feature = "sm-memory"))]
{
SessionManagerAgent::with_runtime(enabled_config(), resolver, data_root.to_path_buf())
}
}
fn goal_store(dir: &TempDir) -> Arc<Mutex<SmGoalStore>> {
let palace = MemPalace::arc();
Arc::new(Mutex::new(SmGoalStore::new(palace, dir.path())))
}
#[tokio::test]
async fn delegate_launches_and_links_session() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"delegate","tasks":[{"workdir":"/repo","prompt":"add login"}]}"#;
let agent = agent_with(decision, tmp.path());
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("build the login feature", &control, &goals)
.await
.expect("delegation loop runs");
assert_eq!(outcome.launched.len(), 1, "exactly one session launched");
let launches = mock.launches();
assert_eq!(launches.len(), 1);
assert_eq!(
launches[0].1.goal_id.as_deref(),
Some(outcome.goal_id.as_str())
);
let store = goals.lock().await;
let goal = store.get(&outcome.goal_id).expect("goal exists");
assert_eq!(goal.sessions.len(), 1, "session linked to goal");
assert_eq!(goal.sessions[0].session_id, outcome.launched[0]);
assert_eq!(goal.status, GoalStatus::InProgress);
}
#[tokio::test]
async fn delegate_delivers_task_to_session() {
let tmp = TempDir::new().unwrap();
let decision =
r#"{"action":"delegate","tasks":[{"workdir":"/r","prompt":"implement the parser"}]}"#;
let agent = agent_with(decision, tmp.path());
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("write a parser", &control, &goals)
.await
.expect("loop runs");
let sends = mock.sends();
assert_eq!(sends.len(), 1, "task delivered exactly once (#1299)");
assert_eq!(
sends[0].0, outcome.launched[0],
"delivered to launched session"
);
assert_eq!(
sends[0].1, "implement the parser",
"the task prompt was delivered"
);
}
#[tokio::test]
async fn delegate_observes_and_updates_progress() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"delegate","tasks":[{"workdir":"/r","prompt":"task"}]}"#;
let agent = agent_with(decision, tmp.path());
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("do the thing", &control, &goals)
.await
.expect("loop runs");
assert!(!outcome.goal_done, "no evidence ⇒ goal not done");
assert_eq!(
outcome.goal_status, "InProgress",
"an observed-but-unverified goal is InProgress, not blocked/failed"
);
let store = goals.lock().await;
let goal = store.get(&outcome.goal_id).expect("goal");
use crate::core::sm::goals::SessionTaskState;
assert_eq!(goal.sessions[0].state, SessionTaskState::Running);
assert_eq!(goal.progress, 0, "no verified tasks ⇒ 0% progress");
}
#[tokio::test]
async fn delegate_gate_blocks_without_evidence() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"delegate","tasks":[{"workdir":"/r","prompt":"task"}]}"#;
let agent = agent_with(decision, tmp.path());
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("ship it", &control, &goals)
.await
.expect("loop runs");
assert!(!outcome.goal_done, "gate blocks Done without evidence");
let store = goals.lock().await;
assert_ne!(
store.get(&outcome.goal_id).unwrap().status,
GoalStatus::Done,
"goal must not be Done"
);
let lower = outcome.reply.to_ascii_lowercase();
assert!(!lower.contains("should be done"));
assert!(!lower.contains("looks complete"));
}
#[tokio::test]
async fn delegate_verifies_and_closes_with_evidence() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"delegate","tasks":[{"workdir":"/r","prompt":"open a PR"}]}"#;
let agent = agent_with(decision, tmp.path());
let evidence = "Opened PR https://github.com/acme/repo/pull/9 ready";
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::with_evidence(evidence));
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("open the PR", &control, &goals)
.await
.expect("loop runs");
assert!(outcome.goal_done, "evidence present ⇒ gate passes ⇒ Done");
let store = goals.lock().await;
let goal = store.get(&outcome.goal_id).expect("goal");
use crate::core::sm::goals::SessionTaskState;
assert_eq!(goal.sessions[0].state, SessionTaskState::Verified);
assert!(
goal.sessions[0]
.evidence
.as_deref()
.unwrap()
.contains("pull/9"),
"evidence captured into the link"
);
assert_eq!(goal.progress, 100);
assert_eq!(goal.status, GoalStatus::Done);
assert_eq!(outcome.goal_status, "Done", "closed goal reports Done");
assert!(outcome.reply.to_ascii_lowercase().contains("done"));
}
#[tokio::test]
async fn delegate_refuses_direct_work_and_redirects() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"do_work","summary":"I will edit main.rs myself"}"#;
let agent = agent_with(decision, tmp.path());
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("just add the flag", &control, &goals)
.await
.expect("loop runs");
assert!(
outcome.launched.is_empty(),
"direct work must NOT launch arbitrary work"
);
assert!(
mock.launches().is_empty(),
"control was never driven to do the work"
);
assert!(mock.sends().is_empty(), "no task delivered (no session)");
assert!(!outcome.goal_done);
let lower = outcome.reply.to_ascii_lowercase();
assert!(
lower.contains("launch a session"),
"reply redirects to launching a session: {}",
outcome.reply
);
assert!(lower.contains("sp1-sp5"), "reply names the prohibition");
assert_eq!(
outcome.goal_status, "Blocked",
"a refused direct-work goal reports Blocked in goal_status"
);
let store = goals.lock().await;
assert_eq!(
store.get(&outcome.goal_id).unwrap().status,
GoalStatus::Blocked,
"a refused direct-work goal is Blocked, not left Pending"
);
}
#[tokio::test]
async fn delegate_respond_talks_to_operator() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"respond","message":"Which repository should I target?"}"#;
let agent = agent_with(decision, tmp.path());
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("do something vague", &control, &goals)
.await
.expect("loop runs");
assert_eq!(outcome.reply, "Which repository should I target?");
assert!(outcome.launched.is_empty());
let store = goals.lock().await;
assert_eq!(
store.get(&outcome.goal_id).unwrap().status,
GoalStatus::Pending
);
}
#[tokio::test]
async fn delegate_fans_out_to_multiple_sessions() {
let tmp = TempDir::new().unwrap();
let decision = r#"{"action":"delegate","tasks":[
{"workdir":"/r","prompt":"backend"},
{"workdir":"/r","prompt":"frontend"}]}"#;
let agent = agent_with(decision, tmp.path());
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let goals = goal_store(&tmp);
let outcome = agent
.delegate_goal("build the app", &control, &goals)
.await
.expect("loop runs");
assert_eq!(outcome.launched.len(), 2);
assert_eq!(mock.launches().len(), 2);
assert_eq!(mock.sends().len(), 2, "both tasks delivered");
let store = goals.lock().await;
assert_eq!(store.get(&outcome.goal_id).unwrap().sessions.len(), 2);
}
#[tokio::test]
async fn delegate_degraded_without_provider() {
use crate::core::sm::agent::DelegationError;
let tmp = TempDir::new().unwrap();
let resolver = Arc::new(MockResolver::degraded());
#[cfg(feature = "sm-memory")]
let agent = SessionManagerAgent::with_runtime(
enabled_config(),
resolver,
tmp.path().to_path_buf(),
None,
);
#[cfg(not(feature = "sm-memory"))]
let agent =
SessionManagerAgent::with_runtime(enabled_config(), resolver, tmp.path().to_path_buf());
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let goals = goal_store(&tmp);
let err = agent
.delegate_goal("anything", &control, &goals)
.await
.expect_err("degraded loop errors gracefully");
assert!(matches!(err, DelegationError::Degraded(_)));
}