mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
289 lines
11 KiB
Rust
289 lines
11 KiB
Rust
//! End-to-end subconscious test with real Ollama, real memory, real SQLite.
|
|
//!
|
|
//! Requires Ollama running at localhost:11434 with a model loaded.
|
|
//! Run with: `cargo test --test subconscious_e2e -- --nocapture --ignored`
|
|
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::json;
|
|
|
|
/// Test config for the heuristic-only ingestion pipeline.
|
|
fn ci_safe_ingestion_config() -> openhuman_core::openhuman::memory::MemoryIngestionConfig {
|
|
openhuman_core::openhuman::memory::MemoryIngestionConfig::default()
|
|
}
|
|
|
|
async fn ingest_doc(
|
|
memory: &openhuman_core::openhuman::memory::UnifiedMemory,
|
|
namespace: &str,
|
|
key: &str,
|
|
title: &str,
|
|
content: &str,
|
|
) -> String {
|
|
use openhuman_core::openhuman::memory::{MemoryIngestionRequest, NamespaceDocumentInput};
|
|
let result = memory
|
|
.ingest_document(MemoryIngestionRequest {
|
|
document: NamespaceDocumentInput {
|
|
namespace: namespace.to_string(),
|
|
key: key.to_string(),
|
|
title: title.to_string(),
|
|
content: content.to_string(),
|
|
source_type: "test".to_string(),
|
|
priority: "high".to_string(),
|
|
tags: Vec::new(),
|
|
metadata: json!({}),
|
|
category: "core".to_string(),
|
|
session_id: None,
|
|
document_id: None,
|
|
},
|
|
config: ci_safe_ingestion_config(),
|
|
})
|
|
.await
|
|
.expect("ingest should succeed");
|
|
result.document_id
|
|
}
|
|
|
|
/// Full two-tick E2E test:
|
|
///
|
|
/// **Tick 1**: Gmail has 3 urgent emails, Notion has a deadline tracker.
|
|
/// → Ollama should detect urgent items → act or escalate.
|
|
///
|
|
/// **Tick 2**: New data — deadline moved, ownership changed.
|
|
/// → Ollama should detect the change → act or escalate on new state.
|
|
///
|
|
/// Verifies:
|
|
/// - Tasks loaded from HEARTBEAT.md seed
|
|
/// - Real Ollama evaluation produces valid decisions
|
|
/// - SQLite log entries created for each tick
|
|
/// - Act tasks produce text output from Ollama
|
|
/// - Second tick sees delta (new data only)
|
|
#[tokio::test]
|
|
#[ignore] // requires running Ollama
|
|
async fn two_tick_e2e_with_real_ollama() {
|
|
use openhuman_core::openhuman::embeddings::NoopEmbedding;
|
|
use openhuman_core::openhuman::memory::{MemoryClient, UnifiedMemory};
|
|
use openhuman_core::openhuman::subconscious::store;
|
|
|
|
// ── Setup workspace ──────────────────────────────────────────────
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let workspace = tmp.path();
|
|
|
|
// Write HEARTBEAT.md
|
|
std::fs::write(
|
|
workspace.join("HEARTBEAT.md"),
|
|
"\
|
|
# Periodic Tasks
|
|
|
|
- Check Gmail for urgent emails that need immediate attention
|
|
- Review Notion project tracker for deadline changes
|
|
- Monitor connected skills for errors or disconnections
|
|
",
|
|
)
|
|
.expect("write heartbeat");
|
|
|
|
// Initialize memory
|
|
let memory = UnifiedMemory::new(workspace, Arc::new(NoopEmbedding), None).expect("init memory");
|
|
let memory_client =
|
|
MemoryClient::from_workspace_dir(workspace.to_path_buf()).expect("memory client");
|
|
|
|
// ── Tick 1: Ingest initial data ──────────────────────────────────
|
|
println!("\n============================================================");
|
|
println!(" TICK 1: Initial state — urgent emails + project tracker");
|
|
println!("============================================================\n");
|
|
|
|
ingest_doc(
|
|
&memory,
|
|
"skill-gmail",
|
|
"urgent-emails-batch1",
|
|
"3 urgent emails in inbox",
|
|
"\
|
|
Email 1: From alice@partner.com — Subject: URGENT: API contract deadline
|
|
Body: The API integration deadline has been moved from Friday to tomorrow (Thursday).
|
|
Please confirm you can deliver by end of day. This is blocking the partner launch.
|
|
|
|
Email 2: From boss@company.com — Subject: Re: Q1 Budget Review
|
|
Body: Need your updated numbers by 3pm today. The board meeting is tomorrow morning.
|
|
Please prioritize this over other tasks.
|
|
|
|
Email 3: From security@company.com — Subject: [ACTION REQUIRED] Password expiry
|
|
Body: Your corporate password expires in 24 hours. Please update it via the portal
|
|
to avoid being locked out of all systems.",
|
|
)
|
|
.await;
|
|
|
|
ingest_doc(
|
|
&memory,
|
|
"skill-notion",
|
|
"q1-tracker-v1",
|
|
"Q1 Delivery Tracker",
|
|
"\
|
|
Project: API Integration
|
|
Status: In Progress
|
|
Deadline: April 5 (Friday)
|
|
Owner: You
|
|
Dependencies: Partner API docs (received), Internal review (pending)
|
|
Notes: On track for Friday delivery. Partner team confirmed their side is ready.
|
|
|
|
Project: Q1 Budget Report
|
|
Status: Draft
|
|
Deadline: Today 3pm
|
|
Owner: You
|
|
Notes: Numbers need updating. Finance sent corrections yesterday.",
|
|
)
|
|
.await;
|
|
|
|
// Build engine with real config
|
|
let mut config = openhuman_core::openhuman::config::Config::default();
|
|
config.workspace_dir = workspace.to_path_buf();
|
|
config.heartbeat.enabled = true;
|
|
config.heartbeat.inference_enabled = true;
|
|
config.heartbeat.interval_minutes = 5;
|
|
config.heartbeat.context_budget_tokens = 40_000;
|
|
config.local_ai.runtime_enabled = true;
|
|
config.local_ai.usage.subconscious = true;
|
|
|
|
let engine = openhuman_core::openhuman::subconscious::SubconsciousEngine::new(
|
|
&config,
|
|
Some(Arc::new(memory_client)),
|
|
);
|
|
|
|
// Run tick 1
|
|
let result1 = engine.tick().await.expect("tick 1 should succeed");
|
|
|
|
println!("\n--- Tick 1 Results ---");
|
|
println!(" Duration: {}ms", result1.duration_ms);
|
|
println!(" Evaluations: {}", result1.evaluations.len());
|
|
println!(" Executed: {}", result1.executed);
|
|
println!(" Escalated: {}", result1.escalated);
|
|
for eval in &result1.evaluations {
|
|
println!(" [{}] {:?} — {}", eval.task_id, eval.decision, eval.reason);
|
|
}
|
|
|
|
// Verify tick 1
|
|
assert!(
|
|
!result1.evaluations.is_empty(),
|
|
"Ollama should produce evaluations for seeded tasks"
|
|
);
|
|
|
|
// Check SQLite log
|
|
let log1 = store::with_connection(workspace, |conn| store::list_log_entries(conn, None, 50))
|
|
.expect("list log");
|
|
println!("\n Log entries after tick 1: {}", log1.len());
|
|
for entry in &log1 {
|
|
println!(
|
|
" [{}] {} — {}",
|
|
entry.task_id,
|
|
entry.decision,
|
|
entry.result.as_deref().unwrap_or("(none)")
|
|
);
|
|
}
|
|
assert!(!log1.is_empty(), "Should have log entries after tick 1");
|
|
|
|
// Check tasks were seeded
|
|
let tasks = store::with_connection(workspace, |conn| store::list_tasks(conn, false))
|
|
.expect("list tasks");
|
|
println!("\n Tasks: {}", tasks.len());
|
|
for t in &tasks {
|
|
println!(
|
|
" [{}] {} (source={:?}, completed={})",
|
|
t.id, t.title, t.source, t.completed
|
|
);
|
|
}
|
|
assert_eq!(tasks.len(), 3, "Should have 3 tasks from HEARTBEAT.md");
|
|
|
|
// ── Tick 2: Ingest NEW data (state change) ──────────────────────
|
|
println!("\n============================================================");
|
|
println!(" TICK 2: State change — deadline moved, new urgent email");
|
|
println!("============================================================\n");
|
|
|
|
ingest_doc(
|
|
&memory,
|
|
"skill-gmail",
|
|
"urgent-deadline-moved",
|
|
"CRITICAL: API deadline moved to TOMORROW",
|
|
"\
|
|
Email from alice@partner.com — Subject: RE: URGENT: API contract deadline
|
|
Body: Update — the deadline has been moved UP to tomorrow (Wednesday) not Thursday.
|
|
The partner CEO is flying in Wednesday evening and wants a demo.
|
|
This is now the #1 priority. Please drop everything else.
|
|
|
|
Email from boss@company.com — Subject: Re: Re: Q1 Budget Review
|
|
Body: Good news — finance approved a 1-day extension on the budget report.
|
|
New deadline is Friday. Focus on the API delivery instead.",
|
|
)
|
|
.await;
|
|
|
|
ingest_doc(
|
|
&memory,
|
|
"skill-notion",
|
|
"q1-tracker-v2",
|
|
"Q1 Delivery Tracker (UPDATED)",
|
|
"\
|
|
Project: API Integration
|
|
Status: AT RISK
|
|
Deadline: TOMORROW (Wednesday) — moved up from Friday
|
|
Owner: You
|
|
Dependencies: Partner API docs (received), Internal review (BLOCKING — not started)
|
|
Notes: CRITICAL — deadline moved up 2 days. Internal review not started.
|
|
Partner CEO demo Wednesday evening. Need to start review NOW.
|
|
Blockers: Internal review team not yet assigned.
|
|
|
|
Project: Q1 Budget Report
|
|
Status: Draft
|
|
Deadline: Friday (extended from today)
|
|
Owner: You
|
|
Notes: Extension granted. Lower priority now.",
|
|
)
|
|
.await;
|
|
|
|
// Run tick 2
|
|
let result2 = engine.tick().await.expect("tick 2 should succeed");
|
|
|
|
println!("\n--- Tick 2 Results ---");
|
|
println!(" Duration: {}ms", result2.duration_ms);
|
|
println!(" Evaluations: {}", result2.evaluations.len());
|
|
println!(" Executed: {}", result2.executed);
|
|
println!(" Escalated: {}", result2.escalated);
|
|
for eval in &result2.evaluations {
|
|
println!(" [{}] {:?} — {}", eval.task_id, eval.decision, eval.reason);
|
|
}
|
|
|
|
// Verify tick 2
|
|
assert!(
|
|
!result2.evaluations.is_empty(),
|
|
"Ollama should produce evaluations for tick 2"
|
|
);
|
|
|
|
// Check cumulative log
|
|
let log2 = store::with_connection(workspace, |conn| store::list_log_entries(conn, None, 50))
|
|
.expect("list log");
|
|
println!("\n Total log entries after tick 2: {}", log2.len());
|
|
assert!(
|
|
log2.len() > log1.len(),
|
|
"Tick 2 should add more log entries"
|
|
);
|
|
|
|
// Check for any escalations
|
|
let escalations = store::with_connection(workspace, |conn| store::list_escalations(conn, None))
|
|
.expect("list escalations");
|
|
println!(" Escalations: {}", escalations.len());
|
|
for esc in &escalations {
|
|
println!(
|
|
" [{}] {} — {} (status={:?})",
|
|
esc.task_id, esc.title, esc.description, esc.status
|
|
);
|
|
}
|
|
|
|
// ── Status check ─────────────────────────────────────────────────
|
|
let status = engine.status().await;
|
|
println!("\n--- Engine Status ---");
|
|
println!(" Enabled: {}", status.enabled);
|
|
println!(" Total ticks: {}", status.total_ticks);
|
|
println!(" Task count: {}", status.task_count);
|
|
println!(" Pending escalations: {}", status.pending_escalations);
|
|
assert_eq!(status.total_ticks, 2);
|
|
|
|
println!("\n============================================================");
|
|
println!(" E2E TEST PASSED");
|
|
println!("============================================================\n");
|
|
}
|