Files
openhuman/tests/agent_triage_dispatch_round23_raw_coverage_e2e.rs
T

212 lines
7.2 KiB
Rust

use openhuman_core::core::event_bus::init_global;
use openhuman_core::openhuman::agent::debug::DumpPromptOptions;
use openhuman_core::openhuman::agent::task_board::{TaskBoardCard, TaskCardStatus};
use openhuman_core::openhuman::agent::task_dispatcher::{dispatch_card, DispatchOutcome};
use openhuman_core::openhuman::agent::triage::{
apply_decision, TriageAction, TriageDecision, TriageResolutionPath, TriageRun, TriggerEnvelope,
};
use openhuman_core::openhuman::todos::ops::{self, BoardLocation, CardPatch};
use serde_json::json;
use std::path::Path;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct WorkspaceEnvGuard {
previous: Option<String>,
}
impl WorkspaceEnvGuard {
fn set(path: &Path) -> Self {
let previous = std::env::var("OPENHUMAN_WORKSPACE").ok();
std::env::set_var("OPENHUMAN_WORKSPACE", path);
Self { previous }
}
}
impl Drop for WorkspaceEnvGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => std::env::set_var("OPENHUMAN_WORKSPACE", value),
None => std::env::remove_var("OPENHUMAN_WORKSPACE"),
}
}
}
fn board_location(workspace_dir: &Path) -> BoardLocation {
BoardLocation::Thread {
workspace_dir: workspace_dir.to_path_buf(),
thread_id: "round23-triage-dispatch".to_string(),
}
}
fn add_card(location: &BoardLocation, title: &str, status: TaskCardStatus) -> TaskBoardCard {
ops::add(
location,
title,
CardPatch {
status: Some(status),
objective: Some(format!("Objective for {title}")),
..Default::default()
},
)
.expect("card should be added")
.cards
.into_iter()
.next()
.expect("snapshot should include added card")
}
fn card_status(location: &BoardLocation, card_id: &str) -> TaskCardStatus {
ops::list(location)
.expect("board should load")
.cards
.into_iter()
.find(|card| card.id == card_id)
.expect("card should exist")
.status
}
fn envelope(external_id: &str) -> TriggerEnvelope {
TriggerEnvelope::from_composio(
"github",
"GITHUB_ISSUE_OPENED",
"round23",
external_id,
json!({ "title": "coverage task" }),
)
}
fn triage_run(action: TriageAction) -> TriageRun {
TriageRun {
decision: TriageDecision {
action,
target_agent: Some("orchestrator".to_string()),
prompt: Some("Handle the linked task card".to_string()),
reason: "round23 coverage".to_string(),
},
used_local: false,
latency_ms: 7,
resolution_path: TriageResolutionPath::Cloud,
}
}
#[tokio::test]
async fn drop_and_acknowledge_gate_pending_linked_cards_without_dispatch() {
let _env_lock = ENV_LOCK.lock().expect("env lock should not be poisoned");
let workspace = tempfile::tempdir().expect("temp workspace");
let _env = WorkspaceEnvGuard::set(workspace.path());
let location = board_location(workspace.path());
let drop_card = add_card(&location, "drop me", TaskCardStatus::Todo);
let drop_envelope =
envelope("round23-drop").with_task_card(drop_card.id.clone(), location.clone());
apply_decision(triage_run(TriageAction::Drop), &drop_envelope)
.await
.expect("drop should only gate the card");
assert_eq!(
card_status(&location, &drop_card.id),
TaskCardStatus::Rejected
);
let ack_card = add_card(&location, "ack me", TaskCardStatus::AwaitingApproval);
let ack_envelope = envelope("round23-ack").with_task_card(ack_card.id.clone(), location);
apply_decision(triage_run(TriageAction::Acknowledge), &ack_envelope)
.await
.expect("acknowledge should only gate the card");
assert_eq!(
card_status(&board_location(workspace.path()), &ack_card.id),
TaskCardStatus::Rejected
);
}
#[tokio::test]
async fn react_on_linked_todo_card_parks_for_plan_approval() {
let _env_lock = ENV_LOCK.lock().expect("env lock should not be poisoned");
let workspace = tempfile::tempdir().expect("temp workspace");
let _env = WorkspaceEnvGuard::set(workspace.path());
let _ = init_global(32);
let location = board_location(workspace.path());
let card = add_card(&location, "needs plan approval", TaskCardStatus::Todo);
let linked = envelope("round23-react").with_task_card(card.id.clone(), location.clone());
apply_decision(triage_run(TriageAction::React), &linked)
.await
.expect("linked todo react should park before autonomous execution");
assert_eq!(
card_status(&location, &card.id),
TaskCardStatus::AwaitingApproval
);
}
#[tokio::test]
async fn dispatcher_rejects_missing_and_stale_non_claimable_cards() {
let _env_lock = ENV_LOCK.lock().expect("env lock should not be poisoned");
let workspace = tempfile::tempdir().expect("temp workspace");
let _env = WorkspaceEnvGuard::set(workspace.path());
let location = board_location(workspace.path());
let missing = TaskBoardCard {
id: "task-missing-round23".to_string(),
title: "missing".to_string(),
status: TaskCardStatus::Todo,
objective: Some("missing objective".to_string()),
plan: vec![],
assigned_agent: None,
allowed_tools: vec![],
approval_mode: None,
acceptance_criteria: vec![],
evidence: vec![],
notes: None,
session_thread_id: None,
blocker: None,
source_metadata: None,
order: 0,
updated_at: String::new(),
};
let missing_err = dispatch_card(location.clone(), missing)
.await
.expect_err("missing card should not be claimable");
assert!(missing_err.contains("not found on board"));
let stale = add_card(&location, "stale card", TaskCardStatus::Todo);
ops::update_status(&location, &stale.id, TaskCardStatus::Done)
.expect("card should be advanced before dispatch");
let stale_err = dispatch_card(location, stale)
.await
.expect_err("stale done card should not be claimable");
assert!(stale_err.contains("claim rejected"));
assert!(stale_err.contains("done"));
}
#[tokio::test]
async fn dispatch_card_returns_awaiting_approval_before_agent_spawn() {
let _env_lock = ENV_LOCK.lock().expect("env lock should not be poisoned");
let workspace = tempfile::tempdir().expect("temp workspace");
let _env = WorkspaceEnvGuard::set(workspace.path());
let _ = init_global(32);
let location = board_location(workspace.path());
let card = add_card(&location, "park explicitly", TaskCardStatus::Todo);
let outcome = dispatch_card(location.clone(), card.clone())
.await
.expect("todo card should park for approval under default autonomy config");
assert!(matches!(outcome, DispatchOutcome::AwaitingApproval));
assert_eq!(
card_status(&location, &card.id),
TaskCardStatus::AwaitingApproval
);
}
#[test]
fn debug_prompt_options_constructor_sets_safe_defaults() {
let options = DumpPromptOptions::new("integrations_agent");
assert_eq!(options.agent_id, "integrations_agent");
assert!(options.toolkit.is_none());
assert!(options.workspace_dir_override.is_none());
assert!(options.model_override.is_none());
}