2026-11-02 12:36:32 +00:00
|
|
|
//! Versioned, transport-neutral wire contracts for Runa clients and servers.
|
|
|
|
|
|
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
|
|
|
pub use runa_core::Value;
|
2026-11-02 12:36:32 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
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
|
|
|
pub const PROTOCOL_VERSION: u16 = 3;
|
2026-11-02 12:36:32 +00:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct Request {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub command: Command,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Request {
|
|
|
|
|
pub fn validate(&self) -> Result<(), ProtocolError> {
|
|
|
|
|
if self.version != PROTOCOL_VERSION {
|
|
|
|
|
return Err(ProtocolError::new(
|
|
|
|
|
ErrorCode::UnsupportedVersion,
|
|
|
|
|
format!("unsupported protocol version {}", self.version),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if self.request_id.trim().is_empty() {
|
|
|
|
|
return Err(ProtocolError::new(
|
|
|
|
|
ErrorCode::InvalidRequest,
|
|
|
|
|
"request_id must not be empty",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match &self.command {
|
|
|
|
|
Command::PublishBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
..
|
|
|
|
|
} => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_optional_revision(*expected_revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::RollbackBehavior {
|
|
|
|
|
name,
|
|
|
|
|
expected_revision,
|
|
|
|
|
target_revision,
|
|
|
|
|
} => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_revision(*expected_revision)?;
|
|
|
|
|
validate_revision(*target_revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::GetBehavior { name } => validate_name(name)?,
|
|
|
|
|
Command::GetBehaviorSource { name, revision } => {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
validate_revision(*revision)?;
|
|
|
|
|
}
|
|
|
|
|
Command::Spawn {
|
|
|
|
|
behavior_name,
|
|
|
|
|
mailbox_capacity,
|
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,
|
2026-11-02 12:36:32 +00:00
|
|
|
} => {
|
|
|
|
|
validate_name(behavior_name)?;
|
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.validate().map_err(invalid_request)?;
|
2026-11-02 12:36:32 +00:00
|
|
|
if *mailbox_capacity == 0 {
|
|
|
|
|
return Err(invalid_request("mailbox_capacity must be positive"));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-11-02 14:19:25 +00:00
|
|
|
Command::Send { actor_ref, message } => {
|
|
|
|
|
validate_actor_ref(actor_ref)?;
|
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.validate().map_err(invalid_request)?;
|
2026-11-02 14:19:25 +00:00
|
|
|
}
|
|
|
|
|
Command::InspectActor { actor_ref }
|
|
|
|
|
| Command::StopActor { actor_ref }
|
|
|
|
|
| Command::RestartActor { actor_ref }
|
|
|
|
|
| Command::PauseActor { actor_ref }
|
|
|
|
|
| Command::ResumeActor { actor_ref }
|
|
|
|
|
| Command::CancelActor { actor_ref } => validate_actor_ref(actor_ref)?,
|
2026-11-02 12:36:32 +00:00
|
|
|
Command::InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref,
|
2026-11-02 12:36:32 +00:00
|
|
|
behavior_name,
|
|
|
|
|
} => {
|
2026-11-02 14:19:25 +00:00
|
|
|
validate_actor_ref(actor_ref)?;
|
2026-11-02 12:36:32 +00:00
|
|
|
validate_name(behavior_name)?;
|
|
|
|
|
}
|
|
|
|
|
Command::Tick {
|
|
|
|
|
instruction_budget, ..
|
|
|
|
|
} if *instruction_budget == 0 => {
|
|
|
|
|
return Err(invalid_request("instruction_budget must be positive"));
|
|
|
|
|
}
|
|
|
|
|
Command::GetBehaviors
|
|
|
|
|
| Command::ListActors
|
|
|
|
|
| Command::GetEvents
|
|
|
|
|
| Command::Tick { .. } => {}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "operation", content = "parameters", rename_all = "snake_case")]
|
|
|
|
|
pub enum Command {
|
|
|
|
|
PublishBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
expected_revision: Option<u64>,
|
|
|
|
|
source: String,
|
|
|
|
|
},
|
|
|
|
|
RollbackBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
expected_revision: u64,
|
|
|
|
|
target_revision: u64,
|
|
|
|
|
},
|
|
|
|
|
GetBehavior {
|
|
|
|
|
name: String,
|
|
|
|
|
},
|
|
|
|
|
GetBehaviors,
|
|
|
|
|
GetBehaviorSource {
|
|
|
|
|
name: String,
|
|
|
|
|
revision: u64,
|
|
|
|
|
},
|
|
|
|
|
Spawn {
|
|
|
|
|
behavior_name: String,
|
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,
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: usize,
|
|
|
|
|
},
|
|
|
|
|
Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
message: MessageDto,
|
|
|
|
|
},
|
|
|
|
|
Tick {
|
|
|
|
|
ticks: usize,
|
|
|
|
|
instruction_budget: usize,
|
|
|
|
|
},
|
|
|
|
|
InspectActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
ListActors,
|
|
|
|
|
GetEvents,
|
|
|
|
|
StopActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
RestartActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
PauseActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
ResumeActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
CancelActor {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
InstallBehavior {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
behavior_name: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct Response {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub request_id: String,
|
|
|
|
|
pub result: Result<Reply, ProtocolError>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
|
|
|
|
|
pub enum Reply {
|
|
|
|
|
Acknowledged,
|
|
|
|
|
Behavior(BehaviorDto),
|
|
|
|
|
Behaviors(Vec<BehaviorDto>),
|
|
|
|
|
BehaviorSource { source: String },
|
2026-11-02 14:19:25 +00:00
|
|
|
ActorSpawned { actor_ref: String },
|
2026-11-02 12:36:32 +00:00
|
|
|
Actor(ActorSnapshotDto),
|
|
|
|
|
Actors(Vec<ActorSnapshotDto>),
|
|
|
|
|
Events(Vec<EventDto>),
|
|
|
|
|
TickCompleted { ticks: usize },
|
|
|
|
|
BehaviorInstalled { implementation_id: String },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ProtocolEvent {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub event: EventDto,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
|
|
|
|
|
pub enum EventDto {
|
|
|
|
|
Spawned {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Committed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Failed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
message: String,
|
|
|
|
|
},
|
|
|
|
|
Stopped {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Restarted {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Paused {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
Resumed {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
InvocationCancelled {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
BehaviorInstalled {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
implementation_id: String,
|
|
|
|
|
},
|
|
|
|
|
StateMigrated {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: String,
|
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
|
|
|
previous_state: Value,
|
|
|
|
|
current_state: Value,
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct BehaviorDto {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub revision: u64,
|
|
|
|
|
pub current_implementation_id: String,
|
|
|
|
|
pub history: Vec<BehaviorVersionDto>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct BehaviorVersionDto {
|
|
|
|
|
pub revision: u64,
|
|
|
|
|
pub implementation_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
pub type MessageDto = Value;
|
2026-11-02 12:36:32 +00:00
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ActorSnapshotDto {
|
2026-11-02 14:19:25 +00:00
|
|
|
pub actor_ref: String,
|
2026-11-02 12:36:32 +00:00
|
|
|
pub mailbox_id: u64,
|
|
|
|
|
pub implementation_id: String,
|
|
|
|
|
pub incarnation: u64,
|
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
|
|
|
pub state: Value,
|
2026-11-02 12:36:32 +00:00
|
|
|
pub status: ActorStatusDto,
|
|
|
|
|
pub execution: Option<ExecutionSnapshotDto>,
|
|
|
|
|
pub queued: Vec<MessageDto>,
|
|
|
|
|
pub reserved: Option<MessageDto>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(tag = "status", content = "message", rename_all = "snake_case")]
|
|
|
|
|
pub enum ActorStatusDto {
|
|
|
|
|
Idle,
|
|
|
|
|
Running,
|
|
|
|
|
Awaiting,
|
|
|
|
|
Paused,
|
|
|
|
|
Stopped,
|
|
|
|
|
Failed(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ExecutionSnapshotDto {
|
|
|
|
|
pub vm_steps: u64,
|
|
|
|
|
pub instruction_pointer: usize,
|
|
|
|
|
pub call_depth: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum ErrorCode {
|
|
|
|
|
InvalidRequest,
|
|
|
|
|
UnsupportedVersion,
|
|
|
|
|
NotFound,
|
|
|
|
|
Conflict,
|
|
|
|
|
CapacityExceeded,
|
|
|
|
|
RuntimeFailure,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(deny_unknown_fields)]
|
|
|
|
|
pub struct ProtocolError {
|
|
|
|
|
pub code: ErrorCode,
|
|
|
|
|
pub message: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProtocolError {
|
|
|
|
|
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
code,
|
|
|
|
|
message: message.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_name(name: &str) -> Result<(), ProtocolError> {
|
|
|
|
|
if name.trim().is_empty() {
|
|
|
|
|
return Err(invalid_request("behavior name must not be empty"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:19:25 +00:00
|
|
|
fn validate_actor_ref(actor_ref: &str) -> Result<(), ProtocolError> {
|
|
|
|
|
if actor_ref.len() != 21
|
|
|
|
|
|| !actor_ref
|
|
|
|
|
.bytes()
|
|
|
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
|
|
|
|
|
{
|
|
|
|
|
return Err(invalid_request(
|
|
|
|
|
"actor_ref must be a 21-character URL-safe identifier",
|
|
|
|
|
));
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_revision(revision: u64) -> Result<(), ProtocolError> {
|
|
|
|
|
if revision == 0 {
|
|
|
|
|
return Err(invalid_request("revision must be positive"));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_optional_revision(revision: Option<u64>) -> Result<(), ProtocolError> {
|
|
|
|
|
if let Some(revision) = revision {
|
|
|
|
|
validate_revision(revision)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn invalid_request(message: impl Into<String>) -> ProtocolError {
|
|
|
|
|
ProtocolError::new(ErrorCode::InvalidRequest, message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
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
|
|
|
fn test_message(kind: i64, value: i64, target: Option<runa_core::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 12:36:32 +00:00
|
|
|
fn request(command: Command) -> Request {
|
|
|
|
|
Request {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "req-1".to_owned(),
|
|
|
|
|
command,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn request_uses_a_stable_tagged_wire_shape() {
|
|
|
|
|
let value = serde_json::to_value(request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "xK8_pQ2-Lm5_012345678".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
|
|
|
message: Value::Integer(9),
|
2026-11-02 12:36:32 +00:00
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
value,
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"version": PROTOCOL_VERSION,
|
|
|
|
|
"request_id": "req-1",
|
|
|
|
|
"command": {
|
|
|
|
|
"operation": "send",
|
|
|
|
|
"parameters": {
|
2026-11-02 14:19:25 +00:00
|
|
|
"actor_ref": "xK8_pQ2-Lm5_012345678",
|
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": { "type": "integer", "data": 9 }
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn requests_validate_versions_and_required_positive_values() {
|
|
|
|
|
let mut request = 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(0),
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: 1,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(request.validate(), Ok(()));
|
|
|
|
|
|
|
|
|
|
request.version += 1;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::UnsupportedVersion
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
request.version = PROTOCOL_VERSION;
|
|
|
|
|
request.command = 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(0),
|
2026-11-02 12:36:32 +00:00
|
|
|
mailbox_capacity: 0,
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-11-02 14:19:25 +00:00
|
|
|
fn requests_reject_empty_correlation_ids_and_malformed_actor_refs() {
|
2026-11-02 12:36:32 +00:00
|
|
|
let mut request = request(Command::GetEvents);
|
|
|
|
|
request.request_id = " ".to_owned();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
request.request_id = "req-2".to_owned();
|
2026-11-02 14:19:25 +00:00
|
|
|
request.command = Command::StopActor {
|
|
|
|
|
actor_ref: String::new(),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
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
|
|
|
fn nested_references_round_trip_and_malformed_references_are_rejected() {
|
|
|
|
|
let request = request(Command::Send {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "xK8_pQ2-Lm5_012345678".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
|
|
|
message: test_message(2, 9, runa_core::ActorRef::parse("Hj2_9sM0-pQ4_7aBcD3eF")),
|
2026-11-02 14:19:25 +00:00
|
|
|
});
|
|
|
|
|
assert_eq!(request.validate(), Ok(()));
|
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
|
|
|
let encoded = serde_json::to_string(&request).unwrap();
|
|
|
|
|
assert_eq!(serde_json::from_str::<Request>(&encoded).unwrap(), request);
|
|
|
|
|
assert!(serde_json::from_str::<Value>(r#"{"type":"actor_ref","data":"bad"}"#).is_err());
|
2026-11-02 12:36:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn response_round_trips_with_typed_error_or_reply() {
|
|
|
|
|
let response = Response {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "req-1".to_owned(),
|
|
|
|
|
result: Err(ProtocolError::new(ErrorCode::NotFound, "unknown actor")),
|
|
|
|
|
};
|
|
|
|
|
let encoded = serde_json::to_string(&response).unwrap();
|
|
|
|
|
let decoded: Response = serde_json::from_str(&encoded).unwrap();
|
|
|
|
|
assert_eq!(decoded, response);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[test]
|
|
|
|
|
fn structured_values_have_unambiguous_wire_types_and_reject_invalid_data() {
|
|
|
|
|
let reference = runa_core::ActorRef::new();
|
|
|
|
|
let value = Value::Record(std::collections::BTreeMap::from([(
|
|
|
|
|
"values".to_owned(),
|
|
|
|
|
Value::List(vec![
|
|
|
|
|
Value::Null,
|
|
|
|
|
Value::Bool(false),
|
|
|
|
|
Value::Integer(i64::MAX),
|
|
|
|
|
Value::String(reference.to_string()),
|
|
|
|
|
Value::ActorRef(reference),
|
|
|
|
|
]),
|
|
|
|
|
)]));
|
|
|
|
|
let encoded = serde_json::to_string(&value).unwrap();
|
|
|
|
|
assert_eq!(serde_json::from_str::<Value>(&encoded).unwrap(), value);
|
|
|
|
|
for encoded in [
|
|
|
|
|
r#"{"type":"integer","data":1.5}"#,
|
|
|
|
|
r#"{"type":"integer","data":9223372036854775808}"#,
|
|
|
|
|
r#"{"type":"actor_ref","data":"invalid"}"#,
|
|
|
|
|
r#"{"type":"unknown","data":1}"#,
|
|
|
|
|
r#"{"type":"bool","data":true,"extra":1}"#,
|
|
|
|
|
] {
|
|
|
|
|
assert!(serde_json::from_str::<Value>(encoded).is_err(), "{encoded}");
|
|
|
|
|
}
|
|
|
|
|
let invalid = Value::List(vec![Value::Null; runa_core::MAX_VALUE_NODES]);
|
|
|
|
|
let spawn = request(Command::Spawn {
|
|
|
|
|
behavior_name: "echo".to_owned(),
|
|
|
|
|
state: invalid.clone(),
|
|
|
|
|
mailbox_capacity: 2,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(
|
|
|
|
|
spawn.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::InvalidRequest
|
|
|
|
|
);
|
|
|
|
|
let send = request(Command::Send {
|
|
|
|
|
actor_ref: reference.to_string(),
|
|
|
|
|
message: invalid,
|
|
|
|
|
});
|
|
|
|
|
assert_eq!(send.validate().unwrap_err().code, ErrorCode::InvalidRequest);
|
|
|
|
|
let mut old = request(Command::ListActors);
|
|
|
|
|
old.version = 2;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
old.validate().unwrap_err().code,
|
|
|
|
|
ErrorCode::UnsupportedVersion
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 12:36:32 +00:00
|
|
|
#[test]
|
|
|
|
|
fn event_envelopes_are_versioned_and_serializable() {
|
|
|
|
|
let event = ProtocolEvent {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
event: EventDto::StateMigrated {
|
2026-11-02 14:19:25 +00:00
|
|
|
actor_ref: "actor_ref_000000000000".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
|
|
|
previous_state: Value::Integer(4),
|
|
|
|
|
current_state: Value::Integer(40),
|
2026-11-02 12:36:32 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let encoded = serde_json::to_string(&event).unwrap();
|
|
|
|
|
let decoded: ProtocolEvent = serde_json::from_str(&encoded).unwrap();
|
|
|
|
|
assert_eq!(decoded, event);
|
|
|
|
|
}
|
|
|
|
|
}
|