Files
galaxy/app/src/ai/blocklist/block_tests.rs
T

594 lines
18 KiB
Rust

use std::path::PathBuf;
use ai::agent::action::{RunAgentsAgentRunConfig, RunAgentsExecutionMode};
use ai::agent::action_result::StartAgentVersion;
use ai::skills::SkillReference;
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
use galaxyui::{App, SingletonEntity};
use settings::Setting;
use super::{
default_collapsible_state_for_orchestration_action,
default_collapsible_state_for_orchestration_message, received_message_collapsible_id,
user_avatar_info_for_conversation_creator, CollapsibleElementState, CollapsibleExpansionState,
UserAvatarInfo,
};
use crate::ai::agent::{AIAgentActionType, StartAgentExecutionMode};
use crate::ai::blocklist::action_model::{
compose_run_agents_child_prompt, run_agents_to_start_agent_mode,
};
use crate::auth::UserUid;
use crate::settings::{AISettings, OrchestrationMessageDisplayMode};
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::user_profiles::{UserProfileWithUID, UserProfiles};
#[test]
fn reasoning_auto_collapses_when_user_has_not_manually_toggled() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
let mut state = CollapsibleElementState::default();
app.update(|ctx| {
state.finish_reasoning(ctx);
});
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Collapsed
));
});
}
#[test]
fn collapsed_initializer_starts_collapsed() {
let state = CollapsibleElementState::collapsed();
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Collapsed
));
}
#[test]
fn orchestration_show_and_collapse_collapses_after_finish() {
let mut state = default_collapsible_state_for_orchestration_message(
OrchestrationMessageDisplayMode::ShowAndCollapse,
);
state.finish_orchestration_message(OrchestrationMessageDisplayMode::ShowAndCollapse);
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Collapsed
));
}
#[test]
fn orchestration_always_show_stays_expanded_after_finish() {
let mut state = default_collapsible_state_for_orchestration_message(
OrchestrationMessageDisplayMode::AlwaysShow,
);
state.finish_orchestration_message(OrchestrationMessageDisplayMode::AlwaysShow);
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: true,
scroll_pinned_to_bottom: false
}
));
}
#[test]
fn orchestration_send_message_starts_collapsed() {
let state = default_collapsible_state_for_orchestration_action(
&AIAgentActionType::SendMessageToAgent {
addresses: vec!["child-agent".to_string()],
subject: "Status".to_string(),
message: "Body".to_string(),
},
OrchestrationMessageDisplayMode::AlwaysCollapse,
)
.expect("send-message actions should get a collapsible state");
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Collapsed
));
}
#[test]
fn orchestration_start_agent_prompt_stays_expanded_for_all_message_modes() {
for display_mode in [
OrchestrationMessageDisplayMode::ShowAndCollapse,
OrchestrationMessageDisplayMode::AlwaysCollapse,
OrchestrationMessageDisplayMode::AlwaysShow,
] {
let state = default_collapsible_state_for_orchestration_action(
&AIAgentActionType::StartAgent {
version: StartAgentVersion::V1,
name: "child-agent".to_string(),
prompt: "Investigate".to_string(),
execution_mode: StartAgentExecutionMode::local_harness("claude-code".to_string()),
lifecycle_subscription: None,
},
display_mode,
)
.expect("start-agent actions should get a collapsible state");
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: false,
scroll_pinned_to_bottom: true
}
));
}
}
#[test]
fn non_orchestration_actions_do_not_get_collapsible_state_defaults() {
assert!(default_collapsible_state_for_orchestration_action(
&AIAgentActionType::OpenCodeReview,
OrchestrationMessageDisplayMode::AlwaysCollapse,
)
.is_none());
}
#[test]
fn orchestration_show_and_collapse_starts_sent_messages_expanded() {
let state = default_collapsible_state_for_orchestration_action(
&AIAgentActionType::SendMessageToAgent {
addresses: vec!["child-agent".to_string()],
subject: "Status".to_string(),
message: "Body".to_string(),
},
OrchestrationMessageDisplayMode::ShowAndCollapse,
)
.expect("send-message actions should get a collapsible state");
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: false,
scroll_pinned_to_bottom: true
}
));
}
#[test]
fn orchestration_always_show_starts_sent_messages_expanded() {
let state = default_collapsible_state_for_orchestration_action(
&AIAgentActionType::SendMessageToAgent {
addresses: vec!["child-agent".to_string()],
subject: "Status".to_string(),
message: "Body".to_string(),
},
OrchestrationMessageDisplayMode::AlwaysShow,
)
.expect("send-message actions should get a collapsible state");
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: false,
scroll_pinned_to_bottom: true
}
));
}
#[test]
fn orchestration_received_messages_follow_initial_message_display_mode() {
let show_and_collapse = default_collapsible_state_for_orchestration_message(
OrchestrationMessageDisplayMode::ShowAndCollapse,
);
assert!(matches!(
show_and_collapse.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: false,
scroll_pinned_to_bottom: true
}
));
let collapsed = default_collapsible_state_for_orchestration_message(
OrchestrationMessageDisplayMode::AlwaysCollapse,
);
assert!(matches!(
collapsed.expansion_state,
CollapsibleExpansionState::Collapsed
));
let expanded = default_collapsible_state_for_orchestration_message(
OrchestrationMessageDisplayMode::AlwaysShow,
);
assert!(matches!(
expanded.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: false,
scroll_pinned_to_bottom: true
}
));
}
#[test]
fn always_show_thinking_stays_expanded_after_finish() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
AISettings::handle(&app).update(&mut app, |settings, ctx| {
settings
.thinking_display_mode
.set_value(crate::settings::ThinkingDisplayMode::AlwaysShow, ctx)
.unwrap();
});
let mut state = CollapsibleElementState::default();
app.update(|ctx| {
state.finish_reasoning(ctx);
});
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: true,
scroll_pinned_to_bottom: false
}
));
});
}
#[test]
fn manual_collapse_while_streaming_stays_collapsed_after_finish() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
let mut state = CollapsibleElementState::default();
state.toggle_expansion();
app.update(|ctx| {
state.finish_reasoning(ctx);
});
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Collapsed
));
});
}
#[test]
fn manual_reexpand_while_streaming_stays_expanded_after_finish() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
let mut state = CollapsibleElementState::default();
state.toggle_expansion();
state.toggle_expansion();
app.update(|ctx| {
state.finish_reasoning(ctx);
});
assert!(matches!(
state.expansion_state,
CollapsibleExpansionState::Expanded {
is_finished: true,
scroll_pinned_to_bottom: false
}
));
});
}
#[test]
fn received_message_collapsible_id_prefixes_row_ids() {
let first = received_message_collapsible_id("message-1");
let second = received_message_collapsible_id("message-2");
assert_eq!(&*first, "received-message:message-1");
assert_eq!(&*second, "received-message:message-2");
assert_ne!(first, second);
}
#[test]
fn user_avatar_info_prefers_conversation_creator_profile() {
App::test((), |app| async move {
let creator = UserProfileWithUID {
firebase_uid: UserUid::new("creator-uid"),
display_name: Some("Creator Name".to_string()),
email: "creator@example.com".to_string(),
photo_url: "https://example.com/creator.png".to_string(),
};
let fallback = UserAvatarInfo {
display_name: "Current User".to_string(),
profile_image_path: Some("https://example.com/current.png".to_string()),
};
app.read(|ctx| {
let avatar_info = user_avatar_info_for_conversation_creator(
Some(&creator),
Some("fallback-uid"),
fallback,
ctx,
);
assert_eq!(avatar_info.display_name, "Creator Name");
assert_eq!(
avatar_info.profile_image_path.as_deref(),
Some("https://example.com/creator.png")
);
});
});
}
#[test]
fn user_avatar_info_uses_cached_profile_for_creator_uid() {
App::test((), |app| async move {
app.add_singleton_model(|_| {
UserProfiles::new(vec![UserProfileWithUID {
firebase_uid: UserUid::new("creator-uid"),
display_name: Some("Cached Creator".to_string()),
email: "cached@example.com".to_string(),
photo_url: "https://example.com/cached.png".to_string(),
}])
});
let fallback = UserAvatarInfo {
display_name: "Current User".to_string(),
profile_image_path: Some("https://example.com/current.png".to_string()),
};
app.read(|ctx| {
let avatar_info =
user_avatar_info_for_conversation_creator(None, Some("creator-uid"), fallback, ctx);
assert_eq!(avatar_info.display_name, "Cached Creator");
assert_eq!(
avatar_info.profile_image_path.as_deref(),
Some("https://example.com/cached.png")
);
});
});
}
#[test]
fn compose_child_prompt_concatenates_when_both_non_empty() {
let composed = compose_run_agents_child_prompt("base", "do X");
assert_eq!(composed, "base\n\ndo X");
}
#[test]
fn compose_child_prompt_uses_base_only_when_per_agent_empty() {
let composed = compose_run_agents_child_prompt("base", "");
assert_eq!(composed, "base");
}
#[test]
fn compose_child_prompt_uses_per_agent_only_when_base_empty() {
let composed = compose_run_agents_child_prompt("", "do X");
assert_eq!(composed, "do X");
}
#[test]
fn compose_child_prompt_returns_empty_when_both_empty() {
let composed = compose_run_agents_child_prompt("", "");
assert_eq!(composed, "");
}
#[test]
fn compose_child_prompt_treats_whitespace_only_base_as_empty() {
let composed = compose_run_agents_child_prompt(" \n", "do X");
assert_eq!(composed, "do X");
}
fn agent_cfg() -> RunAgentsAgentRunConfig {
RunAgentsAgentRunConfig {
name: "child".to_string(),
prompt: "do X".to_string(),
title: "Child".to_string(),
}
}
#[test]
fn remote_arm_propagates_skills_into_skill_references() {
let skills = vec![
SkillReference::BundledSkillId("writing-pr-descriptions".to_string()),
SkillReference::Path(LocalOrRemotePath::Local(PathBuf::from(
"/tmp/skill/SKILL.md",
))),
];
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Remote {
environment_id: "env-1".to_string(),
worker_host: "warp".to_string(),
computer_use_enabled: true,
},
"oz",
"auto",
&skills,
None,
&agent_cfg(),
)
.expect("Remote+oz must convert");
let StartAgentExecutionMode::Remote {
skill_references,
environment_id,
worker_host,
harness_type,
model_id,
computer_use_enabled,
title,
auth_secret_name,
} = mode
else {
panic!("expected Remote start-agent mode");
};
assert_eq!(skill_references, skills);
assert_eq!(environment_id, "env-1");
assert_eq!(worker_host, "warp");
assert_eq!(harness_type, "oz");
assert_eq!(model_id, "auto");
assert!(computer_use_enabled);
assert_eq!(title, "Child");
assert_eq!(auth_secret_name, None);
}
#[test]
fn remote_arm_with_empty_skills_propagates_empty_vec() {
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Remote {
environment_id: "env-1".to_string(),
worker_host: "warp".to_string(),
computer_use_enabled: false,
},
"claude",
"auto",
&[],
None,
&agent_cfg(),
)
.expect("Remote+claude must convert");
let StartAgentExecutionMode::Remote {
skill_references, ..
} = mode
else {
panic!("expected Remote start-agent mode");
};
assert!(skill_references.is_empty());
}
#[test]
fn remote_arm_rejects_opencode() {
let err = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Remote {
environment_id: "env-1".to_string(),
worker_host: "warp".to_string(),
computer_use_enabled: false,
},
"opencode",
"auto",
&[],
None,
&agent_cfg(),
)
.expect_err("Remote+opencode must be rejected");
assert!(err.to_lowercase().contains("opencode"));
}
#[test]
fn local_arm_rejects_disabled_codex() {
let err = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Local,
"codex",
"auto",
&[],
None,
&agent_cfg(),
)
.expect_err("Local+codex must be rejected while disabled");
assert_eq!(err, "Local Codex child agents are temporarily disabled.");
}
#[test]
fn local_arm_allows_claude() {
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Local,
"claude",
"auto",
&[],
None,
&agent_cfg(),
)
.expect("Local+claude should convert");
assert!(matches!(
mode,
StartAgentExecutionMode::Local {
harness_type: Some(ref harness_type),
model_id: Some(ref model_id),
} if harness_type == "claude" && model_id == "auto"
));
}
#[test]
fn remote_arm_propagates_claude_auth_secret_into_mode() {
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Remote {
environment_id: "env-1".to_string(),
worker_host: "warp".to_string(),
computer_use_enabled: false,
},
"claude",
"auto",
&[],
Some("my-claude-key"),
&agent_cfg(),
)
.expect("Remote+claude must convert");
let StartAgentExecutionMode::Remote {
auth_secret_name, ..
} = mode
else {
panic!("expected Remote start-agent mode");
};
assert_eq!(auth_secret_name.as_deref(), Some("my-claude-key"));
}
#[test]
fn remote_arm_filters_whitespace_auth_secret_name_to_none() {
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Remote {
environment_id: "env-1".to_string(),
worker_host: "warp".to_string(),
computer_use_enabled: false,
},
"codex",
"auto",
&[],
Some(" "),
&agent_cfg(),
)
.expect("Remote+codex must convert");
let StartAgentExecutionMode::Remote {
auth_secret_name, ..
} = mode
else {
panic!("expected Remote start-agent mode");
};
assert_eq!(auth_secret_name, None);
}
#[test]
fn local_arm_ignores_auth_secret_name() {
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Local,
"claude",
"auto",
&[],
Some("my-claude-key"),
&agent_cfg(),
)
.expect("Local+claude should convert");
// Local children don't carry an auth_secret_name field.
assert!(matches!(mode, StartAgentExecutionMode::Local { .. }));
}
#[test]
fn should_show_agent_mode_ask_user_question_speedbump_defaults_to_true() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
AISettings::handle(&app).read(&app, |settings, _ctx| {
assert!(*settings.should_show_agent_mode_ask_user_question_speedbump);
});
});
}
#[test]
fn should_show_agent_mode_ask_user_question_speedbump_round_trips_to_false() {
App::test((), |mut app| async move {
initialize_settings_for_tests(&mut app);
AISettings::handle(&app).update(&mut app, |settings, ctx| {
settings
.should_show_agent_mode_ask_user_question_speedbump
.set_value(false, ctx)
.unwrap();
});
AISettings::handle(&app).read(&app, |settings, _ctx| {
assert!(!*settings.should_show_agent_mode_ask_user_question_speedbump);
});
});
}