Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: E2E

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop

concurrency:
group: e2e-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
profile-e2e:
runs-on: ubuntu-latest
environment: ${{ (github.base_ref == 'main' || github.ref == 'refs/heads/main') && 'production' || 'development' }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libgtk-3-dev \
libsoup-3.0-dev \
libjavascriptcoregtk-4.1-dev

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Run E2E profile test
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: cargo test -p clawpal-core --test profile_e2e -- --nocapture
working-directory: src-tauri
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions clawpal-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ russh-sftp = "2.0"
dirs = "5"
async-trait = "0.1"
regex = "1.11"

[dev-dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
12 changes: 8 additions & 4 deletions clawpal-core/src/openclaw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,20 @@ mod tests {

#[cfg(unix)]
fn create_fake_openclaw_script(body: &str) -> String {
use std::io::Write;
use std::os::unix::fs::PermissionsExt;

let dir =
std::env::temp_dir().join(format!("clawpal-core-openclaw-test-{}", Uuid::new_v4()));
fs::create_dir_all(&dir).expect("create temp dir");
let path = dir.join("fake-openclaw.sh");
fs::write(&path, body).expect("write script");
#[cfg(unix)]
// Open → write → fsync → close explicitly to avoid ETXTBSY on exec.
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("chmod");
let mut f = fs::File::create(&path).expect("create script");
f.write_all(body.as_bytes()).expect("write script");
f.sync_all().expect("sync script");
}
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("chmod");
path.to_string_lossy().to_string()
}

Expand Down
96 changes: 96 additions & 0 deletions clawpal-core/tests/profile_e2e.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! E2E test: create an Anthropic (Claude) profile, persist it, and verify
//! the API key works with a real provider probe.
//!
//! Requires `ANTHROPIC_API_KEY` in the environment. The test is skipped
//! automatically when the key is absent so local `cargo test` still passes.

use std::fs;
use std::sync::Mutex;

use clawpal_core::openclaw::OpenclawCli;
use clawpal_core::profile::{self, ModelProfile};
use uuid::Uuid;

static ENV_LOCK: Mutex<()> = Mutex::new(());

fn temp_data_dir() -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!("clawpal-core-profile-e2e-{}", Uuid::new_v4()));
fs::create_dir_all(&path).expect("create temp dir");
path
}

/// Lightweight Anthropic API probe — sends a single-token request to verify
/// the key and model are valid.
fn anthropic_probe(api_key: &str, model: &str) -> Result<(), String> {
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| format!("http client: {e}"))?;

let resp = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&serde_json::json!({
"model": model,
"max_tokens": 1,
"messages": [{"role": "user", "content": "ping"}]
}))
.send()
.map_err(|e| format!("request failed: {e}"))?;

let status = resp.status().as_u16();
if (200..300).contains(&status) {
return Ok(());
}
let body = resp.text().unwrap_or_default();
Err(format!("probe failed (HTTP {status}): {body}"))
}

#[test]
fn e2e_create_anthropic_profile_and_probe() {
let api_key = match std::env::var("ANTHROPIC_API_KEY") {
Ok(k) if !k.trim().is_empty() => k,
_ => {
eprintln!("ANTHROPIC_API_KEY not set — skipping E2E profile test");
return;
}
};

let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let data_dir = temp_data_dir();
std::env::set_var("CLAWPAL_DATA_DIR", &data_dir);

// ── 1. Create & persist profile ────────────────────────────────
let claude_profile = ModelProfile {
id: String::new(), // let upsert assign UUID
name: String::new(),
provider: "anthropic".to_string(),
model: "claude-sonnet-4-20250514".to_string(),
auth_ref: "ANTHROPIC_API_KEY".to_string(),
api_key: Some(api_key.clone()),
base_url: None,
description: Some("E2E test profile".to_string()),
enabled: true,
};

// OpenclawCli is unused by upsert_profile (local storage) but required
// by the signature.
let cli = OpenclawCli::with_bin("__unused__".to_string());
let saved = profile::upsert_profile(&cli, claude_profile).expect("upsert_profile");

assert!(!saved.id.is_empty(), "profile id should be generated");
assert_eq!(saved.provider, "anthropic");
assert_eq!(saved.model, "claude-sonnet-4-20250514");
assert_eq!(saved.name, "anthropic/claude-sonnet-4-20250514");

// ── 2. Verify persistence via list ─────────────────────────────
let profiles = profile::list_profiles(&cli).expect("list_profiles");
assert_eq!(profiles.len(), 1);
assert_eq!(profiles[0].id, saved.id);
assert_eq!(profiles[0].provider, "anthropic");

// ── 3. Real API probe ──────────────────────────────────────────
anthropic_probe(&api_key, &saved.model).expect("Anthropic API probe should succeed");
}