use axum::body::Body;
use axum::http::{Request, StatusCode};
use ocserve_http::{AppState, LlmRegistry, Payloads, Wires};
use serde_json::json;
use std::sync::Arc;
use tower::ServiceExt;
fn state() -> (Arc<AppState>, axum::Router) {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("t.db");
let blobs = Arc::new(ocserve_store::BlobStore::new(dir.path().join("blobs")).unwrap());
let writer = Arc::new(ocserve_store::Writer::spawn(db.clone()).unwrap());
let llm = LlmRegistry {
endpoints: Default::default(),
pricing: Default::default(),
limits: Default::default(),
default_model: ("fake".into(), "m".into()),
systems: Default::default(),
default_agent: "build".into(),
};
let st = AppState::with_wiring(
None,
Payloads::default(),
Wires {
db,
blobs,
writer,
llm,
},
);
std::mem::forget(dir); let app = ocserve_http::router(st.clone());
(st, app)
}
async fn post(app: &axum::Router, uri: &str, body: serde_json::Value) -> (StatusCode, Vec<u8>) {
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
.await
.unwrap()
.to_vec();
(status, bytes)
}
async fn tui_events(
st: &Arc<AppState>,
f: impl std::future::Future<Output = ()>,
) -> Vec<serde_json::Value> {
let mut rx = st.bus.subscribe();
f.await;
let mut out = Vec::new();
while let Ok(frame) = rx.try_recv() {
let v: serde_json::Value = serde_json::from_str(&frame).unwrap();
let payload = &v["payload"];
if payload["type"] == "sync" {
continue;
}
if let Some(t) = payload["type"].as_str()
&& t.starts_with("tui.")
{
out.push(payload.clone());
}
}
out
}
#[tokio::test]
async fn append_prompt_publishes_text_and_true() {
let (st, app) = state();
let events = tui_events(&st, async {
let (s, b) = post(&app, "/tui/append-prompt", json!({"text": "HELLO"})).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(b, b"true");
})
.await;
assert_eq!(events.len(), 1);
assert_eq!(events[0]["type"], "tui.prompt.append");
assert_eq!(events[0]["properties"]["text"], "HELLO");
}
#[tokio::test]
async fn dialog_openers_publish_the_freeze_commands() {
let (st, app) = state();
let events = tui_events(&st, async {
for path in [
"/tui/open-help",
"/tui/open-sessions",
"/tui/open-themes",
"/tui/open-models",
"/tui/submit-prompt",
"/tui/clear-prompt",
] {
let (s, _) = post(&app, path, json!({})).await;
assert_eq!(s, StatusCode::OK, "{path}");
}
})
.await;
let cmds: Vec<_> = events
.iter()
.map(|e| {
e["properties"]["command"]
.as_str()
.unwrap_or("")
.to_string()
})
.collect();
assert_eq!(
cmds,
vec![
"help.show",
"session.list",
"session.list", "model.list",
"prompt.submit",
"prompt.clear",
]
);
}
#[tokio::test]
async fn execute_command_aliases_unknown_and_missing() {
let (st, app) = state();
let events = tui_events(&st, async {
let (s, b) = post(
&app,
"/tui/execute-command",
json!({"command": "session_new"}),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(b, b"true");
})
.await;
assert_eq!(events[0]["properties"]["command"], "session.new");
let (st, app) = state();
let events = tui_events(&st, async {
let (s, _) = post(&app, "/tui/execute-command", json!({"command": "nope"})).await;
assert_eq!(s, StatusCode::OK);
})
.await;
assert_eq!(events.len(), 1);
assert_eq!(events[0]["type"], "tui.command.execute");
assert!(
events[0]["properties"].get("command").is_none(),
"unknown alias must publish empty properties: {events:?}"
);
let (_, app) = state();
let (s, b) = post(&app, "/tui/execute-command", json!({})).await;
assert_eq!(s, StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
assert_eq!(v["name"], "BadRequest");
assert_eq!(v["data"]["kind"], "Payload");
}
#[tokio::test]
async fn toast_defaults_duration_and_requires_nothing() {
let (st, app) = state();
let events = tui_events(&st, async {
let (s, _) = post(
&app,
"/tui/show-toast",
json!({"message": "hi", "variant": "info"}),
)
.await;
assert_eq!(s, StatusCode::OK);
})
.await;
assert_eq!(events[0]["type"], "tui.toast.show");
assert_eq!(
events[0]["properties"]["duration"], 5000,
"DEFAULT_TOAST_DURATION"
);
assert_eq!(events[0]["properties"]["message"], "hi");
}
#[tokio::test]
async fn publish_dispatch_known_types_only() {
let (st, app) = state();
let events = tui_events(&st, async {
let (s, _) = post(
&app,
"/tui/publish",
json!({"type": "tui.prompt.append", "properties": {"text": "PUB"}}),
)
.await;
assert_eq!(s, StatusCode::OK);
let (s, _) = post(
&app,
"/tui/publish",
json!({"type": "tui.unknown.thing", "properties": {}}),
)
.await;
assert_eq!(s, StatusCode::OK);
})
.await;
assert_eq!(events.len(), 1, "only the known type publishes: {events:?}");
assert_eq!(events[0]["properties"]["text"], "PUB");
}
#[tokio::test]
async fn select_session_validates_shape_then_existence() {
let (st, app) = state();
let (s, b) = post(&app, "/tui/select-session", json!({"sessionID": "bad"})).await;
assert_eq!(s, StatusCode::BAD_REQUEST);
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
assert_eq!(
v["data"]["message"],
"Expected a string starting with \"ses\", got \"bad\"\n at [\"sessionID\"]"
);
let (s, b) = post(
&app,
"/tui/select-session",
json!({"sessionID": "ses_none"}),
)
.await;
assert_eq!(s, StatusCode::NOT_FOUND);
let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
assert_eq!(v["name"], "NotFoundError");
ocserve_store::insert_session(
&st.writer,
&json!({
"id": "ses_here", "projectID": "global", "directory": "/w",
"path": "ses_here", "slug": "ses_here", "title": "t", "version": "1",
"time": {"created": 1, "updated": 2},
}),
)
.unwrap();
let events = tui_events(&st, async {
let (s, b) = post(
&app,
"/tui/select-session",
json!({"sessionID": "ses_here"}),
)
.await;
assert_eq!(s, StatusCode::OK);
assert_eq!(b, b"true");
})
.await;
assert_eq!(events[0]["type"], "tui.session.select");
assert_eq!(events[0]["properties"]["sessionID"], "ses_here");
}
#[tokio::test]
async fn control_rendezvous_round_trips_and_next_returns_queued_request() {
let (st, app) = state();
assert!(
st.tui
.submit_request(json!({"path": "/x", "body": {"k": 1}}))
.await,
"queue accepts while a consumer exists"
);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri("/tui/control/next")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let v: serde_json::Value = serde_json::from_slice(
&axum::body::to_bytes(resp.into_body(), 1 << 20)
.await
.unwrap(),
)
.unwrap();
assert_eq!(v["path"], "/x");
let (s, b) = post(&app, "/tui/control/response", json!({"ok": true})).await;
assert_eq!(s, StatusCode::OK);
assert_eq!(b, b"true");
}