#![cfg(unix)]
use std::path::PathBuf;
use khive_runtime::daemon::{
read_frame, write_frame, DaemonRequestFrame, DaemonResponseFrame, PROTOCOL_VERSION,
};
use kkernel::exec::{run_exec, ExecArgs};
use serial_test::serial;
use tokio::net::UnixListener;
struct EnvGuard {
prev_home: Option<std::ffi::OsString>,
prev_cwd: PathBuf,
prev_socket: Option<std::ffi::OsString>,
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.prev_home {
Some(v) => std::env::set_var("HOME", v),
None => std::env::remove_var("HOME"),
}
match &self.prev_socket {
Some(v) => std::env::set_var("KHIVE_SOCKET", v),
None => std::env::remove_var("KHIVE_SOCKET"),
}
let _ = std::env::set_current_dir(&self.prev_cwd);
}
}
fn base_args(db: &str, actor: Option<&str>) -> ExecArgs {
ExecArgs {
plan: false,
ops: Some("stats()".to_string()),
pending_events: false,
db: Some(db.to_string()),
config: None,
namespace: "local".to_string(),
actor: actor.map(str::to_string),
expect_actor: None,
presentation: Some("agent".to_string()),
output_format: None,
verbose: false,
save_file: None,
ops_file: None,
dry_run: false,
serial: false,
atomic: false,
atomic_max_ops: None,
strict: false,
}
}
async fn accept_and_reply(listener: &UnixListener) -> DaemonRequestFrame {
let (mut stream, _) = listener
.accept()
.await
.expect("accept fake-daemon connection");
let req_bytes = read_frame(&mut stream)
.await
.expect("read request frame from run_exec's daemon-forward seam");
let frame: DaemonRequestFrame =
serde_json::from_slice(&req_bytes).expect("decode captured request frame");
let response = DaemonResponseFrame {
ok: true,
result: Some("{}".to_string()),
error: None,
error_detail: None,
namespace_mismatch: false,
config_mismatch: false,
served_config_id: Some(frame.config_id.clone()),
version_mismatch: false,
daemon_protocol_version: PROTOCOL_VERSION,
metrics: None,
request_id: None,
};
let payload = serde_json::to_vec(&response).expect("serialize fake-daemon response");
write_frame(&mut stream, &payload)
.await
.expect("write fake-daemon response");
frame
}
#[tokio::test]
#[serial]
async fn daemon_forward_frame_reflects_actor_pin_not_displaced_fallback() {
let prev_home = std::env::var_os("HOME");
let prev_cwd = std::env::current_dir().expect("read current cwd");
let prev_socket = std::env::var_os("KHIVE_SOCKET");
let guard = EnvGuard {
prev_home,
prev_cwd,
prev_socket,
};
std::env::remove_var("KHIVE_DB");
std::env::remove_var("KHIVE_CONFIG");
std::env::remove_var("KHIVE_EMBEDDING_MODEL");
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
std::env::remove_var("KHIVE_ACTOR");
std::env::remove_var("KHIVE_REQUIRE_ATTRIBUTED_ACTOR");
std::env::remove_var("KHIVE_BRAIN_PROFILE");
std::env::remove_var("KHIVE_OUTPUT_FORMAT");
std::env::set_var("KHIVE_PACKS", "kg");
std::env::remove_var("KHIVE_NO_DAEMON");
let home_dir = tempfile::tempdir().expect("tempdir for isolated HOME");
std::env::set_var("HOME", home_dir.path());
let project_dir = tempfile::tempdir().expect("tempdir for isolated project cwd");
let khive_dir = project_dir.path().join(".khive");
std::fs::create_dir_all(&khive_dir).expect("mkdir project .khive");
std::fs::write(
khive_dir.join("config.toml"),
r#"
[actor]
id = "lambda:fallback"
"#,
)
.expect("write config.toml");
let db_path = khive_dir.join("frame-identity-test.db");
let db_str = db_path.to_str().expect("utf8 db path").to_string();
std::env::set_current_dir(project_dir.path()).expect("chdir into isolated project dir");
let sock_dir = tempfile::tempdir().expect("tempdir for fake daemon socket");
let sock = sock_dir.path().join("khived.sock");
std::env::set_var("KHIVE_SOCKET", &sock);
let listener = UnixListener::bind(&sock).expect("bind fake-daemon socket");
let (unpinned_frame, unpinned_result) = tokio::join!(
accept_and_reply(&listener),
run_exec(base_args(&db_str, None))
);
assert!(
unpinned_result.is_ok(),
"unpinned dispatch must succeed: {unpinned_result:?}"
);
assert_eq!(
unpinned_frame.actor_id.as_deref(),
Some("lambda:fallback"),
"the pre-pin resolved actor must come from the DB-anchored fixture config.toml, \
not an ambient CWD/HOME config: {:?}",
unpinned_frame.actor_id
);
assert!(
unpinned_frame
.visible_namespaces
.iter()
.any(|ns| ns == "lambda:fallback"),
"the pre-pin resolved frame must fold the fixture actor into visible_namespaces: {:?}",
unpinned_frame.visible_namespaces
);
let (pinned_frame, pinned_result) = tokio::join!(
accept_and_reply(&listener),
run_exec(base_args(&db_str, Some("lambda:pinned")))
);
assert!(
pinned_result.is_ok(),
"pinned dispatch must succeed: {pinned_result:?}"
);
assert_eq!(
pinned_frame.actor_id.as_deref(),
Some("lambda:pinned"),
"the forwarded frame's actor_id must be the pin, not the project-config fallback"
);
assert!(
pinned_frame
.visible_namespaces
.iter()
.any(|ns| ns == "lambda:pinned"),
"the forwarded frame's visible_namespaces must include the pinned actor: {:?}",
pinned_frame.visible_namespaces
);
assert!(
!pinned_frame
.visible_namespaces
.iter()
.any(|ns| ns == "lambda:fallback"),
"the forwarded frame's visible_namespaces must NOT retain the displaced fallback \
actor: {:?}",
pinned_frame.visible_namespaces
);
let (local_frame, local_result) = tokio::join!(
accept_and_reply(&listener),
run_exec(base_args(&db_str, Some("local")))
);
assert!(
local_result.is_ok(),
"local-pin dispatch must succeed: {local_result:?}"
);
assert_eq!(
local_frame.actor_id, None,
"the forwarded frame's actor_id must be cleared under a local pin"
);
assert!(
local_frame.visible_namespaces.is_empty(),
"the forwarded frame's visible_namespaces must be empty under a local pin, retaining \
neither the fallback actor nor the previous pin: {:?}",
local_frame.visible_namespaces
);
drop(listener);
drop(guard);
}