feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
use runa_protocol::Value;
|
2026-11-02 13:50:30 +00:00
|
|
|
use std::io::{BufRead, BufReader, Write};
|
|
|
|
|
use std::net::{Shutdown, SocketAddr, TcpStream};
|
|
|
|
|
use std::process::{Child, Command as ProcessCommand, Stdio};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2026-11-02 14:57:26 +00:00
|
|
|
use runa_client::{LocalClient, TcpClient};
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
use runa_protocol::{Command, PROTOCOL_VERSION, Reply, Request, Response};
|
2026-11-02 13:50:30 +00:00
|
|
|
|
|
|
|
|
struct ServerProcess(Child);
|
|
|
|
|
|
|
|
|
|
impl Drop for ServerProcess {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _ = self.0.kill();
|
|
|
|
|
let _ = self.0.wait();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-02 13:40:56 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn local_client_launches_server_and_operates_the_same_live_image() {
|
|
|
|
|
let mut client = LocalClient::spawn(env!("CARGO_BIN_EXE_runa-server"), &[] as &[&str]).unwrap();
|
|
|
|
|
|
|
|
|
|
let published = client
|
|
|
|
|
.request(Command::PublishBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: None,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
source: "pub fn handle(state, message) { (state + message.value, []) }".to_owned(),
|
2026-11-02 13:40:56 +00:00
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(published.request_id, "runa-client-1");
|
|
|
|
|
assert!(matches!(published.result, Ok(Reply::Behavior(_))));
|
|
|
|
|
|
|
|
|
|
let spawned = client
|
|
|
|
|
.request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(5),
|
2026-11-02 13:40:56 +00:00
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(spawned.request_id, "runa-client-2");
|
2026-11-02 14:19:25 +00:00
|
|
|
let actor_ref = match spawned.result {
|
|
|
|
|
Ok(Reply::ActorSpawned { actor_ref }) => actor_ref,
|
|
|
|
|
other => panic!("unexpected spawn response: {other:?}"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(actor_ref.len(), 21);
|
2026-11-02 13:40:56 +00:00
|
|
|
|
|
|
|
|
client
|
|
|
|
|
.request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: actor_ref.clone(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message: test_message(0, 7, None),
|
2026-11-02 13:40:56 +00:00
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
client
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 100,
|
|
|
|
|
instruction_budget: 10,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
2026-11-02 14:19:25 +00:00
|
|
|
let inspected = client.request(Command::InspectActor { actor_ref }).unwrap();
|
2026-11-02 13:40:56 +00:00
|
|
|
assert!(matches!(
|
|
|
|
|
inspected.result,
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
Ok(Reply::Actor(actor)) if actor.state == Value::Integer(12)
|
2026-11-02 13:40:56 +00:00
|
|
|
));
|
|
|
|
|
assert!(client.shutdown().unwrap().success());
|
|
|
|
|
}
|
2026-11-02 13:50:30 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn tcp_server_accepts_local_protocol_requests() {
|
|
|
|
|
let child = ProcessCommand::new(env!("CARGO_BIN_EXE_runa-server"))
|
|
|
|
|
.args(["--listen", "127.0.0.1:0"])
|
|
|
|
|
.stdout(Stdio::null())
|
|
|
|
|
.stderr(Stdio::piped())
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut server = ServerProcess(child);
|
|
|
|
|
let stderr = server.0.stderr.take().unwrap();
|
|
|
|
|
let mut stderr = BufReader::new(stderr);
|
|
|
|
|
let mut startup = String::new();
|
|
|
|
|
stderr.read_line(&mut startup).unwrap();
|
|
|
|
|
let address = startup
|
|
|
|
|
.strip_prefix("runa-server: listening on ")
|
|
|
|
|
.expect("server should report its bound address")
|
|
|
|
|
.trim()
|
|
|
|
|
.parse::<SocketAddr>()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut client = TcpStream::connect_timeout(&address, Duration::from_secs(2)).unwrap();
|
|
|
|
|
let request = Request {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "tcp-process-test".to_owned(),
|
|
|
|
|
command: Command::ListActors,
|
|
|
|
|
};
|
|
|
|
|
writeln!(client, "{}", serde_json::to_string(&request).unwrap()).unwrap();
|
|
|
|
|
client.shutdown(Shutdown::Write).unwrap();
|
|
|
|
|
|
|
|
|
|
let mut response_line = String::new();
|
|
|
|
|
BufReader::new(client)
|
|
|
|
|
.read_line(&mut response_line)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let response: Response = serde_json::from_str(&response_line).unwrap();
|
|
|
|
|
assert_eq!(response.request_id, "tcp-process-test");
|
|
|
|
|
assert_eq!(response.result, Ok(Reply::Actors(Vec::new())));
|
2026-11-02 14:57:26 +00:00
|
|
|
|
|
|
|
|
let mut first = TcpClient::connect(address).unwrap();
|
|
|
|
|
let mut second = TcpClient::connect(address).unwrap();
|
|
|
|
|
let actor_ref = match first
|
|
|
|
|
.request(Command::Spawn {
|
|
|
|
|
behavior_name: "counter".to_owned(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
state: Value::Integer(5),
|
2026-11-02 14:57:26 +00:00
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap()
|
|
|
|
|
{
|
|
|
|
|
Reply::ActorSpawned { actor_ref } => actor_ref,
|
|
|
|
|
other => panic!("unexpected spawn response: {other:?}"),
|
|
|
|
|
};
|
|
|
|
|
assert!(matches!(second.request(Command::InspectActor {
|
|
|
|
|
actor_ref: actor_ref.clone(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
}).unwrap().result, Ok(Reply::Actor(actor)) if actor.state == Value::Integer(5)));
|
2026-11-02 14:57:26 +00:00
|
|
|
assert!(matches!(
|
|
|
|
|
first
|
|
|
|
|
.request(Command::RollbackBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
expected_revision: 1,
|
|
|
|
|
target_revision: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result,
|
|
|
|
|
Ok(Reply::Behavior(behavior)) if behavior.revision == 2
|
|
|
|
|
));
|
|
|
|
|
drop(first);
|
|
|
|
|
drop(second);
|
|
|
|
|
|
|
|
|
|
let mut reconnected = TcpClient::connect(address).unwrap();
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::GetBehavior {
|
|
|
|
|
name: "counter".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result,
|
|
|
|
|
Ok(Reply::Behavior(behavior)) if behavior.revision == 2
|
|
|
|
|
));
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor_ref.clone(),
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
message: Value::Record(std::collections::BTreeMap::from([
|
|
|
|
|
("op".to_owned(), Value::String("add".to_owned())),
|
|
|
|
|
("value".to_owned(), Value::Integer(7)),
|
|
|
|
|
])),
|
2026-11-02 14:57:26 +00:00
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap();
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 100,
|
|
|
|
|
instruction_budget: 10,
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(reconnected.request(Command::InspectActor {
|
feat!: add structured actor state and messages
Introduce owned portable values with distinct actor references, bounded Rune conversion, JSON REPL literals, and nested reference completion. Preserve isolation and atomic transitions across state, messages, effects, migration, inspection, and TCP clients.
BREAKING CHANGE: protocol v3 replaces integer state and kind/value/target_ref messages with tagged values. Rune handlers now take (state, message) and return (new_state, [(actor_reference, message)]). The REPL send command accepts one JSON message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-02 15:56:03 +00:00
|
|
|
actor_ref: actor_ref.clone(),
|
|
|
|
|
}).unwrap().result, Ok(Reply::Actor(actor)) if actor.state == Value::Integer(12)));
|
|
|
|
|
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::PublishBehavior {
|
|
|
|
|
name: "echo".to_owned(),
|
|
|
|
|
expected_revision: None,
|
|
|
|
|
source: "pub fn handle(state, message) { (message, []) }".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap();
|
|
|
|
|
let nested = Value::Record(std::collections::BTreeMap::from([(
|
|
|
|
|
"items".to_owned(),
|
|
|
|
|
Value::List(vec![
|
|
|
|
|
Value::Null,
|
|
|
|
|
Value::Bool(true),
|
|
|
|
|
Value::Integer(-42),
|
|
|
|
|
Value::String("hello world".to_owned()),
|
|
|
|
|
Value::ActorRef(runa_actor::ActorRef::parse(&actor_ref).unwrap()),
|
|
|
|
|
]),
|
|
|
|
|
)]));
|
|
|
|
|
let echo = match reconnected
|
|
|
|
|
.request(Command::Spawn {
|
|
|
|
|
behavior_name: "echo".to_owned(),
|
|
|
|
|
state: nested.clone(),
|
|
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap()
|
|
|
|
|
{
|
|
|
|
|
Reply::ActorSpawned { actor_ref } => actor_ref,
|
|
|
|
|
other => panic!("unexpected spawn: {other:?}"),
|
|
|
|
|
};
|
|
|
|
|
assert!(matches!(reconnected.request(Command::InspectActor {
|
|
|
|
|
actor_ref: echo.clone(),
|
|
|
|
|
}).unwrap().result, Ok(Reply::Actor(actor)) if actor.state == nested));
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: echo.clone(),
|
|
|
|
|
message: nested.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap();
|
|
|
|
|
reconnected
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 100,
|
|
|
|
|
instruction_budget: 10,
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
.result
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(matches!(reconnected.request(Command::InspectActor {
|
|
|
|
|
actor_ref: echo,
|
|
|
|
|
}).unwrap().result, Ok(Reply::Actor(actor)) if actor.state == nested && actor.reserved.is_none()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn test_message(kind: i64, value: i64, target: Option<runa_actor::ActorRef>) -> Value {
|
|
|
|
|
Value::Record(std::collections::BTreeMap::from([
|
|
|
|
|
("kind".to_owned(), Value::Integer(kind)),
|
|
|
|
|
("value".to_owned(), Value::Integer(value)),
|
|
|
|
|
(
|
|
|
|
|
"target".to_owned(),
|
|
|
|
|
target.map_or(Value::Null, Value::ActorRef),
|
|
|
|
|
),
|
|
|
|
|
]))
|
2026-11-02 13:50:30 +00:00
|
|
|
}
|