feat: Refactor node initialization and info retrieval, enhance store actor lifecycle, and refine store replay logic.
This commit is contained in:
@@ -97,6 +97,14 @@ pub fn commands() -> Vec<Command> {
|
|||||||
max_args: 0,
|
max_args: 0,
|
||||||
handler: cmd_status,
|
handler: cmd_status,
|
||||||
},
|
},
|
||||||
|
Command {
|
||||||
|
name: "author-state",
|
||||||
|
args: "[author-hex]",
|
||||||
|
description: "Show author state (default: self)",
|
||||||
|
min_args: 0,
|
||||||
|
max_args: 1,
|
||||||
|
handler: cmd_author_state,
|
||||||
|
},
|
||||||
Command {
|
Command {
|
||||||
name: "help",
|
name: "help",
|
||||||
args: "",
|
args: "",
|
||||||
@@ -215,7 +223,7 @@ fn cmd_help(_node: &LatticeNode, _store: Option<&StoreHandle>, _args: &[String])
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_status(node: &LatticeNode, store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
|
fn cmd_status(node: &LatticeNode, store: Option<&StoreHandle>, _args: &[String]) -> CommandResult {
|
||||||
println!("Node ID: {}", node.node_id());
|
println!("Node ID: {}", hex::encode(node.node_id()));
|
||||||
println!("Data: {}", node.data_path().display());
|
println!("Data: {}", node.data_path().display());
|
||||||
match node.root_store() {
|
match node.root_store() {
|
||||||
Ok(Some(id)) => println!("Root: {}", id),
|
Ok(Some(id)) => println!("Root: {}", id),
|
||||||
@@ -355,3 +363,45 @@ fn cmd_list(_node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -
|
|||||||
fn format_value(v: &[u8]) -> String {
|
fn format_value(v: &[u8]) -> String {
|
||||||
std::str::from_utf8(v).map(String::from).unwrap_or_else(|_| format!("0x{}", hex::encode(v)))
|
std::str::from_utf8(v).map(String::from).unwrap_or_else(|_| format!("0x{}", hex::encode(v)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_author_state(node: &LatticeNode, store: Option<&StoreHandle>, args: &[String]) -> CommandResult {
|
||||||
|
let store = match store {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
eprintln!("Error: no store selected");
|
||||||
|
return CommandResult::Ok;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get author: from arg or default to self
|
||||||
|
let author_bytes: [u8; 32] = if args.is_empty() {
|
||||||
|
node.node_id()
|
||||||
|
} else {
|
||||||
|
let hex_str = args[0].trim_start_matches("0x");
|
||||||
|
match hex::decode(hex_str) {
|
||||||
|
Ok(bytes) if bytes.len() == 32 => bytes.try_into().unwrap(),
|
||||||
|
Ok(bytes) => {
|
||||||
|
eprintln!("Error: author must be 32 bytes, got {}", bytes.len());
|
||||||
|
return CommandResult::Ok;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: invalid hex: {}", e);
|
||||||
|
return CommandResult::Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match store.author_state(&author_bytes) {
|
||||||
|
Ok(Some(state)) => {
|
||||||
|
println!("Author: {}", hex::encode(&author_bytes));
|
||||||
|
println!(" seq: {}", state.seq);
|
||||||
|
println!(" hash: {}", hex::encode(&state.hash));
|
||||||
|
println!(" log_offset: {}", state.log_offset);
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
println!("No state for author: {}", hex::encode(&author_bytes));
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
|
}
|
||||||
|
CommandResult::Ok
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,25 +13,21 @@ fn main() {
|
|||||||
println!("Lattice CLI v{}", env!("CARGO_PKG_VERSION"));
|
println!("Lattice CLI v{}", env!("CARGO_PKG_VERSION"));
|
||||||
println!("Type 'help' for commands, 'quit' to exit.\n");
|
println!("Type 'help' for commands, 'quit' to exit.\n");
|
||||||
|
|
||||||
let (node, info) = match LatticeNodeBuilder::new().build() {
|
let node = match LatticeNodeBuilder::new().build() {
|
||||||
Ok(result) => result,
|
Ok(n) => n,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to initialize: {}", e);
|
eprintln!("Failed to initialize: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let info = node.info();
|
||||||
println!("Node ID: {}", info.node_id);
|
println!("Node ID: {}", info.node_id);
|
||||||
println!("Data: {}", info.data_path);
|
println!("Data: {}", info.data_path);
|
||||||
|
|
||||||
if info.is_new {
|
if !info.stores.is_empty() {
|
||||||
println!("Status: New identity created");
|
|
||||||
} else if !info.stores.is_empty() {
|
|
||||||
println!("Stores: {}", info.stores.len());
|
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() {
|
let mut current_store: Option<StoreHandle> = match node.open_root_store() {
|
||||||
Ok(Some((h, open_info))) => {
|
Ok(Some((h, open_info))) => {
|
||||||
|
|||||||
+46
-32
@@ -44,8 +44,6 @@ pub enum NodeError {
|
|||||||
pub struct NodeInfo {
|
pub struct NodeInfo {
|
||||||
pub node_id: String,
|
pub node_id: String,
|
||||||
pub data_path: String,
|
pub data_path: String,
|
||||||
pub is_new: bool,
|
|
||||||
pub root_store: Option<Uuid>,
|
|
||||||
pub stores: Vec<Uuid>,
|
pub stores: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,11 +61,10 @@ impl LatticeNodeBuilder {
|
|||||||
Self { data_dir: DataDir::default() }
|
Self { data_dir: DataDir::default() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<(LatticeNode, NodeInfo), NodeError> {
|
pub fn build(self) -> Result<LatticeNode, NodeError> {
|
||||||
self.data_dir.ensure_dirs()?;
|
self.data_dir.ensure_dirs()?;
|
||||||
|
|
||||||
let key_path = self.data_dir.identity_key();
|
let key_path = self.data_dir.identity_key();
|
||||||
let is_new = !key_path.exists();
|
|
||||||
let node = if key_path.exists() {
|
let node = if key_path.exists() {
|
||||||
Node::load(&key_path)?
|
Node::load(&key_path)?
|
||||||
} else {
|
} else {
|
||||||
@@ -77,22 +74,12 @@ impl LatticeNodeBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let meta = MetaStore::open(self.data_dir.meta_db())?;
|
let meta = MetaStore::open(self.data_dir.meta_db())?;
|
||||||
let root_store = meta.root_store()?;
|
|
||||||
let stores = meta.list_stores()?;
|
|
||||||
|
|
||||||
let info = NodeInfo {
|
Ok(LatticeNode {
|
||||||
node_id: hex::encode(node.public_key_bytes()),
|
|
||||||
data_path: self.data_dir.base().display().to_string(),
|
|
||||||
is_new,
|
|
||||||
root_store,
|
|
||||||
stores,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((LatticeNode {
|
|
||||||
data_dir: self.data_dir,
|
data_dir: self.data_dir,
|
||||||
node: Rc::new(node),
|
node: Rc::new(node),
|
||||||
meta,
|
meta,
|
||||||
}, info))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +95,16 @@ pub struct LatticeNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LatticeNode {
|
impl LatticeNode {
|
||||||
pub fn node_id(&self) -> String {
|
pub fn info(&self) -> NodeInfo {
|
||||||
hex::encode(self.node.public_key_bytes())
|
NodeInfo {
|
||||||
|
node_id: hex::encode(self.node.public_key_bytes()),
|
||||||
|
data_path: self.data_dir.base().display().to_string(),
|
||||||
|
stores: self.meta.list_stores().unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_id(&self) -> [u8; 32] {
|
||||||
|
self.node.public_key_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_path(&self) -> &Path {
|
pub fn data_path(&self) -> &Path {
|
||||||
@@ -183,7 +178,7 @@ impl LatticeNode {
|
|||||||
let handle = StoreHandle {
|
let handle = StoreHandle {
|
||||||
store_id,
|
store_id,
|
||||||
tx,
|
tx,
|
||||||
actor_handle,
|
actor_handle: Some(actor_handle),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((handle, info))
|
Ok((handle, info))
|
||||||
@@ -194,8 +189,7 @@ impl LatticeNode {
|
|||||||
pub struct StoreHandle {
|
pub struct StoreHandle {
|
||||||
store_id: Uuid,
|
store_id: Uuid,
|
||||||
tx: std::sync::mpsc::Sender<crate::store_actor::StoreCmd>,
|
tx: std::sync::mpsc::Sender<crate::store_actor::StoreCmd>,
|
||||||
#[allow(dead_code)]
|
actor_handle: Option<std::thread::JoinHandle<()>>,
|
||||||
actor_handle: std::thread::JoinHandle<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoreHandle {
|
impl StoreHandle {
|
||||||
@@ -248,6 +242,16 @@ impl StoreHandle {
|
|||||||
.map_err(NodeError::Store)
|
.map_err(NodeError::Store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn author_state(&self, author: &[u8; 32]) -> Result<Option<lattice_core::proto::AuthorState>, NodeError> {
|
||||||
|
use crate::store_actor::StoreCmd;
|
||||||
|
let (resp_tx, resp_rx) = std::sync::mpsc::channel();
|
||||||
|
self.tx.send(StoreCmd::AuthorState { author: *author, resp: resp_tx })
|
||||||
|
.map_err(|_| NodeError::ChannelClosed)?;
|
||||||
|
resp_rx.recv()
|
||||||
|
.map_err(|_| NodeError::ChannelClosed)?
|
||||||
|
.map_err(NodeError::Store)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn put(&self, key: &[u8], value: &[u8]) -> Result<u64, NodeError> {
|
pub fn put(&self, key: &[u8], value: &[u8]) -> Result<u64, NodeError> {
|
||||||
use crate::store_actor::StoreCmd;
|
use crate::store_actor::StoreCmd;
|
||||||
let (resp_tx, resp_rx) = std::sync::mpsc::channel();
|
let (resp_tx, resp_rx) = std::sync::mpsc::channel();
|
||||||
@@ -269,6 +273,16 @@ impl StoreHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for StoreHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Send shutdown command and wait for actor to finish
|
||||||
|
let _ = self.tx.send(crate::store_actor::StoreCmd::Shutdown);
|
||||||
|
if let Some(handle) = self.actor_handle.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -284,11 +298,11 @@ mod tests {
|
|||||||
fn test_create_and_open_store() {
|
fn test_create_and_open_store() {
|
||||||
let data_dir = temp_data_dir("meta_store");
|
let data_dir = temp_data_dir("meta_store");
|
||||||
|
|
||||||
let (node, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create node");
|
.expect("Failed to create node");
|
||||||
|
|
||||||
assert!(info.stores.is_empty());
|
assert!(node.info().stores.is_empty());
|
||||||
|
|
||||||
let store_id = node.create_store().expect("Failed to create store");
|
let store_id = node.create_store().expect("Failed to create store");
|
||||||
|
|
||||||
@@ -307,7 +321,7 @@ mod tests {
|
|||||||
fn test_store_isolation() {
|
fn test_store_isolation() {
|
||||||
let data_dir = temp_data_dir("meta_isolation");
|
let data_dir = temp_data_dir("meta_isolation");
|
||||||
|
|
||||||
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to create node");
|
.expect("Failed to create node");
|
||||||
|
|
||||||
@@ -329,12 +343,12 @@ mod tests {
|
|||||||
fn test_init_creates_root_store() {
|
fn test_init_creates_root_store() {
|
||||||
let data_dir = temp_data_dir("init_root");
|
let data_dir = temp_data_dir("init_root");
|
||||||
|
|
||||||
let (node, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("create node");
|
.expect("create node");
|
||||||
|
|
||||||
// Initially no root store
|
// Initially no root store
|
||||||
assert!(info.root_store.is_none());
|
assert!(node.root_store().unwrap().is_none());
|
||||||
|
|
||||||
// Init creates root store
|
// Init creates root store
|
||||||
let root_id = node.init().expect("init failed");
|
let root_id = node.init().expect("init failed");
|
||||||
@@ -347,7 +361,7 @@ mod tests {
|
|||||||
fn test_duplicate_init_fails() {
|
fn test_duplicate_init_fails() {
|
||||||
let data_dir = temp_data_dir("init_dup");
|
let data_dir = temp_data_dir("init_dup");
|
||||||
|
|
||||||
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("create node");
|
.expect("create node");
|
||||||
|
|
||||||
@@ -368,18 +382,18 @@ mod tests {
|
|||||||
|
|
||||||
// First session: init
|
// First session: init
|
||||||
let root_id = {
|
let root_id = {
|
||||||
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("create node");
|
.expect("create node");
|
||||||
node.init().expect("init")
|
node.init().expect("init")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Second session: root_store should be in info
|
// Second session: root_store should persist
|
||||||
let (_, info) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("reload node");
|
.expect("reload node");
|
||||||
|
|
||||||
assert_eq!(info.root_store, Some(root_id));
|
assert_eq!(node.root_store().unwrap(), Some(root_id));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||||
}
|
}
|
||||||
@@ -388,7 +402,7 @@ mod tests {
|
|||||||
fn test_idempotent_put_and_delete() {
|
fn test_idempotent_put_and_delete() {
|
||||||
let data_dir = temp_data_dir("idempotent");
|
let data_dir = temp_data_dir("idempotent");
|
||||||
|
|
||||||
let (node, _) = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||||
.build()
|
.build()
|
||||||
.expect("create node");
|
.expect("create node");
|
||||||
let store_id = node.init().expect("init");
|
let store_id = node.init().expect("init");
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ pub enum StoreCmd {
|
|||||||
pub enum StoreActorError {
|
pub enum StoreActorError {
|
||||||
Store(StoreError),
|
Store(StoreError),
|
||||||
SigChain(SigChainError),
|
SigChain(SigChainError),
|
||||||
ChannelClosed,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<StoreError> for StoreActorError {
|
impl From<StoreError> for StoreActorError {
|
||||||
@@ -69,7 +68,6 @@ impl std::fmt::Display for StoreActorError {
|
|||||||
match self {
|
match self {
|
||||||
StoreActorError::Store(e) => write!(f, "Store error: {}", e),
|
StoreActorError::Store(e) => write!(f, "Store error: {}", e),
|
||||||
StoreActorError::SigChain(e) => write!(f, "SigChain error: {}", e),
|
StoreActorError::SigChain(e) => write!(f, "SigChain error: {}", e),
|
||||||
StoreActorError::ChannelClosed => write!(f, "Channel closed"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-12
@@ -65,6 +65,7 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Replay a log file and apply all entries to the store (batched)
|
/// Replay a log file and apply all entries to the store (batched)
|
||||||
|
/// Returns the number of newly applied entries (skipped entries not counted)
|
||||||
pub fn replay_log(&self, log_path: impl AsRef<Path>) -> Result<u64, StoreError> {
|
pub fn replay_log(&self, log_path: impl AsRef<Path>) -> Result<u64, StoreError> {
|
||||||
let entries = read_entries(log_path)?;
|
let entries = read_entries(log_path)?;
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
@@ -72,17 +73,20 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let write_txn = self.db.begin_write()?;
|
let write_txn = self.db.begin_write()?;
|
||||||
|
let mut applied = 0u64;
|
||||||
{
|
{
|
||||||
let mut kv_table = write_txn.open_table(KV_TABLE)?;
|
let mut kv_table = write_txn.open_table(KV_TABLE)?;
|
||||||
let mut author_table = write_txn.open_table(AUTHOR_TABLE)?;
|
let mut author_table = write_txn.open_table(AUTHOR_TABLE)?;
|
||||||
|
|
||||||
for signed_entry in &entries {
|
for signed_entry in &entries {
|
||||||
Self::apply_ops_to_tables(signed_entry, &mut kv_table, &mut author_table)?;
|
if Self::apply_ops_to_tables(signed_entry, &mut kv_table, &mut author_table)? {
|
||||||
|
applied += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
write_txn.commit()?;
|
write_txn.commit()?;
|
||||||
|
|
||||||
Ok(entries.len() as u64)
|
Ok(applied)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a single signed entry to the store
|
/// Apply a single signed entry to the store
|
||||||
@@ -98,11 +102,12 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Internal: apply operations from a signed entry to tables
|
/// Internal: apply operations from a signed entry to tables
|
||||||
|
/// Returns true if applied, false if skipped (already applied)
|
||||||
fn apply_ops_to_tables(
|
fn apply_ops_to_tables(
|
||||||
signed_entry: &SignedEntry,
|
signed_entry: &SignedEntry,
|
||||||
kv_table: &mut redb::Table<&[u8], &[u8]>,
|
kv_table: &mut redb::Table<&[u8], &[u8]>,
|
||||||
author_table: &mut redb::Table<&[u8], &[u8]>,
|
author_table: &mut redb::Table<&[u8], &[u8]>,
|
||||||
) -> Result<(), StoreError> {
|
) -> Result<bool, StoreError> {
|
||||||
let entry = Entry::decode(&signed_entry.entry_bytes[..])?;
|
let entry = Entry::decode(&signed_entry.entry_bytes[..])?;
|
||||||
let entry_hash = hash_signed_entry(signed_entry);
|
let entry_hash = hash_signed_entry(signed_entry);
|
||||||
let entry_hlc = entry.timestamp.as_ref().map(|t| (t.wall_time << 16) | t.counter as u64).unwrap_or(0);
|
let entry_hlc = entry.timestamp.as_ref().map(|t| (t.wall_time << 16) | t.counter as u64).unwrap_or(0);
|
||||||
@@ -112,7 +117,7 @@ impl Store {
|
|||||||
if let Some(author_state_bytes) = author_table.get(&author[..])? {
|
if let Some(author_state_bytes) = author_table.get(&author[..])? {
|
||||||
if let Ok(author_state) = AuthorState::decode(author_state_bytes.value()) {
|
if let Ok(author_state) = AuthorState::decode(author_state_bytes.value()) {
|
||||||
if entry.seq <= author_state.seq {
|
if entry.seq <= author_state.seq {
|
||||||
return Ok(()); // Already applied, skip
|
return Ok(false); // Already applied, skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +157,7 @@ impl Store {
|
|||||||
};
|
};
|
||||||
author_table.insert(&author[..], author_state.encode_to_vec().as_slice())?;
|
author_table.insert(&author[..], author_state.encode_to_vec().as_slice())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a new head to a key, removing ancestor heads (idempotent)
|
/// Apply a new head to a key, removing ancestor heads (idempotent)
|
||||||
@@ -772,9 +777,9 @@ mod tests {
|
|||||||
let store = Store::open(&state_path).unwrap(); // Reopen existing state
|
let store = Store::open(&state_path).unwrap(); // Reopen existing state
|
||||||
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 2, "author seq persisted");
|
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 2, "author seq persisted");
|
||||||
|
|
||||||
// Replay log - apply_head skips entries whose parents don't exist
|
// Replay log - entries already applied, skip all
|
||||||
let replayed = store.replay_log(&log_path).unwrap();
|
let replayed = store.replay_log(&log_path).unwrap();
|
||||||
assert_eq!(replayed, 2, "Replayed 2 entries from log");
|
assert_eq!(replayed, 0, "0 new entries (all skipped)");
|
||||||
|
|
||||||
let final_heads = store.get_heads(b"/key").unwrap();
|
let final_heads = store.get_heads(b"/key").unwrap();
|
||||||
assert_eq!(final_heads.len(), 1,
|
assert_eq!(final_heads.len(), 1,
|
||||||
@@ -824,8 +829,8 @@ mod tests {
|
|||||||
let store = Store::open(&state_path).unwrap();
|
let store = Store::open(&state_path).unwrap();
|
||||||
let replayed = store.replay_log(&log_path).unwrap();
|
let replayed = store.replay_log(&log_path).unwrap();
|
||||||
|
|
||||||
// All 3 entries were replayed but skipped (seq check)
|
// All 3 entries were read but skipped (already applied)
|
||||||
assert_eq!(replayed, 3, "Replayed 3 entries from log");
|
assert_eq!(replayed, 0, "0 new entries (all skipped)");
|
||||||
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 3, "seq unchanged");
|
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 3, "seq unchanged");
|
||||||
assert_eq!(store.get_heads(b"/key3").unwrap().len(), 1, "heads unchanged");
|
assert_eq!(store.get_heads(b"/key3").unwrap().len(), 1, "heads unchanged");
|
||||||
|
|
||||||
@@ -876,7 +881,7 @@ mod tests {
|
|||||||
let store = Store::open(&state_path).unwrap();
|
let store = Store::open(&state_path).unwrap();
|
||||||
let replayed = store.replay_log(&log_path).unwrap();
|
let replayed = store.replay_log(&log_path).unwrap();
|
||||||
|
|
||||||
assert_eq!(replayed, 5, "Replayed 5 entries from log");
|
assert_eq!(replayed, 2, "Only 2 new entries applied (3 skipped)");
|
||||||
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 5, "seq updated to 5");
|
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 5, "seq updated to 5");
|
||||||
assert_eq!(store.get_heads(b"/key4").unwrap().len(), 1, "key4 now applied");
|
assert_eq!(store.get_heads(b"/key4").unwrap().len(), 1, "key4 now applied");
|
||||||
assert_eq!(store.get_heads(b"/key5").unwrap().len(), 1, "key5 now applied");
|
assert_eq!(store.get_heads(b"/key5").unwrap().len(), 1, "key5 now applied");
|
||||||
@@ -956,9 +961,9 @@ mod tests {
|
|||||||
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 3, "Restored to seq 3");
|
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 3, "Restored to seq 3");
|
||||||
assert!(store.get_heads(b"/key4").unwrap().is_empty(), "key4 not in restored state");
|
assert!(store.get_heads(b"/key4").unwrap().is_empty(), "key4 not in restored state");
|
||||||
|
|
||||||
// Replay log - should apply entries 4 and 5
|
// Replay log - should apply entries 4 and 5 (skip 1-3)
|
||||||
let replayed = store.replay_log(&log_path).unwrap();
|
let replayed = store.replay_log(&log_path).unwrap();
|
||||||
assert_eq!(replayed, 5, "Replayed 5 entries from log");
|
assert_eq!(replayed, 2, "Only 2 new entries applied (3 skipped)");
|
||||||
|
|
||||||
// Now seq should be 5 and keys 4-5 should exist
|
// Now seq should be 5 and keys 4-5 should exist
|
||||||
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 5, "seq updated to 5");
|
assert_eq!(store.author_state(&author).unwrap().unwrap().seq, 5, "seq updated to 5");
|
||||||
|
|||||||
Reference in New Issue
Block a user