Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions crates/aurorality-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,33 @@ mod tests {
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["ok"], false);
}

#[test]
fn matrix_list_uses_single_envelope() {
let result = plugin_invoke("matrix".into(), "list".into(), "{}".into()).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["ok"], true);
assert!(v["data"].is_array(), "expected a message array, got {v}");
assert!(v["data"].get("ok").is_none());
}

#[test]
fn matrix_send_empty_uses_single_envelope() {
let result = plugin_invoke("matrix".into(), "send".into(), "{}".into()).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["ok"], true);
assert_eq!(v["data"]["accepted"], false);
assert_eq!(v["data"]["reason"], "empty");
assert!(v["data"].get("data").is_none());
}

#[test]
fn stalwart_send_empty_uses_single_envelope() {
let result = plugin_invoke("stalwart".into(), "send".into(), "{}".into()).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["ok"], true);
assert_eq!(v["data"]["accepted"], false);
assert_eq!(v["data"]["reason"], "empty");
assert!(v["data"].get("data").is_none());
}
}
104 changes: 59 additions & 45 deletions crates/aurorality-core/src/mutations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,23 @@ fn int_path_field(m: &Value, field: &str) -> Result<Vec<usize>, String> {

// ── Tree operations ───────────────────────────────────────────────────────────

// NOTE: The raw pointer casts below (`as *mut Vec<Value>`) are needed to work
// around the borrow checker when recursing through `serde_json::Value`. They
// are safe because we only ever follow a single path through the tree at a time
// and never create aliasing mutable references.

#[allow(clippy::ptr_arg)]
fn replace_at(nodes: &mut Vec<Value>, path: &[usize], replacement: Value) {
let Some(&first) = path.first() else { return };
fn children_mut(node: &mut Value) -> Option<&mut Vec<Value>> {
node.get_mut("children")?.as_array_mut()
}

fn replace_at(nodes: &mut [Value], path: &[usize], replacement: Value) {
let Some((&first, rest)) = path.split_first() else {
return;
};
if first >= nodes.len() {
return;
}
if path.len() == 1 {
if rest.is_empty() {
nodes[first] = replacement;
} else {
let children = nodes[first]["children"]
.as_array_mut()
.map(|c| c as *mut Vec<Value>);
if let Some(children) = children {
// SAFETY: The raw pointer derives from an `&mut Vec<Value>` obtained
// via `as_array_mut()`. We follow a single path through the tree,
// never creating aliasing mutable references to the same Vec.
replace_at(unsafe { &mut *children }, &path[1..], replacement);
}
return;
}
if let Some(children) = children_mut(&mut nodes[first]) {
replace_at(children, rest, replacement);
}
}

Expand All @@ -119,56 +113,46 @@ fn insert_at(nodes: &mut Vec<Value>, parent_path: &[usize], idx: usize, node: Va
nodes.insert(safe_idx, node);
return;
}
let Some(&first) = parent_path.first() else {
let Some((&first, rest)) = parent_path.split_first() else {
return;
};
if first >= nodes.len() {
return;
}
let children = nodes[first]["children"]
.as_array_mut()
.map(|c| c as *mut Vec<Value>);
if let Some(children) = children {
// SAFETY: Same pattern as replace_at — single path through the tree,
// no aliasing mutable references.
insert_at(unsafe { &mut *children }, &parent_path[1..], idx, node);
if let Some(children) = children_mut(&mut nodes[first]) {
insert_at(children, rest, idx, node);
}
}

fn remove_at(nodes: &mut Vec<Value>, path: &[usize]) {
let Some(&first) = path.first() else { return };
let Some((&first, rest)) = path.split_first() else {
return;
};
if first >= nodes.len() {
return;
}
if path.len() == 1 {
if rest.is_empty() {
nodes.remove(first);
return;
}
let children = nodes[first]["children"]
.as_array_mut()
.map(|c| c as *mut Vec<Value>);
if let Some(children) = children {
// SAFETY: Same pattern — single path, no aliasing mutable references.
remove_at(unsafe { &mut *children }, &path[1..]);
if let Some(children) = children_mut(&mut nodes[first]) {
remove_at(children, rest);
}
}

#[allow(clippy::ptr_arg)]
fn update_field_at(nodes: &mut Vec<Value>, path: &[usize], field: &str, value: Value) {
let Some(&first) = path.first() else { return };
fn update_field_at(nodes: &mut [Value], path: &[usize], field: &str, value: Value) {
let Some((&first, rest)) = path.split_first() else {
return;
};
if first >= nodes.len() {
return;
}
if path.len() == 1 {
if rest.is_empty() {
nodes[first][field] = value;
return;
}
let children = nodes[first]["children"]
.as_array_mut()
.map(|c| c as *mut Vec<Value>);
if let Some(children) = children {
// SAFETY: Same pattern — single path, no aliasing mutable references.
update_field_at(unsafe { &mut *children }, &path[1..], field, value);
if let Some(children) = children_mut(&mut nodes[first]) {
update_field_at(children, rest, field, value);
}
}

Expand Down Expand Up @@ -221,4 +205,34 @@ mod tests {
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["root"][0]["content"], "hello");
}

fn nested_ir() -> &'static str {
r#"{"version":3,"root":[{"kind":"stack","children":[{"kind":"text","content":"inner"}]}]}"#
}

#[test]
fn nested_replace_update_insert_remove() {
let muts = r#"[{"op":"updateText","path":[0,0],"content":"updated"}]"#;
let result = apply(nested_ir(), muts).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["root"][0]["children"][0]["content"], "updated");

let muts =
r#"[{"op":"replaceNode","path":[0,0],"node":{"kind":"text","content":"replaced"}}]"#;
let result = apply(&result, muts).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["root"][0]["children"][0]["content"], "replaced");

let muts = r#"[{"op":"insertNode","parentPath":[0],"index":1,"node":{"kind":"text","content":"second"}}]"#;
let result = apply(&result, muts).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["root"][0]["children"].as_array().unwrap().len(), 2);
assert_eq!(v["root"][0]["children"][1]["content"], "second");

let muts = r#"[{"op":"removeNode","path":[0,0]}]"#;
let result = apply(&result, muts).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(v["root"][0]["children"].as_array().unwrap().len(), 1);
assert_eq!(v["root"][0]["children"][0]["content"], "second");
}
}
4 changes: 2 additions & 2 deletions crates/aurorality-core/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ pub fn store_path(bundle_id: String) -> String {

/// Read one JSON value by key; returns `None` when missing.
pub fn store_get(bundle_id: String, key: String) -> Result<Option<String>, AurorError> {
let _g = STORE_LOCK.lock().unwrap();
let _g = STORE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = application_support_file(bundle_id.trim());
let map = load_map(&path)?;
Ok(map.get(&key).map(|v| v.to_string()))
}

/// Upsert a JSON value (must parse as [`serde_json::Value`]).
pub fn store_set(bundle_id: String, key: String, json: String) -> Result<(), AurorError> {
let _g = STORE_LOCK.lock().unwrap();
let _g = STORE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let path = application_support_file(bundle_id.trim());
let mut map = load_map(&path)?;
let parsed: serde_json::Value =
Expand Down
25 changes: 8 additions & 17 deletions crates/aurorality-core/src/transport/matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
//! - `MATRIX_ROOM_ID` — target room ID (e.g. `!abc123:matrix.org`)

use crate::bridge::NativePlugin;
use crate::transport::{
envelope_err, envelope_ok, TransportHealth, TransportInfo, TransportMessage,
};
use crate::transport::{TransportHealth, TransportInfo, TransportMessage};

use serde::Deserialize;
use serde_json::Value;
Expand Down Expand Up @@ -226,30 +224,23 @@ impl NativePlugin for MatrixClient {
}
"list" => {
if !self.configured() {
return Ok(envelope_ok(serde_json::json!([])));
}
match self.sync_messages() {
Ok(msgs) => Ok(envelope_ok(serde_json::to_value(&msgs).unwrap_or_default())),
Err(e) => Ok(envelope_err(&e)),
return Ok(serde_json::json!([]));
}
self.sync_messages()
.map(|msgs| serde_json::to_value(&msgs).unwrap_or_default())
}
"send" => {
let text = payload.get("text").and_then(|v| v.as_str()).unwrap_or("");
if text.is_empty() {
return Ok(envelope_ok(
serde_json::json!({"accepted": false, "reason": "empty"}),
));
return Ok(serde_json::json!({"accepted": false, "reason": "empty"}));
}
if !self.configured() {
return Ok(envelope_ok(serde_json::json!({
return Ok(serde_json::json!({
"accepted": false,
"reason": "matrix not configured"
})));
}
match self.send_message(text) {
Ok(resp) => Ok(envelope_ok(resp)),
Err(e) => Ok(envelope_err(&e)),
}));
}
self.send_message(text)
}
_ => Err(format!("unknown matrix method: {method}")),
}
Expand Down
9 changes: 0 additions & 9 deletions crates/aurorality-core/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,3 @@ impl TransportHealth {
}
}
}

/// Helper: build a JSON envelope the plugin bridge expects.
pub fn envelope_ok(data: serde_json::Value) -> serde_json::Value {
serde_json::json!({ "ok": true, "data": data })
}

pub fn envelope_err(msg: &str) -> serde_json::Value {
serde_json::json!({ "ok": false, "error": msg })
}
25 changes: 8 additions & 17 deletions crates/aurorality-core/src/transport/stalwart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
//! If credentials are missing the adapter reports `connected: false` but won't error.

use crate::bridge::NativePlugin;
use crate::transport::{
envelope_err, envelope_ok, TransportHealth, TransportInfo, TransportMessage,
};
use crate::transport::{TransportHealth, TransportInfo, TransportMessage};

use serde_json::Value;

Expand Down Expand Up @@ -270,14 +268,12 @@ impl StalwartClient {

fn handle_list(&mut self) -> Result<Value, String> {
match self.list_messages() {
Ok(messages) => Ok(envelope_ok(
serde_json::to_value(&messages).unwrap_or_default(),
)),
Ok(messages) => Ok(serde_json::to_value(&messages).unwrap_or_default()),
Err(e) => {
if !self.configured() {
Ok(envelope_ok(serde_json::json!([])))
Ok(serde_json::json!([]))
} else {
Ok(envelope_err(&e))
Err(e)
}
}
}
Expand All @@ -286,20 +282,15 @@ impl StalwartClient {
fn handle_send(&mut self, payload: &Value) -> Result<Value, String> {
let text = payload.get("text").and_then(|v| v.as_str()).unwrap_or("");
if text.is_empty() {
return Ok(envelope_ok(
serde_json::json!({"accepted": false, "reason": "empty"}),
));
return Ok(serde_json::json!({"accepted": false, "reason": "empty"}));
}
if !self.configured() {
return Ok(envelope_ok(serde_json::json!({
return Ok(serde_json::json!({
"accepted": false,
"reason": "stalwart not configured"
})));
}
match self.archive_message(text) {
Ok(resp) => Ok(envelope_ok(resp)),
Err(e) => Ok(envelope_err(&e)),
}));
}
self.archive_message(text)
}
}

Expand Down
10 changes: 6 additions & 4 deletions crates/aurorality-core/tests/stalwart_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ mod stalwart_integration {
.expect("STALWART_USERNAME and STALWART_PASSWORD must be set for integration tests");

let result = client.invoke("list", &serde_json::json!({})).unwrap();
let envelope: Value = serde_json::from_value(result).unwrap();
let messages: Value = serde_json::from_value(result).unwrap();

// Should be a valid JSON-RPC-style response
assert_eq!(envelope["ok"], true, "list should succeed: {envelope:?}");
assert!(envelope["data"].is_array(), "data should be an array");
// NativePlugin::invoke returns the payload; plugin_invoke wraps `{ ok, data }`.
assert!(
messages.is_array(),
"list should return a message array: {messages:?}"
);
}

#[test]
Expand Down
Loading