Files
boxmaker/crates/memoryd/tests/honcho_mirror_secret.rs
T
kyleandClaude Opus 5.5 b392af9ca4 review: memory writes from a secret session are not written to Honcho
The journal now records each call's class (absent on the owner's undo and on
older lines, which still read). The mirror turns everything a secret write
would conclude or send to the fleet into one Skip, after its deletions, which
still go; a note whose own header says secret is skipped the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 14:04:42 -07:00

193 lines
6.1 KiB
Rust

//! Memory writes from a `secret` session are never written to Honcho (M5c spec, section 3; review
//! of M5c-1, finding 1): the journal records the call's class, and the mirror turns what such a
//! write would conclude or send to the fleet into one `Skip`, while its deletions still go.
#[path = "support/tmp.rs"]
mod tmp;
use bxcore::{DataClass, MemoryArgs, SessionId, Timestamp};
use memoryd::edit::{Context, apply};
use memoryd::files::Store;
use memoryd::honcho::ledger::{Action, Workspace};
use memoryd::honcho::mirror::{SECRET_WRITE, actions};
use memoryd::journal::{self, Record};
use memoryd::notes::{Kind, Note};
use tmp::TempDir;
const AGENT: &str = "boxmaker0";
const OWNER_PEER: &str = "kyle";
fn v(texts: &[&str]) -> Vec<String> {
texts.iter().map(|t| t.to_string()).collect()
}
fn sid(s: &str) -> SessionId {
SessionId::new(s).unwrap()
}
fn noted(class: DataClass, untrusted: bool, text: &str) -> String {
Note {
kind: Kind::Noted { class, untrusted },
time: "2026-09-28T10:00Z".to_string(),
session: "mm-a".to_string(),
text: text.to_string(),
}
.render()
}
fn record(file: Option<&str>, before: &[&str], after: &[&str], class: Option<&str>) -> Record {
Record {
id: "e1".to_string(),
time: "2026-09-28T10:00:00.000Z".to_string(),
session: "mm-a".to_string(),
call: 1,
action: "add".to_string(),
file: file.map(str::to_string),
to: file.unwrap_or("notes.md").to_string(),
before: v(before),
after: v(after),
notes: Vec::new(),
untrusted: false,
flag: None,
undoes: None,
class: class.map(str::to_string),
}
}
fn of(record: &Record) -> Vec<Action> {
actions(record, Some(&sid("mm-a")), AGENT, OWNER_PEER)
}
fn skip() -> Vec<Action> {
vec![Action::Skip {
why: SECRET_WRITE.to_string(),
}]
}
#[test]
fn a_secret_sessions_notes_are_skipped() {
let mut clean = record(None, &[], &[], Some("secret"));
clean.action = "note".to_string();
clean.notes = vec![
noted(DataClass::Secret, false, "The vault code is 4411."),
noted(DataClass::Secret, false, "And a second one."),
];
assert_eq!(of(&clean), skip(), "no conclusion, one Skip for the record");
let mut untrusted = clean.clone();
untrusted.untrusted = true;
untrusted.notes = vec![noted(DataClass::Secret, true, "The page says 3.2.")];
assert_eq!(of(&untrusted), skip(), "no fleet message");
}
#[test]
fn a_note_marked_secret_is_skipped_without_the_records_class() {
// A line from before the journal had a class: the note's own header still says secret.
let mut old = record(None, &[], &[], None);
old.notes = vec![noted(DataClass::Secret, false, "A secret fact.")];
assert_eq!(of(&old), skip());
}
#[test]
fn a_secret_identity_add_is_skipped() {
let add = record(
Some("user.md"),
&["x"],
&["x", "Kyle's PIN is 1234."],
Some("secret"),
);
assert_eq!(of(&add), skip());
}
#[test]
fn a_secret_identity_remove_still_deletes() {
let remove = record(Some("memory.md"), &["x", "gone"], &["x"], Some("secret"));
assert_eq!(
of(&remove),
vec![Action::Unconclude {
workspace: Workspace::Trusted,
peer: AGENT.to_string(),
text: "gone".to_string(),
}]
);
// A replace: the deletion first, then one Skip in place of the conclusion.
let replace = record(Some("user.md"), &["old"], &["new"], Some("secret"));
assert_eq!(
of(&replace),
vec![
Action::Unconclude {
workspace: Workspace::Trusted,
peer: OWNER_PEER.to_string(),
text: "old".to_string(),
},
Action::Skip {
why: SECRET_WRITE.to_string()
}
]
);
}
#[test]
fn public_and_private_writes_are_unchanged() {
for class in ["public", "private"] {
let add = record(Some("user.md"), &[], &["Prefers UTC."], Some(class));
assert_eq!(
of(&add),
vec![Action::Conclude {
workspace: Workspace::Trusted,
peer: OWNER_PEER.to_string(),
session: sid("mm-a"),
text: "Prefers UTC.".to_string(),
}],
"{class}"
);
}
}
#[test]
fn old_journal_lines_without_a_class_still_read() {
let line = r#"{"id":"2026-09-24-0001","time":"2026-09-24T20:14:37.123Z","session":"mm-s1","call":7,"action":"add","file":"memory.md","to":"memory.md","before":[],"after":["a"],"notes":[],"untrusted":false}"#;
let record: Record = serde_json::from_str(line).unwrap();
assert_eq!(record.class, None);
assert!(!serde_json::to_string(&record).unwrap().contains("class"));
}
#[test]
fn the_journal_records_the_calls_class() {
let dir = TempDir::new("mirror-secret-journal");
let starter = dir.write("etc/soul.md", "Soul.\n");
let store = Store::new(&dir.path().join("memory"));
store.init(&starter).unwrap();
for (taint, name) in [
(DataClass::Public, "public"),
(DataClass::Private, "private"),
(DataClass::Secret, "secret"),
] {
let ctx = Context {
session: "mm-s1".to_string(),
call: 7,
taint,
untrusted: false,
now: Timestamp::parse("2026-09-24T20:14:37.123Z").unwrap(),
};
let args: MemoryArgs = serde_json::from_str(&format!(
r#"{{"action":"add","file":"memory","text":"from a {name} session"}}"#
))
.unwrap();
let outcome = apply(&store, &args, &ctx).unwrap();
journal::record(&store, &ctx, &outcome).unwrap();
let last = journal::records(&store).unwrap().pop().unwrap();
assert_eq!(last.class.as_deref(), Some(name));
}
// An owner's undo has no call, so no class.
let last = journal::records(&store).unwrap().pop().unwrap();
journal::undo(
&store,
&last.id,
Timestamp::parse("2026-09-24T20:15:00.000Z").unwrap(),
)
.unwrap();
let undo = journal::records(&store).unwrap().pop().unwrap();
assert_eq!((undo.action.as_str(), undo.class), ("undo", None));
}