Files
galaxy/crates/local_inference/tests/e2e_scenarios.rs
T

340 lines
10 KiB
Rust

//! End-to-end scenario tests that validate realistic usage patterns for
//! the local inference engine across all task types (classification,
//! prompt suggestion, tab naming).
use instant::Instant;
use local_inference::{
Device, InferenceEngine, InferenceTask, InputCategory, InputClassificationInput,
InputClassificationTask, PromptSuggestionInput, PromptSuggestionTask, TabNamingInput,
TabNamingTask,
};
async fn get_engine() -> InferenceEngine {
InferenceEngine::new(Device::Cpu)
.await
.expect("Failed to initialize engine — is the model downloaded?")
}
// --- Full workflow scenarios ---
/// Simulates a user session where they type various inputs and the classifier
/// routes them correctly between shell and agent modes.
#[tokio::test]
async fn scenario_user_session_mode_switching() {
let engine = get_engine().await;
struct TestCase {
input: &'static str,
recent: Vec<&'static str>,
follow_up: bool,
expected: InputCategory,
}
let cases = vec![
TestCase {
input: "cd ~/projects/myapp",
recent: vec!["ls", "pwd"],
follow_up: false,
expected: InputCategory::Shell,
},
TestCase {
input: "npm start",
recent: vec!["cd ~/projects/myapp", "ls"],
follow_up: false,
expected: InputCategory::Shell,
},
TestCase {
input: "why is my server crashing on startup?",
recent: vec!["npm start", "cd ~/projects/myapp"],
follow_up: false,
expected: InputCategory::AgentPrompt,
},
TestCase {
input: "can you also check the environment variables?",
recent: vec!["npm start"],
follow_up: true,
expected: InputCategory::AgentPrompt,
},
TestCase {
input: "export NODE_ENV=production",
recent: vec!["npm start"],
follow_up: false,
expected: InputCategory::Shell,
},
];
println!("\n--- User Session Mode Switching Scenario ---");
for case in &cases {
let result = InputClassificationTask
.run(
&engine,
InputClassificationInput {
user_input: case.input.to_string(),
recent_commands: case.recent.iter().map(|s| s.to_string()).collect(),
is_follow_up: case.follow_up,
},
)
.await
.expect("classification failed");
println!(
" {:?} -> {:?} (expected {:?}, confidence {:.2})",
case.input, result.category, case.expected, result.confidence
);
assert_eq!(
result.category, case.expected,
"Misclassified {:?}",
case.input
);
}
}
/// Simulates prompt suggestions being generated after various shell commands.
#[tokio::test]
async fn scenario_prompt_suggestions_after_commands() {
let engine = get_engine().await;
println!("\n--- Prompt Suggestions After Commands Scenario ---");
// After a failed build
let suggestions = PromptSuggestionTask
.run(
&engine,
PromptSuggestionInput {
recent_commands: vec![
"cargo build".into(),
"cargo test -- --nocapture".into(),
"cargo build".into(), // repeated = likely still failing
],
current_input: String::new(),
working_directory: "/home/user/projects/rust-app".into(),
},
)
.await
.expect("suggestion failed");
println!(" After repeated builds: {suggestions:?}");
assert!(!suggestions.is_empty());
// After git workflow
let suggestions = PromptSuggestionTask
.run(
&engine,
PromptSuggestionInput {
recent_commands: vec![
"git add .".into(),
"git commit -m 'wip'".into(),
"git push".into(),
],
current_input: String::new(),
working_directory: "/home/user/projects/feature-branch".into(),
},
)
.await
.expect("suggestion failed");
println!(" After git workflow: {suggestions:?}");
assert!(!suggestions.is_empty());
// With partial input
let suggestions = PromptSuggestionTask
.run(
&engine,
PromptSuggestionInput {
recent_commands: vec!["docker ps".into(), "docker logs app".into()],
current_input: "docker".to_string(),
working_directory: "/home/user/deployments".into(),
},
)
.await
.expect("suggestion failed");
println!(" With 'docker' partial input: {suggestions:?}");
assert!(!suggestions.is_empty());
}
/// Simulates tab naming for different development contexts.
#[tokio::test]
async fn scenario_tab_naming_development_contexts() {
let engine = get_engine().await;
println!("\n--- Tab Naming for Development Contexts ---");
struct TabCase {
label: &'static str,
commands: Vec<&'static str>,
cwd: &'static str,
}
let cases = vec![
TabCase {
label: "Git operations",
commands: vec!["git log --oneline", "git branch -a", "git fetch origin"],
cwd: "/home/user/projects/galaxy",
},
TabCase {
label: "Docker deployment",
commands: vec!["docker compose up -d", "docker ps", "docker logs web"],
cwd: "/home/user/services/api",
},
TabCase {
label: "Python data science",
commands: vec![
"jupyter notebook",
"pip install pandas",
"python analysis.py",
],
cwd: "/home/user/research/data-pipeline",
},
TabCase {
label: "Rust development",
commands: vec!["cargo build", "cargo test", "cargo clippy"],
cwd: "/home/user/projects/my-crate",
},
];
for case in &cases {
let name = TabNamingTask
.run(
&engine,
TabNamingInput {
recent_commands: case.commands.iter().map(|s| s.to_string()).collect(),
working_directory: case.cwd.to_string(),
},
)
.await
.expect("tab naming failed");
println!(" {}: \"{}\"", case.label, name);
assert!(!name.is_empty(), "tab name should not be empty");
assert!(
name.split_whitespace().count() <= 6,
"tab name too long for '{}': \"{}\"",
case.label,
name
);
}
}
/// Tests performance: all tasks should complete quickly enough for interactive use.
#[tokio::test]
async fn scenario_performance_under_load() {
let engine = get_engine().await;
println!("\n--- Performance Under Load ---");
// Classification should be fast (< 500ms on CPU)
let start = Instant::now();
for _ in 0..5 {
InputClassificationTask
.run(
&engine,
InputClassificationInput {
user_input: "git push origin main".to_string(),
recent_commands: vec!["git add .".into(), "git commit -m 'test'".into()],
is_follow_up: false,
},
)
.await
.expect("classification failed");
}
let classification_time = start.elapsed();
println!(
" 5 classifications: {:?} (avg {:?})",
classification_time,
classification_time / 5
);
// Tab naming
let start = Instant::now();
TabNamingTask
.run(
&engine,
TabNamingInput {
recent_commands: vec!["make build".into(), "make test".into()],
working_directory: "/home/user/project".into(),
},
)
.await
.expect("tab naming failed");
let tab_time = start.elapsed();
println!(" 1 tab naming: {:?}", tab_time);
// Prompt suggestion
let start = Instant::now();
PromptSuggestionTask
.run(
&engine,
PromptSuggestionInput {
recent_commands: vec!["ls".into(), "cd src".into()],
current_input: String::new(),
working_directory: "/home/user".into(),
},
)
.await
.expect("prompt suggestion failed");
let suggest_time = start.elapsed();
println!(" 1 prompt suggestion: {:?}", suggest_time);
// Classification should be under 2s per call on CPU (generous limit for CI)
assert!(
classification_time / 5 < std::time::Duration::from_secs(2),
"classification too slow: {:?} per call",
classification_time / 5
);
}
/// Tests that the engine can be reused across many calls without degradation.
#[tokio::test]
async fn scenario_engine_stability_across_calls() {
let engine = get_engine().await;
println!("\n--- Engine Stability Across Calls ---");
// Mix different task types in sequence
for i in 0..3 {
// Classification
let result = InputClassificationTask
.run(
&engine,
InputClassificationInput {
user_input: format!("test command {i}"),
recent_commands: vec![],
is_follow_up: false,
},
)
.await
.expect("classification should not degrade");
assert!(result.confidence > 0.0);
// Tab naming
let name = TabNamingTask
.run(
&engine,
TabNamingInput {
recent_commands: vec![format!("cmd {i}")],
working_directory: format!("/tmp/test-{i}"),
},
)
.await
.expect("tab naming should not degrade");
assert!(!name.is_empty());
// Prompt suggestion
let suggestions = PromptSuggestionTask
.run(
&engine,
PromptSuggestionInput {
recent_commands: vec![format!("action {i}")],
current_input: String::new(),
working_directory: "/tmp".into(),
},
)
.await
.expect("prompt suggestion should not degrade");
assert!(!suggestions.is_empty());
println!(" Round {}: all tasks passed", i + 1);
}
}