bxcore: ask_memory's arguments and their shape checks

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-28 22:20:27 -07:00
co-authored by ornith-1.5-35b-a3b
parent 62aaadd17e
commit 3c54096704
4 changed files with 269 additions and 5 deletions
+181
View File
@@ -0,0 +1,181 @@
//! The arguments of `ask_memory`: decoding, the default workspace, unknown keys refused, and the
//! shape checks with the sentences the model is shown (M5c spec, section 9). Do not edit.
use bxcore::AskMemoryArgs;
use bxcore::memory::{ASK_MEMORY, MAX_QUESTION, MAX_WORKSPACE_NAME, MAX_WORKSPACES, OWN_WORKSPACE};
fn args(json: &str) -> AskMemoryArgs {
serde_json::from_str(json).unwrap_or_else(|e| panic!("{json}: {e}"))
}
fn ask(question: &str, workspaces: &[&str]) -> AskMemoryArgs {
AskMemoryArgs {
question: question.to_string(),
workspaces: workspaces.iter().map(|w| w.to_string()).collect(),
}
}
fn refused(value: &AskMemoryArgs) -> String {
value
.check()
.expect_err(&format!("{value:?} must be refused"))
}
#[test]
fn the_name_and_the_limits() {
assert_eq!(ASK_MEMORY, "ask_memory");
assert_eq!(MAX_QUESTION, 2_000);
assert_eq!(MAX_WORKSPACES, 2);
assert_eq!(MAX_WORKSPACE_NAME, 512);
assert_eq!(OWN_WORKSPACE, "own");
}
/// No `workspaces`, or an empty list, asks the agent's own workspace; both are written out with
/// every field, so two spellings of one call read the same in the audit log.
#[test]
fn without_workspaces_it_asks_own() {
for json in [
r#"{"question":"What does the owner prefer?"}"#,
r#"{"question":"What does the owner prefer?","workspaces":[]}"#,
] {
let value = args(json);
assert_eq!(value, ask("What does the owner prefer?", &[]), "{json}");
assert_eq!(value.check(), Ok(()), "{json}");
assert_eq!(value.asked(), ["own"], "{json}");
assert_eq!(
serde_json::to_string(&value).unwrap(),
r#"{"question":"What does the owner prefer?","workspaces":[]}"#
);
}
}
#[test]
fn named_workspaces_round_trip_exactly_and_are_asked_in_order() {
let json = r#"{"question":"Who wrote the launch note?","workspaces":["fleet","boxmaker1"]}"#;
let value = args(json);
assert_eq!(
value,
ask("Who wrote the launch note?", &["fleet", "boxmaker1"])
);
assert_eq!(value.check(), Ok(()));
assert_eq!(value.asked(), ["fleet", "boxmaker1"]);
assert_eq!(serde_json::to_string(&value).unwrap(), json);
let one = args(r#"{"question":"q","workspaces":["untrusted"]}"#);
assert_eq!(one.check(), Ok(()));
assert_eq!(one.asked(), ["untrusted"]);
}
/// `reasoning` was in an earlier draft of the spec and is not a field.
#[test]
fn unknown_keys_missing_questions_and_wrong_types_are_refused() {
for json in [
r#"{"question":"q","reasoning":"low"}"#,
r#"{"question":"q","workspace":"fleet"}"#,
r#"{"question":"q","k":3}"#,
r#"{"workspaces":["own"]}"#,
r#"{"question":"q","workspaces":"fleet"}"#,
r#"{"question":"q","workspaces":[1]}"#,
r#"{"question":null}"#,
] {
assert!(
serde_json::from_str::<AskMemoryArgs>(json).is_err(),
"{json}"
);
}
}
/// The limit is in characters, not bytes: each `é` is two bytes, each `日` three.
#[test]
fn the_question_has_1_to_2000_characters() {
assert_eq!(ask(&"é".repeat(2_000), &[]).check(), Ok(()));
assert_eq!(ask(&"日".repeat(2_000), &["own"]).check(), Ok(()));
assert_eq!(ask("x", &[]).check(), Ok(()));
assert_eq!(
refused(&ask(&"é".repeat(2_001), &[])),
"question has 2001 characters, over the limit of 2000; ask a shorter question"
);
assert_eq!(
refused(&ask(&"x".repeat(5_000), &["own"])),
"question has 5000 characters, over the limit of 2000; ask a shorter question"
);
for empty in ["", " ", " \n\t "] {
assert_eq!(
refused(&ask(empty, &[])),
"question is empty; write the question to ask",
"{empty:?}"
);
}
}
#[test]
fn at_most_two_workspaces_each_named_once() {
assert_eq!(ask("q", &["own", "fleet"]).check(), Ok(()));
assert_eq!(
refused(&ask("q", &["own", "untrusted", "fleet"])),
"workspaces names 3, over the limit of 2; ask about the others in another call"
);
assert_eq!(
refused(&ask("q", &["fleet", "fleet"])),
"workspaces names fleet twice; name each workspace once"
);
assert_eq!(
refused(&ask("q", &["own", "own"])),
"workspaces names own twice; name each workspace once"
);
// Names are compared as written: case is part of a Honcho name.
assert_eq!(ask("q", &["fleet", "Fleet"]).check(), Ok(()));
}
/// Only the form is checked here; whether a name is one of `[honcho]`'s workspaces is `memoryd`'s
/// check.
#[test]
fn a_workspace_name_has_1_to_512_letters_digits_underscores_or_hyphens() {
let form = "a workspace name has 1 to 512 letters, digits, _ or -; use own, untrusted, fleet or \
another agent's name";
for good in [
"own",
"boxmaker1",
"Boxmaker_1-trusted",
"x",
"-_-",
&"a".repeat(512),
] {
assert_eq!(ask("q", &[good]).check(), Ok(()), "{good}");
}
for bad in [
" ",
"own ",
" own",
"a.b",
"a/b",
"a b",
"é",
"own\n",
"a:b",
&"a".repeat(513),
] {
assert_eq!(refused(&ask("q", &[bad])), form, "{bad:?}");
assert_eq!(refused(&ask("q", &["own", bad])), form, "second: {bad:?}");
}
let empty = "a workspace name is empty; name a workspace, or leave workspaces out to ask own";
assert_eq!(refused(&ask("q", &[""])), empty);
assert_eq!(refused(&ask("q", &["fleet", ""])), empty);
}
/// The checks run in a fixed order, so a call with several faults is shown the first: the
/// question, then the number of names, then each name in turn.
#[test]
fn the_first_fault_is_the_one_reported() {
assert_eq!(
refused(&ask("", &["a", "b", "c"])),
"question is empty; write the question to ask"
);
assert_eq!(
refused(&ask("q", &["", "", ""])),
"workspaces names 3, over the limit of 2; ask about the others in another call"
);
assert_eq!(
refused(&ask("q", &["", "a.b"])),
"a workspace name is empty; name a workspace, or leave workspaces out to ask own"
);
}