use super::*;
use crate::actions::verbs::edit;
use crate::monitor::Verb;
use crate::opslog::Origin;
use crate::projects::join::JoinState;
use crate::start::{BallSpec, Payload, Prepared};
use std::path::PathBuf;
mod balls;
mod control;
mod fan;
mod fleet;
mod fork;
mod query;
mod refusals;
mod retarget;
pub(super) fn rt(gesture: Gesture) {
let encoded = encode(&gesture);
assert_eq!(decode(&encoded), Ok(gesture.clone()), "via {encoded}");
}
pub(super) fn p(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn every_action_variant_round_trips() {
rt(Gesture::Act(Action::Message {
workspace: "ws".into(),
agent: "c-1".into(),
content: "hi there".into(),
}));
rt(Gesture::Act(Action::Interrupt {
workspace: "ws".into(),
agent: "c-1".into(),
content: "no, this".into(),
}));
rt(Gesture::Act(Action::Stop {
workspace: "ws".into(),
agent: "c-1".into(),
children: true,
}));
rt(Gesture::Act(Action::Scan {
workspace: "ws".into(),
}));
rt(Gesture::Act(Action::Nudge {
workspace: "ws".into(),
agent: "c-1".into(),
}));
rt(Gesture::Act(Action::DeleteWorkspace {
workspace: "ws".into(),
typed: "alba".into(),
}));
rt(Gesture::Act(Action::DeleteAgent {
workspace: "ws".into(),
agent: "c-1".into(),
typed: "the goal name".into(),
}));
rt(Gesture::Act(Action::Monitor(Verb::Arm {
workspace: "ws".into(),
model: "claude-haiku-4-5".into(),
})));
rt(Gesture::Act(Action::Monitor(Verb::Disarm {
workspace: "ws".into(),
})));
rt(Gesture::Act(Action::Monitor(Verb::Flag {
workspace: "ws".into(),
agent: "c-1".into(),
reason: "it is rewriting an unrelated crate".into(),
})));
for ruling in [
crate::control::judge::Ruling::Pass,
crate::control::judge::Ruling::Hold,
crate::control::judge::Ruling::Refuse,
] {
rt(Gesture::Act(Action::AnswerHold {
workspace: "ws".into(),
agent: "c-1".into(),
ruling,
}));
}
rt(Gesture::Act(Action::Ack));
rt(Gesture::Act(Action::MarkSeen {
workspace: "ws".into(),
agent: "c-1".into(),
}));
rt(Gesture::Act(Action::ClearTrail));
for tools in [Vec::new(), vec![advertised()]] {
rt(Gesture::Act(Action::Advertise { tools }));
}
rt(Gesture::Act(Action::Route(
crate::registry::mailbox::Verb::Invoke(crate::registry::mailbox::Call {
client: "laptop".into(),
tool: "Bash".into(),
input: serde_json::json!({"command": "ls -l", "timeout": 30}),
}),
)));
rt(Gesture::Act(Action::Route(
crate::registry::mailbox::Verb::Complete(crate::registry::mailbox::Completion {
invocation: "inv-1".into(),
capture: crate::registry::mailbox::Capture {
stdout: "hello\n".into(),
stderr: "warned\n".into(),
exit_code: 3,
},
}),
)));
}
fn advertised() -> crate::registry::tools::Tool {
crate::registry::tools::Tool {
name: "Bash".into(),
description: "run a command".into(),
input_schema: serde_json::json!({
"type": "object",
"properties": {"command": {"type": "string", "minLength": 1}},
"required": ["command"],
}),
}
}
#[test]
fn every_ball_action_variant_round_trips() {
rt(Gesture::Act(Action::Close {
project: "proj".into(),
id: "bl-1".into(),
name: "alba".into(),
}));
rt(Gesture::Act(Action::Assign {
project: "proj".into(),
id: "bl-1".into(),
name: "alba".into(),
}));
rt(Gesture::Act(Action::Release {
project: "proj".into(),
id: "bl-1".into(),
name: "alba".into(),
}));
for body in [Some("the body".to_owned()), None] {
rt(Gesture::Act(Action::Create {
project: "proj".into(),
name: "alba".into(),
fields: edit::Create {
title: "a title".into(),
body,
..edit::Create::default()
},
}));
}
for fields in [
edit::Update {
title: Some("t".into()),
body: Some(String::new()),
note: Some("n".into()),
..edit::Update::default()
},
edit::Update::default(),
] {
rt(Gesture::Act(Action::Update {
project: "proj".into(),
id: "bl-1".into(),
name: "alba".into(),
fields,
}));
}
}
#[test]
fn every_payload_rung_round_trips_inside_prepare() {
for payload in [
Payload::Bare,
Payload::Path { dir: p("/work") },
Payload::Ball {
project: "proj".into(),
ball: BallSpec::New {
title: "t".into(),
body: "b".into(),
},
},
] {
rt(Gesture::Act(Action::Prepare {
workspace: "ws".into(),
payload,
}));
}
}
#[test]
fn every_join_state_round_trips_inside_an_existing_ball() {
for join in [
JoinState::ReadyStartable,
JoinState::Blocked,
JoinState::Bound,
JoinState::ClaimedElsewhere,
JoinState::Delivered,
JoinState::UnassignedWorkspace,
JoinState::OrphanedProject,
] {
rt(Gesture::Act(Action::Prepare {
workspace: "ws".into(),
payload: Payload::Ball {
project: "proj".into(),
ball: BallSpec::Existing {
id: "bl-9".into(),
title: "t".into(),
body: "b".into(),
join,
},
},
}));
}
}
#[test]
fn every_origin_round_trips_inside_a_prompt() {
for origin in [Origin::Balls, Origin::Conversation, Origin::World] {
for binding in [None, Some(p("/target"))] {
for seed in [None, Some(0xc0df)] {
rt(Gesture::Act(Action::Prompt {
prepared: Prepared {
workspace: "ws".into(),
binding: binding.clone(),
goal: "the goal".into(),
origin,
},
goal: "edited goal".into(),
seed,
}));
}
}
}
}
#[test]
fn a_stop_without_the_children_field_defaults_to_false() {
let decoded = decode(&serde_json::json!({
"op": "stop", "workspace": "ws", "agent": "c-1"
}));
assert_eq!(
decoded,
Ok(Gesture::Act(Action::Stop {
workspace: "ws".into(),
agent: "c-1".into(),
children: false,
}))
);
}
#[test]
fn the_start_envelope_answers_null_to_anything_but_its_own_two() {
let value = encode_start(&Action::Ack);
assert!(value.is_null(), "not a start gesture, so not an envelope");
assert!(decode(&value).is_err(), "and decode refuses it");
}