mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 08:39:24 +00:00
merge: Resolve conflicts with main, add user-scoped DB methods
Merge main's security hardening (user-scoped job/conversation access, cargo-dist config, CI improvements) into turso branch. Add Database trait methods for user-scoped operations: - list_sandbox_jobs_for_user - sandbox_job_summary_for_user - sandbox_job_belongs_to_user - conversation_belongs_to_user Implemented in both postgres and libsql backends. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
//! Integration tests for the OpenAI-compatible API endpoints.
|
||||
//!
|
||||
//! Uses a mock LLM provider so no real API key is needed.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use ironclaw::channels::web::server::{GatewayState, start_server};
|
||||
use ironclaw::channels::web::sse::SseManager;
|
||||
use ironclaw::channels::web::ws::WsConnectionTracker;
|
||||
use ironclaw::error::LlmError;
|
||||
use ironclaw::llm::{
|
||||
CompletionRequest, CompletionResponse, FinishReason, LlmProvider, ToolCompletionRequest,
|
||||
ToolCompletionResponse,
|
||||
};
|
||||
|
||||
const AUTH_TOKEN: &str = "test-openai-token";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock LLM provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct MockLlmProvider;
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for MockLlmProvider {
|
||||
fn model_name(&self) -> &str {
|
||||
"mock-model-v1"
|
||||
}
|
||||
|
||||
fn cost_per_token(&self) -> (Decimal, Decimal) {
|
||||
(Decimal::ZERO, Decimal::ZERO)
|
||||
}
|
||||
|
||||
async fn complete(&self, req: CompletionRequest) -> Result<CompletionResponse, LlmError> {
|
||||
// Echo the last user message back
|
||||
let user_msg = req
|
||||
.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|m| m.role == ironclaw::llm::Role::User)
|
||||
.map(|m| m.content.clone())
|
||||
.unwrap_or_else(|| "no user message".to_string());
|
||||
|
||||
Ok(CompletionResponse {
|
||||
content: format!("Mock response to: {}", user_msg),
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
finish_reason: FinishReason::Stop,
|
||||
response_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn complete_with_tools(
|
||||
&self,
|
||||
req: ToolCompletionRequest,
|
||||
) -> Result<ToolCompletionResponse, LlmError> {
|
||||
// If tools are provided, return a tool call
|
||||
if let Some(tool) = req.tools.first() {
|
||||
Ok(ToolCompletionResponse {
|
||||
content: None,
|
||||
tool_calls: vec![ironclaw::llm::ToolCall {
|
||||
id: "call_mock_001".to_string(),
|
||||
name: tool.name.clone(),
|
||||
arguments: serde_json::json!({"test": true}),
|
||||
}],
|
||||
input_tokens: 15,
|
||||
output_tokens: 8,
|
||||
finish_reason: FinishReason::ToolUse,
|
||||
response_id: None,
|
||||
})
|
||||
} else {
|
||||
Ok(ToolCompletionResponse {
|
||||
content: Some("No tools available".to_string()),
|
||||
tool_calls: vec![],
|
||||
input_tokens: 10,
|
||||
output_tokens: 4,
|
||||
finish_reason: FinishReason::Stop,
|
||||
response_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_models(&self) -> Result<Vec<String>, LlmError> {
|
||||
Ok(vec![
|
||||
"mock-model-v1".to_string(),
|
||||
"mock-model-v2".to_string(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn start_test_server() -> (SocketAddr, Arc<GatewayState>) {
|
||||
let state = Arc::new(GatewayState {
|
||||
msg_tx: tokio::sync::RwLock::new(None),
|
||||
sse: SseManager::new(),
|
||||
workspace: None,
|
||||
session_manager: None,
|
||||
log_broadcaster: None,
|
||||
extension_manager: None,
|
||||
tool_registry: None,
|
||||
store: None,
|
||||
job_manager: None,
|
||||
prompt_queue: None,
|
||||
user_id: "test-user".to_string(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
|
||||
llm_provider: Some(Arc::new(MockLlmProvider)),
|
||||
chat_rate_limiter: ironclaw::channels::web::server::RateLimiter::new(30, 60),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let bound_addr = start_server(addr, state.clone(), AUTH_TOKEN.to_string())
|
||||
.await
|
||||
.expect("Failed to start test server");
|
||||
|
||||
(bound_addr, state)
|
||||
}
|
||||
|
||||
fn client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_basic() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello world"}
|
||||
]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert_eq!(body["object"], "chat.completion");
|
||||
assert_eq!(body["model"], "mock-model-v1");
|
||||
assert_eq!(body["choices"][0]["finish_reason"], "stop");
|
||||
|
||||
let content = body["choices"][0]["message"]["content"].as_str().unwrap();
|
||||
assert!(
|
||||
content.contains("Hello world"),
|
||||
"Expected echo, got: {}",
|
||||
content
|
||||
);
|
||||
|
||||
// Check usage
|
||||
assert_eq!(body["usage"]["prompt_tokens"], 10);
|
||||
assert_eq!(body["usage"]["completion_tokens"], 5);
|
||||
assert_eq!(body["usage"]["total_tokens"], 15);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_with_system_message() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "user", "content": "What is 2+2?"}
|
||||
],
|
||||
"temperature": 0.5,
|
||||
"max_tokens": 100
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
let content = body["choices"][0]["message"]["content"].as_str().unwrap();
|
||||
assert!(content.contains("2+2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_with_tools() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What's the weather?"}
|
||||
],
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get the weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
|
||||
assert_eq!(body["choices"][0]["finish_reason"], "tool_calls");
|
||||
|
||||
let tool_calls = &body["choices"][0]["message"]["tool_calls"];
|
||||
assert!(tool_calls.is_array());
|
||||
assert_eq!(tool_calls[0]["id"], "call_mock_001");
|
||||
assert_eq!(tool_calls[0]["type"], "function");
|
||||
assert_eq!(tool_calls[0]["function"]["name"], "get_weather");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_streaming() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Stream test"}
|
||||
],
|
||||
"stream": true
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
|
||||
// Check simulated streaming header
|
||||
assert_eq!(
|
||||
resp.headers()
|
||||
.get("x-ironclaw-streaming")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("simulated"),
|
||||
"Expected x-ironclaw-streaming: simulated header"
|
||||
);
|
||||
|
||||
let text = resp.text().await.unwrap();
|
||||
|
||||
// Should contain SSE data lines
|
||||
assert!(
|
||||
text.contains("data:"),
|
||||
"Expected SSE data lines, got: {}",
|
||||
text
|
||||
);
|
||||
// Should end with [DONE]
|
||||
assert!(
|
||||
text.contains("[DONE]"),
|
||||
"Expected [DONE] sentinel, got: {}",
|
||||
text
|
||||
);
|
||||
// Should contain the role chunk
|
||||
assert!(
|
||||
text.contains("\"role\":\"assistant\""),
|
||||
"Expected role chunk, got: {}",
|
||||
text
|
||||
);
|
||||
|
||||
// Collect all content from the chunks
|
||||
let mut full_content = String::new();
|
||||
for line in text.lines() {
|
||||
if let Some(data) = line.strip_prefix("data:") {
|
||||
let data = data.trim();
|
||||
if data == "[DONE]" {
|
||||
continue;
|
||||
}
|
||||
if let Ok(chunk) = serde_json::from_str::<serde_json::Value>(data) {
|
||||
if let Some(content) = chunk["choices"][0]["delta"]["content"].as_str() {
|
||||
full_content.push_str(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
full_content.contains("Stream test"),
|
||||
"Expected reassembled content to contain 'Stream test', got: '{}'",
|
||||
full_content
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_empty_messages() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": []
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 400);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert!(body["error"]["message"].as_str().unwrap().contains("empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_model_mismatch() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 404);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert_eq!(body["error"]["code"], "model_not_found");
|
||||
assert!(
|
||||
body["error"]["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("mock-model-v1")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_no_auth() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
// No auth header
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_models_endpoint() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/models", addr);
|
||||
|
||||
let resp = client()
|
||||
.get(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
|
||||
assert_eq!(body["object"], "list");
|
||||
let data = body["data"].as_array().unwrap();
|
||||
assert_eq!(data.len(), 2);
|
||||
assert_eq!(data[0]["id"], "mock-model-v1");
|
||||
assert_eq!(data[1]["id"], "mock-model-v2");
|
||||
assert_eq!(data[0]["object"], "model");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_models_no_auth() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/models", addr);
|
||||
|
||||
let resp = client().get(&url).send().await.unwrap();
|
||||
assert_eq!(resp.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_llm_provider_returns_503() {
|
||||
// Create state WITHOUT llm_provider
|
||||
let state = Arc::new(GatewayState {
|
||||
msg_tx: tokio::sync::RwLock::new(None),
|
||||
sse: SseManager::new(),
|
||||
workspace: None,
|
||||
session_manager: None,
|
||||
log_broadcaster: None,
|
||||
extension_manager: None,
|
||||
tool_registry: None,
|
||||
store: None,
|
||||
job_manager: None,
|
||||
prompt_queue: None,
|
||||
user_id: "test-user".to_string(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
|
||||
llm_provider: None, // No LLM!
|
||||
chat_rate_limiter: ironclaw::channels::web::server::RateLimiter::new(30, 60),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let bound_addr = start_server(addr, state, AUTH_TOKEN.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let url = format!("http://{}/v1/chat/completions", bound_addr);
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [{"role": "user", "content": "Hi"}]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 503);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chat_completions_body_too_large() {
|
||||
let (addr, _state) = start_test_server().await;
|
||||
let url = format!("http://{}/v1/chat/completions", addr);
|
||||
|
||||
// Build a payload over 1 MB (the gateway's DefaultBodyLimit)
|
||||
let big_content = "x".repeat(2 * 1024 * 1024);
|
||||
let resp = client()
|
||||
.post(&url)
|
||||
.bearer_auth(AUTH_TOKEN)
|
||||
.json(&serde_json::json!({
|
||||
"model": "mock-model-v1",
|
||||
"messages": [{"role": "user", "content": big_content}]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 413);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Verifies the full pairing lifecycle: upsert → list → approve → allowFrom → is_sender_allowed.
|
||||
//! Uses temp directory for isolation.
|
||||
|
||||
use ironclaw::cli::{run_pairing_command_with_store, PairingCommand};
|
||||
use ironclaw::cli::{PairingCommand, run_pairing_command_with_store};
|
||||
use ironclaw::pairing::PairingStore;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -19,10 +19,16 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
||||
let channel = "telegram";
|
||||
|
||||
// 1. Unknown user sends first message -> upsert creates request
|
||||
let r1 = store.upsert_request(channel, "user_12345", Some(serde_json::json!({
|
||||
"chat_id": 999,
|
||||
"username": "alice"
|
||||
}))).unwrap();
|
||||
let r1 = store
|
||||
.upsert_request(
|
||||
channel,
|
||||
"user_12345",
|
||||
Some(serde_json::json!({
|
||||
"chat_id": 999,
|
||||
"username": "alice"
|
||||
})),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(r1.created);
|
||||
assert!(!r1.code.is_empty());
|
||||
assert_eq!(r1.code.len(), 8);
|
||||
@@ -34,7 +40,11 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
||||
assert_eq!(pending[0].code, r1.code);
|
||||
|
||||
// 3. User is not allowed yet
|
||||
assert!(!store.is_sender_allowed(channel, "user_12345", Some("alice")).unwrap());
|
||||
assert!(
|
||||
!store
|
||||
.is_sender_allowed(channel, "user_12345", Some("alice"))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// 4. Approve via code
|
||||
let approved = store.approve(channel, &r1.code).unwrap();
|
||||
@@ -42,8 +52,16 @@ fn test_pairing_flow_unknown_user_to_approved() {
|
||||
assert_eq!(approved.unwrap().id, "user_12345");
|
||||
|
||||
// 5. User is now allowed
|
||||
assert!(store.is_sender_allowed(channel, "user_12345", None).unwrap());
|
||||
assert!(store.is_sender_allowed(channel, "user_12345", Some("alice")).unwrap());
|
||||
assert!(
|
||||
store
|
||||
.is_sender_allowed(channel, "user_12345", None)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.is_sender_allowed(channel, "user_12345", Some("alice"))
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// 6. Pending list is empty
|
||||
let pending_after = store.list_pending(channel).unwrap();
|
||||
@@ -69,7 +87,11 @@ fn test_pairing_flow_cli_approve() {
|
||||
},
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
assert!(store.is_sender_allowed("telegram", "user_999", None).unwrap());
|
||||
assert!(
|
||||
store
|
||||
.is_sender_allowed("telegram", "user_999", None)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -109,4 +131,3 @@ fn test_pairing_multiple_channels_isolated() {
|
||||
store.approve("slack", &r_slack.code).unwrap();
|
||||
assert!(store.is_sender_allowed("slack", "user_b", None).unwrap());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironclaw::channels::Channel;
|
||||
use ironclaw::pairing::PairingStore;
|
||||
use ironclaw::channels::wasm::{
|
||||
ChannelCapabilities, EmitRateLimitConfig, PreparedChannelModule, RegisteredEndpoint,
|
||||
WasmChannel, WasmChannelRouter, WasmChannelRuntime, WasmChannelRuntimeConfig,
|
||||
};
|
||||
use ironclaw::pairing::PairingStore;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Create a test runtime for WASM channel operations.
|
||||
|
||||
@@ -21,6 +21,18 @@ fn get_pool() -> deadpool_postgres::Pool {
|
||||
.expect("Failed to create pool")
|
||||
}
|
||||
|
||||
/// Try to get a connection, returning None if Postgres is unreachable.
|
||||
/// Tests call this to skip gracefully in CI where no database is available.
|
||||
async fn try_connect(pool: &deadpool_postgres::Pool) -> Option<()> {
|
||||
match pool.get().await {
|
||||
Ok(_) => Some(()),
|
||||
Err(e) => {
|
||||
eprintln!("skipping: database unavailable ({e})");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_user(pool: &deadpool_postgres::Pool, user_id: &str) {
|
||||
let conn = pool.get().await.expect("Failed to get connection");
|
||||
conn.execute(
|
||||
@@ -34,6 +46,9 @@ async fn cleanup_user(pool: &deadpool_postgres::Pool, user_id: &str) {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_write_and_read() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_write_read";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -59,6 +74,9 @@ async fn test_workspace_write_and_read() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_append() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_append";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -86,6 +104,9 @@ async fn test_workspace_append() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_nested_paths() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_nested";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -131,6 +152,9 @@ async fn test_workspace_nested_paths() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_delete() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_delete";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -155,6 +179,9 @@ async fn test_workspace_delete() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_memory_operations() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_memory_ops";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -183,6 +210,9 @@ async fn test_workspace_memory_operations() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_daily_log() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_daily_log";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -209,6 +239,9 @@ async fn test_workspace_daily_log() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_fts_search() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_fts_search";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -267,6 +300,9 @@ async fn test_workspace_fts_search() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_hybrid_search_with_mock_embeddings() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_hybrid_search";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -306,6 +342,9 @@ async fn test_workspace_hybrid_search_with_mock_embeddings() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_list_all() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_list_all";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
@@ -331,6 +370,9 @@ async fn test_workspace_list_all() {
|
||||
#[tokio::test]
|
||||
async fn test_workspace_system_prompt() {
|
||||
let pool = get_pool();
|
||||
if try_connect(&pool).await.is_none() {
|
||||
return;
|
||||
}
|
||||
let user_id = "test_system_prompt";
|
||||
cleanup_user(&pool, user_id).await;
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ async fn start_test_server() -> (
|
||||
user_id: "test-user".to_string(),
|
||||
shutdown_tx: tokio::sync::RwLock::new(None),
|
||||
ws_tracker: Some(Arc::new(WsConnectionTracker::new())),
|
||||
llm_provider: None,
|
||||
chat_rate_limiter: ironclaw::channels::web::server::RateLimiter::new(30, 60),
|
||||
});
|
||||
|
||||
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
@@ -66,7 +68,12 @@ async fn connect_ws(
|
||||
addr: SocketAddr,
|
||||
) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> {
|
||||
let url = format!("ws://{}/api/chat/ws?token={}", addr, AUTH_TOKEN);
|
||||
let request = url.into_client_request().unwrap();
|
||||
let mut request = url.into_client_request().unwrap();
|
||||
// Server requires an Origin header from localhost to prevent cross-site WS hijacking.
|
||||
request.headers_mut().insert(
|
||||
"Origin",
|
||||
format!("http://127.0.0.1:{}", addr.port()).parse().unwrap(),
|
||||
);
|
||||
let (stream, _response) = tokio_tungstenite::connect_async(request)
|
||||
.await
|
||||
.expect("Failed to connect WebSocket");
|
||||
|
||||
Reference in New Issue
Block a user