This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ide-retired/crates/el-ide-server/src/tests.rs
T
Will Anderson 1172ab6351 feat: el-ide — native IDE for engram-lang, LSP, type graph, plugin ecosystem
Axum HTTP server (port 7771) serving a single-page IDE with CodeMirror 6
syntax highlighting for engram-lang, a force-directed type graph visualizer,
LSP (completions, hover, diagnostics), SSE-streamed build/run output, a
plugin host with five first-party plugins, and a reasoning panel that proxies
to engram-server. 28 tests across three crates, zero warnings.
2026-04-27 19:12:42 -05:00

236 lines
8.8 KiB
Rust

//! Integration tests for el-ide-server API endpoints.
use std::sync::Arc;
use tokio::sync::Mutex;
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
use el_plugin_host::PluginHost;
use crate::{build_router, config::Config, AppState};
fn test_state(project_path: &str) -> AppState {
AppState {
config: Arc::new(Config {
port: 7771,
project_path: project_path.to_string(),
engram_url: "http://localhost:8742".into(),
}),
plugins: Arc::new(Mutex::new(PluginHost::new())),
}
}
fn test_project_path() -> String {
// Use the examples/hello-project as the test project.
// CARGO_MANIFEST_DIR = el-ide/crates/el-ide-server
let manifest = env!("CARGO_MANIFEST_DIR");
// Normalize: go up three directories from the crate to workspace root, then into examples
let ws_root = std::path::Path::new(manifest)
.parent().unwrap() // crates/
.parent().unwrap() // el-ide/
.to_path_buf();
ws_root.join("examples/hello-project")
.canonicalize()
.unwrap_or_else(|_| ws_root.join("examples/hello-project"))
.to_string_lossy()
.to_string()
}
async fn get_json(app: axum::Router, uri: &str) -> (StatusCode, serde_json::Value) {
let resp = app
.oneshot(Request::get(uri).body(Body::empty()).unwrap())
.await
.unwrap();
let status = resp.status();
let bytes = axum::body::to_bytes(resp.into_body(), 1024 * 1024).await.unwrap();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null);
(status, json)
}
// ── GET / ─────────────────────────────────────────────────────────────────────
#[tokio::test]
async fn test_root_returns_html() {
let state = test_state(&test_project_path());
let app = build_router(state);
let resp = app
.oneshot(Request::get("/").body(Body::empty()).unwrap())
.await
.unwrap();
// The embedded HTML should return 200
assert_eq!(resp.status(), StatusCode::OK);
}
// ── GET /api/files ────────────────────────────────────────────────────────────
#[tokio::test]
async fn test_list_files_returns_entries() {
let project = test_project_path();
let state = test_state(&project);
let app = build_router(state);
let (status, json) = get_json(app, "/api/files?path=.").await;
assert_eq!(status, StatusCode::OK, "body: {json}");
assert!(json.is_array(), "expected array, got {json}");
let arr = json.as_array().unwrap();
assert!(!arr.is_empty(), "expected non-empty file listing");
}
#[tokio::test]
async fn test_list_files_contains_src_dir() {
let project = test_project_path();
let state = test_state(&project);
let app = build_router(state);
let (status, json) = get_json(app, "/api/files?path=.").await;
assert_eq!(status, StatusCode::OK);
let arr = json.as_array().unwrap();
let has_src = arr.iter().any(|e| e["name"] == "src" || e["name"] == "el.toml");
assert!(has_src, "expected src or el.toml in listing; got {arr:?}");
}
// ── GET /api/file ─────────────────────────────────────────────────────────────
#[tokio::test]
async fn test_read_file_main_el() {
let project = test_project_path();
let state = test_state(&project);
let app = build_router(state);
let (status, json) = get_json(app, "/api/file?path=src/main.el").await;
assert_eq!(status, StatusCode::OK, "body: {json}");
assert!(json["content"].is_string(), "expected content field");
let content = json["content"].as_str().unwrap();
assert!(content.contains("fn main"), "expected fn main in content");
}
#[tokio::test]
async fn test_read_missing_file_returns_404() {
let project = test_project_path();
let state = test_state(&project);
let app = build_router(state);
let (status, _) = get_json(app, "/api/file?path=nonexistent.el").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
// ── GET /api/lsp/errors ───────────────────────────────────────────────────────
#[tokio::test]
async fn test_lsp_errors_clean_source() {
let state = test_state(".");
let app = build_router(state);
let source = "fn main() -> Void { let x: Int = 42 }";
let uri = format!("/api/lsp/errors?source={}", urlencoding::encode(source));
let (status, json) = get_json(app, &uri).await;
assert_eq!(status, StatusCode::OK, "body: {json}");
assert!(json.is_array());
let errors: Vec<_> = json.as_array().unwrap().iter()
.filter(|d| d["severity"] == "error")
.collect();
assert!(errors.is_empty(), "unexpected errors: {errors:?}");
}
#[tokio::test]
async fn test_lsp_errors_bad_type() {
let state = test_state(".");
let app = build_router(state);
let source = "let x: NonExistentType = 42";
let uri = format!("/api/lsp/errors?source={}", urlencoding::encode(source));
let (status, json) = get_json(app, &uri).await;
assert_eq!(status, StatusCode::OK, "body: {json}");
assert!(json.is_array());
// Should have at least one diagnostic
assert!(!json.as_array().unwrap().is_empty(), "expected diagnostic for unknown type");
}
// ── GET /api/type-graph ───────────────────────────────────────────────────────
#[tokio::test]
async fn test_type_graph_returns_nodes_and_edges() {
let state = test_state(".");
let app = build_router(state);
let source = "type Point { x: Float y: Float } type Circle { center: Point radius: Float }";
let uri = format!("/api/type-graph?source={}", urlencoding::encode(source));
let (status, json) = get_json(app, &uri).await;
assert_eq!(status, StatusCode::OK, "body: {json}");
assert!(json["nodes"].is_array());
assert!(json["edges"].is_array());
let node_names: Vec<_> = json["nodes"].as_array().unwrap()
.iter().map(|n| n["name"].as_str().unwrap_or("")).collect();
assert!(node_names.contains(&"Point"), "expected Point node");
assert!(node_names.contains(&"Circle"), "expected Circle node");
}
#[tokio::test]
async fn test_type_graph_has_field_edge() {
let state = test_state(".");
let app = build_router(state);
let source = "type Point { x: Float y: Float } type Circle { center: Point radius: Float }";
let uri = format!("/api/type-graph?source={}", urlencoding::encode(source));
let (status, json) = get_json(app, &uri).await;
assert_eq!(status, StatusCode::OK);
let edges = json["edges"].as_array().unwrap();
let has_edge = edges.iter().any(|e| e["from"] == "Circle" && e["to"] == "Point");
assert!(has_edge, "expected Circle->Point edge, edges: {edges:?}");
}
// ── GET /api/plugins ──────────────────────────────────────────────────────────
#[tokio::test]
async fn test_list_plugins_returns_five() {
let state = test_state(".");
let app = build_router(state);
let (status, json) = get_json(app, "/api/plugins").await;
assert_eq!(status, StatusCode::OK, "body: {json}");
let plugins = json.as_array().unwrap();
assert_eq!(plugins.len(), 5, "expected 5 first-party plugins");
}
#[tokio::test]
async fn test_dark_theme_installed() {
let state = test_state(".");
let app = build_router(state);
let (status, json) = get_json(app, "/api/plugins").await;
assert_eq!(status, StatusCode::OK);
let plugins = json.as_array().unwrap();
let dark = plugins.iter().find(|p| p["name"] == "el-theme-dark").unwrap();
assert_eq!(dark["installed"], true);
}
// ── GET /api/lsp/complete ─────────────────────────────────────────────────────
#[tokio::test]
async fn test_completions_return_keywords() {
let state = test_state(".");
let app = build_router(state);
let uri = "/api/lsp/complete?source=&pos=0";
let (status, json) = get_json(app, uri).await;
assert_eq!(status, StatusCode::OK);
assert!(json.is_array());
let labels: Vec<_> = json.as_array().unwrap().iter()
.map(|c| c["label"].as_str().unwrap_or(""))
.collect();
assert!(labels.contains(&"let"), "expected 'let' in completions");
assert!(labels.contains(&"fn"), "expected 'fn' in completions");
}