#![cfg(feature = "store")]
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use futures::executor::block_on;
use ikigai_engine::{Action, Engine};
fn fixture_home() -> &'static Path {
static HOME: OnceLock<PathBuf> = OnceLock::new();
HOME.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("ikigai-cli-ledger-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("config/ikigai")).expect("config home");
std::fs::create_dir_all(dir.join("workspace")).expect("workspace");
std::env::set_var("HOME", &dir);
std::env::set_var("XDG_CONFIG_HOME", dir.join("config"));
ikigai_embedded::set_file_root(dir.join("workspace"));
ikigai_embedded::store::set_store_path(dir.join("store"));
dir
})
.as_path()
}
fn engine() -> Engine {
fixture_home();
Engine::new(ikigai_embedded::kernel())
}
fn line(engine: &Engine, input: &str) -> String {
match block_on(engine.eval_async(input)) {
Action::Output(entry) => entry
.result
.unwrap_or_else(|e| panic!("`{input}` failed: {e}")),
_ => panic!("`{input}` produced a REPL control action rather than output"),
}
}
fn refused(engine: &Engine, input: &str) -> String {
match block_on(engine.eval_async(input)) {
Action::Output(entry) => match entry.result {
Err(e) => e,
Ok(out) => panic!("`{input}` was expected to fail and printed:\n{out}"),
},
_ => panic!("`{input}` produced a REPL control action rather than output"),
}
}
#[test]
fn the_five_verbs_work_as_typed_lines() {
let engine = engine();
let filed = line(
&engine,
"sink urn:iki:ledger:smoke:append Bind the ledger into the embedded host",
);
assert!(filed.starts_with("smoke#1 "), "{filed}");
assert!(
filed.contains("urn:iki:ledger:smoke:item:"),
"an item is minted at its canonical IRI: {filed}"
);
let second = line(
&engine,
"sink urn:iki:ledger:smoke:append priority=0 labels=cli Say what to type",
);
assert!(second.starts_with("smoke#2 "), "{second}");
let list = line(&engine, "source urn:iki:ledger:smoke:items");
assert!(
list.contains("Bind the ledger into the embedded host"),
"{list}"
);
assert!(list.contains("Say what to type"), "{list}");
assert!(list.contains("2 item(s)"), "{list}");
let commented = line(
&engine,
"sink urn:iki:ledger:smoke:comment item=#1 author=brian It resolves from a one-shot",
);
assert!(commented.starts_with("commented on smoke#1"), "{commented}");
let item = line(&engine, "source urn:iki:ledger:smoke:item:1");
assert!(
item.contains("It resolves from a one-shot"),
"the comment is on the item: {item}"
);
let next = line(&engine, "source urn:iki:ledger:smoke:next");
assert!(next.contains("Say what to type"), "{next}");
assert!(
next.contains("policy:"),
"the ranking says what ranked it: {next}"
);
let closed = line(
&engine,
"sink urn:iki:ledger:smoke:close item=#2 reason=done author=brian Shipped",
);
assert!(closed.contains("closed smoke#2"), "{closed}");
let default_listing = line(&engine, "source urn:iki:ledger:smoke:items");
assert!(
!default_listing.contains("Say what to type"),
"the default listing is the OPEN items: {default_listing}"
);
assert!(default_listing.contains("1 item(s)"), "{default_listing}");
let all = line(&engine, "source urn:iki:ledger:smoke:items status=all");
assert!(
all.contains("Say what to type"),
"still in the graph: {all}"
);
assert!(all.contains("2 item(s)"), "{all}");
let ready = line(&engine, "source urn:iki:ledger:smoke:next");
assert!(
!ready.contains("Say what to type"),
"a closed item is not ready: {ready}"
);
}
#[test]
fn the_narrow_per_graph_grants_work_and_the_broad_store_key_does_not() {
use ikigai_embedded::store::{grants_for, Authority};
let broad = engine();
line(
&broad,
"cap urn:cap:ledger:write:grants urn:cap:ledger:read:grants \
urn:cap:store:read urn:cap:store:write",
);
let denied = refused(
&broad,
"sink urn:iki:ledger:grants:append Filed with the keys to the whole dataset",
);
assert!(
denied.to_lowercase().contains("denied") || denied.contains("urn:cap:store:"),
"the broad store key is refused by the narrow door: {denied}"
);
let narrow = engine();
let grants = grants_for("grants", Authority::Write).expect("a valid ledger name");
line(&narrow, &format!("cap {}", grants.join(" ")));
let filed = line(
&narrow,
"sink urn:iki:ledger:grants:append Filed under the narrow per-graph tokens",
);
assert!(filed.starts_with("grants#1 "), "{filed}");
let list = line(&narrow, "source urn:iki:ledger:grants:items");
assert!(
list.contains("Filed under the narrow per-graph tokens"),
"{list}"
);
let item = line(&narrow, "source urn:iki:ledger:grants:item:1");
assert!(
item.contains("Filed under the narrow per-graph tokens"),
"one item reads under the narrow grants, without `urn:cap:store:read`: {item}"
);
}
#[test]
fn the_purge_grant_list_purges_a_deleted_item() {
use ikigai_embedded::store::{grants_for, Authority};
let session = engine();
let grants = grants_for("shred", Authority::Purge).expect("a valid ledger name");
line(&session, &format!("cap {}", grants.join(" ")));
let filed = line(
&session,
"sink urn:iki:ledger:shred:append A token pasted by mistake",
);
assert!(filed.starts_with("shred#1 "), "{filed}");
line(
&session,
"delete urn:iki:ledger:shred:item:1 reason=oops author=brian",
);
let purged = line(
&session,
"delete urn:iki:ledger:shred:purge reason=a-secret author=brian shred#1",
);
assert!(
purged.contains("archived quad(s) destroyed"),
"the purge reached the graveyard under exactly the published grants: {purged}"
);
}
#[test]
fn the_ledger_resources_join_the_composed_catalog() {
let kernel = {
fixture_home();
ikigai_embedded::kernel()
};
let patterns: Vec<String> = kernel
.entries()
.expect("an enumerable root")
.iter()
.map(|e| e.pattern.clone())
.collect();
for pattern in [
"urn:iki:ledger:{ledger}:append",
"urn:iki:ledger:{ledger}:items",
"urn:iki:ledger:{ledger}:item:{id}",
"urn:iki:ledger:{ledger}:next",
"urn:iki:ledger:ledgers",
"urn:iki:ledger:policy:{name}",
] {
assert!(
patterns.iter().any(|p| p == pattern),
"{pattern} is missing from the composed catalog: {patterns:?}"
);
assert!(
kernel.describe_pattern(pattern).is_some(),
"{pattern} describes itself, so the manifold can offer it and the engine can \
route named arguments to it"
);
}
for pattern in ["urn:iki:store:graph-select", "urn:iki:store:graph-update"] {
assert!(
patterns.iter().any(|p| p == pattern),
"{pattern} is missing from the composed catalog"
);
}
}