persona: a rule's words are a clause of her own line outside quotation, its campaign is checked before any effect, a co-stewarded repo is not a rule, and the repo fault names her repos; tests pin the survivors
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3389ead5a2
commit
a26bce249d
5 changed files with 152 additions and 35 deletions
|
|
@ -2008,8 +2008,9 @@ mod tests {
|
|||
refused(|a| if let RulingAuthority::Steward { steward, .. } = a { steward.0 = String::new() }),
|
||||
refused(|a| if let RulingAuthority::Steward { repo, .. } = a { repo.0 = "nope".into() }),
|
||||
refused(|a| if let RulingAuthority::Steward { words, .. } = a { words.0 = "w".repeat(4001) }),
|
||||
refused(|a| if let RulingAuthority::Steward { event, .. } = a { *event = Some("e".repeat(201).as_str().into()) }),
|
||||
];
|
||||
for (fault, field) in fields.iter().zip(["authority.words", "authority.steward", "authority.repo", "authority.words"]) {
|
||||
for (fault, field) in fields.iter().zip(["authority.words", "authority.steward", "authority.repo", "authority.words", "authority.event"]) {
|
||||
assert!(fault.contains(field), "{fault}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,15 @@ More documents.");
|
|||
assert_eq!(INDEX_TEXT_VERSION, 1);
|
||||
}
|
||||
|
||||
/// A Steward ruling is found by the words the peer said as well as by its title and ruling.
|
||||
#[test]
|
||||
fn a_steward_rulings_words_are_indexed() {
|
||||
let mut peer = ruling("R1");
|
||||
peer.operator_quote = None;
|
||||
peer.authority = RulingAuthority::Steward { steward: s("@heimdall:chat"), repo: eureka_pipeline::OrgRepo(REPO.into()), words: "refuse leading zeros".into(), event: None };
|
||||
assert!(index_text(&D::Ruling(peer)).unwrap().lines().any(|line| line == "refuse leading zeros"));
|
||||
}
|
||||
|
||||
fn texts(mind: &Mind<MemoryStore>) -> BTreeMap<String, String> {
|
||||
mind.index_entries(None).unwrap().into_iter().map(|entry| (entry.id.id.0, entry.text)).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@ const OPEN_DIRECT_TEXT_MAX_BYTES: usize = 4000;
|
|||
const RULE_BY_OWN_FAULT: &str = "rule.by: your own decision is not a rule";
|
||||
const RULE_BY_OPERATOR_FAULT: &str = "rule.by: rule records a Mind's decision in her stewardship";
|
||||
const RULE_BY_UNKNOWN_FAULT: &str = "rule.by: not a Mind of the swarm";
|
||||
const RULE_WORDS_FAULT: &str = "rule.words: copy them exactly from one of her lines in this session";
|
||||
const RULE_REPO_FAULT: &str = "rule.repo: not a repo the Mind named stewards";
|
||||
const RULE_WORDS_FAULT: &str = "rule.words: copy them exactly, at least 12 bytes, from one of her lines in this session, outside any quotation";
|
||||
const RULE_REPO_OWN_FAULT: &str = "rule.repo: inside your own stewardship, where a Mind's word is a respected contributor's: weigh it and decide, it is not a rule";
|
||||
const RULE_REPO_UNREAD_FAULT: &str = "rule.repo: no stewardship of the Mind named has been read yet";
|
||||
const RULE_BOUNDS_FAULT: &str = "rule: more entries than one reply may carry, or a field blank or over its length bound";
|
||||
const RULE_CAMPAIGN_FAULT: &str = "rule.campaign: not a campaign in force";
|
||||
|
|
@ -176,6 +176,8 @@ const REMEMBER_MAX: usize = 8;
|
|||
const SETTLE_MAX: usize = 8;
|
||||
/// The most rulings of other Minds one reply records.
|
||||
const RULE_MAX: usize = 4;
|
||||
/// The fewest bytes of her line `words` must copy: a clause, not a letter.
|
||||
const RULE_WORDS_MIN_BYTES: usize = 12;
|
||||
const RULE_TITLE_MAX_BYTES: usize = 200;
|
||||
const RULE_TEXT_MAX_BYTES: usize = 4000;
|
||||
/// A memory's text in bytes, the people it is about, and a settle's reason in characters.
|
||||
|
|
@ -750,7 +752,7 @@ pub struct Organs<'a> {
|
|||
}
|
||||
|
||||
/// Why a start did not complete.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Failure {
|
||||
Held(HeldReason),
|
||||
Chat,
|
||||
|
|
@ -766,6 +768,8 @@ pub enum Failure {
|
|||
Undecodable,
|
||||
/// A reply that parsed and broke the bound of the field this names: the organ's words, never the reply's.
|
||||
OutOfBounds(&'static str),
|
||||
/// A rule named a repo the decider does not steward: the organ's own words, naming what the heartbeat's table says she stewards.
|
||||
RuleRepo(String),
|
||||
RoomNotAllowed,
|
||||
Send,
|
||||
SelfRun,
|
||||
|
|
@ -786,6 +790,7 @@ impl fmt::Display for Failure {
|
|||
Self::Session => f.write_str("your session could not be started or ended without answering"),
|
||||
Self::Undecodable => f.write_str("the reply is not a persona reply"),
|
||||
Self::OutOfBounds(field) => f.write_str(field),
|
||||
Self::RuleRepo(told) => f.write_str(told),
|
||||
Self::RoomNotAllowed => f.write_str("the reply speaks outside your rooms"),
|
||||
Self::Send => f.write_str("a send was refused or failed"),
|
||||
Self::SelfRun => f.write_str("the Self run could not be started"),
|
||||
|
|
@ -856,7 +861,7 @@ struct Progress {
|
|||
}
|
||||
|
||||
/// A refused batch as the start remembers it. The mouth still holds its deliveries, so this needs only their lowest seq.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
struct Refused {
|
||||
lowest: u64,
|
||||
why: Failure,
|
||||
|
|
@ -966,7 +971,7 @@ pub fn carry(organs: &Organs, control: &impl ControlSource, swarm: &impl Control
|
|||
break;
|
||||
}
|
||||
}
|
||||
close_run(organs, &run, progress.answered, progress.last_intent.as_deref(), progress.failure, noted.failures.get())?;
|
||||
close_run(organs, &run, progress.answered, progress.last_intent.as_deref(), progress.failure.clone(), noted.failures.get())?;
|
||||
Ok(Outcome::Served { run, answered: progress.answered, failure: progress.failure, self_run: progress.self_run })
|
||||
}
|
||||
|
||||
|
|
@ -1663,7 +1668,7 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for PersonaInbox<'_, C, S>
|
|||
fn start_line(&mut self) -> Result<String> {
|
||||
self.read_replies();
|
||||
let batch = std::mem::take(&mut self.start);
|
||||
let (note, refusals) = self.progress.refused.map_or((None, 0), |Refused { why, refusals, .. }| (Some(refused_note(why)), refusals));
|
||||
let (note, refusals) = self.progress.refused.clone().map_or((None, 0), |Refused { why, refusals, .. }| (Some(refused_note(why)), refusals));
|
||||
let line = self.whole_line(&batch, note.as_deref())?;
|
||||
Ok(self.write(batch, line, refusals))
|
||||
}
|
||||
|
|
@ -1828,7 +1833,7 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for PersonaInbox<'_, C, S>
|
|||
// Her final answer lands after she took in every line, so a revision still waiting is answered by it and its note, that a send was not sent, would be false.
|
||||
let spent = if final_reply { self.revision.take().map(|revision| revision.batch) } else { None };
|
||||
self.answered_through = self.answered_through.max(batch.iter().chain(spent.iter().flatten()).map(|delivery| delivery.seq).max().unwrap_or(0));
|
||||
if self.progress.refused.is_some_and(|refused| batch.iter().any(|delivery| delivery.seq == refused.lowest)) {
|
||||
if self.progress.refused.as_ref().is_some_and(|refused| batch.iter().any(|delivery| delivery.seq == refused.lowest)) {
|
||||
self.progress.refused = None;
|
||||
}
|
||||
let unanswered = self
|
||||
|
|
@ -1898,9 +1903,10 @@ impl<C: ControlSource, S: ControlSource> SessionInbox for PersonaInbox<'_, C, S>
|
|||
merged.dedup_by_key(|delivery| delivery.seq);
|
||||
// A line in flight may hold a batch at a higher count than this result's, so the start's record only rises. A refusal always has a batch: the result that carries none is a flush alone, and a flush is written alone only when no refused batch waits, so one in flight has its own result first, whose refusal puts it back in the retell this one merges.
|
||||
let held = self.progress.refused.take();
|
||||
let held = held.as_ref();
|
||||
self.progress.refused = merged
|
||||
.first()
|
||||
.map(|first| Refused { lowest: held.map_or(first.seq, |held| held.lowest.min(first.seq)), why, refusals: held.map_or(most, |held| held.refusals.max(most)) });
|
||||
.map(|first| Refused { lowest: held.map_or(first.seq, |held| held.lowest.min(first.seq)), why: why.clone(), refusals: held.map_or(most, |held| held.refusals.max(most)) });
|
||||
self.retell = Some(Retell { batch: merged, why, refusals: most, revision });
|
||||
Ok((true, false))
|
||||
}
|
||||
|
|
@ -1974,6 +1980,13 @@ fn act<C: ControlSource, S: ControlSource>(inbox: &PersonaInbox<'_, C, S>, batch
|
|||
Err(why) => return refuse(why),
|
||||
};
|
||||
|
||||
// A campaign closed since the schema was fixed refuses the reply before any say or open has gone out, so the retell repeats no effect.
|
||||
if !reply.rule.is_empty() {
|
||||
let in_force = campaigns_in_force(organs.mind)?;
|
||||
if reply.rule.iter().any(|rule| !in_force.iter().any(|(slug, _)| *slug == rule.campaign)) {
|
||||
return refuse(Failure::OutOfBounds(RULE_CAMPAIGN_FAULT));
|
||||
}
|
||||
}
|
||||
// Request ids name the batch of deliveries and the say, so a repeat is the same send.
|
||||
let batch_id = match (batch.iter().map(|delivery| delivery.seq).min(), batch.iter().map(|delivery| delivery.seq).max()) {
|
||||
(Some(first), Some(last)) => format!("s{first}-{last}"),
|
||||
|
|
@ -2047,12 +2060,6 @@ fn act<C: ControlSource, S: ControlSource>(inbox: &PersonaInbox<'_, C, S>, batch
|
|||
// The one origin of this turn: its memories and its brief record the same conversation.
|
||||
let origin = origin_of(config, batch, &inbox.origin);
|
||||
let memories = build_memories(&instance, run, turn, &reply, &origin);
|
||||
if !reply.rule.is_empty() {
|
||||
let in_force = campaigns_in_force(organs.mind)?;
|
||||
if reply.rule.iter().any(|rule| !in_force.iter().any(|(slug, _)| *slug == rule.campaign)) {
|
||||
return refuse(Failure::OutOfBounds(RULE_CAMPAIGN_FAULT));
|
||||
}
|
||||
}
|
||||
let rulings = build_rulings(run, turn, &reply, &inbox.seen, date(now));
|
||||
for rule in &reply.rule {
|
||||
outputs.push(TraceItem { provenance: "her own".into(), room: String::new(), text: format!("rule {} {}: {}", rule.by, rule.repo, rule.title) });
|
||||
|
|
@ -2220,36 +2227,66 @@ fn memory_fault(reply: &PersonaReply, people: &[String], open: &[String]) -> Opt
|
|||
/// The field a rule breaks, if any: it records a peer Mind's decision, so `by` is a Mind of the table (neither herself nor the
|
||||
/// operator), `words` are the Mind's own, copied from one of her lines this session showed, and `repo` is one the table says she stewards.
|
||||
/// What the words mean, and whether they fall inside that repo, is her reading; the organ checks the line and the stewardship.
|
||||
fn rule_fault(reply: &PersonaReply, shown: &Shown, addressing: &[Addressee], config: &Config) -> Option<&'static str> {
|
||||
fn rule_fault(reply: &PersonaReply, shown: &Shown, addressing: &[Addressee], config: &Config) -> Option<Failure> {
|
||||
use Failure::OutOfBounds as Out;
|
||||
if reply.rule.len() > RULE_MAX {
|
||||
return Some(RULE_BOUNDS_FAULT);
|
||||
return Some(Out(RULE_BOUNDS_FAULT));
|
||||
}
|
||||
for rule in &reply.rule {
|
||||
let text_ok = |text: &str, max: usize| (1..=max).contains(&text.len()) && !text.trim().is_empty();
|
||||
let title_ok = text_ok(&rule.title, RULE_TITLE_MAX_BYTES) && rule.title.chars().any(char::is_alphanumeric) && !rule.title.chars().any(char::is_control);
|
||||
if rule.by.len() > SHORT_MAX_BYTES || !title_ok || !text_ok(&rule.ruling, RULE_TEXT_MAX_BYTES) || !text_ok(&rule.words, RULE_TEXT_MAX_BYTES) || !repo_in_bounds(&rule.repo) || !campaign_in_bounds(&rule.campaign) {
|
||||
return Some(RULE_BOUNDS_FAULT);
|
||||
return Some(Out(RULE_BOUNDS_FAULT));
|
||||
}
|
||||
match sender_role(config, addressing, &rule.by) {
|
||||
Sender::Persona => return Some(RULE_BY_OWN_FAULT),
|
||||
Sender::Operator => return Some(RULE_BY_OPERATOR_FAULT),
|
||||
Sender::Member => return Some(RULE_BY_UNKNOWN_FAULT),
|
||||
Sender::Persona => return Some(Out(RULE_BY_OWN_FAULT)),
|
||||
Sender::Operator => return Some(Out(RULE_BY_OPERATOR_FAULT)),
|
||||
Sender::Member => return Some(Out(RULE_BY_UNKNOWN_FAULT)),
|
||||
Sender::Mind => {}
|
||||
}
|
||||
if !shown.said.iter().any(|said| said.sender == rule.by && said.text.contains(&rule.words)) {
|
||||
return Some(RULE_WORDS_FAULT);
|
||||
if !shown.said.iter().any(|said| said.sender == rule.by && in_her_own_voice(&said.text, &rule.words)) {
|
||||
return Some(Out(RULE_WORDS_FAULT));
|
||||
}
|
||||
let stewarded = addressing.iter().find(|row| row.matrix_id() == rule.by).map(Addressee::repos).unwrap_or_default();
|
||||
if stewarded.is_empty() {
|
||||
return Some(RULE_REPO_UNREAD_FAULT);
|
||||
return Some(Out(RULE_REPO_UNREAD_FAULT));
|
||||
}
|
||||
if !stewarded.contains(&OrgRepo(rule.repo.clone())) {
|
||||
return Some(RULE_REPO_FAULT);
|
||||
let repo = OrgRepo(rule.repo.clone());
|
||||
if !stewarded.contains(&repo) {
|
||||
let list = stewarded.iter().map(|repo| repo.0.as_str()).collect::<Vec<_>>().join(", ");
|
||||
return Some(Failure::RuleRepo(format!("rule.repo: {} stewards {list}", rule.by)));
|
||||
}
|
||||
if addressing.iter().find(|row| row.matrix_id() == config.own_sender).is_some_and(|own| own.repos().contains(&repo)) {
|
||||
return Some(Out(RULE_REPO_OWN_FAULT));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether `words` are her own: at least `RULE_WORDS_MIN_BYTES`, copied from `line` on whole-word boundaries and outside any quotation
|
||||
/// (straight or curly), so a letter of a greeting, or what she was quoting (the operator's words included), is not hers.
|
||||
fn in_her_own_voice(line: &str, words: &str) -> bool {
|
||||
if words.trim().len() < RULE_WORDS_MIN_BYTES {
|
||||
return false;
|
||||
}
|
||||
line.match_indices(words).any(|(at, found)| {
|
||||
let before = line[..at].chars().next_back();
|
||||
let after = line[at + found.len()..].chars().next();
|
||||
let edge = |c: Option<char>| c.is_none_or(|c| !c.is_alphanumeric());
|
||||
let (mut straight, mut curly) = (false, 0i32);
|
||||
for c in line[..at].chars() {
|
||||
match c {
|
||||
'"' => straight = !straight,
|
||||
'\u{201C}' => curly += 1,
|
||||
'\u{201D}' => curly = (curly - 1).max(0),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let quoted = straight || curly > 0 || found.contains(['"', '\u{201C}', '\u{201D}']);
|
||||
edge(before) && edge(after) && !quoted
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether an open is hers to ask: from a public conversation, of a person this session showed who is neither a Mind nor herself, with
|
||||
/// an invitation the mouth takes (not blank, within its byte bound).
|
||||
fn open_in_bounds(ask: &OpenDirect, conversation: &Conversation, config: &Config, addressing: &[Addressee], people: &[String]) -> bool {
|
||||
|
|
@ -2277,8 +2314,8 @@ fn decode_reply(text: &str, rooms: &[String], seen: &Shown, conversation: &Conve
|
|||
if let Some(field) = memory_fault(&reply, &seen.people, &seen.open) {
|
||||
return Err(Failure::OutOfBounds(field));
|
||||
}
|
||||
if let Some(field) = rule_fault(&reply, seen, addressing, config) {
|
||||
return Err(Failure::OutOfBounds(field));
|
||||
if let Some(failure) = rule_fault(&reply, seen, addressing, config) {
|
||||
return Err(failure);
|
||||
}
|
||||
if reply.open_direct.as_ref().is_some_and(|ask| !open_in_bounds(ask, conversation, config, addressing, &seen.people)) {
|
||||
return Err(Failure::OutOfBounds(OPEN_DIRECT_FAULT));
|
||||
|
|
|
|||
|
|
@ -1783,10 +1783,10 @@ fn outcomes_and_failures_read_as_one_line_each_without_values() {
|
|||
Failure::Memory,
|
||||
Failure::Ack,
|
||||
];
|
||||
for failure in all {
|
||||
for failure in all.clone() {
|
||||
assert!(!failure.to_string().is_empty());
|
||||
}
|
||||
let texts: std::collections::BTreeSet<_> = all.map(|failure| failure.to_string()).into();
|
||||
let texts: std::collections::BTreeSet<_> = all.iter().map(|failure| failure.to_string()).collect();
|
||||
assert_eq!(texts.len(), all.len(), "each failure reads differently");
|
||||
}
|
||||
|
||||
|
|
@ -6387,7 +6387,7 @@ fn a_stale_refusal_is_her_next_input_and_costs_no_refusal() {
|
|||
assert_eq!(sent[1].basis, None, "one revision, then her choice stands, though the room moved again");
|
||||
assert!(sent[1].request_id.contains("-r1-"), "{}", sent[1].request_id);
|
||||
assert!(rig.chat.landed.borrow().values().any(|(_, text)| text == "then the cellar is mine"));
|
||||
assert_eq!((inbox.progress.answered, inbox.progress.failure), (1, None));
|
||||
assert_eq!((inbox.progress.answered, inbox.progress.failure.clone()), (1, None));
|
||||
assert_eq!(rig.chat.acked.borrow().as_slice(), &[2], "the batch is acked once the revision is answered");
|
||||
});
|
||||
}
|
||||
|
|
@ -6444,7 +6444,7 @@ fn eight_stale_refusals_in_a_row_in_different_batches_never_end_the_session() {
|
|||
inbox.tell(false).unwrap();
|
||||
inbox.echoed();
|
||||
inbox.result(&reply_result(&says(&[("swarm", "revised")])), true, false).unwrap();
|
||||
assert_eq!((inbox.progress.failure, inbox.progress.refused.is_none()), (None, true), "round {round}");
|
||||
assert_eq!((inbox.progress.failure.clone(), inbox.progress.refused.is_none()), (None, true), "round {round}");
|
||||
}
|
||||
assert_eq!((inbox.progress.answered, inbox.progress.turn), (8, 16));
|
||||
});
|
||||
|
|
@ -6579,7 +6579,7 @@ fn an_undecodable_revision_stays_final() {
|
|||
inbox.tell(false).unwrap();
|
||||
inbox.echoed();
|
||||
inbox.result(&json!({"type": "result", "is_error": false, "structured_output": {"nonsense": 1}}).to_string(), true, false).unwrap();
|
||||
assert_eq!(inbox.progress.refused.map(|refused| refused.refusals), Some(1), "an undecodable reply is a refusal, as ever");
|
||||
assert_eq!(inbox.progress.refused.as_ref().map(|refused| refused.refusals), Some(1), "an undecodable reply is a refusal, as ever");
|
||||
assert!(inbox.poll().unwrap().pending);
|
||||
inbox.tell(false).unwrap();
|
||||
inbox.echoed();
|
||||
|
|
@ -8507,7 +8507,7 @@ fn a_rule_from_a_minds_own_line_inside_her_stewardship_is_admitted_as_steward_wi
|
|||
fn a_rule_naming_a_repo_the_decider_does_not_steward_is_told_back_and_admits_nothing() {
|
||||
let rig = Rig::new(vec![delivery(1, "swarm", "@libby:chat", "refuse leading zeros")], &thrice(reply_with_rule(rule_json("@libby:chat", "refuse leading zeros", "GameCult/CultLib"))));
|
||||
*rig.table.0.borrow_mut() = Some(crate::heartbeat::Addressing { minds: table_with_libby("GameCult/Bifrost") });
|
||||
assert_eq!(failure_of(rig.go()), Some(Failure::OutOfBounds(RULE_REPO_FAULT)));
|
||||
assert_eq!(failure_of(rig.go()), Some(Failure::RuleRepo("rule.repo: @libby:chat stewards GameCult/Bifrost".into())));
|
||||
assert!(rulings_in(&rig.mind).is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -8544,3 +8544,73 @@ fn words_found_only_in_an_earlier_line_carry_no_event() {
|
|||
let both = Shown { said: [libby_said("refuse leading zeros", Some("$e9")).said, libby_said("refuse leading zeros", None).said].concat(), ..Shown::default() };
|
||||
assert_eq!(event_of(&both), Some(Short("$e9".into())), "a delivery that held the words names its event even when an earlier line repeats them");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn rule_words_must_be_a_clause_of_her_own_line_outside_any_quotation() {
|
||||
let c = config();
|
||||
let table = table_with_libby("GameCult/Bifrost");
|
||||
let decode = |line: &str, words: &str| decode_reply(&reply_with_rule(rule_json("@libby:chat", words, "GameCult/Bifrost")), &c.rooms, &libby_said(line, Some("$e1")), &Conversation::Public, &c, &table).err();
|
||||
let fault = Some(Failure::OutOfBounds(RULE_WORDS_FAULT));
|
||||
assert_eq!(decode("refuse leading zeros", "refuse leading zeros"), None, "the whole line");
|
||||
assert_eq!(decode("he said \"no\" and we refuse leading zeros", "refuse leading zeros"), None, "after a closed quotation");
|
||||
assert_eq!(decode("hello everyone, nice weather today", "e"), fault, "a letter of a greeting");
|
||||
assert_eq!(decode("refuse leading zeros", "refuse"), fault, "under the floor");
|
||||
assert_eq!(decode("xrefuse leading zerosy", "refuse leading zeros"), fault, "inside words");
|
||||
assert_eq!(decode("The operator told me: \"refuse leading zeros\"", "refuse leading zeros"), fault, "the operator's words in straight quotes");
|
||||
assert_eq!(decode("The operator told me: \u{201C}refuse leading zeros\u{201D}", "refuse leading zeros"), fault, "in curly quotes");
|
||||
assert_eq!(decode("she said \"x\"; refuse leading zeros", "x\"; refuse leading"), fault, "words that carry a quotation mark");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rule_inside_the_followers_own_stewardship_is_not_a_rule_even_when_the_decider_co_stewards_it() {
|
||||
let c = config();
|
||||
let mut table = table_with_libby("GameCult/Thing");
|
||||
table[0] = own_addressee(&c).with_stewardships([], [OrgRepo("GameCult/Thing".into())]);
|
||||
let said = libby_said("we refuse leading zeros", Some("$e1"));
|
||||
let decode = |repo: &str, table: &[Addressee]| decode_reply(&reply_with_rule(rule_json("@libby:chat", "refuse leading zeros", repo)), &c.rooms, &said, &Conversation::Public, &c, table).err();
|
||||
assert_eq!(decode("GameCult/Thing", &table), Some(Failure::OutOfBounds(RULE_REPO_OWN_FAULT)));
|
||||
assert_eq!(decode("GameCult/Thing", &table_with_libby("GameCult/Thing")), None, "a repo she does not steward herself stands");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rule_whose_campaign_is_not_in_force_is_refused_before_any_say_goes_out() {
|
||||
let mut rule = rule_json("@libby:chat", "refuse leading zeros", "GameCult/Bifrost");
|
||||
rule["campaign"] = json!("closed-since");
|
||||
let reply = json!({ "intent": "Record it", "say": [{ "room": "swarm", "text": "noted" }], "open_direct": null, "start_self_run": null, "remember": [], "rule": [rule], "settle": [], "urgency_permille": null }).to_string();
|
||||
let rig = Rig::new(vec![delivery(1, "swarm", "@libby:chat", "we refuse leading zeros")], &thrice(reply));
|
||||
*rig.table.0.borrow_mut() = Some(crate::heartbeat::Addressing { minds: table_with_libby("GameCult/Bifrost") });
|
||||
assert_eq!(failure_of(rig.go()), Some(Failure::OutOfBounds(RULE_CAMPAIGN_FAULT)));
|
||||
assert!(rig.sent_texts().is_empty(), "no say goes out under a refused reply: {:?}", rig.sent_texts());
|
||||
assert!(rulings_in(&rig.mind).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_reply_records_at_most_four_rules() {
|
||||
let c = config();
|
||||
let table = table_with_libby("GameCult/Bifrost");
|
||||
let said = libby_said("we refuse leading zeros", Some("$e1"));
|
||||
let many = |n: usize| {
|
||||
let rules: Vec<_> = (0..n).map(|_| rule_json("@libby:chat", "refuse leading zeros", "GameCult/Bifrost")).collect();
|
||||
json!({ "intent": "x", "say": [], "open_direct": null, "start_self_run": null, "remember": [], "rule": rules, "settle": [], "urgency_permille": null }).to_string()
|
||||
};
|
||||
assert!(decode_reply(&many(4), &c.rooms, &said, &Conversation::Public, &c, &table).is_ok());
|
||||
assert_eq!(decode_reply(&many(5), &c.rooms, &said, &Conversation::Public, &c, &table), Err(Failure::OutOfBounds(RULE_BOUNDS_FAULT)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_event_is_the_newest_delivery_and_said_holds_every_line_a_session_showed_in_print_order() {
|
||||
let run = PipelineRef { kind: PipelineKind::Run, id: format!("{INSTANCE}:run:mind-1").as_str().into() };
|
||||
let reply: PersonaReply = serde_json::from_str(&reply_with_rule(rule_json("@libby:chat", "refuse leading zeros", "GameCult/Bifrost"))).unwrap();
|
||||
let said = |event: &str| Said { sender: "@libby:chat".into(), text: "refuse leading zeros".into(), event: Some(event.into()) };
|
||||
let shown = Shown { said: vec![said("$old"), said("$new")], ..Shown::default() };
|
||||
let PipelineDocument::Ruling(ruling) = build_rulings(&run, 1, &reply, &shown, Date("2026-11-11".into())).remove(0) else { panic!() };
|
||||
assert!(matches!(ruling.authority, RulingAuthority::Steward { event: Some(Short(ref e)), .. } if e == "$new"));
|
||||
|
||||
let first = delivery_of(1, "swarm", "@libby:chat", "first", "$a");
|
||||
let second = delivery_of(2, "swarm", "@guest:chat", "second", "$b");
|
||||
let context = ContextLine { sender: "@libby:chat".into(), text: "before".into(), at_unix_ms: 1 };
|
||||
let lines = said_of(&[first, second], &[vec![&context], vec![]]);
|
||||
let seen: Vec<(&str, &str, Option<&str>)> = lines.iter().map(|l| (l.sender.as_str(), l.text.as_str(), l.event.as_deref())).collect();
|
||||
assert_eq!(seen, [("@libby:chat", "before", None), ("@libby:chat", "first", Some("$a")), ("@guest:chat", "second", Some("$b"))]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ mind_body::lease_client: struct SocketLender pub-fields[]
|
|||
mind_body::lease_client: struct TokenFd pub-fields[]
|
||||
mind_body::lease_client::socket: impl Lease for Granted
|
||||
mind_body::persona: # [derive (Clone , Copy , Debug , PartialEq , Eq , Serialize , Deserialize)] # [serde (rename_all = "lowercase")] pub enum SettleOutcome { Done , Declined , Wrong , }
|
||||
mind_body::persona: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Failure { Held (HeldReason) , Chat , Persona , Doctrine , DoctrineTooLarge , Lease , Session , Undecodable , OutOfBounds (& 'static str) , RoomNotAllowed , Send , SelfRun , Memory , Ack , }
|
||||
mind_body::persona: # [derive (Clone , Debug , PartialEq , Eq)] pub enum Failure { Held (HeldReason) , Chat , Persona , Doctrine , DoctrineTooLarge , Lease , Session , Undecodable , OutOfBounds (& 'static str) , RuleRepo (String) , RoomNotAllowed , Send , SelfRun , Memory , Ack , }
|
||||
mind_body::persona: # [derive (Debug , PartialEq , Eq)] pub enum Outcome { Dead (HeldReason) , NoRun , Served { run : PipelineRef , answered : u32 , failure : Option < Failure > , self_run : Option < PipelineRef > } , }
|
||||
mind_body::persona: const AGENT : & str
|
||||
mind_body::persona: const DOCTRINE_MAX_BYTES : usize
|
||||
|
|
|
|||
Loading…
Reference in a new issue