use std::path::PathBuf;
use khive_runtime::{KhiveRuntime, Namespace, RuntimeConfig};
use kkernel::exec::{run_exec, ExecArgs};
use serial_test::serial;
struct EnvGuard {
prev_home: Option<std::ffi::OsString>,
prev_cwd: PathBuf,
}
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"),
}
let _ = std::env::set_current_dir(&self.prev_cwd);
}
}
fn base_args(db: &str, actor: Option<&str>, ops: &str, save_file: &str) -> ExecArgs {
ExecArgs {
plan: false,
ops: Some(ops.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("verbose".to_string()),
output_format: None,
verbose: false,
save_file: Some(save_file.to_string()),
ops_file: None,
dry_run: false,
serial: false,
atomic: false,
atomic_max_ops: None,
strict: true,
}
}
fn contents_from_save_file(path: &std::path::Path) -> Vec<String> {
let raw = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("read save-file {}: {e}", path.display()));
let line = raw
.lines()
.next()
.unwrap_or_else(|| panic!("save-file {} has no result line", path.display()));
let row: serde_json::Value = serde_json::from_str(line).expect("parse result row json");
assert_eq!(row["ok"], true, "list op must have succeeded: {row}");
let items = row["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("list result must contain an items array: {row}"))
.clone();
items
.iter()
.filter_map(|item| item.get("content").and_then(|v| v.as_str()))
.map(str::to_string)
.collect()
}
#[tokio::test]
#[serial]
async fn actor_pin_default_read_exposes_only_local_and_pinned_records() {
let prev_home = std::env::var_os("HOME");
let prev_cwd = std::env::current_dir().expect("read current cwd");
let guard = EnvGuard {
prev_home,
prev_cwd,
};
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::set_var("KHIVE_NO_DAEMON", "1");
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("actor-pin-default-read.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 seed_runtime = KhiveRuntime::new(RuntimeConfig {
db_path: Some(db_path.clone()),
packs: vec!["kg".to_string()],
..RuntimeConfig::no_embeddings()
})
.expect("build model-less seed runtime");
for (namespace, content) in [
("local", "local-record"),
("lambda:fallback", "fallback-record"),
("lambda:pinned", "pinned-record"),
] {
let token = seed_runtime
.authorize(Namespace::parse(namespace).expect("valid seed namespace"))
.expect("authorize seed namespace");
seed_runtime
.create_note(&token, "observation", None, content, None, None, vec![])
.await
.expect("seed observation without embedding");
}
drop(seed_runtime);
let pinned_save = project_dir.path().join("pinned-read.jsonl");
let pinned_args = base_args(
&db_str,
Some("lambda:pinned"),
r#"list(kind="observation")"#,
pinned_save.to_str().expect("utf8 save path"),
);
let pinned_result = run_exec(pinned_args).await;
assert!(
pinned_result.is_ok(),
"default read while pinned to lambda:pinned must succeed: {pinned_result:?}"
);
let pinned_contents = contents_from_save_file(&pinned_save);
assert!(
pinned_contents.iter().any(|c| c == "local-record"),
"--actor lambda:pinned default read must include the local record: {pinned_contents:?}"
);
assert!(
pinned_contents.iter().any(|c| c == "pinned-record"),
"--actor lambda:pinned default read must include the pinned actor's own record: \
{pinned_contents:?}"
);
assert!(
!pinned_contents.iter().any(|c| c == "fallback-record"),
"--actor lambda:pinned default read must NOT expose the displaced project-config \
fallback actor's record: {pinned_contents:?}"
);
assert_eq!(
pinned_contents.len(),
2,
"--actor lambda:pinned default read must expose exactly local + pinned, nothing else: \
{pinned_contents:?}"
);
let local_save = project_dir.path().join("local-read.jsonl");
let local_args = base_args(
&db_str,
Some("local"),
r#"list(kind="observation")"#,
local_save.to_str().expect("utf8 save path"),
);
let local_result = run_exec(local_args).await;
assert!(
local_result.is_ok(),
"default read while pinned to local must succeed: {local_result:?}"
);
let local_contents = contents_from_save_file(&local_save);
assert_eq!(
local_contents,
vec!["local-record".to_string()],
"--actor local default read must expose only the local record, retaining neither the \
displaced fallback actor nor the previously-pinned actor: {local_contents:?}"
);
drop(guard);
}