Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
106 lines
3.6 KiB
Rust
106 lines
3.6 KiB
Rust
//! Integration test — `tools/call` enforces the 60s timeout (MISS-4).
|
|
//!
|
|
//! Strategy: write a fake binary that sleeps significantly longer than the
|
|
//! cap. Use `tokio::test(start_paused = true)` so virtual time can be
|
|
//! advanced past the 60s deadline without the test thread blocking. The
|
|
//! real child is reaped by `kill_on_drop(true)` set in `spawn_and_collect`.
|
|
//!
|
|
//! Real wall-clock spent on this test: a couple of milliseconds — the
|
|
//! `sleep 90` child is spawned but killed before it finishes its first
|
|
//! second.
|
|
|
|
#![cfg(unix)]
|
|
|
|
use kei_mcp::{dispatch, JsonRpcRequest, ServerContext};
|
|
use serde_json::json;
|
|
use std::fs;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
fn write_sleeping_binary(bin_dir: &Path, crate_name: &str) {
|
|
fs::create_dir_all(bin_dir).unwrap();
|
|
let path = bin_dir.join(crate_name);
|
|
// 90s sleep — well past the 60s cap. `kill_on_drop(true)` reaps it.
|
|
let script = "#!/bin/sh\nsleep 90\necho '{\"never\":\"reached\"}'\n";
|
|
fs::write(&path, script).unwrap();
|
|
let mut perms = fs::metadata(&path).unwrap().permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(&path, perms).unwrap();
|
|
}
|
|
|
|
fn write_atom(root: &Path, crate_name: &str, verb: &str) {
|
|
let atoms = root.join(crate_name).join("atoms");
|
|
let schemas = atoms.join("schemas");
|
|
fs::create_dir_all(&schemas).unwrap();
|
|
fs::write(schemas.join(format!("{verb}-input.json")), "{}").unwrap();
|
|
let md = format!(
|
|
r#"---
|
|
atom: {crate_name}::{verb}
|
|
kind: query
|
|
version: "0.1.0"
|
|
input:
|
|
schema: schemas/{verb}-input.json
|
|
side_effects: []
|
|
idempotent: true
|
|
stability: stable
|
|
---
|
|
|
|
# {crate_name}::{verb}
|
|
|
|
Sleeps forever.
|
|
"#
|
|
);
|
|
fs::write(atoms.join(format!("{verb}.md")), md).unwrap();
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn tools_call_returns_internal_error_on_timeout() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let atoms_root = tmp.path().join("atoms-root");
|
|
let bin_dir = tmp.path().join("bin");
|
|
let skills = tmp.path().join("skills");
|
|
fs::create_dir_all(&skills).unwrap();
|
|
write_atom(&atoms_root, "kei-snore", "wait");
|
|
write_sleeping_binary(&bin_dir, "kei-snore");
|
|
|
|
std::env::set_var("KEI_RUNTIME_BIN_DIR", &bin_dir);
|
|
let ctx = ServerContext::new(atoms_root, skills);
|
|
let req = JsonRpcRequest {
|
|
jsonrpc: "2.0".into(),
|
|
id: Some(json!(99)),
|
|
method: "tools/call".into(),
|
|
params: Some(json!({
|
|
"name": "kei-snore::wait",
|
|
"arguments": {}
|
|
})),
|
|
};
|
|
|
|
// Drive dispatch and virtual-time advance concurrently. With
|
|
// `start_paused = true` the `tokio::time::timeout` inside
|
|
// `invoke_atom` only fires once virtual time crosses 60s. We
|
|
// interleave an `advance` step into the same task so the test
|
|
// does not need `tokio::spawn` (which would require `'static`).
|
|
let dispatch_fut = dispatch(req, &ctx);
|
|
let advance_fut = async {
|
|
tokio::task::yield_now().await;
|
|
tokio::time::advance(Duration::from_secs(61)).await;
|
|
// Park forever — the select picks the dispatch outcome.
|
|
std::future::pending::<()>().await;
|
|
};
|
|
let resp = tokio::select! {
|
|
r = dispatch_fut => r,
|
|
_ = advance_fut => panic!("advance branch should never resolve"),
|
|
};
|
|
|
|
std::env::remove_var("KEI_RUNTIME_BIN_DIR");
|
|
|
|
assert!(resp.result.is_none(), "result must be unset on timeout");
|
|
let e = resp.error.expect("error must be set");
|
|
assert_eq!(e.code, -32603, "INTERNAL_ERROR per spec");
|
|
assert!(
|
|
e.message.contains("atom timeout"),
|
|
"expected 'atom timeout' in message, got: {}",
|
|
e.message
|
|
);
|
|
}
|