use crate::slash_registry::{receipt_for, Receipt};
use newt_core::event_journal::{EventKind, JournalEvent};
fn record(
kind: EventKind,
token: &str,
subject: &str,
detail: &str,
via: &str,
) -> Option<JournalEvent> {
if !matches!(receipt_for(token), Receipt::Event) {
return None;
}
let event = JournalEvent::new(kind, subject, detail, via);
let _ = newt_core::event_journal::record_event(event.clone());
Some(event)
}
pub(crate) fn note_appended(bytes: usize) -> Option<JournalEvent> {
record(
EventKind::NoteAppend,
"remember",
"notes",
&format!("{bytes} bytes"),
"/remember",
)
}
pub(crate) fn compressed(
conversation_id: &str,
verb: &str,
how: &str,
tokens_before: usize,
tokens_after: usize,
) -> Option<JournalEvent> {
record(
EventKind::Compression,
"compress",
conversation_id,
&format!("{how}: {tokens_before} -> {tokens_after} tokens"),
&format!("/{verb}"),
)
}
pub(crate) fn decision_reopened(conversation_id: &str, ordinal: usize) -> Option<JournalEvent> {
record(
EventKind::Reopen,
"undo-lock",
conversation_id,
&format!("assumption {ordinal}"),
"/undo-lock",
)
}
pub(crate) fn dock_switched(sub: &str) -> Option<JournalEvent> {
let kind = match sub {
"disable" | "off" => EventKind::Kill,
"enable" | "on" => EventKind::Grant,
_ => return None,
};
record(
kind,
"dock",
"remote-htmx-docking",
if kind == EventKind::Kill {
"kill-switch on"
} else {
"kill-switch off"
},
&format!("/dock {sub}"),
)
}
pub(crate) fn conversation_op(input: &str, op: &str, id: &str) -> Option<JournalEvent> {
record(
EventKind::ConversationOp,
"resume",
id,
op,
&route_of(input),
)
}
pub(crate) fn conversation_titled(verb: &str, id: &str, created: bool) -> Option<JournalEvent> {
record(
EventKind::ConversationOp,
"rename",
id,
if created { "title" } else { "rename" },
&format!("/{verb}"),
)
}
fn route_of(input: &str) -> String {
let body = input.trim().trim_start_matches('/').trim();
let mut words = body.split_whitespace();
match (words.next(), words.next()) {
(Some(door), Some(verb)) => format!("/{door} {verb}"),
(Some(door), None) => format!("/{door}"),
_ => "/".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn guard() -> newt_core::test_guard::GlobalSettingsGuard {
newt_core::test_guard::GlobalSettingsGuard::acquire()
}
#[test]
fn a_note_append_records_its_size_and_never_the_note() {
let _g = guard();
let event = note_appended(42).expect("/remember is an Event row");
assert_eq!(event.kind, EventKind::NoteAppend);
assert_eq!(event.subject, "notes");
assert_eq!(event.detail, "42 bytes");
assert_eq!(event.via, "/remember");
}
#[test]
fn compress_and_compact_are_two_routes_to_one_kind() {
let _g = guard();
let compress = compressed("conv-1", "compress", "prune + summary", 900, 400)
.expect("/compress is an Event row");
let compact = compressed("conv-1", "compact", "prune + summary", 900, 400)
.expect("/compress is an Event row");
assert_eq!(compress.kind, EventKind::Compression);
assert_eq!(compact.kind, EventKind::Compression);
assert_eq!(compress.subject, "conv-1");
assert_eq!(compress.detail, "prune + summary: 900 -> 400 tokens");
assert_eq!(compress.via, "/compress");
assert_eq!(compact.via, "/compact");
assert_ne!(compress.via, compact.via);
}
#[test]
fn a_reopened_decision_records_the_assumption_it_reopened() {
let _g = guard();
let event = decision_reopened("conv-7", 2).expect("/undo-lock is an Event row");
assert_eq!(event.kind, EventKind::Reopen);
assert_eq!(event.subject, "conv-7");
assert_eq!(event.detail, "assumption 2");
assert_eq!(event.via, "/undo-lock");
}
#[test]
fn the_dock_kill_switch_records_both_directions_as_different_kinds() {
let _g = guard();
let off = dock_switched("disable").expect("/dock is an Event row");
let on = dock_switched("enable").expect("/dock is an Event row");
assert_eq!(off.kind, EventKind::Kill);
assert_eq!(off.detail, "kill-switch on");
assert_eq!(off.via, "/dock disable");
assert_eq!(on.kind, EventKind::Grant);
assert_eq!(on.detail, "kill-switch off");
assert_eq!(on.via, "/dock enable");
assert_eq!(off.subject, on.subject);
}
#[test]
fn the_dock_aliases_keep_their_own_routes() {
let _g = guard();
assert_eq!(dock_switched("off").expect("alias").via, "/dock off");
assert_eq!(dock_switched("on").expect("alias").via, "/dock on");
}
#[test]
fn a_dock_read_or_typo_records_nothing() {
let _g = guard();
assert!(dock_switched("status").is_none());
assert!(dock_switched("").is_none());
assert!(dock_switched("disabl").is_none());
}
#[test]
fn a_conversation_op_records_the_id_the_op_and_the_typed_route() {
let _g = guard();
let deleted =
conversation_op("/resume delete abc123", "delete", "abc123").expect("Event row");
assert_eq!(deleted.kind, EventKind::ConversationOp);
assert_eq!(deleted.subject, "abc123");
assert_eq!(deleted.detail, "delete");
assert_eq!(deleted.via, "/resume delete");
}
#[test]
fn the_delete_alias_is_its_own_route() {
let _g = guard();
let rm = conversation_op("/resume rm abc123", "delete", "abc123").expect("Event row");
assert_eq!(rm.detail, "delete", "the op is canonical");
assert_eq!(rm.via, "/resume rm", "the route is what was typed");
}
#[test]
fn the_route_never_carries_the_operator_s_words() {
let _g = guard();
let renamed = conversation_op("/resume rename abc123 the secret plan", "rename", "abc123")
.expect("Event row");
assert_eq!(renamed.via, "/resume rename");
assert!(
!renamed.via.contains("secret") && !renamed.detail.contains("secret"),
"the typed title reached the journal: {renamed:?}"
);
}
#[test]
fn retitling_the_active_conversation_distinguishes_titling_from_renaming() {
let _g = guard();
let renamed = conversation_titled("rename", "conv-9", false).expect("Event row");
let titled = conversation_titled("name", "conv-9", true).expect("Event row");
assert_eq!(renamed.kind, EventKind::ConversationOp);
assert_eq!(renamed.detail, "rename");
assert_eq!(renamed.via, "/rename");
assert_eq!(titled.detail, "title");
assert_eq!(titled.via, "/name");
}
#[test]
fn a_row_with_no_declared_destination_records_nothing() {
let _g = guard();
assert!(
matches!(receipt_for("crew"), Receipt::Missing),
"this test needs a row that is still Missing to be meaningful"
);
assert!(record(EventKind::Kill, "crew", "s", "d", "/crew").is_none());
assert!(matches!(receipt_for("status"), Receipt::None_));
assert!(record(EventKind::Kill, "status", "s", "d", "/status").is_none());
assert!(record(EventKind::Kill, "zzznotacommand", "s", "d", "/x").is_none());
assert!(matches!(receipt_for("rounds"), Receipt::Journal));
assert!(record(EventKind::Grant, "rounds", "s", "d", "/rounds").is_none());
}
#[test]
fn an_event_row_does_record() {
let _g = guard();
assert!(matches!(receipt_for("remember"), Receipt::Event));
assert!(record(EventKind::NoteAppend, "remember", "notes", "0 bytes", "/x").is_some());
}
#[test]
fn the_route_of_a_bare_verb_is_the_verb() {
assert_eq!(route_of("/resume"), "/resume");
assert_eq!(route_of(" /resume delete x "), "/resume delete");
}
}