use std::io::{self, Read, Write};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use wimux_vt::Cell;
pub mod transport;
pub const PROTOCOL_VERSION: Version = Version { major: 0, minor: 1 };
pub const PIPE_PREFIX: &str = r"\\.\pipe\wimux";
const MAX_FRAME_LEN: u32 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
pub major: u16,
pub minor: u16,
}
impl Version {
pub fn is_compatible_with(self, other: Version) -> bool {
self.major == other.major
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hello {
pub client_version: Version,
pub client_build: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HelloReply {
Ok {
server_version: Version,
},
VersionMismatch {
server_version: Version,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentTemplate {
pub name: String,
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum AgentStatus {
Working,
Idle,
Attention,
Done,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub name: String,
pub windows: u32,
pub attached: bool,
pub activity: bool,
pub bell: bool,
pub agent: bool,
pub agent_status: Option<AgentStatus>,
pub group: Option<String>,
pub cwd: Option<String>,
pub branch: Option<String>,
pub color: Option<String>,
pub pinned: bool,
pub layout_rev: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SplitDir {
LeftRight,
TopBottom,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PaneKind {
Terminal,
Web { url: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum LayoutNode {
Leaf {
pane_id: u64,
kind: PaneKind,
},
Split {
node_id: u32,
dir: SplitDir,
ratio: f32,
a: Box<LayoutNode>,
b: Box<LayoutNode>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub cols: u16,
pub rows: u16,
pub cursor_col: u16,
pub cursor_row: u16,
pub cells: Vec<Cell>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WindowInfo {
pub name: Option<String>,
pub cwd: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NotificationInfo {
pub session: String,
pub title: Option<String>,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PaneInfo {
pub pane_id: u64,
pub cwd: Option<String>,
pub running: bool,
pub exit_code: Option<i32>,
pub log_path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BatchInfo {
pub group: String,
pub sessions: Vec<String>,
pub base_repo: String,
pub base_branch: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentResult {
pub session: String,
pub index: u32,
pub branch: String,
pub status: Option<AgentStatus>,
pub files_changed: u32,
pub insertions: u32,
pub deletions: u32,
pub untracked: u32,
pub has_commits: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientMessage {
Hello(Hello),
NewSession {
name: Option<String>,
cols: u16,
rows: u16,
},
Attach {
name: String,
cols: u16,
rows: u16,
},
List,
Kill {
name: String,
},
Input(Vec<u8>),
AttachGui {
session: String,
},
PaneInput {
pane_id: u64,
bytes: Vec<u8>,
},
PaneResize {
pane_id: u64,
cols: u16,
rows: u16,
},
SplitPane {
pane_id: u64,
dir: SplitDir,
},
ClosePane {
pane_id: u64,
},
FocusPane {
pane_id: u64,
},
SetSplitRatio {
node_id: u32,
ratio: f32,
},
CreateSession {
name: Option<String>,
},
RenameSession {
from: String,
to: String,
},
SendKeys {
session: String,
keys: Vec<u8>,
},
Command {
session: String,
command: String,
},
Resize {
cols: u16,
rows: u16,
},
Detach,
Shutdown,
Ping,
ListAgentTemplates,
CreateAgentSession {
name: Option<String>,
template: String,
prompt: String,
cwd: Option<String>,
},
CreateAgentBatch {
template: String,
prompt: String,
base_repo: String,
count: u32,
},
NewWindow,
SelectWindow {
index: u32,
},
CloseWindow {
index: u32,
},
RenameWindow {
index: u32,
name: String,
},
ListWindows,
ReorderSessions {
names: Vec<String>,
},
SetSessionColor {
name: String,
color: Option<String>,
},
SetSessionPinned {
name: String,
pinned: bool,
},
TakeNotifications,
MarkSessionRead {
name: String,
},
MarkSessionUnread {
name: String,
},
SpawnPane {
session: String,
from_pane: Option<u64>,
dir: SplitDir,
cwd: Option<String>,
program: String,
args: Vec<String>,
},
CapturePane {
session: String,
pane: u64,
},
ListPanes {
session: String,
},
SendKeysPane {
session: String,
pane: u64,
keys: Vec<u8>,
},
KillPane {
session: String,
pane: u64,
},
ListBatches,
ReviewBatch {
group: String,
},
DiffAgent {
session: String,
},
OpenPr {
session: String,
title: Option<String>,
body: Option<String>,
},
OpenWebPane {
session: String,
from_pane: Option<u64>,
dir: SplitDir,
url: String,
},
WebNavigate {
session: String,
pane: u64,
url: String,
},
WebBack {
session: String,
pane: u64,
},
WebForward {
session: String,
pane: u64,
},
BrowserLaunch,
BrowserClose,
BrowserStatus,
BrowserNavigate {
url: String,
},
BrowserUrl,
BrowserSnapshot,
BrowserScreenshot,
BrowserClick {
ref_: String,
},
BrowserType {
ref_: String,
text: String,
},
BrowserPress {
key: String,
ref_: Option<String>,
},
BrowserScroll {
ref_: Option<String>,
dy: Option<i64>,
},
BrowserWait {
text: Option<String>,
ms: Option<u64>,
settle: bool,
},
BrowserEval {
js: String,
},
BrowserSelect {
ref_: String,
value: String,
},
BrowserAddScript {
js: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerMessage {
Hello(HelloReply),
Sessions(Vec<SessionInfo>),
Attached {
name: String,
},
Frame(Frame),
PaneExited {
code: u32,
},
Detached,
PaneSnapshot {
pane_id: u64,
bytes: Vec<u8>,
},
PaneOutput {
pane_id: u64,
bytes: Vec<u8>,
},
WindowLayout {
tree: LayoutNode,
active: u64,
},
SessionCreated {
name: String,
},
SetClipboard(String),
CommandResult(String),
Error(String),
Pong,
Ok,
AgentTemplates(Vec<AgentTemplate>),
BatchCreated {
group: String,
sessions: Vec<String>,
},
WindowList {
windows: Vec<WindowInfo>,
active: u32,
},
Notifications(Vec<NotificationInfo>),
PaneSpawned {
pane_id: u64,
},
PaneCapture(String),
PaneList(Vec<PaneInfo>),
Batches(Vec<BatchInfo>),
BatchReview(Vec<AgentResult>),
AgentDiff(String),
PrOpened {
url: String,
},
BrowserState {
running: bool,
url: Option<String>,
},
BrowserText(String),
BrowserShot {
path: String,
},
}
pub fn send<W: Write, T: Serialize>(w: &mut W, msg: &T) -> io::Result<()> {
let body =
postcard::to_allocvec(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let len = u32::try_from(body.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "message trop grand"))?;
w.write_all(&len.to_le_bytes())?;
w.write_all(&body)?;
w.flush()
}
pub fn recv<R: Read, T: DeserializeOwned>(r: &mut R) -> io::Result<T> {
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf);
if len > MAX_FRAME_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"message dépassant la taille maximale",
));
}
let mut body = vec![0u8; len as usize];
r.read_exact(&mut body)?;
postcard::from_bytes(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meme_majeure_est_compatible() {
let a = Version { major: 1, minor: 0 };
let b = Version { major: 1, minor: 7 };
assert!(a.is_compatible_with(b));
}
#[test]
fn majeure_differente_est_incompatible() {
let a = Version { major: 1, minor: 0 };
let b = Version { major: 2, minor: 0 };
assert!(!a.is_compatible_with(b));
}
#[test]
fn version_saffiche() {
assert_eq!(PROTOCOL_VERSION.to_string(), "0.1");
}
#[test]
fn aller_retour_message() {
let msg = ClientMessage::NewSession {
name: Some("dev".into()),
cols: 80,
rows: 24,
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cursor = io::Cursor::new(buf);
let decoded: ClientMessage = recv(&mut cursor).unwrap();
match decoded {
ClientMessage::NewSession { name, cols, rows } => {
assert_eq!(name.as_deref(), Some("dev"));
assert_eq!((cols, rows), (80, 24));
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn eof_propre_quand_le_pair_ferme() {
let mut cursor = io::Cursor::new(Vec::new());
let res: io::Result<ClientMessage> = recv(&mut cursor);
assert_eq!(res.unwrap_err().kind(), io::ErrorKind::UnexpectedEof);
}
#[test]
fn aller_retour_attach_gui() {
let msg = ClientMessage::AttachGui {
session: "dev".into(),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::AttachGui { session } => assert_eq!(session, "dev"),
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_pane_output() {
let msg = ServerMessage::PaneOutput {
pane_id: 7,
bytes: b"hello".to_vec(),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::PaneOutput { pane_id, bytes } => {
assert_eq!(pane_id, 7);
assert_eq!(bytes, b"hello");
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_create_session() {
let msg = ClientMessage::CreateSession {
name: Some("dev".into()),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::CreateSession { name } => assert_eq!(name.as_deref(), Some("dev")),
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_rename_session() {
let msg = ClientMessage::RenameSession {
from: "a".into(),
to: "b".into(),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::RenameSession { from, to } => {
assert_eq!(from, "a");
assert_eq!(to, "b");
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_split_pane() {
let msg = ClientMessage::SplitPane {
pane_id: 3,
dir: SplitDir::TopBottom,
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::SplitPane { pane_id, dir } => {
assert_eq!(pane_id, 3);
assert_eq!(dir, SplitDir::TopBottom);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_window_layout() {
let tree = LayoutNode::Split {
node_id: 1,
dir: SplitDir::LeftRight,
ratio: 0.5,
a: Box::new(LayoutNode::Leaf {
pane_id: 10,
kind: PaneKind::Terminal,
}),
b: Box::new(LayoutNode::Leaf {
pane_id: 11,
kind: PaneKind::Terminal,
}),
};
let msg = ServerMessage::WindowLayout {
tree: tree.clone(),
active: 10,
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::WindowLayout { tree: got, active } => {
assert_eq!(active, 10);
assert_eq!(got, tree);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_session_info_activite() {
let info = SessionInfo {
name: "dev".into(),
windows: 2,
attached: true,
activity: true,
bell: false,
agent: false,
agent_status: None,
group: None,
cwd: None,
branch: None,
color: None,
pinned: false,
layout_rev: 0,
};
let msg = ServerMessage::Sessions(vec![info]);
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::Sessions(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0].name, "dev");
assert_eq!(v[0].windows, 2);
assert!(v[0].attached);
assert!(v[0].activity);
assert!(!v[0].bell);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_session_info_agent() {
let info = SessionInfo {
name: "bot".into(),
windows: 1,
attached: false,
activity: false,
bell: false,
agent: true,
agent_status: Some(AgentStatus::Working),
group: Some("batch0".into()),
cwd: None,
branch: None,
color: None,
pinned: false,
layout_rev: 0,
};
let msg = ServerMessage::Sessions(vec![info]);
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::Sessions(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0].name, "bot");
assert!(v[0].agent);
assert_eq!(v[0].agent_status, Some(AgentStatus::Working));
assert_eq!(v[0].group.as_deref(), Some("batch0"));
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_agent_template() {
let tpl = AgentTemplate {
name: "claude".into(),
program: "claude".into(),
args: vec!["-p".into(), "{prompt}".into()],
};
let msg = ServerMessage::AgentTemplates(vec![tpl.clone()]);
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::AgentTemplates(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0], tpl);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_list_agent_templates() {
let msg = ClientMessage::ListAgentTemplates;
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
assert!(matches!(
recv::<_, ClientMessage>(&mut cur).unwrap(),
ClientMessage::ListAgentTemplates
));
}
#[test]
fn aller_retour_create_agent_session() {
let msg = ClientMessage::CreateAgentSession {
name: Some("bot".into()),
template: "claude".into(),
prompt: "corrige le bug".into(),
cwd: Some("C:\\proj".into()),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::CreateAgentSession {
name,
template,
prompt,
cwd,
} => {
assert_eq!(name.as_deref(), Some("bot"));
assert_eq!(template, "claude");
assert_eq!(prompt, "corrige le bug");
assert_eq!(cwd.as_deref(), Some("C:\\proj"));
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_create_agent_batch() {
let msg = ClientMessage::CreateAgentBatch {
template: "echo".into(),
prompt: "corrige le bug".into(),
base_repo: "C:\\proj".into(),
count: 3,
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::CreateAgentBatch {
template,
prompt,
base_repo,
count,
} => {
assert_eq!(template, "echo");
assert_eq!(prompt, "corrige le bug");
assert_eq!(base_repo, "C:\\proj");
assert_eq!(count, 3);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_batch_created() {
let msg = ServerMessage::BatchCreated {
group: "batch0".into(),
sessions: vec!["echo-batch0-0".into(), "echo-batch0-1".into()],
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::BatchCreated { group, sessions } => {
assert_eq!(group, "batch0");
assert_eq!(sessions, vec!["echo-batch0-0", "echo-batch0-1"]);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_window_info_et_liste() {
let msg = ServerMessage::WindowList {
windows: vec![
WindowInfo {
name: Some("build".into()),
cwd: Some("C:\\proj".into()),
},
WindowInfo {
name: None,
cwd: None,
},
],
active: 1,
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::WindowList { windows, active } => {
assert_eq!(active, 1);
assert_eq!(windows.len(), 2);
assert_eq!(windows[0].name.as_deref(), Some("build"));
assert_eq!(windows[1].name, None);
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_new_window() {
let mut buf = Vec::new();
send(&mut buf, &ClientMessage::NewWindow).unwrap();
let mut cur = io::Cursor::new(buf);
assert!(matches!(
recv::<_, ClientMessage>(&mut cur).unwrap(),
ClientMessage::NewWindow
));
}
#[test]
fn aller_retour_select_et_close_window() {
let mut buf = Vec::new();
send(&mut buf, &ClientMessage::SelectWindow { index: 2 }).unwrap();
send(&mut buf, &ClientMessage::CloseWindow { index: 3 }).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::SelectWindow { index } => assert_eq!(index, 2),
_ => panic!("mauvais variant"),
}
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::CloseWindow { index } => assert_eq!(index, 3),
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_rename_window() {
let msg = ClientMessage::RenameWindow {
index: 0,
name: "build".into(),
};
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ClientMessage>(&mut cur).unwrap() {
ClientMessage::RenameWindow { index, name } => {
assert_eq!(index, 0);
assert_eq!(name, "build");
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_session_info_cwd_branche() {
let info = SessionInfo {
name: "dev".into(),
windows: 1,
attached: true,
activity: false,
bell: false,
agent: false,
agent_status: None,
group: None,
cwd: Some("C:\\proj\\wimux".into()),
branch: Some("main".into()),
color: Some("#e8833a".into()),
pinned: true,
layout_rev: 0,
};
let msg = ServerMessage::Sessions(vec![info]);
let mut buf = Vec::new();
send(&mut buf, &msg).unwrap();
let mut cur = io::Cursor::new(buf);
match recv::<_, ServerMessage>(&mut cur).unwrap() {
ServerMessage::Sessions(v) => {
assert_eq!(v.len(), 1);
assert_eq!(v[0].cwd.as_deref(), Some("C:\\proj\\wimux"));
assert_eq!(v[0].branch.as_deref(), Some("main"));
}
_ => panic!("mauvais variant"),
}
}
#[test]
fn aller_retour_spawn_pane_et_pane_list() {
let msg = ClientMessage::SpawnPane {
session: "s".into(),
from_pane: Some(3),
dir: SplitDir::LeftRight,
cwd: Some("C:\\repo".into()),
program: "claude".into(),
args: vec!["-p".into(), "tache".into()],
};
let bytes = postcard::to_allocvec(&msg).unwrap();
match postcard::from_bytes::<ClientMessage>(&bytes).unwrap() {
ClientMessage::SpawnPane {
program,
args,
from_pane,
..
} => {
assert_eq!(program, "claude");
assert_eq!(args, vec!["-p".to_string(), "tache".to_string()]);
assert_eq!(from_pane, Some(3));
}
_ => panic!("variante inattendue"),
}
let info = PaneInfo {
pane_id: 7,
cwd: Some("C:\\repo".into()),
running: true,
exit_code: None,
log_path: Some("C:\\log\\7.log".into()),
};
let reply = ServerMessage::PaneList(vec![info.clone()]);
let bytes = postcard::to_allocvec(&reply).unwrap();
match postcard::from_bytes::<ServerMessage>(&bytes).unwrap() {
ServerMessage::PaneList(v) => assert_eq!(v[0], info),
_ => panic!("variante inattendue"),
}
}
#[test]
fn aller_retour_review_batch_et_open_pr() {
let msg = ClientMessage::OpenPr {
session: "claude-batch0-1".into(),
title: Some("fix: gérer le payload vide".into()),
body: None,
};
let bytes = postcard::to_allocvec(&msg).unwrap();
match postcard::from_bytes::<ClientMessage>(&bytes).unwrap() {
ClientMessage::OpenPr {
session,
title,
body,
} => {
assert_eq!(session, "claude-batch0-1");
assert_eq!(title.as_deref(), Some("fix: gérer le payload vide"));
assert_eq!(body, None);
}
_ => panic!("variante inattendue"),
}
let res = AgentResult {
session: "claude-batch0-1".into(),
index: 1,
branch: "wimux/batch0/1".into(),
status: Some(AgentStatus::Done),
files_changed: 3,
insertions: 42,
deletions: 7,
untracked: 2,
has_commits: true,
};
let reply = ServerMessage::BatchReview(vec![res.clone()]);
let bytes = postcard::to_allocvec(&reply).unwrap();
match postcard::from_bytes::<ServerMessage>(&bytes).unwrap() {
ServerMessage::BatchReview(v) => assert_eq!(v[0], res),
_ => panic!("variante inattendue"),
}
let info = BatchInfo {
group: "batch0".into(),
sessions: vec!["claude-batch0-0".into(), "claude-batch0-1".into()],
base_repo: "C:\\repo".into(),
base_branch: "main".into(),
};
let bytes = postcard::to_allocvec(&ServerMessage::Batches(vec![info.clone()])).unwrap();
match postcard::from_bytes::<ServerMessage>(&bytes).unwrap() {
ServerMessage::Batches(v) => assert_eq!(v[0], info),
_ => panic!("variante inattendue"),
}
}
#[test]
fn aller_retour_leaf_web_et_messages_navigateur() {
let tree = LayoutNode::Leaf {
pane_id: 4,
kind: PaneKind::Web {
url: "http://localhost:5173/".into(),
},
};
let bytes = postcard::to_allocvec(&tree).unwrap();
match postcard::from_bytes::<LayoutNode>(&bytes).unwrap() {
LayoutNode::Leaf { pane_id, kind } => {
assert_eq!(pane_id, 4);
assert_eq!(
kind,
PaneKind::Web {
url: "http://localhost:5173/".into()
}
);
}
_ => panic!("attendu une feuille"),
}
let tree = LayoutNode::Leaf {
pane_id: 1,
kind: PaneKind::Terminal,
};
let bytes = postcard::to_allocvec(&tree).unwrap();
match postcard::from_bytes::<LayoutNode>(&bytes).unwrap() {
LayoutNode::Leaf { kind, .. } => assert_eq!(kind, PaneKind::Terminal),
_ => panic!("attendu une feuille"),
}
let msg = ClientMessage::OpenWebPane {
session: "s".into(),
from_pane: Some(2),
dir: SplitDir::LeftRight,
url: "http://localhost:3000/".into(),
};
let bytes = postcard::to_allocvec(&msg).unwrap();
match postcard::from_bytes::<ClientMessage>(&bytes).unwrap() {
ClientMessage::OpenWebPane { from_pane, url, .. } => {
assert_eq!(from_pane, Some(2));
assert_eq!(url, "http://localhost:3000/");
}
_ => panic!("variante inattendue"),
}
let msg = ClientMessage::WebBack {
session: "s".into(),
pane: 4,
};
let bytes = postcard::to_allocvec(&msg).unwrap();
assert!(matches!(
postcard::from_bytes::<ClientMessage>(&bytes).unwrap(),
ClientMessage::WebBack { pane: 4, .. }
));
}
#[test]
fn aller_retour_messages_navigateur() {
let msg = ClientMessage::BrowserNavigate {
url: "http://localhost:8899/".into(),
};
let bytes = postcard::to_allocvec(&msg).unwrap();
assert!(matches!(
postcard::from_bytes::<ClientMessage>(&bytes).unwrap(),
ClientMessage::BrowserNavigate { url } if url == "http://localhost:8899/"
));
let reply = ServerMessage::BrowserState {
running: true,
url: Some("http://localhost:8899/".into()),
};
let bytes = postcard::to_allocvec(&reply).unwrap();
match postcard::from_bytes::<ServerMessage>(&bytes).unwrap() {
ServerMessage::BrowserState { running, url } => {
assert!(running);
assert_eq!(url.as_deref(), Some("http://localhost:8899/"));
}
_ => panic!("variante inattendue"),
}
let shot = ServerMessage::BrowserShot {
path: "C:\\x\\1.png".into(),
};
let bytes = postcard::to_allocvec(&shot).unwrap();
assert!(matches!(
postcard::from_bytes::<ServerMessage>(&bytes).unwrap(),
ServerMessage::BrowserShot { path } if path == "C:\\x\\1.png"
));
}
}