feat: introduce global meta store and root store concept, and update CLI to manage active store

This commit is contained in:
2025-12-21 23:59:08 +01:00
parent f45c6ccfcf
commit 346ebccee7
15 changed files with 851 additions and 283 deletions
+211 -54
View File
@@ -1,12 +1,19 @@
//! CLI command handlers (presentation layer)
//! CLI command handlers
use crate::node::LatticeNode;
use crate::node::{LatticeNode, StoreHandle};
use lattice_core::Uuid;
use std::time::Instant;
/// Command handler function type
pub type Handler = fn(&mut LatticeNode, &[String]);
/// Result of a command that may switch stores
pub enum CommandResult {
/// No store change
Ok,
/// Switch to this store
SwitchTo(StoreHandle),
}
pub type Handler = fn(&LatticeNode, Option<&StoreHandle>, &[String]) -> CommandResult;
/// Command definition
pub struct Command {
pub name: &'static str,
pub args: &'static str,
@@ -16,9 +23,40 @@ pub struct Command {
pub handler: Handler,
}
/// Build the command registry
pub fn commands() -> Vec<Command> {
vec![
Command {
name: "init",
args: "",
description: "Initialize node with root store",
min_args: 0,
max_args: 0,
handler: cmd_init,
},
Command {
name: "create-store",
args: "",
description: "Create a new store",
min_args: 0,
max_args: 0,
handler: cmd_create_store,
},
Command {
name: "use",
args: "<uuid>",
description: "Switch to a store",
min_args: 1,
max_args: 1,
handler: cmd_use_store,
},
Command {
name: "list-stores",
args: "",
description: "List all stores",
min_args: 0,
max_args: 0,
handler: cmd_list_stores,
},
Command {
name: "put",
args: "<key> <value>",
@@ -54,7 +92,7 @@ pub fn commands() -> Vec<Command> {
Command {
name: "status",
args: "",
description: "Show node statistics",
description: "Show node/store info",
min_args: 0,
max_args: 0,
handler: cmd_status,
@@ -70,83 +108,202 @@ pub fn commands() -> Vec<Command> {
]
}
/// Print help from the command registry
fn cmd_help(_node: &mut LatticeNode, _args: &[String]) {
println!("\nLattice Commands:");
for cmd in commands() {
if cmd.args.is_empty() {
println!(" {:<18} {}", cmd.name, cmd.description);
} else {
println!(" {} {:<10} {}", cmd.name, cmd.args, cmd.description);
// --- Store management ---
fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
match node.init() {
Ok(store_id) => {
println!("Initialized with root store: {}", store_id);
match node.open_store(store_id) {
Ok((handle, _)) => CommandResult::SwitchTo(handle),
Err(e) => {
eprintln!("Warning: {}", e);
CommandResult::Ok
}
}
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
println!(" quit Exit the CLI");
println!("\nTip: Use quotes for values with spaces: put \"my key\" \"hello world\"\n");
}
fn cmd_status(node: &mut LatticeNode, _args: &[String]) {
let status = node.status();
println!("--- Node Status ---");
println!("Node ID: {}", status.node_id);
println!("Data Dir: {}", status.data_dir);
println!("Log Sequence: {}", status.log_seq);
println!("Applied Entries: {}", status.applied_seq);
println!("-------------------");
}
fn cmd_put(node: &mut LatticeNode, args: &[String]) {
let start = Instant::now();
match node.put(&args[0], args[1].as_bytes()) {
Ok(seq) => println!("OK (seq: {}, time: {:.2?})", seq, start.elapsed()),
Err(e) => eprintln!("Error: {}", e),
fn cmd_create_store(node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
match node.create_store() {
Ok(store_id) => {
println!("Created store: {}", store_id);
match node.open_store(store_id) {
Ok((handle, _)) => {
println!("Switched to new store");
CommandResult::SwitchTo(handle)
}
Err(e) => {
eprintln!("Warning: {}", e);
CommandResult::Ok
}
}
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
}
fn cmd_get(node: &mut LatticeNode, args: &[String]) {
fn cmd_use_store(node: &LatticeNode, _store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
let store_id = match Uuid::parse_str(&args[0]) {
Ok(id) => id,
Err(_) => {
eprintln!("Error: invalid UUID '{}'", args[0]);
return CommandResult::Ok;
}
};
let start = Instant::now();
match node.get(&args[0]) {
Ok(Some(value)) => {
println!("{}", format_value(&value));
match node.open_store(store_id) {
Ok((handle, info)) => {
if info.entries_replayed > 0 {
println!("Replayed {} entries ({:.2?})", info.entries_replayed, start.elapsed());
} else {
println!("Switched to store {}", store_id);
}
CommandResult::SwitchTo(handle)
}
Err(e) => {
eprintln!("Error: {}", e);
CommandResult::Ok
}
}
}
fn cmd_list_stores(node: &LatticeNode, store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
let stores = match node.list_stores() {
Ok(s) => s,
Err(e) => {
eprintln!("Error: {}", e);
return CommandResult::Ok;
}
};
let current_id = store.map(|s| s.id());
if stores.is_empty() {
println!("No stores. Use 'init' or 'create-store'.");
} else {
for store_id in stores {
let marker = if Some(store_id) == current_id { " *" } else { "" };
println!("{}{}", store_id, marker);
}
}
CommandResult::Ok
}
// --- Info ---
fn cmd_help(_node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
println!("\nCommands:");
for cmd in commands() {
if cmd.args.is_empty() {
println!(" {:<16} {}", cmd.name, cmd.description);
} else {
println!(" {} {:<8} {}", cmd.name, cmd.args, cmd.description);
}
}
println!(" quit Exit");
println!();
CommandResult::Ok
}
fn cmd_status(node: &LatticeNode, store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
println!("Node ID: {}", node.node_id());
println!("Data: {}", node.data_path().display());
match node.root_store() {
Ok(Some(id)) => println!("Root: {}", id),
Ok(None) => println!("Root: (not set)"),
Err(_) => println!("Root: (error)"),
}
if let Some(h) = store {
println!("Store: {}", h.id());
println!("Log Seq: {}", h.log_seq());
println!("Applied: {}", h.applied_seq().unwrap_or(0));
} else {
println!("Store: (none)");
}
CommandResult::Ok
}
// --- KV ---
fn cmd_put(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
let Some(h) = store else {
println!("No store selected. Use 'init' or 'use <uuid>'");
return CommandResult::Ok;
};
let start = Instant::now();
match h.put(&args[0], args[1].as_bytes()) {
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
fn cmd_get(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
let Some(h) = store else {
println!("No store selected. Use 'init' or 'use <uuid>'");
return CommandResult::Ok;
};
let start = Instant::now();
match h.get(&args[0]) {
Ok(Some(v)) => {
println!("{}", format_value(&v));
println!("({:.2?})", start.elapsed());
}
Ok(None) => println!("(nil)"),
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
fn cmd_delete(node: &mut LatticeNode, args: &[String]) {
fn cmd_delete(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
let Some(h) = store else {
println!("No store selected. Use 'init' or 'use <uuid>'");
return CommandResult::Ok;
};
let start = Instant::now();
match node.delete(&args[0]) {
Ok(seq) => println!("OK (seq: {}, time: {:.2?})", seq, start.elapsed()),
match h.delete(&args[0]) {
Ok(seq) => println!("OK (seq: {}, {:.2?})", seq, start.elapsed()),
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
fn cmd_list(node: &mut LatticeNode, args: &[String]) {
fn cmd_list(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
let Some(h) = store else {
println!("No store selected. Use 'init' or 'use <uuid>'");
return CommandResult::Ok;
};
let verbose = args.first().map(|a| a == "-v").unwrap_or(false);
let start = Instant::now();
match node.list() {
match h.list() {
Ok(entries) => {
if entries.is_empty() {
println!("(empty)");
return;
}
for (key, value) in &entries {
if verbose {
println!("{} = {} ({} bytes)", key, format_value(value), value.len());
} else {
println!("{} = {}", key, format_value(value));
} else {
for (k, v) in &entries {
if verbose {
println!("{} = {} ({} bytes)", k, format_value(v), v.len());
} else {
println!("{} = {}", k, format_value(v));
}
}
println!("({} keys, {:.2?})", entries.len(), start.elapsed());
}
println!("({} keys, {:.2?})", entries.len(), start.elapsed());
}
Err(e) => eprintln!("Error: {}", e),
}
CommandResult::Ok
}
fn format_value(value: &[u8]) -> String {
match std::str::from_utf8(value) {
Ok(s) => s.to_string(),
Err(_) => format!("0x{}", hex::encode(value)),
}
fn format_value(v: &[u8]) -> String {
std::str::from_utf8(v).map(String::from).unwrap_or_else(|_| format!("0x{}", hex::encode(v)))
}
+54 -35
View File
@@ -3,7 +3,8 @@
mod node;
mod commands;
use node::LatticeNodeBuilder;
use commands::CommandResult;
use node::{LatticeNodeBuilder, StoreHandle};
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
@@ -11,35 +12,59 @@ fn main() {
println!("Lattice CLI v{}", env!("CARGO_PKG_VERSION"));
println!("Type 'help' for commands, 'quit' to exit.\n");
let mut node = match LatticeNodeBuilder::new().build() {
Ok((n, info)) => {
println!("Node ID: {}", info.node_id);
println!("Data: {}", info.data_path);
if info.is_new {
println!("Status: New identity created");
} else if info.entries_replayed > 0 {
println!("Replay: {} log entries applied", info.entries_replayed);
}
println!();
n
}
let (node, info) = match LatticeNodeBuilder::new().build() {
Ok(result) => result,
Err(e) => {
eprintln!("Failed to initialize node: {}", e);
eprintln!("Hint: If data is corrupted, remove the data directory and restart.");
eprintln!("Failed to initialize: {}", e);
return;
}
};
println!("Node ID: {}", info.node_id);
println!("Data: {}", info.data_path);
if info.is_new {
println!("Status: New identity created");
} else if !info.stores.is_empty() {
println!("Stores: {}", info.stores.len());
}
if let Some(root) = info.root_store {
println!("Root: {}", root);
}
let mut current_store: Option<StoreHandle> = match node.open_root_store() {
Ok(Some((h, open_info))) => {
if open_info.entries_replayed > 0 {
println!("Root: {} (replayed {})", open_info.store_id, open_info.entries_replayed);
} else {
println!("Root: {}", open_info.store_id);
}
Some(h)
}
Ok(None) => {
println!("Status: Not initialized (use 'init')");
None
}
Err(e) => {
eprintln!("Warning: {}", e);
None
}
};
println!();
let mut rl = DefaultEditor::new().expect("Failed to create editor");
let cmds = commands::commands();
loop {
match rl.readline("lattice> ") {
let prompt = match &current_store {
Some(h) => format!("lattice:{}> ", &h.id().to_string()[..8]),
None => "lattice:no-store> ".to_string(),
};
match rl.readline(&prompt) {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.is_empty() { continue; }
let _ = rl.add_history_entry(line);
let args = match shlex::split(line) {
@@ -50,40 +75,34 @@ fn main() {
}
};
let cmd_name = match args.first() {
Some(c) => c.as_str(),
None => continue,
};
// Handle quit specially
let cmd_name = args.first().map(|s| s.as_str()).unwrap_or("");
if cmd_name == "quit" || cmd_name == "exit" {
println!("Goodbye!");
break;
}
// Look up command in registry
match cmds.iter().find(|c| c.name == cmd_name) {
Some(cmd) => {
let cmd_args = &args[1..];
if cmd_args.len() < cmd.min_args || cmd_args.len() > cmd.max_args {
if cmd.min_args == cmd.max_args {
println!("Usage: {} {}", cmd.name, cmd.args);
} else {
println!("Usage: {} {} (got {} args)", cmd.name, cmd.args, cmd_args.len());
}
println!("Usage: {} {}", cmd.name, cmd.args);
} else {
(cmd.handler)(&mut node, cmd_args);
match (cmd.handler)(&node, current_store.as_ref(), cmd_args) {
CommandResult::Ok => {}
CommandResult::SwitchTo(h) => current_store = Some(h),
}
}
}
None => println!("Unknown command: '{}'. Type 'help' for commands.", cmd_name),
None => println!("Unknown: '{}'. Type 'help'.", cmd_name),
}
}
Err(ReadlineError::Interrupted | ReadlineError::Eof) => {
println!("Goodbye!");
break;
}
Err(err) => {
eprintln!("Error: {:?}", err);
Err(e) => {
eprintln!("Error: {:?}", e);
break;
}
}
+219 -138
View File
@@ -1,18 +1,18 @@
//! Lattice Node API
//!
//! A programmatic interface to a local Lattice node.
//! Local Lattice node API with multi-store support
use lattice_core::{
DataDir, EntryBuilder, Node, SigChain, Store,
DataDir, EntryBuilder, MetaStore, Node, SigChain, Store, Uuid,
hlc::HLC,
log::LogError,
meta_store::MetaStoreError,
sigchain::SigChainError,
store::StoreError,
};
use std::path::Path;
use std::rc::Rc;
use std::cell::RefCell;
use thiserror::Error;
/// Errors that can occur during node operations
#[derive(Error, Debug)]
pub enum NodeError {
#[error("IO error: {0}")]
@@ -21,6 +21,9 @@ pub enum NodeError {
#[error("Store error: {0}")]
Store(#[from] StoreError),
#[error("MetaStore error: {0}")]
MetaStore(#[from] MetaStoreError),
#[error("SigChain error: {0}")]
SigChain(#[from] SigChainError),
@@ -29,43 +32,36 @@ pub enum NodeError {
#[error("Node error: {0}")]
Node(#[from] lattice_core::node::NodeError),
#[error("Already initialized")]
AlreadyInitialized,
}
/// Info returned when building a node
pub struct NodeInfo {
pub node_id: String,
pub data_path: String,
pub is_new: bool,
pub root_store: Option<Uuid>,
pub stores: Vec<Uuid>,
}
pub struct StoreInfo {
pub store_id: Uuid,
pub entries_replayed: u64,
}
/// Status information about the node
pub struct NodeStatus {
pub node_id: String,
pub data_dir: String,
pub log_seq: u64,
pub applied_seq: u64,
}
/// Builder for creating a fully initialized LatticeNode
pub struct LatticeNodeBuilder {
data_dir: DataDir,
pub data_dir: DataDir,
}
impl LatticeNodeBuilder {
/// Create a builder with the default data directory
pub fn new() -> Self {
Self {
data_dir: DataDir::default(),
}
Self { data_dir: DataDir::default() }
}
/// Build and initialize the node
pub fn build(self) -> Result<(LatticeNode, NodeInfo), NodeError> {
// Create directories
self.data_dir.ensure_dirs()?;
// Load or create node identity
let key_path = self.data_dir.identity_key();
let is_new = !key_path.exists();
let node = if key_path.exists() {
@@ -76,113 +72,164 @@ impl LatticeNodeBuilder {
node
};
let author_id_hex = hex::encode(node.public_key_bytes());
// Load or create sigchain
let log_path = self.data_dir.log_file(&author_id_hex);
let sigchain = if log_path.exists() {
SigChain::from_log(&log_path, node.public_key_bytes())?
} else {
SigChain::new(&log_path, node.public_key_bytes())
};
// Open store and replay log
let store = Store::open(self.data_dir.state_db())?;
let entries_replayed = if log_path.exists() {
store.replay_log(&log_path)?
} else {
0
};
let meta = MetaStore::open(self.data_dir.meta_db())?;
let root_store = meta.root_store()?;
let stores = meta.list_stores()?;
let info = NodeInfo {
node_id: hex::encode(node.public_key_bytes()),
data_path: self.data_dir.base().display().to_string(),
is_new,
entries_replayed,
root_store,
stores,
};
Ok((LatticeNode {
data_dir: self.data_dir,
node,
sigchain,
store,
node: Rc::new(node),
meta,
}, info))
}
}
impl Default for LatticeNodeBuilder {
fn default() -> Self {
Self::new()
}
fn default() -> Self { Self::new() }
}
/// A fully initialized Lattice node
///
/// Use `LatticeNodeBuilder` to create an instance.
/// A local Lattice node (manages identity and store registry)
pub struct LatticeNode {
data_dir: DataDir,
node: Node,
sigchain: SigChain,
store: Store,
node: Rc<Node>,
meta: MetaStore,
}
impl LatticeNode {
/// Get the node's public key as hex
pub fn node_id(&self) -> String {
hex::encode(self.node.public_key_bytes())
}
/// Get the path to the data directory
pub fn data_path(&self) -> &Path {
self.data_dir.base()
}
/// Get the current status of the node
pub fn status(&self) -> NodeStatus {
NodeStatus {
node_id: self.node_id(),
data_dir: self.data_dir.base().display().to_string(),
log_seq: self.sigchain.len(),
applied_seq: self.store.last_seq().unwrap_or(0),
/// Get the root store ID
pub fn root_store(&self) -> Result<Option<Uuid>, NodeError> {
Ok(self.meta.root_store()?)
}
/// Open the root store if set
pub fn open_root_store(&self) -> Result<Option<(StoreHandle, StoreInfo)>, NodeError> {
match self.meta.root_store()? {
Some(id) => Ok(Some(self.open_store(id)?)),
None => Ok(None),
}
}
/// Put a key-value pair
pub fn put(&mut self, key: &str, value: &[u8]) -> Result<u64, NodeError> {
let entry = EntryBuilder::new(self.sigchain.next_seq(), HLC::now())
.prev_hash(self.sigchain.last_hash().to_vec())
.put(key, value.to_vec())
.sign(&self.node);
self.commit_entry(entry)
/// Initialize the node with a root store (fails if already initialized)
pub fn init(&self) -> Result<Uuid, NodeError> {
if self.meta.root_store()?.is_some() {
return Err(NodeError::AlreadyInitialized);
}
let store_id = self.create_store()?;
self.meta.set_root_store(store_id)?;
Ok(store_id)
}
/// Get a value by key
pub fn list_stores(&self) -> Result<Vec<Uuid>, NodeError> {
Ok(self.meta.list_stores()?)
}
pub fn create_store(&self) -> Result<Uuid, NodeError> {
let store_id = Uuid::new_v4();
self.data_dir.ensure_store_dirs(store_id)?;
let _ = Store::open(self.data_dir.store_state_db(store_id))?;
self.meta.add_store(store_id)?;
Ok(store_id)
}
pub fn open_store(&self, store_id: Uuid) -> Result<(StoreHandle, StoreInfo), NodeError> {
self.data_dir.ensure_store_dirs(store_id)?;
let author_id_hex = hex::encode(self.node.public_key_bytes());
let log_path = self.data_dir.store_log_file(store_id, &author_id_hex);
let sigchain = if log_path.exists() {
SigChain::from_log(&log_path, *store_id.as_bytes(), self.node.public_key_bytes())?
} else {
SigChain::new(&log_path, *store_id.as_bytes(), self.node.public_key_bytes())
};
let store = Store::open(self.data_dir.store_state_db(store_id))?;
let entries_replayed = if log_path.exists() {
store.replay_log(&log_path)?
} else {
0
};
let info = StoreInfo { store_id, entries_replayed };
let handle = StoreHandle {
store_id,
node: Rc::clone(&self.node),
sigchain: RefCell::new(sigchain),
store,
};
Ok((handle, info))
}
}
/// A handle to a specific store with KV operations
pub struct StoreHandle {
store_id: Uuid,
node: Rc<Node>,
sigchain: RefCell<SigChain>,
store: Store,
}
impl StoreHandle {
pub fn id(&self) -> Uuid { self.store_id }
pub fn get(&self, key: &str) -> Result<Option<Vec<u8>>, NodeError> {
Ok(self.store.get(key)?)
}
/// List all key-value pairs
pub fn list(&self) -> Result<Vec<(String, Vec<u8>)>, NodeError> {
Ok(self.store.list_all()?)
}
/// Delete a key
pub fn delete(&mut self, key: &str) -> Result<u64, NodeError> {
let entry = EntryBuilder::new(self.sigchain.next_seq(), HLC::now())
.prev_hash(self.sigchain.last_hash().to_vec())
.delete(key)
.sign(&self.node);
self.commit_entry(entry)
pub fn log_seq(&self) -> u64 {
self.sigchain.borrow().len()
}
/// Commit a signed entry: append to log via sigchain, then apply to store
fn commit_entry(&mut self, entry: lattice_core::proto::SignedEntry) -> Result<u64, NodeError> {
self.sigchain.append(&entry)?;
self.store.apply_entry(&entry)?;
pub fn applied_seq(&self) -> Result<u64, NodeError> {
Ok(self.store.last_seq()?)
}
Ok(self.sigchain.len())
pub fn put(&self, key: &str, value: &[u8]) -> Result<u64, NodeError> {
self.commit_entry(|b| b.put(key, value.to_vec()))
}
pub fn delete(&self, key: &str) -> Result<u64, NodeError> {
self.commit_entry(|b| b.delete(key))
}
fn commit_entry<F>(&self, build: F) -> Result<u64, NodeError>
where
F: FnOnce(EntryBuilder) -> EntryBuilder,
{
let mut sigchain = self.sigchain.borrow_mut();
let seq = sigchain.len() + 1;
let prev_hash = sigchain.last_hash();
let builder = EntryBuilder::new(seq, HLC::now())
.store_id(self.store_id.as_bytes().to_vec())
.prev_hash(prev_hash.to_vec());
let entry = build(builder).sign(&self.node);
sigchain.append(&entry)?;
self.store.apply_entry(&entry)?;
Ok(seq)
}
}
@@ -193,77 +240,111 @@ mod tests {
fn temp_data_dir(name: &str) -> DataDir {
let path = temp_dir().join(format!("lattice_node_test_{}", name));
// Clean up from previous runs
let _ = std::fs::remove_dir_all(&path);
DataDir::new(path)
}
#[test]
fn test_put_survives_restart() {
let data_dir = temp_data_dir("restart");
fn test_create_and_open_store() {
let data_dir = temp_data_dir("meta_store");
// First session: put a value
{
let (mut node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to create node");
node.put("/test/key", b"hello").expect("put failed");
assert_eq!(node.get("/test/key").unwrap(), Some(b"hello".to_vec()));
}
let (node, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to create node");
// Second session: value should still be there
{
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to create node on restart");
assert_eq!(node.get("/test/key").unwrap(), Some(b"hello".to_vec()));
assert_eq!(node.status().log_seq, 1);
}
assert!(info.stores.is_empty());
let store_id = node.create_store().expect("Failed to create store");
// Verify it's in the list
let stores = node.list_stores().expect("list failed");
assert!(stores.contains(&store_id));
let (handle, _) = node.open_store(store_id).expect("Failed to open store");
handle.put("/key", b"value").expect("put failed");
assert_eq!(handle.get("/key").unwrap(), Some(b"value".to_vec()));
// Cleanup
let _ = std::fs::remove_dir_all(data_dir.base());
}
#[test]
fn test_log_replay_after_db_deletion() {
let data_dir = temp_data_dir("replay");
fn test_store_isolation() {
let data_dir = temp_data_dir("meta_isolation");
// First session: put some values
{
let (mut node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to create node");
node.put("/key1", b"value1").expect("put failed");
node.put("/key2", b"value2").expect("put failed");
node.delete("/key1").expect("delete failed");
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to create node");
let store_a = node.create_store().expect("create A");
let store_b = node.create_store().expect("create B");
let (handle_a, _) = node.open_store(store_a).expect("open A");
handle_a.put("/key", b"from A").expect("put A");
let (handle_b, _) = node.open_store(store_b).expect("open B");
assert_eq!(handle_b.get("/key").unwrap(), None);
assert_eq!(handle_a.get("/key").unwrap(), Some(b"from A".to_vec()));
let _ = std::fs::remove_dir_all(data_dir.base());
}
#[test]
fn test_init_creates_root_store() {
let data_dir = temp_data_dir("init_root");
let (node, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("create node");
// Initially no root store
assert!(info.root_store.is_none());
// Init creates root store
let root_id = node.init().expect("init failed");
assert_eq!(node.root_store().unwrap(), Some(root_id));
let _ = std::fs::remove_dir_all(data_dir.base());
}
#[test]
fn test_duplicate_init_fails() {
let data_dir = temp_data_dir("init_dup");
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("create node");
node.init().expect("first init");
// Second init should fail
match node.init() {
Err(NodeError::AlreadyInitialized) => (),
other => panic!("Expected AlreadyInitialized, got {:?}", other),
}
// Delete state.db but keep the log
let db_path = data_dir.state_db();
std::fs::remove_file(&db_path).expect("Failed to delete state.db");
assert!(!db_path.exists(), "state.db should be deleted");
let _ = std::fs::remove_dir_all(data_dir.base());
}
#[test]
fn test_root_store_in_info_after_init() {
let data_dir = temp_data_dir("init_info");
// Third session: log should be replayed to reconstruct state
{
let (node, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
// First session: init
let root_id = {
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("Failed to rebuild node from log");
// Should have replayed 3 entries
assert_eq!(info.entries_replayed, 3);
// key1 was deleted
assert_eq!(node.get("/key1").unwrap(), None);
// key2 should still exist
assert_eq!(node.get("/key2").unwrap(), Some(b"value2".to_vec()));
// log seq should be 3 (put, put, delete)
assert_eq!(node.status().log_seq, 3);
}
.expect("create node");
node.init().expect("init")
};
// Second session: root_store should be in info
let (_, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
.build()
.expect("reload node");
assert_eq!(info.root_store, Some(root_id));
// Cleanup
let _ = std::fs::remove_dir_all(data_dir.base());
}
}