libnoa 0.1.1

AI-native distributed version control system with per-agent workspace isolation, JSONL append-only logs, snapshot-based history, and full git protocol compatibility
Documentation
use std::sync::Arc;

use axum::{
    body::Body,
    http::{Method, Request, StatusCode},
};
use tower::ServiceExt;

use libnoa::server::{router, AppState};

async fn make_app() -> (tempfile::TempDir, axum::Router) {
    let tmp = tempfile::TempDir::new().unwrap();
    let db = Arc::new(
        redb::Database::builder()
            .create(tmp.path().join("server-test.redb"))
            .unwrap(),
    );
    let state = AppState::new(db);
    let app = router(state);
    (tmp, app)
}

fn make_request(method: Method, uri: &str, body: Option<String>) -> Request<Body> {
    let mut builder = Request::builder().method(method).uri(uri);
    if let Some(b) = body {
        builder = builder.header("content-type", "application/json");
        builder.body(Body::from(b)).unwrap()
    } else {
        builder.body(Body::empty()).unwrap()
    }
}

#[tokio::test]
async fn test_list_refs_empty() {
    let (_tmp, app) = make_app().await;
    let req = make_request(Method::GET, "/api/v1/refs", None);
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_push_ref() {
    let (_tmp, app) = make_app().await;
    let body = r#"{"name": "main", "id": "noa_test123"}"#.to_string();
    let req = make_request(Method::POST, "/api/v1/refs", Some(body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
}

#[tokio::test]
async fn test_upload_blob_and_get() {
    let (_tmp, app) = make_app().await;

    use base64::Engine;
    let content = base64::engine::general_purpose::STANDARD.encode(b"hello noa server");
    let upload_body = format!(r#"{{"blobs": [{{"content": "{}"}}]}}"#, content);
    let req = make_request(Method::POST, "/api/v1/blobs", Some(upload_body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body_bytes = axum::body::to_bytes(resp.into_body(), 1024 * 1024)
        .await
        .unwrap();
    let result: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
    let hash = result["ids"][0].as_str().unwrap().to_string();
    assert!(!hash.is_empty());
}

#[tokio::test]
async fn test_get_blob_not_found() {
    let (_tmp, app) = make_app().await;
    let req = make_request(Method::GET, "/api/v1/blob/nonexistent_hash", None);
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn test_get_tree_not_found() {
    let (_tmp, app) = make_app().await;
    let req = make_request(Method::GET, "/api/v1/tree/nonexistent_hash", None);
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn test_list_workspaces() {
    let (_tmp, app) = make_app().await;
    let req = make_request(Method::GET, "/api/v1/workspaces", None);
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_list_snapshots() {
    let (_tmp, app) = make_app().await;
    let req = make_request(Method::GET, "/api/v1/snapshots", None);
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_create_workspace() {
    let (_tmp, app) = make_app().await;
    let body = r#"{"workspace": {"name": "test-ws", "head": "noa_base", "base": "noa_base", "agent_id": null, "last_seq": 0, "created_at": 1000, "updated_at": 1000}}"#.to_string();
    let req = make_request(Method::POST, "/api/v1/workspaces", Some(body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
}

#[tokio::test]
async fn test_upload_trees() {
    let (_tmp, app) = make_app().await;
    let body =
        r#"{"trees": [{"entries": [{"name": "main.rs", "kind": "Blob", "id": "hash123"}]}]}"#
            .to_string();
    let req = make_request(Method::POST, "/api/v1/trees", Some(body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_create_snapshot() {
    let (_tmp, app) = make_app().await;
    let body = r#"{"snapshot": {"id": "noa_snap001", "tree_hash": "tree123", "parents": [], "workspace": "default", "author": "test", "timestamp": 1000, "message": "test snapshot"}}"#.to_string();
    let req = make_request(Method::POST, "/api/v1/snapshots", Some(body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
}

#[tokio::test]
async fn test_upload_blobs_invalid_base64() {
    let (_tmp, app) = make_app().await;
    let body = r#"{"blobs": [{"content": "not-valid-base64!!!"}]}"#.to_string();
    let req = make_request(Method::POST, "/api/v1/blobs", Some(body));
    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}