Explain this: the local model reads delivery failures, verdicts, logs and settings #56

Merged
jcoffey-dev merged 2 commits from feat/ai-explain into main 2026-09-26 08:30:45 +00:00
2 changed files with 32 additions and 4 deletions
Showing only changes of commit 866d7d3ed5 - Show all commits
+8 -3
View File
@@ -36,7 +36,7 @@ use registry::{
jmap::IntoValue, jmap::IntoValue,
schema::{ schema::{
enums::SpamClassifyResult, enums::SpamClassifyResult,
prelude::ObjectType, prelude::{OBJ_SINGLETON, Object, ObjectType},
structs::{QueuedMessage, QueuedRecipient, RecipientStatus}, structs::{QueuedMessage, QueuedRecipient, RecipientStatus},
}, },
types::{EnumImpl, id::ObjectId}, types::{EnumImpl, id::ObjectId},
@@ -431,8 +431,13 @@ a negative one toward legitimate mail. The result follows the total against the
let Ok(id) = Id::from_str(id) else { let Ok(id) = Id::from_str(id) else {
return Ok(Err(not_found())); return Ok(Err(not_found()));
}; };
let Some(stored) = server.registry().get(ObjectId::new(object_type, id)).await? else { // A singleton never saved holds its defaults, as its /get shows it
return Ok(Err(not_found())); let stored = match server.registry().get(ObjectId::new(object_type, id)).await? {
Some(stored) => stored,
None if id.is_singleton() && object_type.flags() & OBJ_SINGLETON != 0 => {
Object::from(object_type)
}
None => return Ok(Err(not_found())),
}; };
let stored = serde_json::to_value(stored.into_value()).unwrap_or_default(); let stored = serde_json::to_value(stored.into_value()).unwrap_or_default();
let current = stored.get(property.as_str()).cloned().unwrap_or(serde_json::Value::Null); let current = stored.get(property.as_str()).cloned().unwrap_or(serde_json::Value::Null);
+24 -1
View File
@@ -25,7 +25,7 @@ use registry::{
Role, Role,
}, },
}, },
types::EnumImpl, types::{EnumImpl, id::ObjectId},
}; };
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::time::Duration; use std::time::Duration;
@@ -95,6 +95,29 @@ pub async fn test(test: &mut TestServer) {
assert!(user.contains("Current value: true"), "{user}"); assert!(user.contains("Current value: true"), "{user}");
assert!(user.contains("-----BEGIN DETAILS "), "{user}"); assert!(user.contains("-----BEGIN DETAILS "), "{user}");
// A singleton never saved is explained with its defaults, as /get shows it
let spam_settings = ObjectId::new(ObjectType::SpamSettings, Id::singleton());
assert!(
test.server
.registry()
.get(spam_settings)
.await
.unwrap()
.is_none(),
"x:SpamSettings is stored; pick a singleton the suite never saves"
);
stub.set(Mode::Answer("Mail scoring this much is spam.".into()));
let (created, failed) = admin
.explain(json!({"@type": "Setting", "object": "x:SpamSettings",
"id": "singleton", "property": "scoreSpam"}))
.await;
created.unwrap_or_else(|| panic!("unsaved singleton: {failed}"));
let (_, user) = messages(&stub.last().1);
assert!(
user.contains("Current value: 5"),
"unsaved singleton: {user}"
);
// Acceptance test 7: a secret setting is refused, not masked (EX-9) // Acceptance test 7: a secret setting is refused, not masked (EX-9)
let before = stub.count(); let before = stub.count();
for (object, property) in [("x:AiModel", "httpAuth"), ("x:AcmeProvider", "accountKey")] { for (object, property) in [("x:AiModel", "httpAuth"), ("x:AcmeProvider", "accountKey")] {