diff --git a/src/engine/db/mod.rs b/src/engine/db/mod.rs index 333c323..7e08985 100644 --- a/src/engine/db/mod.rs +++ b/src/engine/db/mod.rs @@ -1216,12 +1216,23 @@ impl Db { let mut snapshot: Vec = self.accounts.values().cloned().map(|a| Event::AccountRegistered(Box::new(a))).collect(); for c in self.channels.values() { snapshot.push(Event::ChannelRegistered { name: c.name.clone(), founder: c.founder.clone(), ts: c.ts }); + // ChannelRegistered replays last_used back to the registration ts, so a + // channel active since registration would have its inactivity-expiry clock + // reset by every compaction (and could then be wrongly expired). Restore it. + if c.last_used > c.ts { + snapshot.push(Event::ChannelUsed { channel: c.name.clone(), ts: c.last_used }); + } if !c.lock_on.is_empty() || !c.lock_off.is_empty() { snapshot.push(Event::ChannelMlock { name: c.name.clone(), on: c.lock_on.clone(), off: c.lock_off.clone(), params: c.lock_params.clone() }); } for a in &c.access { snapshot.push(Event::ChannelAccessAdd { channel: c.name.clone(), account: a.account.clone(), level: a.level.clone() }); } + // LEVELS overrides live only in c.levels; without this they reset to the + // tier defaults on every compaction. + for (cap, role) in &c.levels { + snapshot.push(Event::ChannelLevelSet { channel: c.name.clone(), cap: cap.clone(), role: role.clone() }); + } for k in &c.akick { snapshot.push(Event::ChannelAkickAdd { channel: c.name.clone(), mask: k.mask.clone(), reason: k.reason.clone() }); } diff --git a/src/engine/db/tests.rs b/src/engine/db/tests.rs index 2ee29c9..bfafcec 100644 --- a/src/engine/db/tests.rs +++ b/src/engine/db/tests.rs @@ -458,6 +458,30 @@ assert_eq!(db.certfps("bob"), &[kept][..], "kept cert survives"); } + #[test] + fn compaction_preserves_channel_last_used_and_levels() { + let p = tmp("compact-chan"); + let mut db = Db::open(&p, "local"); + db.scram_iterations = 4096; + db.register("alice", "pw", None).unwrap(); + db.register_channel("#chan", "alice").unwrap(); + let reg_ts = db.channel("#chan").unwrap().ts; + // A LEVELS override and recent activity, both held outside ChannelRegistered. + db.level_set("#chan", "SET", "AOP").unwrap(); + let active = reg_ts + 100_000; // past the 1-day activity-coalesce threshold + db.mark_channel_used("#chan", active); + assert_eq!(db.channel("#chan").unwrap().last_used, active); + + db.compact().unwrap(); + drop(db); + let db = Db::open(&p, "local"); + let c = db.channel("#chan").expect("channel survives compaction"); + // Before the fix, the snapshot emitted only ChannelRegistered, so replay + // reset last_used to reg_ts (risking wrong expiry) and dropped the override. + assert_eq!(c.last_used, active, "inactivity clock survives compaction"); + assert!(c.levels.iter().any(|(cap, role)| cap == "SET" && role == "AOP"), "LEVELS override survives: {:?}", c.levels); + } + // An account provisioned from a SCRAM verifier alone (external authority) // must authenticate over the PLAIN / IDENTIFY path (db.authenticate), not // only over SCRAM — the verifier is the sole credential, so a backfilled