Initial commit: aether-tools-rs workspace
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
//! End-to-end integration tests for aether-ask.
|
||||
//!
|
||||
//! Spawns the actual HTTP server on a real port and verifies the
|
||||
//! /api/stats, /q/<question>, and POST /ask flows.
|
||||
|
||||
use serde_json::json;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn binary_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_BIN_EXE_aether-ask"))
|
||||
}
|
||||
|
||||
struct Ask {
|
||||
child: Child,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Ask {
|
||||
fn spawn(data_dir: &PathBuf) -> Self {
|
||||
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
|
||||
let port = 31000 + ((stamp % 1000) as u16);
|
||||
let mut cmd = Command::new(binary_path());
|
||||
cmd.env("AETHER_RECALL_DIR", data_dir)
|
||||
.env("AETHER_ASK_PORT", port.to_string())
|
||||
.env("OLLAMA_HOST", "127.0.0.1")
|
||||
.env("OLLAMA_PORT", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
let mut child = cmd.spawn().expect("spawn aether-ask");
|
||||
|
||||
// Wait for it to bind
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
for _ in 0..50 {
|
||||
if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(50)).is_ok() {
|
||||
return Self { child, port };
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
panic!("aether-ask did not bind to {}", addr);
|
||||
}
|
||||
|
||||
fn http(&self, req: &str) -> String {
|
||||
let mut s = TcpStream::connect(("127.0.0.1", self.port)).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(3))).ok();
|
||||
s.write_all(req.as_bytes()).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
s.read_to_end(&mut buf).ok();
|
||||
String::from_utf8_lossy(&buf).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ask {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stats_endpoint_returns_json() {
|
||||
let dir = tempdir_in("/tmp");
|
||||
let ask = Ask::spawn(&dir);
|
||||
let r = ask.http("GET /api/stats HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assert!(r.contains("\"records\":"), "got: {r}");
|
||||
assert!(r.contains("\"hit_rate\":"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_root_serves_html() {
|
||||
let dir = tempdir_in("/tmp");
|
||||
let ask = Ask::spawn(&dir);
|
||||
let r = ask.http("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
|
||||
assert!(r.starts_with("HTTP/1.1 200"));
|
||||
assert!(r.contains("AetherAsk"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_ask_returns_json() {
|
||||
let dir = tempdir_in("/tmp");
|
||||
let ask = Ask::spawn(&dir);
|
||||
let body = json!({"question":"hello-world-test-q"}).to_string();
|
||||
let r = ask.http(&format!(
|
||||
"POST /ask HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
||||
body.len(), body
|
||||
));
|
||||
assert!(r.starts_with("HTTP/1.1 200"));
|
||||
let body = r.split("\r\n\r\n").nth(1).unwrap_or("");
|
||||
let json: serde_json::Value = serde_json::from_str(body).expect("json body");
|
||||
// No ollama: should report the source as neural with the error message
|
||||
// or, if cached from a prior ask, maybe deterministic. ollama is
|
||||
// unreachable so we expect the [error] fallback body path.
|
||||
let answer = json["answer"].as_str().unwrap();
|
||||
// Either successfully recorded with deterministic fallback, or stored
|
||||
// the ollama error. We don't enforce the exact text, only that answer field exists.
|
||||
assert!(!answer.is_empty());
|
||||
// The next call must hit the cache (deterministic)
|
||||
let body2 = json!({"question":"hello-world-test-q"}).to_string();
|
||||
let r2 = ask.http(&format!(
|
||||
"POST /ask HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
||||
body2.len(), body2
|
||||
));
|
||||
let body2 = r2.split("\r\n\r\n").nth(1).unwrap_or("");
|
||||
let json2: serde_json::Value = serde_json::from_str(body2).unwrap();
|
||||
assert_eq!(json2["source"], "deterministic",
|
||||
"second identical ask must be a cache hit, got: {json2:?}");
|
||||
assert_eq!(json2["answer"], json["answer"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_q_route_responds_with_deterministic_after_seeding() {
|
||||
let dir = tempdir_in("/tmp");
|
||||
let ask = Ask::spawn(&dir);
|
||||
// First, record a value directly via /api/... no — we have only /q/ and /ask.
|
||||
// So we issue the same query twice. First ollama fails -> [error …]; second is cache hit.
|
||||
let q = "second-call-determ-test";
|
||||
let _ = ask.http(&format!("GET /q/{q} HTTP/1.1\r\nHost: localhost\r\n\r\n"));
|
||||
let r2 = ask.http(&format!("GET /q/{q} HTTP/1.1\r\nHost: localhost\r\n\r\n"));
|
||||
assert!(r2.contains("[deterministic]"), "got: {r2}");
|
||||
}
|
||||
|
||||
fn tempdir_in(parent: &str) -> PathBuf {
|
||||
let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
|
||||
let p = PathBuf::from(parent).join(format!("aether-ask-it-{}", stamp));
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
p
|
||||
}
|
||||
Reference in New Issue
Block a user