Archived
1172ab6351
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.
236 lines
8.8 KiB
Rust
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");
|
|
}
|