use std::borrow::Cow;
use std::io::Write;
use std::time::Duration;
use rmcp::{
model::{
ArgumentInfo, CallToolRequestParams, CompleteRequestParams, GetPromptRequestParams,
ReadResourceRequestParams, Reference, ResourceReference,
},
transport::TokioChildProcess,
ServiceExt,
};
use serde_json::{json, Map, Value};
use tokio::time::{sleep, timeout};
use algocline_app::PRESET_CATALOG_VERSION;
fn call_params(name: &str, args: Value) -> CallToolRequestParams {
let arguments = match args {
Value::Object(map) => Some(map),
_ => None,
};
let mut p = CallToolRequestParams::default();
p.name = Cow::Owned(name.to_string());
p.arguments = arguments;
p
}
fn call_params_empty(name: &str) -> CallToolRequestParams {
let mut p = CallToolRequestParams::default();
p.name = Cow::Owned(name.to_string());
p.arguments = Some(Map::new());
p
}
async fn connect() -> rmcp::service::RunningService<rmcp::RoleClient, ()> {
let bin = std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")));
let transport = TokioChildProcess::new(tokio::process::Command::new(bin))
.expect("failed to spawn alc server");
().serve(transport)
.await
.expect("failed to initialize MCP session")
}
fn extract_text(result: &rmcp::model::CallToolResult) -> &str {
result
.content
.first()
.and_then(|c| c.raw.as_text())
.map(|t| t.text.as_str())
.unwrap_or("")
}
fn redact_uuids(text: &str) -> String {
let re = regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
.expect("invalid regex");
re.replace_all(text, "<UUID>").to_string()
}
fn redact_paths(text: &str) -> String {
if let Some(home) = dirs::home_dir() {
text.replace(home.to_str().unwrap_or(""), "<HOME>")
} else {
text.to_string()
}
}
fn redact_gh_credentials(text: &str) -> String {
let re_user_name = regex::Regex::new(r#""user_name":\s*"[^"]*""#).expect("invalid regex");
let re_user_email = regex::Regex::new(r#""user_email":\s*"[^"]*""#).expect("invalid regex");
let re_ssh_found = regex::Regex::new(r#""found":\s*\[[^\]]*\]"#).expect("invalid regex");
let re_origin_err = regex::Regex::new(r#"("error":\s*)"[^"]*""#).expect("invalid regex");
let text = re_user_name
.replace_all(text, r#""user_name": "<GIT_USER_NAME>""#)
.into_owned();
let text = re_user_email
.replace_all(&text, r#""user_email": "<GIT_USER_EMAIL>""#)
.into_owned();
let text = re_ssh_found
.replace_all(&text, r#""found": ["<REDACTED>"]"#)
.into_owned();
let text = re_origin_err
.replace_all(&text, r#"$1"<REDACTED>""#)
.into_owned();
text
}
fn redact(text: &str) -> String {
redact_gh_credentials(&redact_paths(&redact_uuids(text)))
}
async fn connect_with_alc_home(
alc_home: &std::path::Path,
) -> rmcp::service::RunningService<rmcp::RoleClient, ()> {
let bin = std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")));
let packages_path = alc_home.join("packages");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("ALC_HOME", alc_home)
.env("ALC_PACKAGES_PATH", &packages_path);
let transport = TokioChildProcess::new(cmd).expect("failed to spawn alc server");
().serve(transport)
.await
.expect("failed to initialize MCP session")
}
async fn read_resource(
client: &rmcp::service::RunningService<rmcp::RoleClient, ()>,
uri: &str,
) -> Result<rmcp::model::ReadResourceResult, rmcp::service::ServiceError> {
client
.read_resource(ReadResourceRequestParams::new(uri.to_string()))
.await
}
fn resource_text(contents: &rmcp::model::ResourceContents) -> (&str, &str) {
match contents {
rmcp::model::ResourceContents::TextResourceContents { uri, text, .. } => {
(uri.as_str(), text.as_str())
}
rmcp::model::ResourceContents::BlobResourceContents { uri, .. } => {
panic!("expected TextResourceContents, got BlobResourceContents for URI {uri}")
}
}
}
async fn call_json(
client: &rmcp::service::RunningService<rmcp::RoleClient, ()>,
name: &str,
args: Value,
) -> Value {
let result = client
.call_tool(call_params(name, args))
.await
.expect("call_tool failed");
let text = extract_text(&result);
serde_json::from_str(text).unwrap_or_else(|e| panic!("JSON parse failed: {e}\nraw: {text}"))
}
async fn poll_until_paused(
client: &rmcp::service::RunningService<rmcp::RoleClient, ()>,
session_id: &str,
max_wait: Duration,
) -> Value {
timeout(max_wait, async {
loop {
let state = call_json(client, "alc_v2_state", json!({"session_id": session_id})).await;
if state["state"].as_str() == Some("paused") {
return state;
}
sleep(Duration::from_millis(10)).await;
}
})
.await
.expect("timed out waiting for paused state")
}
#[tokio::test]
async fn test_list_tools() {
let client = connect().await;
let tools = client
.list_all_tools()
.await
.expect("list_all_tools failed");
let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
names.sort();
insta::assert_json_snapshot!("list_tools", names);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_info() {
let client = connect().await;
let result = client
.call_tool(call_params_empty("alc_info"))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let redacted = redact(text);
insta::assert_snapshot!("alc_info", redacted);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_empty() {
let client = connect().await;
let result = client
.call_tool(call_params_empty("alc_status"))
.await
.expect("call_tool failed");
let text = extract_text(&result);
insta::assert_snapshot!("alc_status_empty", text);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_preset_meta_empty_registry() {
let client = connect().await;
let resp = call_json(&client, "alc_status", json!({ "pending_filter": "meta" })).await;
assert_eq!(resp["active_sessions"], 0);
assert_eq!(resp["sessions"].as_array().unwrap().len(), 0);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_preset_preview_and_full_empty_registry() {
let client = connect().await;
for preset in ["preview", "full"] {
let resp = call_json(&client, "alc_status", json!({ "pending_filter": preset })).await;
assert_eq!(
resp["active_sessions"], 0,
"preset '{preset}' should return empty-registry shape"
);
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_unknown_preset_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_status",
json!({ "pending_filter": "bogus" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("unknown pending_filter preset"),
"expected typed error, got: {text}"
);
assert!(
text.contains("bogus"),
"error should echo the bad preset name, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_bad_shape_errors() {
let client = connect().await;
let result = client
.call_tool(call_params("alc_status", json!({ "pending_filter": true })))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("pending_filter must be a preset name"),
"expected shape error, got: {text}"
);
assert!(
text.contains("bool"),
"error should name the bad type, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_custom_object_filter() {
let client = connect().await;
let resp = call_json(
&client,
"alc_status",
json!({
"pending_filter": {
"query_id": true,
"prompt": { "mode": "preview", "chars": 50 }
}
}),
)
.await;
assert_eq!(resp["active_sessions"], 0);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_status_paused_session_projection() {
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({ "code": "return alc.llm('What is 2+2?')" }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id").to_string();
let resp = call_json(
&client,
"alc_status",
json!({
"session_id": session_id,
"pending_filter": "meta",
}),
)
.await;
assert_eq!(resp["pending_queries"], 1, "should report 1 pending query");
let pending = resp["pending"]
.as_array()
.expect("pending array should be emitted when filter is set");
assert_eq!(pending.len(), 1);
assert!(
pending[0]["query_id"].is_string(),
"query_id must be present"
);
assert!(
pending[0]["max_tokens"].is_number(),
"max_tokens must be present"
);
assert!(
pending[0].get("prompt").is_none(),
"meta preset must not project prompt"
);
assert!(
pending[0].get("prompt_preview").is_none(),
"meta preset must not project prompt_preview"
);
let resp = call_json(
&client,
"alc_status",
json!({
"session_id": session_id,
"pending_filter": "preview",
}),
)
.await;
let pending = resp["pending"].as_array().expect("pending array");
assert!(
pending[0]["prompt_preview"].is_string(),
"preview preset must project prompt_preview"
);
let _ = call_json(
&client,
"alc_continue",
json!({ "session_id": session_id, "response": "4" }),
)
.await;
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_run_pure_lua() {
let client = connect().await;
let result = client
.call_tool(call_params("alc_run", json!({ "code": "return 1 + 2" })))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let parsed: Value = serde_json::from_str(text).expect("response should be JSON");
assert_eq!(parsed["status"], "completed");
assert_eq!(parsed["result"], 3);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_run_lua_error() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_run",
json!({ "code": "error('intentional test error')" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("intentional test error"),
"expected error message in response, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_run_ctx_stringified_json_normalized_to_table() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_run",
json!({
"code": "return type(ctx) .. \"/\" .. tostring(ctx.task)",
"ctx": "{\"task\":\"probe\"}",
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let parsed: Value = serde_json::from_str(text).expect("response should be JSON");
assert_eq!(parsed["status"], "completed");
assert_eq!(
parsed["result"], "table/probe",
"stringified ctx must be auto-decoded into a table, got {parsed}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_run_ctx_schema_is_object() {
let client = connect().await;
let tools = client
.list_all_tools()
.await
.expect("list_all_tools failed");
let alc_run = tools
.iter()
.find(|t| t.name.as_ref() == "alc_run")
.expect("alc_run tool should be present");
let schema_json: Value =
serde_json::to_value(&alc_run.input_schema).expect("schema serializable");
let ctx_schema = schema_json
.get("properties")
.and_then(|p| p.get("ctx"))
.unwrap_or_else(|| panic!("alc_run schema missing `ctx`: {schema_json}"));
let rendered = serde_json::to_string(ctx_schema).expect("ctx schema serializable");
assert!(
rendered.contains("\"object\""),
"ctx schema should constrain to object (got {rendered})"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_continue_invalid_session() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_continue",
json!({
"session_id": "00000000-0000-0000-0000-000000000000",
"response": "test"
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.to_lowercase().contains("not found")
|| text.to_lowercase().contains("no session")
|| text.to_lowercase().contains("unknown"),
"expected 'not found' error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_llm_single_roundtrip() {
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({ "code": "return alc.llm('What is 2+2?')" }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id missing");
assert!(resp["prompt"].as_str().is_some(), "prompt missing");
assert!(
resp.get("query_id").is_some(),
"query_id missing in response"
);
let resp = call_json(
&client,
"alc_continue",
json!({ "session_id": session_id, "response": "4" }),
)
.await;
assert_eq!(resp["status"], "completed");
assert_eq!(resp["result"], "4");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_llm_batch_roundtrip() {
let client = connect().await;
let code = r#"
local results = alc.llm_batch({
{ prompt = "Say A" },
{ prompt = "Say B" },
})
return results
"#;
let resp = call_json(&client, "alc_run", json!({ "code": code })).await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id missing");
let queries = resp["queries"].as_array().expect("queries array missing");
assert_eq!(queries.len(), 2);
let q0_id = queries[0]["id"].as_str().expect("q-0 id missing");
let q1_id = queries[1]["id"].as_str().expect("q-1 id missing");
let resp = call_json(
&client,
"alc_continue",
json!({
"session_id": session_id,
"responses": [
{ "query_id": q0_id, "response": "Alpha" },
{ "query_id": q1_id, "response": "Beta" },
]
}),
)
.await;
assert_eq!(resp["status"], "completed");
let result = resp["result"].as_array().expect("result should be array");
assert_eq!(result.len(), 2);
assert_eq!(result[0], "Alpha");
assert_eq!(result[1], "Beta");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_cache_hit_miss() {
let client = connect().await;
let code = r#"
local r1 = alc.cache("cached prompt")
local r2 = alc.cache("cached prompt")
local info = alc.cache_info()
return { r1 = r1, r2 = r2, info = info }
"#;
let resp = call_json(&client, "alc_run", json!({ "code": code })).await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id missing");
let resp = call_json(
&client,
"alc_continue",
json!({ "session_id": session_id, "response": "cached_value" }),
)
.await;
assert_eq!(resp["status"], "completed");
let result = &resp["result"];
assert_eq!(result["r1"], "cached_value");
assert_eq!(
result["r2"], "cached_value",
"cache hit should return same value"
);
assert_eq!(result["info"]["hits"], 1);
assert_eq!(result["info"]["misses"], 1);
assert_eq!(result["info"]["entries"], 1);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_parallel_roundtrip() {
let client = connect().await;
let code = r#"
local items = {"apple", "banana"}
local results = alc.parallel(items, function(item, i)
return "Describe " .. item
end)
return results
"#;
let resp = call_json(&client, "alc_run", json!({ "code": code })).await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"].as_str().expect("session_id missing");
let queries = resp["queries"].as_array().expect("queries missing");
assert_eq!(queries.len(), 2);
let q0_id = queries[0]["id"].as_str().expect("id missing");
let q1_id = queries[1]["id"].as_str().expect("id missing");
let resp = call_json(
&client,
"alc_continue",
json!({
"session_id": session_id,
"responses": [
{ "query_id": q0_id, "response": "A red fruit" },
{ "query_id": q1_id, "response": "A yellow fruit" },
]
}),
)
.await;
assert_eq!(resp["status"], "completed");
let result = resp["result"].as_array().expect("result array");
assert_eq!(result[0], "A red fruit");
assert_eq!(result[1], "A yellow fruit");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_fork_roundtrip() {
let client = connect().await;
let tmp_dir = tempfile::tempdir().expect("failed to create tempdir");
let coll_a = tmp_dir.path().join("coll_a");
let pkg_a_dir = coll_a.join("e2e_fork_a");
std::fs::create_dir_all(&pkg_a_dir).expect("mkdir");
let mut f = std::fs::File::create(pkg_a_dir.join("init.lua")).expect("create init.lua");
write!(
f,
r#"local M = {{}}
M.meta = {{ name = "e2e_fork_a", version = "0.1.0", description = "E2E fork A" }}
function M.run(ctx)
return alc.llm("Fork A: " .. (ctx.task or ""))
end
return M"#
)
.expect("write init.lua");
let coll_b = tmp_dir.path().join("coll_b");
let pkg_b_dir = coll_b.join("e2e_fork_b");
std::fs::create_dir_all(&pkg_b_dir).expect("mkdir");
let mut f = std::fs::File::create(pkg_b_dir.join("init.lua")).expect("create init.lua");
write!(
f,
r#"local M = {{}}
M.meta = {{ name = "e2e_fork_b", version = "0.1.0", description = "E2E fork B" }}
function M.run(ctx)
return alc.llm("Fork B: " .. (ctx.task or ""))
end
return M"#
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_a.to_string_lossy() }),
)
.await;
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_b.to_string_lossy() }),
)
.await;
let code = r#"
local results = alc.fork({"e2e_fork_a", "e2e_fork_b"}, ctx)
return results
"#;
let resp = call_json(
&client,
"alc_run",
json!({ "code": code, "ctx": { "task": "test" } }),
)
.await;
assert_eq!(resp["status"], "needs_response");
let session_id = resp["session_id"]
.as_str()
.expect("session_id missing")
.to_string();
let mut completed = false;
let mut final_resp = resp;
let mut iterations = 0;
const MAX_ITERATIONS: usize = 20;
while !completed {
iterations += 1;
assert!(
iterations <= MAX_ITERATIONS,
"fork test exceeded {MAX_ITERATIONS} iterations — possible infinite loop"
);
if final_resp["status"] == "needs_response" {
let session = final_resp["session_id"]
.as_str()
.unwrap_or(&session_id)
.to_string();
if let Some(queries) = final_resp["queries"].as_array() {
let responses: Vec<Value> = queries
.iter()
.map(|q| {
let qid = q["id"].as_str().expect("query id");
let prompt = q["prompt"].as_str().unwrap_or("");
let answer = if prompt.contains("Fork A") {
"Answer A"
} else {
"Answer B"
};
json!({ "query_id": qid, "response": answer })
})
.collect();
final_resp = call_json(
&client,
"alc_continue",
json!({ "session_id": session, "responses": responses }),
)
.await;
} else {
let prompt = final_resp["prompt"].as_str().unwrap_or("");
let answer = if prompt.contains("Fork A") {
"Answer A"
} else {
"Answer B"
};
final_resp = call_json(
&client,
"alc_continue",
json!({ "session_id": session, "response": answer }),
)
.await;
}
} else {
completed = true;
}
}
assert_eq!(final_resp["status"], "completed");
let result = final_resp["result"]
.as_array()
.expect("result should be array");
assert_eq!(result.len(), 2);
let strategy_a = &result[0];
assert_eq!(strategy_a["strategy"], "e2e_fork_a");
assert_eq!(strategy_a["ok"], true);
assert_eq!(strategy_a["result"], "Answer A");
let strategy_b = &result[1];
assert_eq!(strategy_b["strategy"], "e2e_fork_b");
assert_eq!(strategy_b["ok"], true);
assert_eq!(strategy_b["result"], "Answer B");
if let Some(home) = dirs::home_dir() {
let pkg_cache = home.join(".algocline").join("packages");
let _ = std::fs::remove_dir_all(pkg_cache.join("e2e_fork_a"));
let _ = std::fs::remove_dir_all(pkg_cache.join("e2e_fork_b"));
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_returns_types_path() {
let client = connect().await;
let tmp_dir = tempfile::tempdir().expect("tempdir");
let coll_dir = tmp_dir.path().join("e2e_types_test_coll");
let pkg_dir = coll_dir.join("e2e_types_test");
std::fs::create_dir_all(&pkg_dir).expect("mkdir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_types_test", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
let resp = call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
assert_eq!(resp["installed"], json!(["e2e_types_test"]));
assert!(
resp["types_path"].is_string(),
"types_path should be present in pkg_install response"
);
let types_path = resp["types_path"].as_str().unwrap();
assert!(
types_path.ends_with("types/alc.d.lua"),
"types_path should end with types/alc.d.lua, got: {types_path}"
);
if let Some(home) = dirs::home_dir() {
let pkg_cache = home.join(".algocline").join("packages");
let _ = std::fs::remove_dir_all(pkg_cache.join("e2e_types_test"));
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_returns_alc_shapes_types_path() {
let client = connect().await;
let tmp_dir = tempfile::tempdir().expect("tempdir");
let coll_dir = tmp_dir.path().join("e2e_alc_shapes_types_test_coll");
let pkg_dir = coll_dir.join("e2e_alc_shapes_types_test");
std::fs::create_dir_all(&pkg_dir).expect("mkdir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_alc_shapes_types_test", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
let resp = call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
assert_eq!(resp["installed"], json!(["e2e_alc_shapes_types_test"]));
if resp["alc_shapes_types_path"].is_string() {
let alc_shapes_path = resp["alc_shapes_types_path"].as_str().unwrap();
assert!(
alc_shapes_path.ends_with("types/alc_shapes.d.lua"),
"alc_shapes_types_path should end with types/alc_shapes.d.lua, got: {alc_shapes_path}"
);
}
if resp["types_path"].is_string() {
let types_path = resp["types_path"].as_str().unwrap();
assert!(
types_path.ends_with("types/alc.d.lua"),
"types_path should end with types/alc.d.lua, got: {types_path}"
);
}
if let Some(home) = dirs::home_dir() {
let pkg_cache = home.join(".algocline").join("packages");
let _ = std::fs::remove_dir_all(pkg_cache.join("e2e_alc_shapes_types_test"));
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_remove_scope_global_cleans_manifest_not_files() {
let client = connect().await;
let tmp_dir = tempfile::tempdir().expect("tempdir");
let pkg_name = "e2e_remove_global";
let coll_dir = tmp_dir.path().join("e2e_remove_global_coll");
let pkg_dir = coll_dir.join(pkg_name);
std::fs::create_dir_all(&pkg_dir).expect("mkdir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_remove_global", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
let home = dirs::home_dir().expect("home");
let manifest_path = home.join(".algocline").join("installed.json");
let cache_dir = home.join(".algocline").join("packages").join(pkg_name);
let before: Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).expect("manifest read"))
.expect("manifest JSON");
assert!(
before["packages"][pkg_name].is_object(),
"precondition: manifest must contain '{pkg_name}' before remove"
);
assert!(cache_dir.exists(), "precondition: cache dir must exist");
let resp = call_json(
&client,
"alc_pkg_remove",
json!({ "name": pkg_name, "scope": "global" }),
)
.await;
assert_eq!(resp["removed"], pkg_name);
assert_eq!(resp["scope"], "global");
let after: Value =
serde_json::from_str(&std::fs::read_to_string(&manifest_path).expect("manifest read"))
.expect("manifest JSON");
assert!(
after["packages"][pkg_name].is_null(),
"manifest still contains '{pkg_name}' after scope=global remove"
);
assert!(
cache_dir.exists(),
"scope=global must not delete ~/.algocline/packages/{pkg_name}/"
);
let _ = std::fs::remove_dir_all(&cache_dir);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_variant_scope_link_then_run_require() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("failed to create tempdir");
let project_root = tmp.path();
std::fs::write(project_root.join("alc.toml"), "[packages]\n").expect("write alc.toml");
let coll_dir = tmp.path().join("variant_src");
let pkg_dir = coll_dir.join("e2e_variant_pkg");
std::fs::create_dir_all(&pkg_dir).expect("mkdir pkg_dir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"return { value = "from-variant" }"#,
)
.expect("write init.lua");
let link_resp = call_json(
&client,
"alc_pkg_link",
json!({
"path": coll_dir.to_string_lossy(),
"scope": "variant",
"project_root": project_root.to_string_lossy(),
}),
)
.await;
assert!(
link_resp.get("error").is_none(),
"alc_pkg_link should succeed, got: {link_resp}"
);
assert!(
project_root.join("alc.local.toml").exists(),
"alc.local.toml should have been created"
);
let run_resp = call_json(
&client,
"alc_run",
json!({
"code": r#"return require("e2e_variant_pkg").value"#,
"project_root": project_root.to_string_lossy(),
}),
)
.await;
assert_eq!(
run_resp["status"], "completed",
"alc_run should complete, got: {run_resp}"
);
assert_eq!(
run_resp["result"], "from-variant",
"variant pkg should be resolved and return its sentinel value"
);
let list_resp = call_json(
&client,
"alc_pkg_list",
json!({ "project_root": project_root.to_string_lossy() }),
)
.await;
let packages = list_resp["packages"]
.as_array()
.expect("packages array missing");
let entry = packages
.iter()
.find(|p| p["name"] == "e2e_variant_pkg")
.expect("e2e_variant_pkg not found in alc_pkg_list");
assert_eq!(entry["scope"], "variant");
assert_eq!(entry["active"], true);
assert_eq!(entry["resolved_source_kind"], "variant");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_repair_reinstalls_deleted_dir() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let coll_dir = tmp.path().join("e2e_repair_coll");
let source = coll_dir.join("e2e_repair_pkg");
std::fs::create_dir_all(&source).expect("mkdir");
std::fs::write(
source.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_repair_pkg", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
let dest = dirs::home_dir()
.expect("home")
.join(".algocline")
.join("packages")
.join("e2e_repair_pkg");
assert!(dest.exists(), "dest should exist after install");
std::fs::remove_dir_all(&dest).expect("rm dest");
assert!(!dest.exists());
let resp = call_json(
&client,
"alc_pkg_repair",
json!({ "name": "e2e_repair_pkg" }),
)
.await;
let repaired = resp["repaired"].as_array().expect("repaired array missing");
assert_eq!(repaired.len(), 1, "one repair expected, got: {resp}");
assert_eq!(repaired[0]["name"], "e2e_repair_pkg");
assert_eq!(repaired[0]["kind"], "installed_missing");
assert!(dest.exists(), "dest should be restored after repair");
let _ = std::fs::remove_dir_all(&dest);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_reports_installed_missing() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let coll_dir = tmp.path().join("e2e_doctor_coll");
let source = coll_dir.join("e2e_doctor_pkg");
std::fs::create_dir_all(&source).expect("mkdir");
std::fs::write(
source.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_doctor_pkg", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
let dest = dirs::home_dir()
.expect("home")
.join(".algocline")
.join("packages")
.join("e2e_doctor_pkg");
assert!(dest.exists(), "dest should exist after install");
std::fs::remove_dir_all(&dest).expect("rm dest");
assert!(!dest.exists());
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_doctor_pkg" }),
)
.await;
let installed_missing = resp["installed_missing"]
.as_array()
.expect("installed_missing array missing");
let entry = installed_missing
.iter()
.find(|e| e["name"] == "e2e_doctor_pkg")
.unwrap_or_else(|| panic!("e2e_doctor_pkg not found in installed_missing, got: {resp}"));
assert_eq!(entry["kind"], "installed_missing");
assert!(
!dest.exists(),
"dest must not be resurrected by doctor (read-only)"
);
let _ = call_json(
&client,
"alc_pkg_repair",
json!({ "name": "e2e_doctor_pkg" }),
)
.await;
let _ = std::fs::remove_dir_all(&dest);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unknown_pkg_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_pkg_doctor",
json!({ "name": "nonexistent_xyz_pkg" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("not found in installed.json"),
"expected unknown-pkg error message, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_shape_error() {
let client = connect().await;
let outcome = client
.call_tool(call_params("alc_pkg_doctor", json!({ "name": 123 })))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
let has_type_error = text.contains("invalid type")
|| text.contains("expected a string")
|| text.contains("expected string");
assert!(
is_error || has_type_error,
"expected shape error (is_error=true or type-mismatch text), got is_error={is_error:?}, text: {text}"
);
}
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("invalid type") && msg.contains("string"),
"expected invalid-type error from param deserialization, got: {msg}"
);
}
}
client.cancel().await.expect("cancel failed");
}
fn setup_hub_fixture() -> tempfile::TempDir {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("fake_pkg");
std::fs::create_dir_all(&pkg_dir).expect("mkdir fake_pkg");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = {
name = "fake_pkg",
version = "0.1.0",
category = "test",
description = "fake package used by e2e tests",
}
M.spec = {}
return M
"#,
)
.expect("write init.lua");
std::fs::write(
tmp.path().join("configs.toml"),
r#"[context7]
projectTitle = "test"
description = "test"
rules = []
[devin]
project_name = "test"
"#,
)
.expect("write configs.toml");
tmp
}
#[tokio::test]
async fn test_alc_hub_reindex_ok() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let resp = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir,
"output_path": output_path,
}),
)
.await;
let pkg_count = resp
.get("package_count")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("expected package_count u64 in response: {resp}"));
assert!(
pkg_count > 0,
"expected at least one package in reindex output, got {pkg_count}: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_ok() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let out_dir_path = tmp.path().join("docs");
let out_dir = out_dir_path.to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"out_dir": out_dir,
}),
)
.await;
assert!(
resp.get("source_dir").is_some(),
"expected source_dir in gendoc response, got: {resp}"
);
let narrative = out_dir_path.join("narrative").join("fake_pkg.md");
assert!(
narrative.exists(),
"expected narrative/fake_pkg.md to be generated at {} (gendoc resp: {resp})",
narrative.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_with_toml_config_context7() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let config_path = tmp
.path()
.join("configs.toml")
.to_str()
.expect("utf-8 path")
.to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let _resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir.clone(),
"projections": ["context7"],
"config_path": config_path,
}),
)
.await;
let context7_json = tmp.path().join("context7.json");
assert!(
context7_json.exists(),
"expected context7 projection to be generated at {}",
context7_json.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_core_defaults_without_alc_toml() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir.clone(),
"projections": ["context7", "devin"],
}),
)
.await;
let context7_json = tmp.path().join("context7.json");
let devin_wiki = tmp.path().join(".devin").join("wiki.json");
assert!(
context7_json.exists(),
"expected context7.json from core defaults, got resp: {resp}"
);
assert!(
devin_wiki.exists(),
"expected .devin/wiki.json from core defaults, got resp: {resp}"
);
let c7_text = std::fs::read_to_string(&context7_json).expect("read context7.json");
let c7: serde_json::Value = serde_json::from_str(&c7_text).expect("parse context7.json");
assert!(
c7.get("projectTitle").and_then(|v| v.as_str()).is_some(),
"expected projectTitle to be a non-null string in context7.json, got: {c7_text}"
);
assert!(
c7.get("description").and_then(|v| v.as_str()).is_some(),
"expected description to be a non-null string in context7.json, got: {c7_text}"
);
let dv_text = std::fs::read_to_string(&devin_wiki).expect("read wiki.json");
let dv: serde_json::Value = serde_json::from_str(&dv_text).expect("parse wiki.json");
assert!(
dv.get("repo_notes").and_then(|v| v.as_array()).is_some(),
"expected repo_notes array in wiki.json, got: {dv_text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_with_alc_toml_hub_sections() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
std::fs::write(
tmp.path().join("alc.toml"),
r#"[hub]
name = "e2e-test-project"
[hub.context7]
description = "E2E test project description"
extra_rules = ["Always write tests"]
[hub.devin]
extra_repo_notes = ["Use Rust for performance-critical paths"]
"#,
)
.expect("write alc.toml");
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir.clone(),
"projections": ["context7", "devin"],
}),
)
.await;
let context7_json = tmp.path().join("context7.json");
let devin_wiki = tmp.path().join(".devin").join("wiki.json");
assert!(
context7_json.exists(),
"expected context7.json with alc.toml hub sections, got resp: {resp}"
);
assert!(
devin_wiki.exists(),
"expected .devin/wiki.json with alc.toml hub sections, got resp: {resp}"
);
let c7_text = std::fs::read_to_string(&context7_json).expect("read context7.json");
let c7: serde_json::Value = serde_json::from_str(&c7_text).expect("parse context7.json");
assert_eq!(
c7.get("projectTitle").and_then(|v| v.as_str()),
Some("e2e-test-project"),
"expected projectTitle = 'e2e-test-project' from [hub].name, got: {c7_text}"
);
assert_eq!(
c7.get("description").and_then(|v| v.as_str()),
Some("E2E test project description"),
"expected description from [hub.context7].description, got: {c7_text}"
);
let dv_text = std::fs::read_to_string(&devin_wiki).expect("read wiki.json");
let dv: serde_json::Value = serde_json::from_str(&dv_text).expect("parse wiki.json");
assert!(
dv.get("repo_notes").and_then(|v| v.as_array()).is_some(),
"expected repo_notes array in wiki.json, got: {dv_text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_rejects_lua_config_path() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let lua_path = tmp.path().join("config.lua");
std::fs::write(&lua_path, "return {}").expect("write config.lua");
let config_path = lua_path.to_str().expect("utf-8 path").to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let outcome = client
.call_tool(call_params(
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"projections": ["context7"],
"config_path": config_path,
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for .lua config_path, got is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("'.lua' is no longer supported"),
"expected '.lua' is no longer supported in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_rejects_mutually_exclusive_rules() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
std::fs::write(tmp.path().join("rules.txt"), "Rule one\n").expect("write rules.txt");
std::fs::write(
tmp.path().join("alc.toml"),
r#"[hub.context7]
rules_file = "rules.txt"
rules_override = ["Conflict rule"]
"#,
)
.expect("write alc.toml");
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let outcome = client
.call_tool(call_params(
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"projections": ["context7"],
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for mutually-exclusive rules, got is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("mutually exclusive"),
"expected 'mutually exclusive' in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_unknown_projection_rejected() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let outcome = client
.call_tool(call_params(
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"projections": ["unknown_projection"],
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for unknown projection, got is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("unknown projection"),
"expected unknown projection error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_narrative_projection_generates_per_pkg_md() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir_path = tmp.path().join("docs_narrative");
let out_dir = out_dir_path.to_str().expect("utf-8 path").to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"out_dir": out_dir,
"projections": ["narrative"],
}),
)
.await;
let narrative = out_dir_path.join("narrative").join("fake_pkg.md");
assert!(
narrative.exists(),
"expected narrative/fake_pkg.md at {} (resp: {resp})",
narrative.display()
);
let len = std::fs::metadata(&narrative).expect("metadata").len();
assert!(
len > 0,
"expected non-empty narrative/fake_pkg.md at {} (resp: {resp})",
narrative.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_gendoc_llms_projection_generates_llms_txt() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir_path = tmp.path().join("docs_llms");
let out_dir = out_dir_path.to_str().expect("utf-8 path").to_string();
let _ = call_json(
&client,
"alc_hub_reindex",
json!({
"source_dir": source_dir.clone(),
"output_path": output_path,
}),
)
.await;
let resp = call_json(
&client,
"alc_hub_gendoc",
json!({
"source_dir": source_dir,
"out_dir": out_dir,
"projections": ["llms"],
}),
)
.await;
let llms_txt = out_dir_path.join("llms.txt");
assert!(
llms_txt.exists(),
"expected llms.txt at {} (resp: {resp})",
llms_txt.display()
);
let len = std::fs::metadata(&llms_txt).expect("metadata").len();
assert!(
len > 0,
"expected non-empty llms.txt at {} (resp: {resp})",
llms_txt.display()
);
let llms_full_txt = out_dir_path.join("llms-full.txt");
assert!(
llms_full_txt.exists(),
"expected llms-full.txt at {} (resp: {resp})",
llms_full_txt.display()
);
let len_full = std::fs::metadata(&llms_full_txt).expect("metadata").len();
assert!(
len_full > 0,
"expected non-empty llms-full.txt at {} (resp: {resp})",
llms_full_txt.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_ok() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = tmp
.path()
.join("docs")
.to_str()
.expect("utf-8 path")
.to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
)
.await;
assert_eq!(
resp.get("preset_catalog_version").and_then(|v| v.as_str()),
Some(PRESET_CATALOG_VERSION),
"expected preset_catalog_version in dist response, got: {resp}"
);
assert!(
resp.get("preset").is_none(),
"expected no preset object when preset omitted, got: {resp}"
);
let reindex = resp
.get("reindex")
.unwrap_or_else(|| panic!("expected reindex field, got: {resp}"));
let pkg_count = reindex
.get("package_count")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("expected reindex.package_count u64, got: {resp}"));
assert!(
pkg_count > 0,
"expected reindex.package_count > 0, got {pkg_count}: {resp}"
);
let gendoc = resp
.get("gendoc")
.unwrap_or_else(|| panic!("expected gendoc field, got: {resp}"));
assert!(
gendoc.is_object(),
"expected gendoc to be a JSON object, got: {resp}"
);
assert!(
gendoc.get("stdout").is_some(),
"dist.gendoc must include stdout field",
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_info_includes_preset_catalog_version() {
let client = connect().await;
let resp = call_json(&client, "alc_info", json!({})).await;
assert_eq!(
resp.get("preset_catalog_version").and_then(|v| v.as_str()),
Some(PRESET_CATALOG_VERSION),
"expected preset_catalog_version in alc_info, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_hub_search_local_indices_merges_results() {
let alc_home = tempfile::tempdir().expect("tempdir failed");
std::fs::create_dir_all(alc_home.path().join("packages")).expect("create packages dir failed");
let idx_dir = tempfile::tempdir().expect("tempdir for index failed");
let idx_path = idx_dir.path().join("hub_index.json");
let idx_json = serde_json::json!({
"schema_version": "hub_index/v0",
"updated_at": "2026-05-15T00:00:00Z",
"packages": [
{
"name": "testpkg",
"version": "0.1.0",
"description": "A local-index test package",
"category": "testing"
}
]
});
std::fs::write(&idx_path, idx_json.to_string()).expect("write hub_index.json failed");
let client = connect_with_alc_home(alc_home.path()).await;
let resp = call_json(
&client,
"alc_hub_search",
json!({
"query": "testpkg",
"local_indices": [idx_path.to_str().expect("utf-8 path")],
"verbose": "full",
}),
)
.await;
let results = resp["results"].as_array().expect("results must be array");
let found = results
.iter()
.any(|r| r.get("name").and_then(|v| v.as_str()) == Some("testpkg"));
assert!(
found,
"expected testpkg in results from local_indices, got: {resp}"
);
assert!(
resp.get("sources").is_some(),
"expected top-level 'sources' in response, got: {resp}"
);
if let Some(warnings) = resp.get("warnings").and_then(|w| w.as_array()) {
let idx_str = idx_path.to_str().unwrap_or("");
let has_path_warning = warnings
.iter()
.any(|w| w.as_str().map(|s| s.contains(idx_str)).unwrap_or(false));
assert!(
!has_path_warning,
"unexpected warning referencing local index path: {warnings:?}"
);
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_hub_search_matches_tags() {
let alc_home = tempfile::tempdir().expect("tempdir failed");
std::fs::create_dir_all(alc_home.path().join("packages")).expect("create packages dir failed");
let idx_dir = tempfile::tempdir().expect("tempdir for index failed");
let idx_path = idx_dir.path().join("hub_index.json");
let idx_json = serde_json::json!({
"schema_version": "hub_index/v0",
"updated_at": "2026-05-26T00:00:00Z",
"packages": [
{
"name": "tagged_pkg",
"version": "0.1.0",
"description": "A package with metadata",
"category": "testing",
"tags": ["swarm", "primitive"]
}
]
});
std::fs::write(&idx_path, idx_json.to_string()).expect("write hub_index.json failed");
let client = connect_with_alc_home(alc_home.path()).await;
let resp = call_json(
&client,
"alc_hub_search",
json!({
"query": "primitive",
"local_indices": [idx_path.to_str().expect("utf-8 path")],
"verbose": "full",
}),
)
.await;
let results = resp["results"].as_array().expect("results must be array");
let found = results
.iter()
.any(|r| r.get("name").and_then(|v| v.as_str()) == Some("tagged_pkg"));
assert!(
found,
"expected tagged_pkg found via tag search, got: {resp}"
);
let entry = results
.iter()
.find(|r| r.get("name").and_then(|v| v.as_str()) == Some("tagged_pkg"))
.expect("tagged_pkg must exist");
let tags = entry.get("tags").and_then(|t| t.as_array());
assert!(tags.is_some(), "tags field must be projected in full mode");
let tag_strs: Vec<&str> = tags.unwrap().iter().filter_map(|v| v.as_str()).collect();
assert!(
tag_strs.contains(&"swarm") && tag_strs.contains(&"primitive"),
"tags must contain swarm and primitive, got: {tag_strs:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_hub_search_no_tags_does_not_match_tag_query() {
let alc_home = tempfile::tempdir().expect("tempdir failed");
std::fs::create_dir_all(alc_home.path().join("packages")).expect("create packages dir failed");
let idx_dir = tempfile::tempdir().expect("tempdir for index failed");
let idx_path = idx_dir.path().join("hub_index.json");
let idx_json = serde_json::json!({
"schema_version": "hub_index/v0",
"updated_at": "2026-05-26T00:00:00Z",
"packages": [
{
"name": "no_tags_pkg",
"version": "0.1.0",
"description": "Plain package",
"category": "testing"
}
]
});
std::fs::write(&idx_path, idx_json.to_string()).expect("write hub_index.json failed");
let client = connect_with_alc_home(alc_home.path()).await;
let resp = call_json(
&client,
"alc_hub_search",
json!({
"query": "primitive",
"local_indices": [idx_path.to_str().expect("utf-8 path")],
}),
)
.await;
let results = resp["results"].as_array().expect("results must be array");
let found = results
.iter()
.any(|r| r.get("name").and_then(|v| v.as_str()) == Some("no_tags_pkg"));
assert!(
!found,
"no_tags_pkg should NOT match tag-only query, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_preset_publish_uses_alc_toml_override() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("alc.toml"),
r#"[packages]
[hub.dist]
[hub.dist.presets.publish]
projections = ["context7"]
config_path = "configs.toml"
"#,
)
.expect("write alc.toml");
let hub_dir = root.join("hub");
std::fs::create_dir_all(&hub_dir).expect("mkdir hub");
std::fs::write(
hub_dir.join("configs.toml"),
r#"[context7]
projectTitle = "test"
description = "test"
rules = []
"#,
)
.expect("write configs.toml");
let pkg_dir = hub_dir.join("fake_pkg");
std::fs::create_dir_all(&pkg_dir).expect("mkdir fake_pkg");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = {
name = "fake_pkg",
version = "0.1.0",
category = "test",
description = "fake package used by preset e2e",
}
M.spec = {}
return M
"#,
)
.expect("write init.lua");
let source_dir = hub_dir.to_str().expect("utf-8 path").to_string();
let project_root = root.to_str().expect("utf-8 path").to_string();
let output_path = hub_dir
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = hub_dir
.join("docs")
.to_str()
.expect("utf-8 path")
.to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"project_root": project_root,
"output_path": output_path,
"out_dir": out_dir,
"preset": "publish",
}),
)
.await;
assert_eq!(
resp.get("preset_catalog_version").and_then(|v| v.as_str()),
Some(PRESET_CATALOG_VERSION),
"expected preset_catalog_version in dist response, got: {resp}"
);
let preset = resp
.get("preset")
.unwrap_or_else(|| panic!("expected preset object, got: {resp}"));
assert_eq!(preset.get("name").and_then(|v| v.as_str()), Some("publish"));
let resolved = preset
.get("resolved")
.unwrap_or_else(|| panic!("expected preset.resolved, got: {preset}"));
let projections = resolved
.get("projections")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("expected projections array, got: {resolved}"));
let projection_names: Vec<&str> = projections
.iter()
.map(|v| v.as_str().expect("projection string"))
.collect();
assert_eq!(projection_names, vec!["context7"]);
let context7_json = hub_dir.join("context7.json");
assert!(
context7_json.exists(),
"expected context7.json at {}",
context7_json.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn host_mode_paused_session_survives_appservice_restart() {
let tmp = tempfile::Builder::new()
.prefix("alc-e2e-")
.tempdir_in("/tmp")
.expect("tempdir in /tmp");
let alc_home = tmp.path();
let client1 = connect_with_alc_home(alc_home).await;
let resp1 = call_json(
&client1,
"alc_run",
json!({
"code": "return alc.llm('What is 1+1?')",
"host_mode": true,
}),
)
.await;
assert_eq!(
resp1.get("status").and_then(|v| v.as_str()),
Some("needs_response"),
"host_mode=true alc_run must pause on alc.llm; got: {resp1}"
);
let session_id = resp1
.get("session_id")
.and_then(|v| v.as_str())
.unwrap_or_else(|| panic!("session_id missing from alc_run response: {resp1}"))
.to_string();
client1.cancel().await.expect("cancel client1");
let client2 = connect_with_alc_home(alc_home).await;
let resp2 = call_json(
&client2,
"alc_continue",
json!({
"session_id": session_id,
"response": "2",
}),
)
.await;
let err_text = resp2.get("error").and_then(|v| v.as_str()).unwrap_or("");
assert!(
!err_text.contains("session not found in pool") && !err_text.contains("not found"),
"worker must survive MCP restart and be resumable via registry.json; \
got error: {err_text:?}, full response: {resp2}"
);
let status = resp2.get("status").and_then(|v| v.as_str()).unwrap_or("");
assert!(
status == "completed" || status == "needs_response",
"continue after restart must succeed (status=completed or needs_response), \
got status={status:?}, full response: {resp2}"
);
client2.cancel().await.expect("cancel client2");
}
#[tokio::test]
async fn test_alc_hub_dist_with_toml_config_context7() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let config_path = tmp
.path()
.join("configs.toml")
.to_str()
.expect("utf-8 path")
.to_string();
let _resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"projections": ["context7"],
"config_path": config_path,
}),
)
.await;
let context7_json = tmp.path().join("context7.json");
assert!(
context7_json.exists(),
"expected context7 projection to be generated at {}",
context7_json.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_gendoc_failure_includes_reindex_result() {
let client = connect().await;
let tmp = setup_hub_fixture();
let source_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let output_path = tmp
.path()
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let outcome = client
.call_tool(call_params(
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"projections": ["invalid_projection_xyz"],
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true when gendoc fails after reindex, got: is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("dist: gendoc failed"),
"expected dist gendoc failure prefix, got: {text}"
);
assert!(
text.contains("reindex result (succeeded):"),
"expected reindex result to be embedded in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_fixture() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hub_dist_sample");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir_path = root.join("docs");
let out_dir = out_dir_path.to_str().expect("utf-8 path").to_string();
let config_path = root
.join("tools/gendoc.toml")
.to_str()
.expect("utf-8 path")
.to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
"projections": ["hub", "context7", "devin"],
"config_path": config_path,
}),
)
.await;
let reindex = resp
.get("reindex")
.unwrap_or_else(|| panic!("expected reindex field, got: {resp}"));
let _gendoc = resp
.get("gendoc")
.unwrap_or_else(|| panic!("expected gendoc field, got: {resp}"));
let pkg_count = reindex
.get("package_count")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| panic!("expected reindex.package_count u64, got: {resp}"));
assert_eq!(
pkg_count, 3,
"expected exactly 3 packages indexed, got {pkg_count}: {resp}"
);
for pkg in &["pkg_alpha", "pkg_beta", "pkg_gamma"] {
let narrative = out_dir_path.join("narrative").join(format!("{pkg}.md"));
assert!(
narrative.exists(),
"expected narrative/{pkg}.md at {}",
narrative.display()
);
}
let llms_full_path = out_dir_path.join("llms-full.txt");
assert!(
llms_full_path.exists(),
"expected llms-full.txt at {}",
llms_full_path.display()
);
let llms_full = std::fs::read_to_string(&llms_full_path).expect("read llms-full.txt");
for token in &[
"ALPHA_SIGNAL_BOOLEAN_TABLE",
"BETA_SIGNAL_INSTRUMENT_DESCRIBE",
"GAMMA_SIGNAL_NESTED_SHAPE",
] {
assert!(
llms_full.contains(token),
"expected signal token '{token}' in llms-full.txt"
);
}
let llms_path = out_dir_path.join("llms.txt");
assert!(
llms_path.exists(),
"expected llms.txt at {}",
llms_path.display()
);
let llms = std::fs::read_to_string(&llms_path).expect("read llms.txt");
for pkg in &["pkg_alpha", "pkg_beta", "pkg_gamma"] {
assert!(
llms.contains(pkg),
"expected '{pkg}' in llms.txt index, got:\n{llms}"
);
}
let context7 = root.join("context7.json");
assert!(
context7.exists(),
"expected context7.json at {}",
context7.display()
);
let devin_wiki = root.join(".devin/wiki.json");
assert!(
devin_wiki.exists(),
"expected .devin/wiki.json at {}",
devin_wiki.display()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_fixture_mirror_version_match() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/hub_dist_sample_version_match");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
)
.await;
assert!(
resp.get("reindex").is_some(),
"expected reindex field on version-match success, got: {resp}"
);
assert!(
resp.get("gendoc").is_some(),
"expected gendoc field on version-match success, got: {resp}"
);
let out_dir_path = root.join("docs");
let llms_full_path = out_dir_path.join("llms-full.txt");
assert!(
llms_full_path.exists(),
"expected llms-full.txt at {}",
llms_full_path.display()
);
let llms_full = std::fs::read_to_string(&llms_full_path).expect("read llms-full.txt");
assert!(
llms_full.contains("VMATCH_SIGNAL_ALPHA"),
"expected VMATCH_SIGNAL_ALPHA signal token in llms-full.txt"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_fixture_mirror_version_mismatch() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/hub_dist_sample_version_mismatch");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let outcome = client
.call_tool(call_params(
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true on version mismatch, got: is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("0.25.1"),
"expected embedded version '0.25.1' in error text, got: {text}"
);
assert!(
text.contains("9.9.9"),
"expected mirror version '9.9.9' in error text, got: {text}"
);
assert!(
text.contains("CHANGELOG"),
"expected CHANGELOG hint in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_luacats_projection() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hub_dist_sample");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
"projections": ["luacats"],
}),
)
.await;
assert!(
resp.get("reindex").is_some(),
"expected reindex field in response, got: {resp}"
);
assert!(
resp.get("gendoc").is_some(),
"expected gendoc field in response, got: {resp}"
);
let luacats_path = root.join("types").join("alc_shapes.d.lua");
assert!(
luacats_path.exists(),
"expected types/alc_shapes.d.lua at {}",
luacats_path.display()
);
let content = std::fs::read_to_string(&luacats_path).expect("read alc_shapes.d.lua");
let class_count = content.matches("---@class ").count();
assert!(
class_count >= 3,
"expected at least 3 '---@class ' lines in alc_shapes.d.lua, got {class_count}"
);
assert!(
content.contains("---@class AlcResultVoted"),
"expected '---@class AlcResultVoted' in alc_shapes.d.lua"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_reindex_failure() {
let client = connect().await;
let outcome = client
.call_tool(call_params(
"alc_hub_dist",
json!({
"source_dir": "/nonexistent/path/for/dist/test",
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true on reindex failure, got: is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("reindex failed"),
"expected 'reindex failed' in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
fn copy_fixture_tree(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_fixture_tree(&entry.path(), &dst.join(entry.file_name()))?;
} else {
std::fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}
#[tokio::test]
async fn test_alc_hub_dist_compat_declared_in_range() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/hub_dist_sample_compat_declared");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
)
.await;
assert!(
resp.get("reindex").is_some(),
"expected reindex field on compat-declared success, got: {resp}"
);
assert!(
resp.get("gendoc").is_some(),
"expected gendoc field on compat-declared success, got: {resp}"
);
if let Some(gendoc) = resp.get("gendoc") {
if let Some(warnings) = gendoc.get("warnings") {
let warnings_str = warnings.to_string();
assert!(
!warnings_str.contains("alc_shapes_compat not declared"),
"unexpected compat-undeclared warning for declared package: {warnings_str}"
);
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_compat_declared_out_of_range() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/hub_dist_sample_compat_out_of_range");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let outcome = client
.call_tool(call_params(
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true on out-of-range compat, got: is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("pkg_alpha"),
"expected pkg_name 'pkg_alpha' in error text, got: {text}"
);
assert!(
text.contains(">=0.26.0, <0.27"),
"expected declared_range '>=0.26.0, <0.27' in error text, got: {text}"
);
assert!(
text.contains("0.25.1"),
"expected actual_version '0.25.1' in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_pkg_scaffold_basic() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_pkg_scaffold",
json!({
"name": "my_pkg",
"target_dir": target_dir,
}),
)
.await;
assert_eq!(
resp.get("status").and_then(|v| v.as_str()),
Some("ok"),
"expected status=ok, got: {resp}"
);
let init_lua = tmp.path().join("my_pkg").join("init.lua");
assert!(
init_lua.exists(),
"expected init.lua at {}",
init_lua.display()
);
let content = std::fs::read_to_string(&init_lua).expect("read init.lua");
assert!(
content.contains(r#"name = "my_pkg""#),
"expected name field in content"
);
assert!(
content.contains("alc_shapes_compat = \">=0.25.0, <0.26\""),
"expected compat range in content, got excerpt: {}",
&content[..content.len().min(400)]
);
assert!(
content.contains("function M.run(ctx)"),
"expected M.run stub"
);
assert!(content.contains("T.shape"), "expected T.shape reference");
assert!(content.contains("return M"), "expected return M");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_pkg_scaffold_with_category_and_description() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_pkg_scaffold",
json!({
"name": "my_pkg",
"target_dir": target_dir,
"category": "selection",
"description": "test pkg",
}),
)
.await;
assert_eq!(
resp.get("status").and_then(|v| v.as_str()),
Some("ok"),
"expected status=ok, got: {resp}"
);
let content =
std::fs::read_to_string(tmp.path().join("my_pkg").join("init.lua")).expect("read init.lua");
assert!(
content.contains(r#"category = "selection""#),
"expected uncommented category in content"
);
assert!(
content.contains(r#"description = "test pkg""#),
"expected uncommented description in content"
);
assert!(
!content.contains("-- category ="),
"unexpected commented-out category placeholder"
);
assert!(
!content.contains("-- description ="),
"unexpected commented-out description placeholder"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_pkg_scaffold_already_exists() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("my_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create dir");
std::fs::write(pkg_dir.join("init.lua"), "-- existing").expect("write existing");
let target_dir = tmp.path().to_str().expect("utf-8 path").to_string();
let outcome = client
.call_tool(call_params(
"alc_pkg_scaffold",
json!({
"name": "my_pkg",
"target_dir": target_dir,
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for AlreadyExists, got: is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("already exists"),
"expected 'already exists' in error text, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_pkg_scaffold_name_invalid() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().to_str().expect("utf-8 path").to_string();
for bad_name in &["", "1bad", "has/slash"] {
let outcome = client
.call_tool(call_params(
"alc_pkg_scaffold",
json!({
"name": bad_name,
"target_dir": target_dir,
}),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for name={bad_name:?}, got text: {text}"
);
assert!(
text.contains("invalid"),
"expected 'invalid' in error text for name={bad_name:?}, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err for name={bad_name:?}: {e}"),
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_dist_compat_undeclared_warns() {
let client = connect().await;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let fixture_src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/hub_dist_sample_compat_undeclared");
copy_fixture_tree(&fixture_src, root).expect("copy fixture");
let source_dir = root.to_str().expect("utf-8 path").to_string();
let output_path = root
.join("hub_index.json")
.to_str()
.expect("utf-8 path")
.to_string();
let out_dir = root.join("docs").to_str().expect("utf-8 path").to_string();
let resp = call_json(
&client,
"alc_hub_dist",
json!({
"source_dir": source_dir,
"output_path": output_path,
"out_dir": out_dir,
}),
)
.await;
assert!(
resp.get("reindex").is_some(),
"expected reindex field on undeclared compat success, got: {resp}"
);
assert!(
resp.get("gendoc").is_some(),
"expected gendoc field on undeclared compat success, got: {resp}"
);
let gendoc = resp.get("gendoc").expect("gendoc field present");
let warnings = gendoc.get("warnings").expect("warnings field in gendoc");
let warnings_str = warnings.to_string();
assert!(
warnings_str.contains("alc_shapes_compat not declared"),
"expected 'alc_shapes_compat not declared' in warnings, got: {warnings_str}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_list_returns_two_fixed() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.list_resources(None)
.await
.expect("list_resources failed");
assert_eq!(
result.resources.len(),
3,
"expected 3 fixed resources (types x2 + hub/index), got: {:?}",
result
.resources
.iter()
.map(|r| r.raw.uri.as_str())
.collect::<Vec<_>>()
);
let uris: Vec<&str> = result
.resources
.iter()
.map(|r| r.raw.uri.as_str())
.collect();
assert!(
uris.contains(&"alc://types/alc.d.lua"),
"expected alc://types/alc.d.lua in fixed list"
);
assert!(
uris.contains(&"alc://types/alc_shapes.d.lua"),
"expected alc://types/alc_shapes.d.lua in fixed list"
);
assert!(
uris.contains(&"alc://hub/index"),
"expected alc://hub/index in fixed list"
);
assert!(
result.resources.iter().any(|r| r.raw.title.is_some()),
"expected at least one fixed resource to have a title field"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_templates_list_returns_eight() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.list_resource_templates(None)
.await
.expect("list_resource_templates failed");
assert_eq!(
result.resource_templates.len(),
8,
"expected 8 resource templates, got: {:?}",
result
.resource_templates
.iter()
.map(|t| t.raw.uri_template.as_str())
.collect::<Vec<_>>()
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_hub_index_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://hub/index")
.await
.expect("read alc://hub/index failed");
assert_eq!(result.contents.len(), 1);
let (uri, text) = resource_text(&result.contents[0]);
assert_eq!(uri, "alc://hub/index");
let parsed: serde_json::Value =
serde_json::from_str(text).expect("hub/index response must be valid JSON");
assert_eq!(
parsed.get("schema_version").and_then(|v| v.as_str()),
Some("hub_index/v0"),
"schema_version must be hub_index/v0"
);
let packages = parsed
.get("packages")
.and_then(|p| p.as_array())
.expect("packages must be an array");
assert!(
packages.is_empty(),
"clean install must return empty packages, got: {packages:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_hub_unknown_path_errors() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let params = ReadResourceRequestParams::new("alc://hub/unknown".to_string());
let result = client.read_resource(params).await;
assert!(
result.is_err(),
"alc://hub/unknown must return an error, got: {result:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_types_alc_d_lua() {
let tmp = tempfile::tempdir().expect("tempdir");
let types_dir = tmp.path().join("types");
std::fs::create_dir_all(&types_dir).expect("create types dir");
std::fs::write(
types_dir.join("alc.d.lua"),
"-- alc type stubs\n---@class alc\nalc = {}\n",
)
.expect("write alc.d.lua");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://types/alc.d.lua")
.await
.expect("read_resource alc.d.lua failed");
assert_eq!(result.contents.len(), 1);
let (uri, text) = resource_text(&result.contents[0]);
assert!(
text.contains("alc type stubs"),
"unexpected content: {text}"
);
assert_eq!(uri, "alc://types/alc.d.lua");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_types_alc_shapes_d_lua() {
let tmp = tempfile::tempdir().expect("tempdir");
let types_dir = tmp.path().join("types");
std::fs::create_dir_all(&types_dir).expect("create types dir");
std::fs::write(
types_dir.join("alc_shapes.d.lua"),
"-- alc_shapes type stubs\n",
)
.expect("write alc_shapes.d.lua");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://types/alc_shapes.d.lua")
.await
.expect("read_resource alc_shapes.d.lua failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
assert!(text.contains("alc_shapes"), "unexpected content: {text}");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_pkg_init_lua() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("my_e2e_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'my_e2e_pkg', version = '0.1.0' }\nreturn M\n",
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://packages/my_e2e_pkg/init.lua")
.await
.expect("read_resource pkg init.lua failed");
assert_eq!(result.contents.len(), 1);
let (uri, text) = resource_text(&result.contents[0]);
assert!(text.contains("my_e2e_pkg"), "unexpected content: {text}");
assert_eq!(uri, "alc://packages/my_e2e_pkg/init.lua");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_pkg_meta() {
let home_tmp = tempfile::tempdir().expect("home tempdir");
let pkg_dir = home_tmp.path().join("packages").join("my_e2e_meta_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"local M = {}\n",
"M.meta = { name = 'my_e2e_meta_pkg', version = '0.2.0', description = 'test' }\n",
"return M\n"
),
)
.expect("write init.lua");
let client = connect_with_alc_home(home_tmp.path()).await;
let result = read_resource(&client, "alc://packages/my_e2e_meta_pkg/meta")
.await
.expect("read_resource pkg meta failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
let meta: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("meta JSON parse failed: {e}\nraw: {text}"));
assert_eq!(meta["name"], "my_e2e_meta_pkg");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_pkg_narrative_ok() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("narrative_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"--- narrative_pkg — Stdpkg reference smoke test.\n",
"---\n",
"--- A short summary paragraph.\n",
"---\n",
"--- ## Overview\n",
"---\n",
"--- Stdpkg reference content served from docstring.\n",
"local M = {}\n",
"M.meta = { name = 'narrative_pkg', version = '0.1.0',\n",
" category = 'test',\n",
" description = 'Stdpkg reference smoke test' }\n",
"return M\n",
),
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://packages/narrative_pkg/narrative")
.await
.expect("read_resource pkg narrative failed");
assert_eq!(result.contents.len(), 1);
let (uri, text) = resource_text(&result.contents[0]);
assert_eq!(uri, "alc://packages/narrative_pkg/narrative");
assert!(
text.contains("narrative_pkg"),
"pkg name missing in narrative: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_pkg_narrative_missing_returns_not_found() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let err = read_resource(&client, "alc://packages/no_narrative_pkg/narrative")
.await
.expect_err("read_resource must error when pkg is not installed");
let msg = format!("{err:?}");
assert!(
msg.contains("not found") || msg.contains("32002"),
"expected NotFound error, got: {msg}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_list_resource_templates_includes_narrative() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.list_resource_templates(None)
.await
.expect("list_resource_templates failed");
let uris: Vec<String> = result
.resource_templates
.iter()
.map(|t| t.uri_template.to_string())
.collect();
assert!(
uris.iter().any(|u| u == "alc://packages/{name}/narrative"),
"narrative template missing, got: {uris:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_narrative_resolves_via_m_docs_when_declared() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("docs_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"--- docs_pkg — docstring-driven narrative test.\n",
"---\n",
"--- Narrative from docstring, not from external file.\n",
"local M = {}\n",
"M.meta = { name = 'docs_pkg', version = '0.1.0',\n",
" category = 'test',\n",
" description = 'docstring-driven narrative test' }\n",
"M.docs = { narrative = 'overview.md', schema_version = 1 }\n",
"return M\n",
),
)
.expect("write init.lua");
std::fs::write(
pkg_dir.join("overview.md"),
"# docs_pkg\n\nExternal file — must NOT be returned (#1778221491-39903).\n",
)
.expect("write overview.md");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://packages/docs_pkg/narrative")
.await
.expect("read_resource pkg narrative failed");
let (_, text) = resource_text(&result.contents[0]);
assert!(
text.contains("docs_pkg"),
"pkg name missing in narrative, got: {text}"
);
assert!(
!text.contains("External file"),
"external file must not be served, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_narrative_falls_back_to_convention_without_m_docs() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("legacy_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"--- legacy_pkg — docstring narrative without M.docs.\n",
"---\n",
"--- Narrative from docstring only.\n",
"local M = {}\n",
"M.meta = { name = 'legacy_pkg', version = '0.1.0',\n",
" category = 'test',\n",
" description = 'docstring narrative without M.docs' }\n",
"return M\n",
),
)
.expect("write init.lua");
std::fs::write(
pkg_dir.join("narrative.md"),
"# legacy_pkg\n\nConvention path — must NOT be returned (#1778221491-39903).\n",
)
.expect("write narrative.md");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://packages/legacy_pkg/narrative")
.await
.expect("read_resource pkg narrative failed");
let (_, text) = resource_text(&result.contents[0]);
assert!(
text.contains("legacy_pkg"),
"pkg name missing in narrative, got: {text}"
);
assert!(
!text.contains("Convention path — must NOT be returned"),
"convention file must not be served, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_narrative_rejects_path_traversal_in_m_docs() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("evil_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"--- evil_pkg — path traversal guard test.\n",
"---\n",
"--- M.docs.narrative path traversal is inert after #1778221491-39903.\n",
"local M = {}\n",
"M.meta = { name = 'evil_pkg', version = '0.1.0',\n",
" category = 'test',\n",
" description = 'path traversal guard test' }\n",
"M.docs = { narrative = '../../etc/passwd' }\n",
"return M\n",
),
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://packages/evil_pkg/narrative")
.await
.expect("read_resource must succeed — M.docs.narrative path traversal is inert");
let (_, text) = resource_text(&result.contents[0]);
assert!(
text.contains("evil_pkg"),
"pkg name missing in narrative, got: {text}"
);
assert!(
!text.contains("root:") && !text.contains("/bin/"),
"/etc/passwd content must not appear in narrative, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_unknown_uri_returns_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let err = read_resource(&client, "alc://unknown/resource")
.await
.expect_err("expected error for unknown URI");
let msg = err.to_string();
assert!(
!msg.is_empty(),
"expected non-empty error for unknown URI, got empty"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_invalid_scheme_returns_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let err = read_resource(&client, "https://example.com/foo")
.await
.expect_err("expected error for invalid scheme");
let msg = err.to_string();
assert!(
!msg.is_empty(),
"expected non-empty error for invalid scheme, got empty"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_traversal_uri_returns_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let err = read_resource(&client, "alc://types/../etc/passwd")
.await
.expect_err("expected error for traversal URI");
let msg = err.to_string();
assert!(
!msg.is_empty(),
"expected non-empty error for traversal URI, got empty"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_scenario() {
let tmp = tempfile::tempdir().expect("tempdir");
let scenarios_dir = tmp.path().join("scenarios");
std::fs::create_dir_all(&scenarios_dir).expect("create scenarios dir");
std::fs::write(
scenarios_dir.join("my_scenario.lua"),
"-- scenario\nlocal S = {}\nS.cases = {}\nreturn S\n",
)
.expect("write scenario");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, "alc://scenarios/my_scenario")
.await
.expect("read_resource scenario failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
assert!(text.contains("scenario"), "unexpected content: {text}");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_card() {
let tmp = tempfile::tempdir().expect("tempdir");
let card_id = "mypkg_20260401T000000_aabbcc";
let card_dir = tmp.path().join("cards").join("mypkg");
std::fs::create_dir_all(&card_dir).expect("create card dir");
std::fs::write(
card_dir.join(format!("{card_id}.toml")),
concat!(
"schema_version = \"card/v0\"\n",
"card_id = \"mypkg_20260401T000000_aabbcc\"\n",
"created_at = \"2026-04-01T00:00:00Z\"\n",
"[pkg]\n",
"name = \"mypkg\"\n",
),
)
.expect("write card toml");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, &format!("alc://cards/{card_id}"))
.await
.expect("read_resource card failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
let card: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("card JSON parse failed: {e}\nraw: {text}"));
assert_eq!(card["card_id"], card_id);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_card_samples_pagination() {
let tmp = tempfile::tempdir().expect("tempdir");
let card_id = "mypkg_20260401T000000_aabbcc";
let card_dir = tmp.path().join("cards").join("mypkg");
std::fs::create_dir_all(&card_dir).expect("create card dir");
std::fs::write(
card_dir.join(format!("{card_id}.toml")),
concat!(
"schema_version = \"card/v0\"\n",
"card_id = \"mypkg_20260401T000000_aabbcc\"\n",
"created_at = \"2026-04-01T00:00:00Z\"\n",
"[pkg]\n",
"name = \"mypkg\"\n",
),
)
.expect("write card toml");
let jsonl = "{\"case_idx\":0,\"score\":1.0}\n{\"case_idx\":1,\"score\":0.5}\n";
std::fs::write(card_dir.join(format!("{card_id}.samples.jsonl")), jsonl)
.expect("write samples jsonl");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(
&client,
&format!("alc://cards/{card_id}/samples?offset=0&limit=2"),
)
.await
.expect("read_resource card samples failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
let body: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("samples JSON parse failed: {e}\nraw: {text}"));
assert!(
body.is_array() || body.is_object(),
"expected JSON array or object response, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_eval_detail() {
let tmp = tempfile::tempdir().expect("tempdir");
let eval_id = "mystrategy_1745000000";
let evals_dir = tmp.path().join("evals");
std::fs::create_dir_all(&evals_dir).expect("create evals dir");
std::fs::write(
evals_dir.join(format!("{eval_id}.json")),
r#"{"eval_id":"mystrategy_1745000000","strategy":"mystrategy","pass_rate":0.8}"#,
)
.expect("write eval json");
let client = connect_with_alc_home(tmp.path()).await;
let result = read_resource(&client, &format!("alc://eval/{eval_id}"))
.await
.expect("read_resource eval detail failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
let eval: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("eval JSON parse failed: {e}\nraw: {text}"));
assert_eq!(eval["eval_id"], eval_id);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resource_read_logs_pagination() {
let tmp = tempfile::tempdir().expect("tempdir");
let session_id = "ses-e2e-log-test";
let logs_dir = tmp.path().join("logs");
std::fs::create_dir_all(&logs_dir).expect("create logs dir");
std::fs::write(
logs_dir.join(format!("{session_id}.json")),
r#"{"session_id":"ses-e2e-log-test","rounds":[]}"#,
)
.expect("write log json");
let bin = std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")));
let packages_path = tmp.path().join("packages");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("ALC_HOME", tmp.path())
.env("ALC_PACKAGES_PATH", &packages_path)
.env("ALC_LOG_DIR", &logs_dir);
let transport = TokioChildProcess::new(cmd).expect("spawn alc server");
let client = ().serve(transport).await.expect("initialize MCP session");
let result = read_resource(
&client,
&format!("alc://logs/{session_id}?limit=10&max_chars=500"),
)
.await
.expect("read_resource logs failed");
assert_eq!(result.contents.len(), 1);
let (_uri, text) = resource_text(&result.contents[0]);
assert!(!text.is_empty(), "expected non-empty log response");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_multifile_healthy() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let source_root = tempfile::tempdir().expect("source tempdir");
let coll_root = source_root.path();
let pkg_src = coll_root.join("e2e_doctor_complete");
std::fs::create_dir_all(&pkg_src).expect("mkdir pkg_src");
std::fs::write(
pkg_src.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_doctor_complete", version = "0.1.0" }
local sub = require("e2e_doctor_complete.sub")
function M.run(ctx) return sub.hello() end
return M"#,
)
.expect("write init.lua");
std::fs::write(
pkg_src.join("sub.lua"),
r#"return { hello = function() return "hi" end }"#,
)
.expect("write sub.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_root.to_string_lossy() }),
)
.await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_doctor_complete" }),
)
.await;
let healthy = resp["healthy"].as_array().expect("healthy array missing");
assert!(
healthy.iter().any(|e| e["name"] == "e2e_doctor_complete"),
"e2e_doctor_complete should be in healthy bucket, got: {resp}"
);
let incomplete = resp["incomplete_pkg"]
.as_array()
.expect("incomplete_pkg array missing");
assert!(
incomplete.is_empty(),
"incomplete_pkg should be empty for complete package, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_incomplete_pkg_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let source_root = tempfile::tempdir().expect("source tempdir");
let coll_root = source_root.path();
let pkg_src = coll_root.join("e2e_doctor_incomplete");
std::fs::create_dir_all(&pkg_src).expect("mkdir pkg_src");
std::fs::write(
pkg_src.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_doctor_incomplete", version = "0.1.0" }
local sub = require("e2e_doctor_incomplete.missing_sub")
function M.run(ctx) return sub.run(ctx) end
return M"#,
)
.expect("write init.lua");
std::fs::write(
pkg_src.join("missing_sub.lua"),
r#"return { run = function(ctx) return "ok" end }"#,
)
.expect("write missing_sub.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_root.to_string_lossy() }),
)
.await;
let installed_dest = tmp.path().join("packages").join("e2e_doctor_incomplete");
assert!(
installed_dest.exists(),
"package should be installed at {installed_dest:?}"
);
std::fs::remove_file(installed_dest.join("missing_sub.lua")).expect("remove missing_sub.lua");
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_doctor_incomplete" }),
)
.await;
let incomplete = resp["incomplete_pkg"]
.as_array()
.expect("incomplete_pkg array missing");
let entry = incomplete
.iter()
.find(|e| e["name"] == "e2e_doctor_incomplete")
.unwrap_or_else(|| {
panic!("e2e_doctor_incomplete not in incomplete_pkg bucket, got: {resp}")
});
assert_eq!(entry["kind"], "incomplete_pkg", "kind field: {entry}");
let missing = entry["missing_subs"]
.as_array()
.expect("missing_subs array missing");
assert!(
missing.iter().any(|s| s == "missing_sub"),
"missing_subs should contain 'missing_sub', got: {missing:?}"
);
let suggestion = entry["suggestion"].as_str().unwrap_or("");
assert!(
!suggestion.is_empty(),
"suggestion should not be empty: {entry}"
);
let healthy = resp["healthy"].as_array().expect("healthy array");
assert!(
!healthy.iter().any(|e| e["name"] == "e2e_doctor_incomplete"),
"incomplete pkg must not appear in healthy: {resp}"
);
client.cancel().await.expect("cancel failed");
}
fn pre_install_pkg(alc_home: &std::path::Path, name: &str, files: &[(&str, &str)]) {
let pkg_dir = alc_home.join("packages").join(name);
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
for (filename, content) in files {
std::fs::write(pkg_dir.join(filename), content).expect("write pkg file");
}
let manifest_path = alc_home.join("installed.json");
let mut manifest: serde_json::Value = if manifest_path.exists() {
serde_json::from_str(&std::fs::read_to_string(&manifest_path).expect("read installed.json"))
.expect("parse installed.json")
} else {
json!({ "packages": {} })
};
manifest["packages"][name] = json!({
"version": "0.1.0",
"source": { "type": "path", "path": pkg_dir.to_string_lossy() },
"installed_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z",
});
std::fs::write(
&manifest_path,
serde_json::to_string_pretty(&manifest).expect("serialize manifest"),
)
.expect("write installed.json");
}
#[tokio::test]
async fn test_pkg_doctor_narrative_declared_missing_warns() {
let tmp = tempfile::tempdir().expect("tempdir");
pre_install_pkg(
tmp.path(),
"e2e_narr_declared",
&[(
"init.lua",
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_narr_declared', version = '0.1.0' }\n",
"M.docs = { narrative = 'overview.md', schema_version = 1 }\n",
"function M.run(ctx) return ctx end\n",
"return M\n",
),
)],
);
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_narr_declared" }),
)
.await;
assert!(
resp["narrative_issues"].is_null(),
"narrative_issues must be absent after decommission, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_narrative_unmigrated_info() {
let tmp = tempfile::tempdir().expect("tempdir");
pre_install_pkg(
tmp.path(),
"e2e_narr_unmigrated",
&[
(
"init.lua",
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_narr_unmigrated', version = '0.1.0' }\n",
"function M.run(ctx) return ctx end\n",
"return M\n",
),
),
(
"narrative.md",
"# e2e_narr_unmigrated\n\nAdoption candidate.\n",
),
],
);
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_narr_unmigrated" }),
)
.await;
assert!(
resp["narrative_issues"].is_null(),
"narrative_issues must be absent after decommission, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unregistered_pkg_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("e2e_unreg_foo");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'e2e_unreg_foo', version = '0.1.0' }\nreturn M\n",
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(&client, "alc_pkg_doctor", json!({})).await;
let bucket = resp["unregistered_pkg"]
.as_array()
.expect("unregistered_pkg array missing");
let entry = bucket
.iter()
.find(|e| e["name"] == "e2e_unreg_foo")
.unwrap_or_else(|| panic!("e2e_unreg_foo not found in unregistered_pkg, got: {resp}"));
assert_eq!(entry["kind"], "unregistered_pkg", "kind mismatch: {entry}");
assert_eq!(entry["source"], "unknown", "source mismatch: {entry}");
assert!(
entry["reason"]
.as_str()
.unwrap_or("")
.contains("physical dir with init.lua"),
"reason should mention 'physical dir with init.lua': {entry}"
);
let suggestion = entry["suggestion"]
.as_array()
.expect("suggestion must be an array for unregistered_pkg");
assert_eq!(
suggestion.len(),
4,
"suggestion must have exactly 4 elements: {suggestion:?}"
);
for (i, elem) in suggestion.iter().enumerate() {
assert!(
elem.is_string(),
"suggestion[{i}] must be a string, got: {elem}"
);
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unregistered_pkg_empty_dir_skipped() {
let tmp = tempfile::tempdir().expect("tempdir");
let empty_dir = tmp.path().join("packages").join("e2e_unreg_empty");
std::fs::create_dir_all(&empty_dir).expect("create empty dir");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(&client, "alc_pkg_doctor", json!({})).await;
let bucket = resp["unregistered_pkg"]
.as_array()
.expect("unregistered_pkg array missing");
assert!(
!bucket.iter().any(|e| e["name"] == "e2e_unreg_empty"),
"empty dir must not appear in unregistered_pkg, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unregistered_pkg_manifest_registered_skipped() {
let tmp = tempfile::tempdir().expect("tempdir");
let source_root = tempfile::tempdir().expect("source tempdir");
let pkg_src = source_root.path().join("e2e_unreg_bar");
std::fs::create_dir_all(&pkg_src).expect("mkdir pkg_src");
std::fs::write(
pkg_src.join("init.lua"),
"local M = {}\nM.meta = { name = 'e2e_unreg_bar', version = '0.1.0' }\nreturn M\n",
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
call_json(
&client,
"alc_pkg_install",
json!({ "url": source_root.path().to_string_lossy() }),
)
.await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_unreg_bar" }),
)
.await;
let bucket = resp["unregistered_pkg"]
.as_array()
.expect("unregistered_pkg array missing");
assert!(
!bucket.iter().any(|e| e["name"] == "e2e_unreg_bar"),
"manifest-registered pkg must not appear in unregistered_pkg, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unregistered_pkg_alc_toml_path_dep_skipped() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("e2e_unreg_baz");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'e2e_unreg_baz', version = '0.1.0' }\nreturn M\n",
)
.expect("write init.lua");
let project_root = tmp.path();
let alc_toml_content = format!(
"[packages.e2e_unreg_baz]\npath = \"{}\"\n",
pkg_dir.display()
);
std::fs::write(project_root.join("alc.toml"), &alc_toml_content).expect("write alc.toml");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "project_root": project_root.to_string_lossy() }),
)
.await;
let bucket = resp["unregistered_pkg"]
.as_array()
.expect("unregistered_pkg array missing");
assert!(
!bucket.iter().any(|e| e["name"] == "e2e_unreg_baz"),
"path-dep in alc.toml must not appear in unregistered_pkg (false positive), got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_unregistered_pkg_target_filter_routes_to_bucket() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("e2e_unreg_quux");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'e2e_unreg_quux', version = '0.1.0' }\nreturn M\n",
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_unreg_quux" }),
)
.await;
let bucket = resp["unregistered_pkg"]
.as_array()
.expect("unregistered_pkg array missing");
assert!(
bucket.iter().any(|e| e["name"] == "e2e_unreg_quux"),
"e2e_unreg_quux must appear in unregistered_pkg when target_filter is Some, got: {resp}"
);
let entry = bucket
.iter()
.find(|e| e["name"] == "e2e_unreg_quux")
.expect("entry not found");
let suggestion = entry["suggestion"]
.as_array()
.expect("suggestion must be an array");
assert_eq!(
suggestion.len(),
4,
"suggestion must have exactly 4 elements: {suggestion:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_narrative_clean_state_silent() {
let tmp = tempfile::tempdir().expect("tempdir");
pre_install_pkg(
tmp.path(),
"e2e_narr_clean",
&[
(
"init.lua",
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_narr_clean', version = '0.1.0' }\n",
"M.docs = { narrative = 'narrative.md', schema_version = 1 }\n",
"function M.run(ctx) return ctx end\n",
"return M\n",
),
),
("narrative.md", "# clean\n"),
],
);
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_narr_clean" }),
)
.await;
assert!(
resp["narrative_issues"].is_null(),
"narrative_issues must be absent after decommission, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_force_overwrites_collection() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let repo_root = tempfile::tempdir().expect("repo root");
let pkg_subdir = repo_root.path().join("e2e_force_col");
std::fs::create_dir_all(&pkg_subdir).expect("mkdir pkg_subdir");
std::fs::write(
pkg_subdir.join("init.lua"),
"local M = {}\nM.meta = { name = \"e2e_force_col\", version = \"1.0.0\" }\nreturn M",
)
.expect("write init.lua v1");
let git_init = std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(repo_root.path())
.output()
.expect("git init");
assert!(git_init.status.success(), "git init failed");
let _ = std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo_root.path())
.output();
let _ = std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo_root.path())
.output();
let git_add = std::process::Command::new("git")
.args(["add", "."])
.current_dir(repo_root.path())
.output()
.expect("git add");
assert!(git_add.status.success(), "git add failed");
let git_commit = std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(repo_root.path())
.output()
.expect("git commit");
assert!(git_commit.status.success(), "git commit failed");
let repo_url = format!("file://{}", repo_root.path().to_string_lossy());
let resp1 = call_json(&client, "alc_pkg_install", json!({ "url": repo_url })).await;
let installed1 = resp1["installed"].as_array().expect("installed array");
assert!(
installed1.iter().any(|v| v == "e2e_force_col"),
"e2e_force_col should be installed on first call: {resp1}"
);
let installed_init = tmp
.path()
.join("packages")
.join("e2e_force_col")
.join("init.lua");
std::fs::write(&installed_init, "-- stale").expect("write stale");
let resp2 = call_json(
&client,
"alc_pkg_install",
json!({ "url": repo_url, "force": true }),
)
.await;
let installed2 = resp2["installed"]
.as_array()
.expect("installed array (force)");
assert!(
installed2.iter().any(|v| v == "e2e_force_col"),
"e2e_force_col should be in installed (not skipped) after force=true: {resp2}"
);
let content = std::fs::read_to_string(&installed_init).expect("read after force install");
assert!(
content.contains("1.0.0"),
"installed init.lua should be restored from git after force overwrite, got: {content}"
);
assert!(
!content.contains("stale"),
"stale content must be removed after force overwrite"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_force_default_is_skip() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let repo_root = tempfile::tempdir().expect("repo root");
let pkg_subdir = repo_root.path().join("e2e_skip_col");
std::fs::create_dir_all(&pkg_subdir).expect("mkdir pkg_subdir");
std::fs::write(
pkg_subdir.join("init.lua"),
"local M = {}\nM.meta = { name = \"e2e_skip_col\", version = \"1.0.0\" }\nreturn M",
)
.expect("write init.lua");
let _ = std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(repo_root.path())
.output();
let _ = std::process::Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo_root.path())
.output();
let _ = std::process::Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo_root.path())
.output();
let _ = std::process::Command::new("git")
.args(["add", "."])
.current_dir(repo_root.path())
.output();
let _ = std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(repo_root.path())
.output();
let repo_url = format!("file://{}", repo_root.path().to_string_lossy());
call_json(&client, "alc_pkg_install", json!({ "url": repo_url })).await;
let resp2 = call_json(&client, "alc_pkg_install", json!({ "url": repo_url })).await;
let skipped = resp2["skipped"].as_array().expect("skipped array");
assert!(
skipped.iter().any(|v| v == "e2e_skip_col"),
"e2e_skip_col should be in skipped when force is omitted: {resp2}"
);
let installed = resp2["installed"].as_array().expect("installed array");
assert!(
!installed.iter().any(|v| v == "e2e_skip_col"),
"e2e_skip_col must not be in installed when force is omitted: {resp2}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_invalid_force_type() {
let client = connect().await;
let outcome = client
.call_tool(call_params(
"alc_pkg_install",
json!({ "url": "github.com/user/pkg", "force": "notbool" }),
))
.await;
match outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
let has_type_error = text.contains("invalid type")
|| text.contains("expected a boolean")
|| text.contains("expected bool");
assert!(
is_error || has_type_error,
"expected shape error (is_error=true or type-mismatch text), got is_error={is_error:?}, text: {text}"
);
}
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("invalid type") || msg.contains("expected a boolean"),
"expected invalid-type error from param deserialization, got: {msg}"
);
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_state_no_lost_update_under_concurrent_writes() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let ns = "e2e_concurrent_state";
let code = r#"return alc.state.incr("c", 1, 0)"#;
let (r1, r2) = tokio::join!(
call_json(
&client,
"alc_run",
json!({ "code": code, "ctx": { "_ns": ns } })
),
call_json(
&client,
"alc_run",
json!({ "code": code, "ctx": { "_ns": ns } })
),
);
assert_eq!(r1["status"], "completed", "run 1 failed: {r1}");
assert_eq!(r2["status"], "completed", "run 2 failed: {r2}");
let v1 = r1["result"]
.as_f64()
.unwrap_or_else(|| panic!("r1 result not a number: {r1}")) as u64;
let v2 = r2["result"]
.as_f64()
.unwrap_or_else(|| panic!("r2 result not a number: {r2}")) as u64;
let mut pair = [v1, v2];
pair.sort();
assert_eq!(
pair,
[1, 2],
"expected incr results to be {{1, 2}} (no lost update), got {v1} and {v2}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_lua_print_does_not_corrupt_transport() {
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({ "code": r#"print("dbg: hello from lua"); return 42"# }),
)
.await;
assert_eq!(
resp["status"], "completed",
"alc_run with print failed: {resp}"
);
assert_eq!(resp["result"], 42);
let resp2 = call_json(&client, "alc_run", json!({ "code": "return 99" })).await;
assert_eq!(
resp2["status"], "completed",
"follow-up call failed: {resp2}"
);
assert_eq!(resp2["result"], 99);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_complete_packages_empty_home() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(tmp.path().join("packages")).expect("mkdir packages");
let client = connect_with_alc_home(tmp.path()).await;
let params = CompleteRequestParams::new(
Reference::Resource(ResourceReference {
uri: "alc://packages/{name}/init.lua".to_string(),
}),
ArgumentInfo {
name: "name".to_string(),
value: "".to_string(),
},
);
let result = client
.complete(params)
.await
.expect("completion/complete failed");
assert!(
result.completion.values.len() <= 100,
"values length must not exceed cap=100, got {}",
result.completion.values.len()
);
for v in &result.completion.values {
assert!(!v.is_empty(), "completion value must not be empty string");
}
if result.completion.values.is_empty() {
assert_ne!(
result.completion.has_more,
Some(true),
"has_more must not be true when values is empty"
);
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_complete_packages_returns_installed_name() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("e2e_complete_test_pkg");
std::fs::create_dir_all(&pkg_dir).expect("mkdir pkg");
std::fs::write(
pkg_dir.join("init.lua"),
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_complete_test_pkg', version = '0.1.0' }\n",
"return M\n",
),
)
.expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let params = CompleteRequestParams::new(
Reference::Resource(ResourceReference {
uri: "alc://packages/{name}/init.lua".to_string(),
}),
ArgumentInfo {
name: "name".to_string(),
value: "".to_string(),
},
);
let result = client
.complete(params)
.await
.expect("completion/complete failed");
assert!(
result
.completion
.values
.iter()
.any(|v| v == "e2e_complete_test_pkg"),
"expected 'e2e_complete_test_pkg' in completion values, got: {:?}",
result.completion.values
);
assert!(
result.completion.total.unwrap_or(0) >= 1,
"expected total >= 1 when at least one package is installed"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_complete_packages_prefix_filter() {
let tmp = tempfile::tempdir().expect("tempdir");
for name in &["e2e_c_alpha", "other_pkg"] {
let dir = tmp.path().join("packages").join(name);
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(
dir.join("init.lua"),
format!(
"local M = {{}}\nM.meta = {{ name = '{}', version = '0.1.0' }}\nreturn M\n",
name
),
)
.expect("write init.lua");
}
let client = connect_with_alc_home(tmp.path()).await;
let params = CompleteRequestParams::new(
Reference::Resource(ResourceReference {
uri: "alc://packages/{name}/init.lua".to_string(),
}),
ArgumentInfo {
name: "name".to_string(),
value: "e2e_c".to_string(),
},
);
let result = client
.complete(params)
.await
.expect("completion/complete failed");
assert!(
result.completion.values.iter().any(|v| v == "e2e_c_alpha"),
"expected 'e2e_c_alpha' in completion values for prefix 'e2e_c', got: {:?}",
result.completion.values
);
assert!(
!result.completion.values.iter().any(|v| v == "other_pkg"),
"unexpected 'other_pkg' in filtered completion values: {:?}",
result.completion.values
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_hub_index_aggregate_warnings_emitted_on_corrupt_cache() {
let tmp = tempfile::tempdir().expect("tempdir");
let alc_home = tmp.path();
let url_good = "https://good.example.com/index.json";
let url_bad = "https://bad-corrupt.example.com/index.json";
fn fnv1a_hex(url: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325u64;
for b in url.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0100_0000_01b3u64);
}
format!("{h:016x}")
}
let hub_cache_dir = alc_home.join("hub_cache");
std::fs::create_dir_all(&hub_cache_dir).expect("mkdir hub_cache");
let good_index = serde_json::json!({
"schema_version": "hub_index/v0",
"updated_at": "2026-01-01T00:00:00Z",
"packages": [{
"name": "good_pkg",
"version": "0.1.0",
"source": { "type": "unknown" },
"card_count": 0
}]
});
std::fs::write(
hub_cache_dir.join(format!("{}.json", fnv1a_hex(url_good))),
serde_json::to_string(&good_index).expect("serialize"),
)
.expect("write good cache");
std::fs::write(
hub_cache_dir.join(format!("{}.json", fnv1a_hex(url_bad))),
b"{{{{ definitely not valid json",
)
.expect("write corrupt cache");
let reg_json = serde_json::json!({
"registries": [
{ "source": url_good, "origin": "pkg_install", "added_at": "2026-01-01T00:00:00Z" },
{ "source": url_bad, "origin": "pkg_install", "added_at": "2026-01-01T00:00:00Z" }
]
});
std::fs::write(
alc_home.join("hub_registries.json"),
serde_json::to_string(®_json).expect("serialize"),
)
.expect("write hub_registries");
let client = connect_with_alc_home(alc_home).await;
let result = read_resource(&client, "alc://hub/index")
.await
.expect("read alc://hub/index failed");
assert_eq!(result.contents.len(), 1);
let (uri, text) = resource_text(&result.contents[0]);
assert_eq!(uri, "alc://hub/index");
let body: Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("hub/index JSON parse failed: {e}\nraw: {text}"));
let packages = body["packages"].as_array().expect("packages must be array");
let has_good_pkg = packages
.iter()
.any(|p| p["name"].as_str() == Some("good_pkg"));
assert!(
has_good_pkg,
"expected good_pkg from valid source, got packages: {packages:?}"
);
let warnings = body["warnings"].as_array().expect("warnings must be array");
assert!(
!warnings.is_empty(),
"expected non-empty warnings array, got: {body}"
);
let bad_url_mentioned = warnings.iter().any(|w| {
w.as_str()
.map(|s| s.contains("bad-corrupt.example.com"))
.unwrap_or(false)
});
assert!(
bad_url_mentioned,
"expected warning to mention the corrupt URL, got warnings: {warnings:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_complete_returns_empty_for_unsupported_arg_name() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let params = CompleteRequestParams::new(
Reference::Resource(ResourceReference {
uri: "alc://logs/{session_id}".to_string(),
}),
ArgumentInfo {
name: "session_id".to_string(),
value: "".to_string(),
},
);
let result = client
.complete(params)
.await
.expect("completion/complete for logs domain must not error");
assert!(
result.completion.values.is_empty(),
"expected empty values for logs domain (no session-list API), got: {:?}",
result.completion.values
);
assert_eq!(
result.completion.total,
Some(0),
"expected total=0 for empty completion, got: {:?}",
result.completion.total
);
assert_ne!(
result.completion.has_more,
Some(true),
"has_more must not be true when values is empty"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_mcp_resources_complete_prompt_ref_returns_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let params = CompleteRequestParams::new(
Reference::for_prompt("nonexistent_prompt"),
ArgumentInfo {
name: "arg".to_string(),
value: "".to_string(),
},
);
let result = client
.complete(params)
.await
.expect("completion/complete for prompt ref must not error");
assert!(
result.completion.values.is_empty(),
"expected empty values for prompt ref, got: {:?}",
result.completion.values
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_session_new_activates_pin_for_subsequent_pkg_list() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let project_dir = tmp.path().join("proj-A");
std::fs::create_dir_all(&project_dir).expect("create project dir");
std::fs::write(project_dir.join("alc.toml"), "[packages]\n").expect("write alc.toml");
let resp = call_json(
&client,
"alc_session_new",
json!({ "project_root": project_dir.to_string_lossy() }),
)
.await;
assert_eq!(resp["mode"], "default");
assert_eq!(
resp["project_root"],
json!(project_dir.to_string_lossy().to_string())
);
assert!(
resp["session_id"]
.as_str()
.unwrap_or("")
.starts_with("alc-sess-"),
"session_id must use alc-sess- prefix, got {:?}",
resp["session_id"]
);
let listing = call_json(&client, "alc_pkg_list", json!({})).await;
let project_root = listing["project_root"].as_str().unwrap_or("");
assert_eq!(
project_root,
project_dir.to_string_lossy().as_ref(),
"pkg_list project_root must reflect the activated session pin, got {project_root}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_session_new_test_mode_records_in_response() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(&client, "alc_session_new", json!({ "mode": "test" })).await;
assert_eq!(resp["mode"], "test");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_legacy_fallback_unchanged_when_session_not_activated() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let listing_no_session = call_json(&client, "alc_pkg_list", json!({})).await;
assert!(
listing_no_session.is_object(),
"pkg_list legacy path must return a JSON object, got: {listing_no_session}"
);
let project_dir = tmp.path().join("legacy-flip");
std::fs::create_dir_all(&project_dir).expect("create project dir");
std::fs::write(project_dir.join("alc.toml"), "[packages]\n").expect("write alc.toml");
let _ = call_json(
&client,
"alc_session_new",
json!({ "project_root": project_dir.to_string_lossy() }),
)
.await;
let listing_with_session = call_json(&client, "alc_pkg_list", json!({})).await;
assert_eq!(
listing_with_session["project_root"].as_str().unwrap_or(""),
project_dir.to_string_lossy().as_ref(),
"after activation, pkg_list must reflect the session pin"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_per_call_project_root_overrides_session_pin() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let pinned = tmp.path().join("pinned");
let override_dir = tmp.path().join("override");
std::fs::create_dir_all(&pinned).expect("create pinned");
std::fs::create_dir_all(&override_dir).expect("create override");
std::fs::write(pinned.join("alc.toml"), "[packages]\n").expect("write pinned alc.toml");
std::fs::write(override_dir.join("alc.toml"), "[packages]\n").expect("write override alc.toml");
let _ = call_json(
&client,
"alc_session_new",
json!({ "project_root": pinned.to_string_lossy() }),
)
.await;
let listing = call_json(
&client,
"alc_pkg_list",
json!({ "project_root": override_dir.to_string_lossy() }),
)
.await;
assert_eq!(
listing["project_root"].as_str().unwrap_or(""),
override_dir.to_string_lossy().as_ref(),
"explicit per-call project_root must override session pin"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_card_analyze_roundtrip() {
let tmp = tempfile::tempdir().expect("tempdir");
let card_id_a = "e2e_20260510T000000_aabbcc";
let card_pkg = "e2e";
let card_dir = tmp.path().join("cards").join(card_pkg);
std::fs::create_dir_all(&card_dir).expect("create card dir");
std::fs::write(
card_dir.join(format!("{card_id_a}.toml")),
concat!(
"schema_version = \"card/v0\"\n",
"card_id = \"e2e_20260510T000000_aabbcc\"\n",
"created_at = \"2026-05-10T00:00:00Z\"\n",
"[pkg]\n",
"name = \"e2e\"\n",
),
)
.expect("write card A toml");
let card_id_b = "e2e_20260510T000001_bbccdd";
std::fs::write(
card_dir.join(format!("{card_id_b}.toml")),
concat!(
"schema_version = \"card/v0\"\n",
"card_id = \"e2e_20260510T000001_bbccdd\"\n",
"created_at = \"2026-05-10T00:00:00Z\"\n",
"[pkg]\n",
"name = \"e2e\"\n",
),
)
.expect("write card B toml");
std::fs::write(
card_dir.join(format!("{card_id_b}.samples.jsonl")),
"{\"case_idx\":0,\"admission\":\"fail\",\"status\":\"fail\"}\n",
)
.expect("write card B samples");
let client = connect_with_alc_home(tmp.path()).await;
let packages_dir =
std::path::PathBuf::from(std::env::var("HOME").expect("HOME")).join(".algocline/packages");
call_json(
&client,
"alc_pkg_install",
json!({ "url": packages_dir.to_string_lossy().as_ref() }),
)
.await;
let resp_a = call_json(&client, "alc_card_analyze", json!({ "card_id": card_id_a })).await;
assert_eq!(
resp_a["status"], "completed",
"zero-sample card must complete immediately, got: {resp_a}"
);
assert!(
resp_a["result"]["pattern"].as_str().is_some(),
"pattern must be a string at result.pattern, got: {resp_a}"
);
assert!(
resp_a["result"]["suggested_change"].as_str().is_some(),
"suggested_change must be a string at result.suggested_change, got: {resp_a}"
);
assert!(
resp_a["result"]["confidence"].as_f64().is_some(),
"confidence must be a number at result.confidence, got: {resp_a}"
);
assert!(
resp_a["result"].get("card").is_none(),
"echo of 'card' must be removed from result: {resp_a}"
);
assert!(
resp_a["result"].get("samples").is_none(),
"echo of 'samples' must be removed from result: {resp_a}"
);
let resp_b = call_json(&client, "alc_card_analyze", json!({ "card_id": card_id_b })).await;
assert_eq!(
resp_b["status"], "needs_response",
"failure-sample card must pause for LLM, got: {resp_b}"
);
let session_id = resp_b["session_id"]
.as_str()
.expect("session_id missing from needs_response");
let stub = r#"{"pattern":"prompt too vague","suggested_change":"add explicit constraints","confidence":0.8}"#;
let resp_cont = call_json(
&client,
"alc_continue",
json!({ "session_id": session_id, "response": stub }),
)
.await;
assert_eq!(
resp_cont["status"], "completed",
"expected completed after alc_continue, got: {resp_cont}"
);
assert!(
resp_cont["result"]["result"]["pattern"].as_str().is_some(),
"pattern must be present in result.result after round-trip, got: {resp_cont}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_card_analyze_unknown_card_id() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.call_tool(call_params(
"alc_card_analyze",
json!({ "card_id": "nonexistent_card_id_xyz" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("nonexistent_card_id_xyz"),
"error must reference the unknown card_id, got: {text}"
);
assert!(
text.contains("not found"),
"error must say 'not found', got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_card_analyze_shape_violation() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let outcome = client
.call_tool(call_params("alc_card_analyze", json!({ "card_id": true })))
.await;
match outcome {
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("boolean") || msg.contains("string") || msg.contains("invalid"),
"McpError message must describe a type/shape error, got: {msg}"
);
}
Ok(result) => {
let text = extract_text(&result);
let is_err = result.is_error.unwrap_or(false);
let parsed: Option<serde_json::Value> = serde_json::from_str(text).ok();
let is_completed = parsed.as_ref().is_some_and(|v| v["status"] == "completed");
assert!(
is_err || !is_completed,
"shape violation must not produce a successful completed result, got: {text}"
);
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_advice_rejects_library_package() {
let alc_home = tempfile::tempdir().expect("tempdir");
let pkg_dir = alc_home.path().join("packages").join("test_lib_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "test_lib_pkg", version = "0.1.0", type = "library" }
M.helpers = {}
return M"#,
)
.expect("write init.lua");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_advice",
json!({ "strategy": "test_lib_pkg", "task": "test" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("library"),
"expected error mentioning 'library', got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_eval_rejects_library_package() {
let alc_home = tempfile::tempdir().expect("tempdir");
let pkg_dir = alc_home.path().join("packages").join("test_lib_pkg");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "test_lib_pkg", version = "0.1.0", type = "library" }
M.helpers = {}
return M"#,
)
.expect("write init.lua");
let evalframe_dir = alc_home.path().join("packages").join("evalframe");
std::fs::create_dir_all(&evalframe_dir).expect("create evalframe dir");
std::fs::write(
evalframe_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "evalframe", version = "0.1.0", type = "runnable" }
M.run = function(ctx) return {} end
return M"#,
)
.expect("write evalframe init.lua");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_eval",
json!({ "strategy": "test_lib_pkg", "scenario": "return {cases={}}" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("library"),
"expected error mentioning 'library', got: {text}"
);
client.cancel().await.expect("cancel failed");
}
fn alc_bin() -> String {
std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")))
}
#[test]
fn test_cli_help_short() {
let output = std::process::Command::new(alc_bin())
.arg("-h")
.output()
.expect("failed to run alc -h");
assert!(
output.status.success(),
"alc -h exited with non-zero status: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Usage:"),
"alc -h stdout missing 'Usage:': {stdout}"
);
}
#[test]
fn test_cli_help_long() {
let output = std::process::Command::new(alc_bin())
.arg("--help")
.output()
.expect("failed to run alc --help");
assert!(
output.status.success(),
"alc --help exited with non-zero status: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("init"),
"alc --help stdout missing 'init': {stdout}"
);
assert!(
stdout.contains("update"),
"alc --help stdout missing 'update': {stdout}"
);
}
#[test]
fn test_cli_version() {
let output = std::process::Command::new(alc_bin())
.arg("-V")
.output()
.expect("failed to run alc -V");
assert!(
output.status.success(),
"alc -V exited with non-zero status: {}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("alc"),
"alc -V stdout missing binary name 'alc': {stdout}"
);
assert!(
stdout.contains(env!("CARGO_PKG_VERSION")),
"alc -V stdout missing version '{}': {stdout}",
env!("CARGO_PKG_VERSION")
);
}
#[tokio::test]
async fn test_list_prompts_round_trip() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let prompts = client
.list_all_prompts()
.await
.expect("list_all_prompts failed");
let advice = prompts
.iter()
.find(|p| p.name == "advice")
.expect("`advice` prompt missing from list");
let advice_args = advice
.arguments
.as_deref()
.expect("advice must declare arguments");
assert_eq!(advice_args.len(), 1, "advice expects exactly 1 argument");
assert_eq!(advice_args[0].name, "task");
assert_eq!(
advice_args[0].required,
Some(true),
"advice.task must be required"
);
let new_pkg = prompts
.iter()
.find(|p| p.name == "new_package")
.expect("`new_package` prompt missing from list");
let new_pkg_args = new_pkg
.arguments
.as_deref()
.expect("new_package must declare arguments");
assert_eq!(
new_pkg_args.len(),
2,
"new_package expects exactly 2 arguments"
);
let name_arg = new_pkg_args
.iter()
.find(|a| a.name == "name")
.expect("new_package.name argument missing");
assert_eq!(
name_arg.required,
Some(true),
"new_package.name must be required"
);
let category_arg = new_pkg_args
.iter()
.find(|a| a.name == "category")
.expect("new_package.category argument missing");
assert_eq!(
category_arg.required,
Some(false),
"new_package.category must be optional"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_get_prompt_round_trip() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let task_text = "解の精度を上げて";
let mut arguments = serde_json::Map::new();
arguments.insert(
"task".to_string(),
serde_json::Value::String(task_text.to_string()),
);
let result = client
.get_prompt(GetPromptRequestParams::new("advice").with_arguments(arguments))
.await
.expect("get_prompt failed");
assert_eq!(
result.messages.len(),
1,
"expected exactly 1 message in get_prompt result"
);
let msg = &result.messages[0];
assert_eq!(
msg.role,
rmcp::model::PromptMessageRole::User,
"message role must be User"
);
let text = match &msg.content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
other => panic!("expected Text content, got: {other:?}"),
};
assert!(
text.contains(task_text),
"message text must contain the task value '{task_text}', got: {text}"
);
assert!(
!text.contains("${task}"),
"placeholder '${{task}}' must be substituted, but was found in: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_run_basic() {
let client = connect().await;
let resp = call_json(&client, "alc_v2_run", json!({ "code": "return {ok=true}" })).await;
assert!(
resp["session_id"].is_string(),
"expected session_id string in response, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_state_unknown_session_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_v2_state",
json!({ "session_id": "ses-nonexistent-00000000" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.to_lowercase().contains("not found"),
"expected 'not found' error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_state_idempotent() {
let client = connect().await;
let run_resp = call_json(&client, "alc_v2_run", json!({ "code": "return {ok=true}" })).await;
let sid = run_resp["session_id"].as_str().expect("session_id");
let first = timeout(Duration::from_secs(5), async {
loop {
let s = call_json(&client, "alc_v2_state", json!({"session_id": sid})).await;
let state_tag = s["state"].as_str().unwrap_or("");
if matches!(state_tag, "done" | "cancelled" | "failed") {
return s;
}
sleep(Duration::from_millis(20)).await;
}
})
.await
.expect("session did not reach terminal state within 5s");
let second = call_json(&client, "alc_v2_state", json!({"session_id": sid})).await;
assert_eq!(
first, second,
"alc_v2_state must be idempotent — two calls returned different JSON"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_resume_unknown_session_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_v2_resume",
json!({
"session_id": "ses-nonexistent-00000000",
"payload": {
"payload_kind": "single",
"response": "ok",
"query_id": "q-0",
"usage": null
}
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.to_lowercase().contains("not found"),
"expected 'not found' error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_resume_not_paused_errors() {
let client = connect().await;
let run_resp = call_json(&client, "alc_v2_run", json!({ "code": "return 42" })).await;
let sid = run_resp["session_id"].as_str().expect("session_id");
timeout(Duration::from_secs(5), async {
loop {
let s = call_json(&client, "alc_v2_state", json!({"session_id": sid})).await;
if s["state"].as_str() == Some("done") {
return;
}
sleep(Duration::from_millis(20)).await;
}
})
.await
.expect("session did not reach done state within 5s");
let result = client
.call_tool(call_params(
"alc_v2_resume",
json!({
"session_id": sid,
"payload": {
"payload_kind": "single",
"response": "ok",
"query_id": "q-0",
"usage": null
}
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.to_lowercase().contains("not paused"),
"expected 'not paused' error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_cancel_unknown_session_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_v2_cancel",
json!({ "session_id": "ses-nonexistent-00000000" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.to_lowercase().contains("not found"),
"expected 'not found' error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_cancel_idempotent() {
let client = connect().await;
let run_resp = call_json(
&client,
"alc_v2_run",
json!({ "code": "return alc.llm('hi')" }),
)
.await;
let sid = run_resp["session_id"].as_str().expect("session_id");
let _state = poll_until_paused(&client, sid, Duration::from_secs(5)).await;
let first = call_json(&client, "alc_v2_cancel", json!({ "session_id": sid })).await;
let _ = first;
let second_result = client
.call_tool(call_params("alc_v2_cancel", json!({ "session_id": sid })))
.await
.expect("call_tool failed");
let second_text = extract_text(&second_result);
let second_json: Value = serde_json::from_str(second_text)
.unwrap_or_else(|e| panic!("second cancel must return JSON, got: {second_text} ({e})"));
assert_eq!(
second_json,
json!({}),
"second cancel must return {{}} for idempotent terminal cancel, got: {second_json}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_run_paused_lifecycle() {
let client = connect().await;
let run_resp = call_json(
&client,
"alc_v2_run",
json!({ "code": "return alc.llm('hi')" }),
)
.await;
let sid = run_resp["session_id"].as_str().expect("session_id");
let state = poll_until_paused(&client, sid, Duration::from_secs(5)).await;
assert_eq!(
state["state"].as_str(),
Some("paused"),
"session must be in paused state"
);
let state_redacted = {
let s = serde_json::to_string_pretty(&state).expect("serialize state");
let s = redact_uuids(&s);
let re = regex::Regex::new(r"\b[0-9]{13}\b").expect("regex");
re.replace_all(&s, "<TIMESTAMP_MS>").to_string()
};
insta::assert_snapshot!("alc_v2_state_paused", state_redacted);
let query_id = state["prompts"]
.as_array()
.and_then(|arr| arr.first())
.and_then(|q| q["query_id"].as_str())
.unwrap_or("q-0")
.to_string();
let resume_resp = call_json(
&client,
"alc_v2_resume",
json!({
"session_id": sid,
"payload": {
"payload_kind": "single",
"response": "ok",
"query_id": query_id,
"usage": null
}
}),
)
.await;
let outcome = resume_resp["outcome"].as_str().unwrap_or("");
assert!(
outcome == "continued" || outcome == "terminal",
"resume outcome must be 'continued' or 'terminal', got: {outcome}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_resume_and_cancel_accept_stringified_payload() {
let client = connect().await;
let spawn = call_json(
&client,
"alc_v2_run",
json!({"code": "local r = alc.llm(\"ping?\"); return \"got: \" .. r"}),
)
.await;
let sid = spawn["session_id"]
.as_str()
.expect("session_id")
.to_string();
poll_until_paused(&client, &sid, Duration::from_secs(2)).await;
let payload_str = serde_json::to_string(&json!({
"payload_kind": "single",
"response": "pong",
"query_id": "q-0",
"usage": null
}))
.expect("encode payload");
let resume_resp = call_json(
&client,
"alc_v2_resume",
json!({
"session_id": sid,
"payload": payload_str,
}),
)
.await;
let outcome = resume_resp["outcome"].as_str().unwrap_or("");
assert!(
outcome == "continued" || outcome == "terminal",
"stringified payload must be accepted; got outcome: {outcome} / full: {resume_resp}"
);
let reason_str = serde_json::to_string(&json!({
"code": "user",
"detail": "smoke test",
"requested_at": 0,
}))
.expect("encode reason");
let _ = call_json(
&client,
"alc_v2_cancel",
json!({"session_id": sid, "reason": reason_str}),
)
.await;
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_v2_run_shape_validation() {
let client = connect().await;
let outcome = client.call_tool(call_params("alc_v2_run", json!({}))).await;
match outcome {
Err(_service_err) => {
}
Ok(result) => {
let text = extract_text(&result);
let parsed: Option<Value> = serde_json::from_str(text).ok();
assert!(
parsed.as_ref().and_then(|v| v.get("session_id")).is_none(),
"alc_v2_run with missing code must not return a session_id, got: {text}"
);
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_install_collection_only_after_single_removal() {
let tmp = tempfile::tempdir().expect("tempdir");
let alc_home = tmp.path();
let registries_json = serde_json::json!({
"registries": [
{
"source": "https://github.com/ynishi/algocline-bundled-packages",
"origin": "pkg_install",
"added_at": "2026-01-01T00:00:00Z"
}
]
});
std::fs::write(
alc_home.join("hub_registries.json"),
serde_json::to_string(®istries_json).expect("serialize hub_registries"),
)
.expect("write hub_registries.json");
let installed_pkg_dir = alc_home.join("packages").join("e2e_crux2_pkg");
std::fs::create_dir_all(&installed_pkg_dir).expect("mkdir installed pkg");
std::fs::write(
installed_pkg_dir.join("init.lua"),
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_crux2_pkg', version = '0.1.0' }\n",
"function M.run(ctx) return 'crux2' end\n",
"return M\n",
),
)
.expect("write installed pkg init.lua");
let client = connect_with_alc_home(alc_home).await;
let src_tmp = tempfile::tempdir().expect("src tempdir");
let coll_dir = src_tmp.path().join("e2e_crux2_coll");
let pkg_subdir = coll_dir.join("e2e_crux2_install_pkg");
std::fs::create_dir_all(&pkg_subdir).expect("mkdir pkg subdir");
std::fs::write(
pkg_subdir.join("init.lua"),
concat!(
"local M = {}\n",
"M.meta = { name = 'e2e_crux2_install_pkg', version = '0.1.0' }\n",
"function M.run(ctx) return 'ok' end\n",
"return M\n",
),
)
.expect("write collection pkg init.lua");
let resp = call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_dir.to_string_lossy() }),
)
.await;
assert!(
resp["installed"]
.as_array()
.map(|a| a.iter().any(|v| v == "e2e_crux2_install_pkg"))
.unwrap_or(false),
"e2e_crux2_install_pkg should appear in installed, got: {resp}"
);
assert!(
resp.get("mode").is_none() || resp["mode"] != "single",
"response must not contain mode=single after Single-mode removal, got: {resp}"
);
let single_tmp = tempfile::tempdir().expect("single tempdir");
let single_dir = single_tmp.path().join("e2e_crux2_single");
std::fs::create_dir_all(&single_dir).expect("mkdir single dir");
std::fs::write(
single_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'should_fail', version = '0.1.0' }\nreturn M\n",
)
.expect("write single-layout init.lua");
let single_outcome = client
.call_tool(call_params(
"alc_pkg_install",
json!({ "url": single_dir.to_string_lossy() }),
))
.await;
match single_outcome {
Ok(result) => {
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for Single-layout install after v0.36.0 removal, \
got is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("v0.36.0"),
"error message must cite v0.36.0 removal, got: {text}"
);
}
Err(e) => panic!("unexpected call_tool Err for Single-layout install: {e}"),
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_inline_passing() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let lua_code = concat!(
"local describe, it, expect = lust.describe, lust.it, lust.expect\n",
"describe('suite', function()\n",
" it('passes', function() expect(1).to.equal(1) end)\n",
"end)\n",
);
let resp = call_json(&client, "alc_pkg_test", json!({ "code": lua_code })).await;
assert_eq!(resp["passed"], 1, "expected passed=1, got: {resp}");
assert_eq!(resp["failed"], 0, "expected failed=0, got: {resp}");
assert_eq!(resp["pending"], 0, "expected pending=0, got: {resp}");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_exclusivity_error_zero_inputs() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.call_tool(call_params_empty("alc_pkg_test"))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("provide exactly one of pkg, code_file, code"),
"expected exclusivity error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_pkg_not_found() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.call_tool(call_params(
"alc_pkg_test",
json!({ "pkg": "nonexistent_pkg_xyz" }),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("nonexistent_pkg_xyz"),
"error must name the missing package, got: {text}"
);
assert!(
text.contains("not found"),
"error must say not found, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_auto_search_paths_default_includes_installed() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("dummy_e2e_pkg_auto");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(pkg_dir.join("init.lua"), "return {}\n").expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let lua_code = concat!(
"local describe, it, expect = lust.describe, lust.it, lust.expect\n",
"describe('auto', function()\n",
" it('ok', function() expect(1).to.equal(1) end)\n",
"end)\n",
);
let resp = call_json(&client, "alc_pkg_test", json!({ "code": lua_code })).await;
assert_eq!(resp["passed"], 1, "expected passed=1, got: {resp}");
let paths = resp["resolved_search_paths"]
.as_array()
.expect("resolved_search_paths must be an array");
let found = paths.iter().any(|row| {
row["name"].as_str() == Some("dummy_e2e_pkg_auto")
&& row["source"].as_str() == Some("installed")
});
assert!(
found,
"expected dummy_e2e_pkg_auto/installed in resolved_search_paths, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_auto_search_paths_false_skips_auto() {
let tmp = tempfile::tempdir().expect("tempdir");
let pkg_dir = tmp.path().join("packages").join("dummy_e2e_pkg_auto");
std::fs::create_dir_all(&pkg_dir).expect("create pkg dir");
std::fs::write(pkg_dir.join("init.lua"), "return {}\n").expect("write init.lua");
let client = connect_with_alc_home(tmp.path()).await;
let lua_code = concat!(
"local describe, it, expect = lust.describe, lust.it, lust.expect\n",
"describe('opt_out', function()\n",
" it('ok', function() expect(1).to.equal(1) end)\n",
"end)\n",
);
let resp = call_json(
&client,
"alc_pkg_test",
json!({ "code": lua_code, "auto_search_paths": false }),
)
.await;
assert_eq!(
resp["resolved_search_paths"],
json!([]),
"expected empty resolved_search_paths with auto_search_paths=false, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_auto_search_paths_resolves_alc_local_toml() {
let tmp_home = tempfile::tempdir().expect("tempdir alc_home");
let tmp_project = tempfile::tempdir().expect("tempdir project");
let linked_pkg_dir = tmp_project.path().join("packages").join("local_pkg_e2e");
std::fs::create_dir_all(&linked_pkg_dir).expect("create linked pkg dir");
std::fs::write(linked_pkg_dir.join("init.lua"), "return {}\n").expect("write init.lua");
let local_toml_content = format!(
"[packages.local_pkg_e2e]\npath = \"{}\"\n",
linked_pkg_dir.to_str().expect("utf8 path")
);
std::fs::write(
tmp_project.path().join("alc.local.toml"),
&local_toml_content,
)
.expect("write alc.local.toml");
let client = connect_with_alc_home(tmp_home.path()).await;
let lua_code = concat!(
"local describe, it, expect = lust.describe, lust.it, lust.expect\n",
"describe('local_toml', function()\n",
" it('ok', function() expect(1).to.equal(1) end)\n",
"end)\n",
);
let resp = call_json(
&client,
"alc_pkg_test",
json!({
"code": lua_code,
"project_root": tmp_project.path().to_str().expect("utf8 path"),
}),
)
.await;
assert_eq!(resp["passed"], 1, "expected passed=1, got: {resp}");
let paths = resp["resolved_search_paths"]
.as_array()
.expect("resolved_search_paths must be an array");
let found = paths.iter().any(|row| {
row["name"].as_str() == Some("local_pkg_e2e")
&& row["source"].as_str() == Some("alc.local.toml")
});
assert!(
found,
"expected local_pkg_e2e/alc.local.toml in resolved_search_paths, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_test_pkg_mode_resolves_collection_root() {
let tmp_home = tempfile::tempdir().expect("tempdir alc_home");
let tmp_collection = tempfile::tempdir().expect("tempdir collection");
std::fs::write(
tmp_collection.path().join("alc.toml"),
"[hub]\nname = \"e2e-test-collection\"\n\n[packages]\n",
)
.expect("write alc.toml");
let pkg_dir = tmp_collection.path().join("test_collection_pkg");
let spec_dir = pkg_dir.join("spec");
std::fs::create_dir_all(&spec_dir).expect("create pkg dirs");
std::fs::write(
pkg_dir.join("init.lua"),
"local M = {}\nM.meta = { name = 'test_collection_pkg', version = '0.0.1', description = 'e2e', category = 'test' }\nM.spec = { entries = {} }\nfunction M.run(ctx) return (ctx and ctx.input or '') .. ' ok' end\nreturn M\n",
)
.expect("write init.lua");
std::fs::write(
spec_dir.join("test_collection_pkg_spec.lua"),
"local describe, it, expect = lust.describe, lust.it, lust.expect\ndescribe('collection', function()\n it('discovered', function() expect(1).to.equal(1) end)\nend)\n",
)
.expect("write spec");
let client = connect_with_alc_home(tmp_home.path()).await;
let resp = call_json(
&client,
"alc_pkg_test",
json!({
"pkg": "test_collection_pkg",
"project_root": tmp_collection.path().to_str().expect("utf8 path"),
}),
)
.await;
assert_eq!(
resp["passed"], 1,
"expected passed=1 (package discovered via project_root), got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_env_inject_readable() {
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({
"code": r#"return alc.env.FOO"#,
"ctx": { "env": { "inject": { "FOO": "bar" } } }
}),
)
.await;
assert_eq!(resp["status"], "completed", "unexpected status: {resp}");
assert_eq!(
resp["result"].as_str().unwrap_or(""),
"bar",
"alc.env.FOO should return inject value 'bar', got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_env_readonly_write_errors() {
let client = connect().await;
let result = client
.call_tool(call_params(
"alc_run",
json!({
"code": r#"alc.env.X = 1; return "unreached""#,
"ctx": { "env": { "inject": {} } }
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("readonly") || result.is_error == Some(true),
"expected readonly error, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_env_use_undeclared_returns_nil() {
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({
"code": r#"local e = alc.env:use{"FOO"}; return tostring(e.UNDECLARED)"#,
"ctx": { "env": { "inject": { "FOO": "bar", "UNDECLARED": "leak" } } }
}),
)
.await;
assert_eq!(resp["status"], "completed", "unexpected status: {resp}");
assert_eq!(
resp["result"].as_str().unwrap_or(""),
"nil",
"undeclared key via :use{{}} should return nil, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_env_source_priority_inject_over_dotenv_over_os() {
let dir = tempfile::tempdir().expect("tempdir");
let dotenv_path = dir.path().join(".env");
std::fs::write(&dotenv_path, "KEY=from_dotenv\n").expect("write dotenv");
std::env::set_var("KEY", "from_os");
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({
"code": r#"return alc.env.KEY"#,
"ctx": {
"env": {
"inject": { "KEY": "from_inject" },
"dotenv": dotenv_path.to_str().expect("dotenv path utf8"),
"allow_os": true
}
}
}),
)
.await;
std::env::remove_var("KEY");
assert_eq!(resp["status"], "completed", "unexpected status: {resp}");
assert_eq!(
resp["result"].as_str().unwrap_or(""),
"from_inject",
"inject must shadow dotenv and OS env, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_env_dotenv_file_parse() {
let dir = tempfile::tempdir().expect("tempdir");
let dotenv_path = dir.path().join(".env");
std::fs::write(&dotenv_path, "FOO=bar\nBAZ=\"line1\\nline2\"\n").expect("write dotenv");
let client = connect().await;
let resp = call_json(
&client,
"alc_run",
json!({
"code": r#"return alc.env.FOO .. "|" .. alc.env.BAZ"#,
"ctx": { "env": { "dotenv": dotenv_path.to_str().expect("dotenv path utf8") } }
}),
)
.await;
assert_eq!(resp["status"], "completed", "unexpected status: {resp}");
assert_eq!(
resp["result"].as_str().unwrap_or(""),
"bar|line1\nline2",
"dotenv FOO and BAZ should be parsed correctly, got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_missing_meta_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let source_root = tempfile::tempdir().expect("source tempdir");
let coll_root = source_root.path();
let pkg_src = coll_root.join("e2e_doctor_missing_meta");
std::fs::create_dir_all(&pkg_src).expect("mkdir pkg_src");
std::fs::write(
pkg_src.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_doctor_missing_meta", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_root.to_string_lossy() }),
)
.await;
let installed = tmp.path().join("packages").join("e2e_doctor_missing_meta");
std::fs::write(
installed.join("init.lua"),
r#"local M = {}
M.meta = { version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("rewrite init.lua without name");
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_doctor_missing_meta" }),
)
.await;
let missing_meta = resp["missing_meta"]
.as_array()
.expect("missing_meta array missing");
let entry = missing_meta
.iter()
.find(|e| e["name"] == "e2e_doctor_missing_meta")
.unwrap_or_else(|| {
panic!("e2e_doctor_missing_meta not found in missing_meta, got: {resp}")
});
assert_eq!(entry["kind"], "missing_meta");
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_missing_hub_index_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let project_tmp = tempfile::tempdir().expect("project tempdir");
let project_root = project_tmp.path();
for pkg_name in ["pkg_alpha", "pkg_beta"] {
let pkg_dir = project_root.join(pkg_name);
std::fs::create_dir_all(&pkg_dir).expect("mkdir pkg_dir");
std::fs::write(
pkg_dir.join("init.lua"),
format!(
r#"local M = {{}}
M.meta = {{ name = {pkg_name:?}, version = "0.1.0" }}
return M"#
),
)
.expect("write init.lua");
}
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "project_root": project_root.to_string_lossy() }),
)
.await;
let missing_hub_index = resp["missing_hub_index"]
.as_array()
.expect("missing_hub_index array missing");
assert_eq!(
missing_hub_index.len(),
1,
"expected 1 missing_hub_index entry, got: {resp}"
);
let entry = &missing_hub_index[0];
assert_eq!(entry["kind"], "missing_hub_index");
assert!(
entry["pkg_count"].as_u64().unwrap_or(0) >= 2,
"pkg_count should be >= 2, got: {entry}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_stale_cache_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let cache_dir = tmp.path().join("hub_cache");
std::fs::create_dir_all(&cache_dir).expect("mkdir hub_cache");
let stale_path = cache_dir.join("deadbeef00000000.json");
std::fs::write(&stale_path, r#"{"entries":[]}"#).expect("write cache");
let past = std::time::SystemTime::now() - std::time::Duration::from_secs(7200);
let times = std::fs::FileTimes::new().set_modified(past);
let f = std::fs::OpenOptions::new()
.write(true)
.open(&stale_path)
.expect("open for backdate");
f.set_times(times).expect("set_times");
let client = connect_with_alc_home(tmp.path()).await;
let resp = call_json(&client, "alc_pkg_doctor", json!({})).await;
let stale = resp["stale_cache"]
.as_array()
.expect("stale_cache array missing");
assert_eq!(stale.len(), 1, "expected 1 stale_cache entry: {resp}");
let entry = &stale[0];
assert_eq!(entry["kind"], "stale_cache");
assert!(
entry["path"]
.as_str()
.unwrap_or("")
.ends_with("deadbeef00000000.json"),
"path mismatch: {entry}"
);
assert!(
entry["age_secs"].as_u64().unwrap_or(0) >= 7200,
"age_secs too small: {entry}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_pkg_doctor_spec_missing_detected() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let source_root = tempfile::tempdir().expect("source tempdir");
let coll_root = source_root.path();
let pkg_src = coll_root.join("e2e_doctor_spec_missing");
std::fs::create_dir_all(&pkg_src).expect("mkdir pkg_src");
std::fs::write(
pkg_src.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_doctor_spec_missing", version = "0.1.0" }
function M.run(ctx) return "ok" end
return M"#,
)
.expect("write init.lua");
call_json(
&client,
"alc_pkg_install",
json!({ "url": coll_root.to_string_lossy() }),
)
.await;
let installed = tmp.path().join("packages").join("e2e_doctor_spec_missing");
std::fs::create_dir_all(installed.join("spec")).expect("mkdir spec");
let resp = call_json(
&client,
"alc_pkg_doctor",
json!({ "name": "e2e_doctor_spec_missing" }),
)
.await;
let spec_missing = resp["spec_missing"]
.as_array()
.expect("spec_missing array missing");
let entry = spec_missing
.iter()
.find(|e| e["name"] == "e2e_doctor_spec_missing")
.unwrap_or_else(|| {
panic!("e2e_doctor_spec_missing not found in spec_missing, got: {resp}")
});
assert_eq!(entry["kind"], "spec_missing");
client.cancel().await.expect("cancel failed");
}
async fn connect_with_alc_home_and_env(
alc_home: &std::path::Path,
extra_env: &[(&str, &str)],
) -> rmcp::service::RunningService<rmcp::RoleClient, ()> {
let bin = std::env::var("CARGO_BIN_EXE_alc")
.unwrap_or_else(|_| format!("{}/target/debug/alc", env!("CARGO_MANIFEST_DIR")));
let packages_path = alc_home.join("packages");
let mut cmd = tokio::process::Command::new(bin);
cmd.env("ALC_HOME", alc_home)
.env("ALC_PACKAGES_PATH", &packages_path);
for (k, v) in extra_env {
cmd.env(k, v);
}
let transport = TokioChildProcess::new(cmd).expect("failed to spawn alc server");
().serve(transport)
.await
.expect("failed to initialize MCP session")
}
#[tokio::test]
async fn test_alc_setting_resolve_returns_all_when_target_absent() {
let alc_home_tmp = tempfile::tempdir().expect("alc_home tempdir");
let project_tmp = tempfile::tempdir().expect("project tempdir");
std::fs::write(
alc_home_tmp.path().join("config.toml"),
"[setting.journal]\npath = \"/global/j.md\"\n\n[setting.advisor]\npath = \"/global/a.md\"\n",
)
.expect("write config.toml");
std::fs::write(
project_tmp.path().join("alc.toml"),
"[setting.journal]\npath = \"/project/j.md\"\n",
)
.expect("write alc.toml");
let client = connect_with_alc_home_and_env(
alc_home_tmp.path(),
&[
("ALC_SETTING_JOURNAL_PKG", "true"),
(
"ALC_PROJECT_ROOT",
project_tmp.path().to_str().expect("project root utf8"),
),
],
)
.await;
let resp = call_json(&client, "alc_setting_resolve", json!({})).await;
assert!(
resp.get("resolved").is_some(),
"response must contain 'resolved', got: {resp}"
);
assert!(
resp.get("sources").is_some(),
"response must contain 'sources', got: {resp}"
);
assert_eq!(
resp["resolved"]["journal"]["path"].as_str(),
Some("/project/j.md"),
"project should win for journal.path, got: {resp}"
);
assert_eq!(
resp["resolved"]["journal"]["pkg"].as_bool(),
Some(true),
"env should add journal.pkg=true, got: {resp}"
);
assert_eq!(
resp["resolved"]["advisor"]["path"].as_str(),
Some("/global/a.md"),
"global should provide advisor.path, got: {resp}"
);
assert_eq!(
resp["sources"]["journal"]["path"].as_str(),
Some("project"),
"journal.path source should be 'project', got: {resp}"
);
assert_eq!(
resp["sources"]["journal"]["pkg"].as_str(),
Some("env"),
"journal.pkg source should be 'env', got: {resp}"
);
assert_eq!(
resp["sources"]["advisor"]["path"].as_str(),
Some("global"),
"advisor.path source should be 'global', got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_setting_resolve_target_filter() {
let alc_home_tmp = tempfile::tempdir().expect("alc_home tempdir");
std::fs::write(
alc_home_tmp.path().join("config.toml"),
"[setting.journal]\npath = \"/global/j.md\"\n\n[setting.advisor]\nmodel = \"gpt4\"\n",
)
.expect("write config.toml");
let client = connect_with_alc_home(alc_home_tmp.path()).await;
let resp = call_json(
&client,
"alc_setting_resolve",
json!({ "target": "journal" }),
)
.await;
assert!(
resp["resolved"].get("journal").is_some(),
"resolved should contain 'journal', got: {resp}"
);
assert!(
resp["resolved"].get("advisor").is_none(),
"resolved must NOT contain 'advisor' when filtering for 'journal', got: {resp}"
);
assert!(
resp["sources"].get("journal").is_some(),
"sources should contain 'journal', got: {resp}"
);
assert!(
resp["sources"].get("advisor").is_none(),
"sources must NOT contain 'advisor' when filtering for 'journal', got: {resp}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_setting_resolve_rejects_invalid_target_name() {
let tmp = tempfile::tempdir().expect("tempdir");
let client = connect_with_alc_home(tmp.path()).await;
let result = client
.call_tool(call_params(
"alc_setting_resolve",
json!({ "target": "Invalid-Name" }),
))
.await
.expect("call_tool should not fail at transport level");
let is_error = result.is_error.unwrap_or(false);
let text = extract_text(&result);
assert!(
is_error,
"expected is_error=true for invalid target name, got is_error={is_error:?}, text: {text}"
);
assert!(
text.contains("invalid characters"),
"error message should mention 'invalid characters', got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn e2e_vm_eval_detects_runnable_package() {
let alc_home = tempfile::tempdir().unwrap();
pre_install_pkg(
alc_home.path(),
"vm_eval_runnable_pkg",
&[(
"init.lua",
concat!(
"local M = {}\n",
"M.meta = { name = \"vm_eval_runnable_pkg\", version = \"0.1.0\" }\n",
"function M.run(ctx) return {} end\n",
"return M\n",
),
)],
);
let client = connect_with_alc_home(alc_home.path()).await;
let result = call_json(
&client,
"alc_pkg_list",
serde_json::json!({ "limit": 0, "fields": ["name", "type"] }),
)
.await;
let packages = result["packages"]
.as_array()
.expect("packages must be an array");
let entry = packages
.iter()
.find(|p| p["name"] == "vm_eval_runnable_pkg")
.unwrap_or_else(|| panic!("vm_eval_runnable_pkg not found, got: {result}"));
assert_eq!(
entry["type"], "runnable",
"M.run defined => type must be 'runnable', got: {entry}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn e2e_vm_eval_library_rejected_by_alc_advice() {
let alc_home = tempfile::tempdir().unwrap();
pre_install_pkg(
alc_home.path(),
"vm_eval_library_reject",
&[(
"init.lua",
concat!(
"local M = {}\n",
"M.meta = { name = \"vm_eval_library_reject\", version = \"0.1.0\" }\n",
"return M\n",
),
)],
);
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_advice",
serde_json::json!({ "strategy": "vm_eval_library_reject", "task": "test" }),
))
.await
.expect("call_tool must not fail at transport level");
let text = extract_text(&result);
assert!(
text.contains("library") || text.contains("vm_eval_library_reject"),
"error message must name the pkg or mention 'library', got: {text}"
);
client.cancel().await.expect("cancel failed");
}
async fn setup_card_publish_fixture() -> (tempfile::TempDir, tempfile::TempDir, String, String) {
let alc_home = tempfile::tempdir().expect("alc_home tempdir");
let bare = tempfile::tempdir().expect("bare repo tempdir");
let out = tokio::process::Command::new("git")
.args(["init", "--bare"])
.arg(bare.path())
.output()
.await
.expect("git init --bare failed");
assert!(
out.status.success(),
"git init --bare failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let card_id = "test_card_001";
let cards_dir = alc_home.path().join("cards").join("test_pkg");
std::fs::create_dir_all(&cards_dir).expect("create cards dir");
let card_toml = format!(
r#"schema_version = "card/v0"
card_id = "{card_id}"
created_at = "2026-06-12T00:00:00Z"
[pkg]
name = "test_pkg"
"#
);
std::fs::write(cards_dir.join(format!("{card_id}.toml")), card_toml).expect("write card toml");
let target_url = format!("file://{}", bare.path().display());
(alc_home, bare, card_id.to_string(), target_url)
}
#[tokio::test]
async fn test_alc_card_publish_happy_path() {
let (alc_home, _bare, card_id, target_url) = setup_card_publish_fixture().await;
let client = connect_with_alc_home(alc_home.path()).await;
let resp = call_json(
&client,
"alc_card_publish",
json!({
"card_id": card_id,
"target_repo": target_url,
}),
)
.await;
assert!(
resp.get("published_url").and_then(|v| v.as_str()).is_some(),
"expected published_url in response, got: {resp}"
);
assert!(
resp.get("commit_hash")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false),
"expected non-empty commit_hash in response, got: {resp}"
);
let reindex = resp
.get("reindex_status")
.expect("reindex_status missing from response");
assert_eq!(
reindex.get("ok").and_then(|v| v.as_bool()),
Some(true),
"expected reindex_status.ok == true, got: {reindex}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_card_publish_push_failure_typed_error() {
let alc_home = tempfile::tempdir().expect("alc_home tempdir");
let card_id = "test_card_push_fail";
let cards_dir = alc_home.path().join("cards").join("test_pkg");
std::fs::create_dir_all(&cards_dir).expect("create cards dir");
let card_toml = format!(
r#"schema_version = "card/v0"
card_id = "{card_id}"
created_at = "2026-06-12T00:00:00Z"
[pkg]
name = "test_pkg"
"#
);
std::fs::write(cards_dir.join(format!("{card_id}.toml")), card_toml).expect("write card toml");
let bad_url = "file:///nonexistent/path/to/repo";
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_card_publish",
json!({
"card_id": card_id,
"target_repo": bad_url,
}),
))
.await
.expect("call_tool failed");
assert!(
result.is_error.unwrap_or(false),
"expected isError=true for push failure, got: {:?}",
result
);
let text = extract_text(&result);
assert!(
!text.is_empty(),
"error text must not be empty for push failure"
);
assert!(
!text.contains("published_url"),
"push failure must not return a success object, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_card_publish_reindex_failure_isolated() {
let (alc_home, _bare, card_id, target_url) = setup_card_publish_fixture().await;
std::fs::write(
alc_home.path().join("installed.json"),
b"{not valid json{{{",
)
.expect("write corrupt installed.json");
let client = connect_with_alc_home(alc_home.path()).await;
let resp = call_json(
&client,
"alc_card_publish",
json!({
"card_id": card_id,
"target_repo": target_url,
}),
)
.await;
assert!(
resp.get("published_url").and_then(|v| v.as_str()).is_some(),
"published_url must be present even when reindex fails, got: {resp}"
);
assert!(
resp.get("commit_hash")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false),
"commit_hash must be present even when reindex fails, got: {resp}"
);
let reindex = resp
.get("reindex_status")
.expect("reindex_status missing from response");
assert_eq!(
reindex.get("ok").and_then(|v| v.as_bool()),
Some(false),
"expected reindex_status.ok == false when installed.json is corrupt, got: {reindex}"
);
assert!(
reindex
.get("error")
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false),
"expected non-empty reindex_status.error, got: {reindex}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_advice_resolves_variant_scope_package() {
let alc_home = tempfile::tempdir().expect("tempdir alc_home");
let client = connect_with_alc_home(alc_home.path()).await;
let project_tmp = tempfile::tempdir().expect("tempdir project");
let project_root = project_tmp.path();
std::fs::write(project_root.join("alc.toml"), "[packages]\n").expect("write alc.toml");
let pkg_src = project_tmp.path().join("variant_strategy_src");
let pkg_dir = pkg_src.join("e2e_variant_strategy");
std::fs::create_dir_all(&pkg_dir).expect("mkdir pkg_dir");
std::fs::write(
pkg_dir.join("init.lua"),
r#"local M = {}
M.meta = { name = "e2e_variant_strategy", version = "0.1.0", type = "runnable" }
M.run = function(ctx) return { answer = "variant-ok" } end
return M"#,
)
.expect("write init.lua");
let link_resp = call_json(
&client,
"alc_pkg_link",
json!({
"path": pkg_src.to_string_lossy(),
"scope": "variant",
"project_root": project_root.to_string_lossy(),
}),
)
.await;
assert!(
link_resp.get("error").is_none(),
"alc_pkg_link should succeed, got: {link_resp}"
);
let result = client
.call_tool(call_params(
"alc_advice",
json!({
"strategy": "e2e_variant_strategy",
"task": "ping",
"project_root": project_root.to_string_lossy(),
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
!text.contains("not found after installing bundled collection"),
"variant-linked strategy should not produce a not-found error, got: {text}"
);
assert!(
text.contains("variant-ok"),
"variant-linked strategy should execute and return variant-ok marker, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_advice_returns_not_found_when_all_tiers_absent() {
let alc_home = tempfile::tempdir().expect("tempdir alc_home");
let client = connect_with_alc_home(alc_home.path()).await;
let project_tmp = tempfile::tempdir().expect("tempdir project");
let project_root = project_tmp.path();
std::fs::write(project_root.join("alc.toml"), "[packages]\n").expect("write alc.toml");
let result = client
.call_tool(call_params(
"alc_advice",
json!({
"strategy": "nonexistent_pkg_xyz_e2e",
"project_root": project_root.to_string_lossy(),
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
assert!(
text.contains("not found after installing bundled collection"),
"expected not-found error when all tiers lack the package, got: {text}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_list_happy_path() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
let key = "test-task";
let ns_dir = alc_home.path().join("state").join(ns);
std::fs::create_dir_all(&ns_dir).expect("create state ns dir");
let fixture = serde_json::json!({
"_token_value": "test-token",
"data": {
"completed_steps": ["step_a", "step_b", "step_c"],
"field_x": "value_x",
"field_y": "value_y"
},
"identity": {"task_id": "test-task"}
});
std::fs::write(
ns_dir.join(format!("{key}.json")),
serde_json::to_string(&fixture).unwrap(),
)
.expect("write fixture");
let client = connect_with_alc_home(alc_home.path()).await;
let v = call_json(&client, "alc_state_list", json!({"namespace": ns})).await;
let keys = v["keys"]
.as_array()
.expect("response must have a 'keys' array");
assert!(
keys.iter().any(|k| k.as_str() == Some(key)),
"expected key '{key}' in keys array, got: {keys:?}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_list_unsafe_segment() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_state_list",
json!({"namespace": "../escape"}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let err: serde_json::Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("typed error must be JSON: {e}\nraw: {text}"));
assert_eq!(
err["error"].as_str(),
Some("UNSAFE_SEGMENT"),
"expected UNSAFE_SEGMENT error code, got: {err}"
);
assert_eq!(
err["which"].as_str(),
Some("namespace"),
"expected which=namespace, got: {err}"
);
assert_eq!(
err["value"].as_str(),
Some("../escape"),
"expected value=../escape, got: {err}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_show_happy_path() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
let key = "test-task";
let ns_dir = alc_home.path().join("state").join(ns);
std::fs::create_dir_all(&ns_dir).expect("create state ns dir");
let fixture = serde_json::json!({
"_token_value": "test-token",
"data": {
"completed_steps": ["step_a", "step_b", "step_c"],
"field_x": "value_x",
"field_y": "value_y"
},
"identity": {"task_id": "test-task"}
});
std::fs::write(
ns_dir.join(format!("{key}.json")),
serde_json::to_string(&fixture).unwrap(),
)
.expect("write fixture");
let client = connect_with_alc_home(alc_home.path()).await;
let v = call_json(
&client,
"alc_state_show",
json!({"namespace": ns, "key": key}),
)
.await;
assert_eq!(
v["_token_value"].as_str(),
Some("test-token"),
"expected _token_value=test-token, got: {v}"
);
assert_eq!(
v["identity"]["task_id"].as_str(),
Some("test-task"),
"expected identity.task_id=test-task, got: {v}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_show_not_found() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
std::fs::create_dir_all(alc_home.path().join("state").join(ns)).expect("create state ns dir");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_state_show",
json!({"namespace": ns, "key": "nonexistent"}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let err_json: serde_json::Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("typed error must be JSON: {e}\nraw: {text}"));
assert_eq!(
err_json["error"].as_str(),
Some("NOT_FOUND"),
"expected NOT_FOUND typed code, got: {err_json}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_reset_happy_path() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
let key = "test-task";
let ns_dir = alc_home.path().join("state").join(ns);
std::fs::create_dir_all(&ns_dir).expect("create state ns dir");
let fixture = serde_json::json!({
"_token_value": "test-token",
"data": {
"completed_steps": ["step_a", "step_b", "step_c"],
"field_x": "value_x",
"field_y": "value_y"
},
"identity": {"task_id": "test-task"}
});
std::fs::write(
ns_dir.join(format!("{key}.json")),
serde_json::to_string(&fixture).unwrap(),
)
.expect("write fixture");
let client = connect_with_alc_home(alc_home.path()).await;
let v = call_json(
&client,
"alc_state_reset",
json!({
"namespace": ns,
"key": key,
"steps": ["step_a", "step_b"],
"fields": ["field_x"]
}),
)
.await;
assert_eq!(v["ok"].as_bool(), Some(true), "expected ok=true, got: {v}");
assert_eq!(
v["steps_removed"].as_u64(),
Some(2),
"expected steps_removed=2, got: {v}"
);
assert_eq!(
v["fields_removed"].as_u64(),
Some(1),
"expected fields_removed=1, got: {v}"
);
assert_eq!(
v["steps_input"],
json!(["step_a", "step_b"]),
"expected steps_input=[step_a,step_b], got: {v}"
);
assert_eq!(
v["fields_input"],
json!(["field_x"]),
"expected fields_input=[field_x], got: {v}"
);
let bak = v["backup_path"]
.as_str()
.expect("backup_path must be a string");
assert!(
bak.ends_with(".json.bak"),
"backup_path must end with .json.bak, got: {bak}"
);
assert!(
std::path::Path::new(bak).exists(),
".bak file must exist on disk: {bak}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_reset_not_found() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
std::fs::create_dir_all(alc_home.path().join("state").join(ns)).expect("create state ns dir");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_state_reset",
json!({
"namespace": ns,
"key": "nonexistent",
"steps": [],
"fields": []
}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let err_json: serde_json::Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("typed error must be JSON: {e}\nraw: {text}"));
assert_eq!(
err_json["error"].as_str(),
Some("NOT_FOUND"),
"expected NOT_FOUND typed code, got: {err_json}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_set_happy_path() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
let key = "set-task";
let client = connect_with_alc_home(alc_home.path()).await;
let v = call_json(
&client,
"alc_state_set",
json!({"namespace": ns, "key": key, "value": {"data": {"score": 42}}}),
)
.await;
assert_eq!(v["ok"].as_bool(), Some(true), "expected ok=true, got: {v}");
let shown = call_json(
&client,
"alc_state_show",
json!({"namespace": ns, "key": key}),
)
.await;
assert_eq!(
shown["data"]["score"].as_u64(),
Some(42),
"expected data.score=42 after set, got: {shown}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_set_wrong_namespace_type_errors() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let client = connect_with_alc_home(alc_home.path()).await;
let outcome = client
.call_tool(call_params(
"alc_state_set",
json!({"namespace": 42, "key": "k", "value": {"x": 1}}),
))
.await;
match outcome {
Err(_) => {
}
Ok(result) => {
let text = extract_text(&result);
let parsed: Option<serde_json::Value> = serde_json::from_str(text).ok();
assert!(
parsed.as_ref().and_then(|v| v.get("ok")).is_none(),
"alc_state_set with wrong namespace type must not return ok=true, got: {text}"
);
}
}
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_delete_happy_path() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let ns = "orch";
let key = "del-task";
let ns_dir = alc_home.path().join("state").join(ns);
std::fs::create_dir_all(&ns_dir).expect("create state ns dir");
let fixture =
serde_json::json!({"_token_value": "tok", "data": {}, "identity": {"task_id": key}});
std::fs::write(
ns_dir.join(format!("{key}.json")),
serde_json::to_string(&fixture).unwrap(),
)
.expect("write fixture");
let client = connect_with_alc_home(alc_home.path()).await;
let v1 = call_json(
&client,
"alc_state_delete",
json!({"namespace": ns, "key": key}),
)
.await;
assert_eq!(
v1["ok"].as_bool(),
Some(true),
"expected ok=true, got: {v1}"
);
assert_eq!(
v1["existed"].as_bool(),
Some(true),
"expected existed=true on first delete, got: {v1}"
);
let v2 = call_json(
&client,
"alc_state_delete",
json!({"namespace": ns, "key": key}),
)
.await;
assert_eq!(
v2["ok"].as_bool(),
Some(true),
"expected ok=true, got: {v2}"
);
assert_eq!(
v2["existed"].as_bool(),
Some(false),
"expected existed=false on second delete, got: {v2}"
);
client.cancel().await.expect("cancel failed");
}
#[tokio::test]
async fn test_alc_state_delete_unsafe_segment() {
let alc_home = tempfile::TempDir::new().expect("tempdir");
let client = connect_with_alc_home(alc_home.path()).await;
let result = client
.call_tool(call_params(
"alc_state_delete",
json!({"namespace": "orch", "key": "../escape"}),
))
.await
.expect("call_tool failed");
let text = extract_text(&result);
let err: serde_json::Value = serde_json::from_str(text)
.unwrap_or_else(|e| panic!("typed error must be JSON: {e}\nraw: {text}"));
assert_eq!(
err["error"].as_str(),
Some("UNSAFE_SEGMENT"),
"expected UNSAFE_SEGMENT error code, got: {err}"
);
assert_eq!(
err["which"].as_str(),
Some("key"),
"expected which=key, got: {err}"
);
client.cancel().await.expect("cancel failed");
}