atagen

headroom

public
Star0
Watch0
b4f9722db37afb8ec0881327edf1a085c821d6a9
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git

Blame: crates/headroom-client/src/client.rs

dd6b8e786e1fatagen2026-05-27 15:49 UTC1
//! blocking [`Client`].
dd6b8e786e1fatagen2026-05-27 15:49 UTC2
dd6b8e786e1fatagen2026-05-27 15:49 UTC3
use std::collections::VecDeque;
dd6b8e786e1fatagen2026-05-27 15:49 UTC4
use std::io::{BufReader, BufWriter};
dd6b8e786e1fatagen2026-05-27 15:49 UTC5
use std::os::unix::net::UnixStream;
dd6b8e786e1fatagen2026-05-27 15:49 UTC6
use std::path::{Path, PathBuf};
dd6b8e786e1fatagen2026-05-27 15:49 UTC7
dd6b8e786e1fatagen2026-05-27 15:49 UTC8
use serde::de::DeserializeOwned;
dd6b8e786e1fatagen2026-05-27 15:49 UTC9
dd6b8e786e1fatagen2026-05-27 15:49 UTC10
use headroom_ipc::{
dd6b8e786e1fatagen2026-05-27 15:49 UTC11
    default_socket_path, Codec, Event, HelloData, Op, ProtoError, Request, Response,
dd6b8e786e1fatagen2026-05-27 15:49 UTC12
    ResponsePayload, Route, ServerFrame, Status, Topic,
dd6b8e786e1fatagen2026-05-27 15:49 UTC13
};
dd6b8e786e1fatagen2026-05-27 15:49 UTC14
dd6b8e786e1fatagen2026-05-27 15:49 UTC15
/// errors from the blocking client.
dd6b8e786e1fatagen2026-05-27 15:49 UTC16
#[derive(Debug, thiserror::Error)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC17
pub enum ClientError {
dd6b8e786e1fatagen2026-05-27 15:49 UTC18
    #[error("ipc: {0}")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC19
    Ipc(#[from] headroom_ipc::Error),
dd6b8e786e1fatagen2026-05-27 15:49 UTC20
dd6b8e786e1fatagen2026-05-27 15:49 UTC21
    /// server's first frame was not the `hello` event.
dd6b8e786e1fatagen2026-05-27 15:49 UTC22
    #[error("expected hello event from server, got {0}")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC23
    BadHello(String),
dd6b8e786e1fatagen2026-05-27 15:49 UTC24
dd6b8e786e1fatagen2026-05-27 15:49 UTC25
    /// response id we never issued.
dd6b8e786e1fatagen2026-05-27 15:49 UTC26
    #[error("response with unknown id {0}")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC27
    UnknownResponseId(u64),
dd6b8e786e1fatagen2026-05-27 15:49 UTC28
dd6b8e786e1fatagen2026-05-27 15:49 UTC29
    /// protocol-level error for an op.
dd6b8e786e1fatagen2026-05-27 15:49 UTC30
    #[error("server error: {0}")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC31
    Protocol(#[from] ProtoError),
dd6b8e786e1fatagen2026-05-27 15:49 UTC32
dd6b8e786e1fatagen2026-05-27 15:49 UTC33
    #[error("no default socket path (XDG_RUNTIME_DIR unset and /proc/self/status unreadable)")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC34
    NoDefaultPath,
dd6b8e786e1fatagen2026-05-27 15:49 UTC35
dd6b8e786e1fatagen2026-05-27 15:49 UTC36
    /// typed-helper response failed to deserialize into the expected shape.
dd6b8e786e1fatagen2026-05-27 15:49 UTC37
    #[error("response shape mismatch: {0}")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC38
    DecodeResult(serde_json::Error),
dd6b8e786e1fatagen2026-05-27 15:49 UTC39
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC40
dd6b8e786e1fatagen2026-05-27 15:49 UTC41
/// blocking client for the headroom control protocol.
dd6b8e786e1fatagen2026-05-27 15:49 UTC42
///
dd6b8e786e1fatagen2026-05-27 15:49 UTC43
/// single-threaded by construction; for concurrent request/response and event
dd6b8e786e1fatagen2026-05-27 15:49 UTC44
/// consumption, open two connections.
dd6b8e786e1fatagen2026-05-27 15:49 UTC45
pub struct Client {
dd6b8e786e1fatagen2026-05-27 15:49 UTC46
    reader: BufReader<UnixStream>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC47
    writer: BufWriter<UnixStream>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC48
    codec: Codec,
dd6b8e786e1fatagen2026-05-27 15:49 UTC49
    next_id: u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC50
    pending_events: VecDeque<Event>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC51
    hello: HelloData,
dd6b8e786e1fatagen2026-05-27 15:49 UTC52
    socket_path: PathBuf,
dd6b8e786e1fatagen2026-05-27 15:49 UTC53
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC54
dd6b8e786e1fatagen2026-05-27 15:49 UTC55
impl Client {
dd6b8e786e1fatagen2026-05-27 15:49 UTC56
    /// connect at the default socket path.
dd6b8e786e1fatagen2026-05-27 15:49 UTC57
    pub fn connect() -> Result<Self, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC58
        let path = default_socket_path().ok_or(ClientError::NoDefaultPath)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC59
        Self::connect_at(&path)
dd6b8e786e1fatagen2026-05-27 15:49 UTC60
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC61
dd6b8e786e1fatagen2026-05-27 15:49 UTC62
    /// connect at the given socket path.
dd6b8e786e1fatagen2026-05-27 15:49 UTC63
    pub fn connect_at(path: &Path) -> Result<Self, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC64
        let stream = UnixStream::connect(path).map_err(|e| ClientError::Ipc(e.into()))?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC65
        let reader_half = stream.try_clone().map_err(|e| ClientError::Ipc(e.into()))?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC66
        let writer_half = stream;
dd6b8e786e1fatagen2026-05-27 15:49 UTC67
dd6b8e786e1fatagen2026-05-27 15:49 UTC68
        let mut me = Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC69
            reader: BufReader::new(reader_half),
dd6b8e786e1fatagen2026-05-27 15:49 UTC70
            writer: BufWriter::new(writer_half),
dd6b8e786e1fatagen2026-05-27 15:49 UTC71
            codec: Codec::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC72
            next_id: 1,
dd6b8e786e1fatagen2026-05-27 15:49 UTC73
            pending_events: VecDeque::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC74
            // placeholder; populated by handshake below
dd6b8e786e1fatagen2026-05-27 15:49 UTC75
            hello: HelloData {
dd6b8e786e1fatagen2026-05-27 15:49 UTC76
                daemon: String::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC77
                version: String::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC78
                protocol: 0,
dd6b8e786e1fatagen2026-05-27 15:49 UTC79
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC80
            socket_path: path.to_path_buf(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC81
        };
dd6b8e786e1fatagen2026-05-27 15:49 UTC82
dd6b8e786e1fatagen2026-05-27 15:49 UTC83
        me.handshake()?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC84
        Ok(me)
dd6b8e786e1fatagen2026-05-27 15:49 UTC85
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC86
dd6b8e786e1fatagen2026-05-27 15:49 UTC87
    fn handshake(&mut self) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC88
        let frame: ServerFrame = self.codec.read(&mut self.reader)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC89
        match frame {
dd6b8e786e1fatagen2026-05-27 15:49 UTC90
            ServerFrame::Event(ev)
dd6b8e786e1fatagen2026-05-27 15:49 UTC91
                if ev.topic == Topic::Control && ev.event.as_str() == "hello" =>
dd6b8e786e1fatagen2026-05-27 15:49 UTC92
            {
dd6b8e786e1fatagen2026-05-27 15:49 UTC93
                let hello: HelloData =
dd6b8e786e1fatagen2026-05-27 15:49 UTC94
                    serde_json::from_value(ev.data).map_err(ClientError::DecodeResult)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC95
                self.hello = hello;
dd6b8e786e1fatagen2026-05-27 15:49 UTC96
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC97
            ServerFrame::Event(ev) => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC98
                return Err(ClientError::BadHello(format!(
dd6b8e786e1fatagen2026-05-27 15:49 UTC99
                    "{} event on {}",
dd6b8e786e1fatagen2026-05-27 15:49 UTC100
                    ev.event, ev.topic
dd6b8e786e1fatagen2026-05-27 15:49 UTC101
                )))
dd6b8e786e1fatagen2026-05-27 15:49 UTC102
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC103
            ServerFrame::Response(r) => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC104
                return Err(ClientError::BadHello(format!("response id={}", r.id)))
dd6b8e786e1fatagen2026-05-27 15:49 UTC105
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC106
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC107
dd6b8e786e1fatagen2026-05-27 15:49 UTC108
        // best effort for older daemons
dd6b8e786e1fatagen2026-05-27 15:49 UTC109
        self.send_raw(Op::Hello {
dd6b8e786e1fatagen2026-05-27 15:49 UTC110
            protocol: headroom_ipc::PROTOCOL_VERSION,
dd6b8e786e1fatagen2026-05-27 15:49 UTC111
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC112
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC113
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC114
dd6b8e786e1fatagen2026-05-27 15:49 UTC115
    /// `hello` payload received on connect.
dd6b8e786e1fatagen2026-05-27 15:49 UTC116
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC117
    pub fn hello(&self) -> &HelloData {
dd6b8e786e1fatagen2026-05-27 15:49 UTC118
        &self.hello
dd6b8e786e1fatagen2026-05-27 15:49 UTC119
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC120
dd6b8e786e1fatagen2026-05-27 15:49 UTC121
    /// socket path this client is connected to.
dd6b8e786e1fatagen2026-05-27 15:49 UTC122
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC123
    pub fn socket_path(&self) -> &Path {
dd6b8e786e1fatagen2026-05-27 15:49 UTC124
        &self.socket_path
dd6b8e786e1fatagen2026-05-27 15:49 UTC125
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC126
dd6b8e786e1fatagen2026-05-27 15:49 UTC127
    fn alloc_id(&mut self) -> u64 {
dd6b8e786e1fatagen2026-05-27 15:49 UTC128
        let id = self.next_id;
dd6b8e786e1fatagen2026-05-27 15:49 UTC129
        // wrap: u64::MAX requests on one connection won't happen, but be correct
dd6b8e786e1fatagen2026-05-27 15:49 UTC130
        self.next_id = self.next_id.wrapping_add(1);
dd6b8e786e1fatagen2026-05-27 15:49 UTC131
        id
dd6b8e786e1fatagen2026-05-27 15:49 UTC132
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC133
dd6b8e786e1fatagen2026-05-27 15:49 UTC134
    /// send a request, block until the paired response arrives.
dd6b8e786e1fatagen2026-05-27 15:49 UTC135
    ///
dd6b8e786e1fatagen2026-05-27 15:49 UTC136
    /// stray events received meanwhile are queued for [`next_event`](Self::next_event).
dd6b8e786e1fatagen2026-05-27 15:49 UTC137
    pub fn send(&mut self, op: Op) -> Result<serde_json::Value, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC138
        let payload = self.send_raw(op)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC139
        match payload {
dd6b8e786e1fatagen2026-05-27 15:49 UTC140
            ResponsePayload::Ok { result } => Ok(result),
dd6b8e786e1fatagen2026-05-27 15:49 UTC141
            ResponsePayload::Err { error } => Err(ClientError::Protocol(error)),
dd6b8e786e1fatagen2026-05-27 15:49 UTC142
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC143
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC144
dd6b8e786e1fatagen2026-05-27 15:49 UTC145
    /// like [`send`](Self::send) but returns the raw [`ResponsePayload`] —
dd6b8e786e1fatagen2026-05-27 15:49 UTC146
    /// keeps a protocol-level error in-band instead of as [`ClientError::Protocol`].
dd6b8e786e1fatagen2026-05-27 15:49 UTC147
    pub fn send_raw(&mut self, op: Op) -> Result<ResponsePayload, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC148
        let id = self.alloc_id();
dd6b8e786e1fatagen2026-05-27 15:49 UTC149
        let req = Request::new(id, op);
dd6b8e786e1fatagen2026-05-27 15:49 UTC150
        self.codec.write(&mut self.writer, &req)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC151
dd6b8e786e1fatagen2026-05-27 15:49 UTC152
        loop {
dd6b8e786e1fatagen2026-05-27 15:49 UTC153
            let frame: ServerFrame = self.codec.read(&mut self.reader)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC154
            match frame {
dd6b8e786e1fatagen2026-05-27 15:49 UTC155
                ServerFrame::Response(Response {
dd6b8e786e1fatagen2026-05-27 15:49 UTC156
                    id: rid,
dd6b8e786e1fatagen2026-05-27 15:49 UTC157
                    payload: _,
dd6b8e786e1fatagen2026-05-27 15:49 UTC158
                }) if rid != id => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC159
                    return Err(ClientError::UnknownResponseId(rid));
dd6b8e786e1fatagen2026-05-27 15:49 UTC160
                }
dd6b8e786e1fatagen2026-05-27 15:49 UTC161
                ServerFrame::Response(Response { payload, .. }) => return Ok(payload),
dd6b8e786e1fatagen2026-05-27 15:49 UTC162
                ServerFrame::Event(ev) => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC163
                    self.pending_events.push_back(ev);
dd6b8e786e1fatagen2026-05-27 15:49 UTC164
                }
dd6b8e786e1fatagen2026-05-27 15:49 UTC165
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC166
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC167
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC168
dd6b8e786e1fatagen2026-05-27 15:49 UTC169
    /// block until the next event arrives. drains the queue before reading the socket.
dd6b8e786e1fatagen2026-05-27 15:49 UTC170
    ///
dd6b8e786e1fatagen2026-05-27 15:49 UTC171
    /// an unsolicited response (no matching request) is rejected as
dd6b8e786e1fatagen2026-05-27 15:49 UTC172
    /// [`ClientError::UnknownResponseId`].
dd6b8e786e1fatagen2026-05-27 15:49 UTC173
    pub fn next_event(&mut self) -> Result<Event, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC174
        if let Some(ev) = self.pending_events.pop_front() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC175
            return Ok(ev);
dd6b8e786e1fatagen2026-05-27 15:49 UTC176
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC177
        match self.codec.read::<_, ServerFrame>(&mut self.reader)? {
dd6b8e786e1fatagen2026-05-27 15:49 UTC178
            ServerFrame::Event(ev) => Ok(ev),
dd6b8e786e1fatagen2026-05-27 15:49 UTC179
            ServerFrame::Response(r) => Err(ClientError::UnknownResponseId(r.id)),
dd6b8e786e1fatagen2026-05-27 15:49 UTC180
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC181
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC182
dd6b8e786e1fatagen2026-05-27 15:49 UTC183
    /// pop a queued event without blocking. does **not** read from the socket.
dd6b8e786e1fatagen2026-05-27 15:49 UTC184
    pub fn pending_event(&mut self) -> Option<Event> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC185
        self.pending_events.pop_front()
dd6b8e786e1fatagen2026-05-27 15:49 UTC186
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC187
dd6b8e786e1fatagen2026-05-27 15:49 UTC188
    // typed convenience wrappers
dd6b8e786e1fatagen2026-05-27 15:49 UTC189
dd6b8e786e1fatagen2026-05-27 15:49 UTC190
    fn send_into<T: DeserializeOwned>(&mut self, op: Op) -> Result<T, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC191
        let value = self.send(op)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC192
        serde_json::from_value(value).map_err(ClientError::DecodeResult)
dd6b8e786e1fatagen2026-05-27 15:49 UTC193
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC194
dd6b8e786e1fatagen2026-05-27 15:49 UTC195
    /// `status`
dd6b8e786e1fatagen2026-05-27 15:49 UTC196
    pub fn status(&mut self) -> Result<Status, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC197
        self.send_into(Op::Status)
dd6b8e786e1fatagen2026-05-27 15:49 UTC198
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC199
dd6b8e786e1fatagen2026-05-27 15:49 UTC200
    /// `profile.list`
dd6b8e786e1fatagen2026-05-27 15:49 UTC201
    pub fn profile_list(&mut self) -> Result<Vec<headroom_ipc::ProfileInfo>, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC202
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC203
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC204
            profiles: Vec<headroom_ipc::ProfileInfo>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC205
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC206
        let body: Body = self.send_into(Op::ProfileList)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC207
        Ok(body.profiles)
dd6b8e786e1fatagen2026-05-27 15:49 UTC208
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC209
dd6b8e786e1fatagen2026-05-27 15:49 UTC210
    /// `profile.use`
dd6b8e786e1fatagen2026-05-27 15:49 UTC211
    pub fn profile_use(&mut self, name: &str) -> Result<String, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC212
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC213
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC214
            name: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC215
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC216
        let body: Body = self.send_into(Op::ProfileUse {
dd6b8e786e1fatagen2026-05-27 15:49 UTC217
            name: name.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC218
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC219
        Ok(body.name)
dd6b8e786e1fatagen2026-05-27 15:49 UTC220
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC221
dd6b8e786e1fatagen2026-05-27 15:49 UTC222
    /// `profile.show`
dd6b8e786e1fatagen2026-05-27 15:49 UTC223
    pub fn profile_show(
dd6b8e786e1fatagen2026-05-27 15:49 UTC224
        &mut self,
dd6b8e786e1fatagen2026-05-27 15:49 UTC225
        name: Option<&str>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC226
    ) -> Result<serde_json::Value, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC227
        self.send(Op::ProfileShow {
dd6b8e786e1fatagen2026-05-27 15:49 UTC228
            name: name.map(String::from),
dd6b8e786e1fatagen2026-05-27 15:49 UTC229
        })
dd6b8e786e1fatagen2026-05-27 15:49 UTC230
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC231
dd6b8e786e1fatagen2026-05-27 15:49 UTC232
    /// `profile.reload`
dd6b8e786e1fatagen2026-05-27 15:49 UTC233
    pub fn profile_reload(&mut self) -> Result<Vec<String>, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC234
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC235
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC236
            reloaded: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC237
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC238
        let body: Body = self.send_into(Op::ProfileReload)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC239
        Ok(body.reloaded)
dd6b8e786e1fatagen2026-05-27 15:49 UTC240
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC241
dd6b8e786e1fatagen2026-05-27 15:49 UTC242
    /// `route.list`
dd6b8e786e1fatagen2026-05-27 15:49 UTC243
    pub fn route_list(&mut self) -> Result<headroom_ipc::RouteList, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC244
        self.send_into(Op::RouteList)
dd6b8e786e1fatagen2026-05-27 15:49 UTC245
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC246
dd6b8e786e1fatagen2026-05-27 15:49 UTC247
    /// `route.set`
dd6b8e786e1fatagen2026-05-27 15:49 UTC248
    pub fn route_set(&mut self, app: &str, to: Route) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC249
        let _: serde_json::Value = self.send(Op::RouteSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC250
            app: app.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC251
            to,
dd6b8e786e1fatagen2026-05-27 15:49 UTC252
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC253
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC254
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC255
dd6b8e786e1fatagen2026-05-27 15:49 UTC256
    /// `route.unset`
dd6b8e786e1fatagen2026-05-27 15:49 UTC257
    pub fn route_unset(&mut self, app: &str) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC258
        let _: serde_json::Value = self.send(Op::RouteUnset {
dd6b8e786e1fatagen2026-05-27 15:49 UTC259
            app: app.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC260
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC261
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC262
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC263
dd6b8e786e1fatagen2026-05-27 15:49 UTC264
    /// `route.stream`
dd6b8e786e1fatagen2026-05-27 15:49 UTC265
    pub fn route_stream(&mut self, node_id: u32, to: Route) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC266
        let _: serde_json::Value = self.send(Op::RouteStream { node_id, to })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC267
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC268
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC269
dd6b8e786e1fatagen2026-05-27 15:49 UTC270
    /// `setting.get`
dd6b8e786e1fatagen2026-05-27 15:49 UTC271
    pub fn setting_get(&mut self, key: &str) -> Result<serde_json::Value, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC272
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC273
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC274
            #[allow(dead_code)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC275
            key: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC276
            value: serde_json::Value,
dd6b8e786e1fatagen2026-05-27 15:49 UTC277
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC278
        let body: Body = self.send_into(Op::SettingGet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC279
            key: key.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC280
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC281
        Ok(body.value)
dd6b8e786e1fatagen2026-05-27 15:49 UTC282
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC283
dd6b8e786e1fatagen2026-05-27 15:49 UTC284
    /// `setting.set`
dd6b8e786e1fatagen2026-05-27 15:49 UTC285
    pub fn setting_set(
dd6b8e786e1fatagen2026-05-27 15:49 UTC286
        &mut self,
dd6b8e786e1fatagen2026-05-27 15:49 UTC287
        key: &str,
dd6b8e786e1fatagen2026-05-27 15:49 UTC288
        value: serde_json::Value,
dd6b8e786e1fatagen2026-05-27 15:49 UTC289
    ) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC290
        let _: serde_json::Value = self.send(Op::SettingSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC291
            key: key.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC292
            value,
dd6b8e786e1fatagen2026-05-27 15:49 UTC293
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC294
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC295
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC296
dd6b8e786e1fatagen2026-05-27 15:49 UTC297
    /// `setting.clear` — returns whether an override was present.
dd6b8e786e1fatagen2026-05-27 15:49 UTC298
    pub fn setting_clear(&mut self, key: &str) -> Result<bool, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC299
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC300
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC301
            cleared: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC302
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC303
        let body: Body = self.send_into(Op::SettingClear {
dd6b8e786e1fatagen2026-05-27 15:49 UTC304
            key: key.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC305
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC306
        Ok(body.cleared)
dd6b8e786e1fatagen2026-05-27 15:49 UTC307
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC308
dd6b8e786e1fatagen2026-05-27 15:49 UTC309
    /// `setting.reset` — returns how many overrides were cleared.
dd6b8e786e1fatagen2026-05-27 15:49 UTC310
    pub fn setting_reset(&mut self) -> Result<usize, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC311
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC312
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC313
            cleared: usize,
dd6b8e786e1fatagen2026-05-27 15:49 UTC314
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC315
        let body: Body = self.send_into(Op::SettingReset)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC316
        Ok(body.cleared)
dd6b8e786e1fatagen2026-05-27 15:49 UTC317
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC318
dd6b8e786e1fatagen2026-05-27 15:49 UTC319
    /// `bypass.set`
dd6b8e786e1fatagen2026-05-27 15:49 UTC320
    pub fn bypass_set(&mut self, enabled: bool) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC321
        let _: serde_json::Value = self.send(Op::BypassSet { enabled })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC322
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC323
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC324
dd6b8e786e1fatagen2026-05-27 15:49 UTC325
    /// `per-app.list`
dd6b8e786e1fatagen2026-05-27 15:49 UTC326
    pub fn layer_a_list(
dd6b8e786e1fatagen2026-05-27 15:49 UTC327
        &mut self,
dd6b8e786e1fatagen2026-05-27 15:49 UTC328
    ) -> Result<Vec<headroom_ipc::LayerASnapshot>, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC329
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC330
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC331
            layer_a: Vec<headroom_ipc::LayerASnapshot>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC332
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC333
        let body: Body = self.send_into(Op::LayerAList)?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC334
        Ok(body.layer_a)
dd6b8e786e1fatagen2026-05-27 15:49 UTC335
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC336
dd6b8e786e1fatagen2026-05-27 15:49 UTC337
    /// `per-app.set`
dd6b8e786e1fatagen2026-05-27 15:49 UTC338
    pub fn per_app_set(&mut self, app: &str, enabled: bool) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC339
        let _: serde_json::Value = self.send(Op::PerAppSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC340
            app: app.to_owned(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC341
            enabled,
dd6b8e786e1fatagen2026-05-27 15:49 UTC342
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC343
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC344
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC345
dd6b8e786e1fatagen2026-05-27 15:49 UTC346
    /// `per-app.master`
dd6b8e786e1fatagen2026-05-27 15:49 UTC347
    pub fn per_app_master(&mut self, enabled: bool) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC348
        let _: serde_json::Value = self.send(Op::PerAppMaster { enabled })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC349
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC350
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC351
dd6b8e786e1fatagen2026-05-27 15:49 UTC352
    /// `per-app.reset`
dd6b8e786e1fatagen2026-05-27 15:49 UTC353
    pub fn layer_a_reset(&mut self, node_id: u32) -> Result<(), ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC354
        let _: serde_json::Value = self.send(Op::LayerAReset { node_id })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC355
        Ok(())
dd6b8e786e1fatagen2026-05-27 15:49 UTC356
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC357
dd6b8e786e1fatagen2026-05-27 15:49 UTC358
    /// `subscribe`
dd6b8e786e1fatagen2026-05-27 15:49 UTC359
    pub fn subscribe(&mut self, topics: &[Topic]) -> Result<Vec<Topic>, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC360
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC361
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC362
            subscribed: Vec<Topic>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC363
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC364
        let body: Body = self.send_into(Op::Subscribe {
dd6b8e786e1fatagen2026-05-27 15:49 UTC365
            topics: topics.to_vec(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC366
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC367
        Ok(body.subscribed)
dd6b8e786e1fatagen2026-05-27 15:49 UTC368
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC369
dd6b8e786e1fatagen2026-05-27 15:49 UTC370
    /// `unsubscribe`
dd6b8e786e1fatagen2026-05-27 15:49 UTC371
    pub fn unsubscribe(&mut self, topics: &[Topic]) -> Result<Vec<Topic>, ClientError> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC372
        #[derive(serde::Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC373
        struct Body {
dd6b8e786e1fatagen2026-05-27 15:49 UTC374
            unsubscribed: Vec<Topic>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC375
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC376
        let body: Body = self.send_into(Op::Unsubscribe {
dd6b8e786e1fatagen2026-05-27 15:49 UTC377
            topics: topics.to_vec(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC378
        })?;
dd6b8e786e1fatagen2026-05-27 15:49 UTC379
        Ok(body.unsubscribed)
dd6b8e786e1fatagen2026-05-27 15:49 UTC380
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC381
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC382
dd6b8e786e1fatagen2026-05-27 15:49 UTC383
#[cfg(test)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC384
mod tests {
dd6b8e786e1fatagen2026-05-27 15:49 UTC385
    use super::*;
dd6b8e786e1fatagen2026-05-27 15:49 UTC386
    use std::io::{BufReader, BufWriter};
dd6b8e786e1fatagen2026-05-27 15:49 UTC387
    use std::os::unix::net::UnixStream;
dd6b8e786e1fatagen2026-05-27 15:49 UTC388
    use std::thread;
dd6b8e786e1fatagen2026-05-27 15:49 UTC389
dd6b8e786e1fatagen2026-05-27 15:49 UTC390
    use headroom_ipc::{Codec, Event, HelloData, Op, Request, Response, ServerFrame, Topic};
dd6b8e786e1fatagen2026-05-27 15:49 UTC391
dd6b8e786e1fatagen2026-05-27 15:49 UTC392
    /// A tiny in-process server that runs on the other end of a
dd6b8e786e1fatagen2026-05-27 15:49 UTC393
    /// `UnixStream::pair`. Knows just enough to exercise the client.
dd6b8e786e1fatagen2026-05-27 15:49 UTC394
    fn spawn_test_server() -> (UnixStream, thread::JoinHandle<()>) {
dd6b8e786e1fatagen2026-05-27 15:49 UTC395
        let (a, b) = UnixStream::pair().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC396
        let server_handle = thread::spawn(move || {
dd6b8e786e1fatagen2026-05-27 15:49 UTC397
            let codec = Codec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC398
            let read_side = b.try_clone().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC399
            let mut reader = BufReader::new(read_side);
dd6b8e786e1fatagen2026-05-27 15:49 UTC400
            let mut writer = BufWriter::new(b);
dd6b8e786e1fatagen2026-05-27 15:49 UTC401
dd6b8e786e1fatagen2026-05-27 15:49 UTC402
            // Send hello.
dd6b8e786e1fatagen2026-05-27 15:49 UTC403
            let hello = Event::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC404
                Topic::Control,
dd6b8e786e1fatagen2026-05-27 15:49 UTC405
                "hello",
dd6b8e786e1fatagen2026-05-27 15:49 UTC406
                &HelloData {
dd6b8e786e1fatagen2026-05-27 15:49 UTC407
                    daemon: "headroom".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC408
                    version: "0.1.0-test".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC409
                    protocol: headroom_ipc::PROTOCOL_VERSION,
dd6b8e786e1fatagen2026-05-27 15:49 UTC410
                },
dd6b8e786e1fatagen2026-05-27 15:49 UTC411
            )
dd6b8e786e1fatagen2026-05-27 15:49 UTC412
            .unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC413
            codec
dd6b8e786e1fatagen2026-05-27 15:49 UTC414
                .write(&mut writer, &ServerFrame::Event(hello))
dd6b8e786e1fatagen2026-05-27 15:49 UTC415
                .unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC416
dd6b8e786e1fatagen2026-05-27 15:49 UTC417
            // Serve one round.
dd6b8e786e1fatagen2026-05-27 15:49 UTC418
            loop {
dd6b8e786e1fatagen2026-05-27 15:49 UTC419
                let req: Request = match codec.read(&mut reader) {
dd6b8e786e1fatagen2026-05-27 15:49 UTC420
                    Ok(r) => r,
dd6b8e786e1fatagen2026-05-27 15:49 UTC421
                    Err(_) => return,
dd6b8e786e1fatagen2026-05-27 15:49 UTC422
                };
dd6b8e786e1fatagen2026-05-27 15:49 UTC423
                let resp = match req.op {
dd6b8e786e1fatagen2026-05-27 15:49 UTC424
                    Op::Status => Response::ok(
dd6b8e786e1fatagen2026-05-27 15:49 UTC425
                        req.id,
dd6b8e786e1fatagen2026-05-27 15:49 UTC426
                        &serde_json::json!({
dd6b8e786e1fatagen2026-05-27 15:49 UTC427
                            "version": "0.1.0-test",
dd6b8e786e1fatagen2026-05-27 15:49 UTC428
                            "protocol": headroom_ipc::PROTOCOL_VERSION,
dd6b8e786e1fatagen2026-05-27 15:49 UTC429
                            "uptime_s": 0u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC430
                            "profile": "default",
dd6b8e786e1fatagen2026-05-27 15:49 UTC431
                            "bypass": false,
dd6b8e786e1fatagen2026-05-27 15:49 UTC432
                            "sinks": {
dd6b8e786e1fatagen2026-05-27 15:49 UTC433
                                "processed": {"ready": false},
dd6b8e786e1fatagen2026-05-27 15:49 UTC434
                                "real":      {"ready": false},
dd6b8e786e1fatagen2026-05-27 15:49 UTC435
                            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC436
                            "streams": []
dd6b8e786e1fatagen2026-05-27 15:49 UTC437
                        }),
dd6b8e786e1fatagen2026-05-27 15:49 UTC438
                    )
dd6b8e786e1fatagen2026-05-27 15:49 UTC439
                    .unwrap(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC440
                    Op::ProfileUse { name } => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC441
                        Response::ok(req.id, &serde_json::json!({ "name": name })).unwrap()
dd6b8e786e1fatagen2026-05-27 15:49 UTC442
                    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC443
                    Op::Subscribe { topics } => {
dd6b8e786e1fatagen2026-05-27 15:49 UTC444
                        // Acknowledge, then push one event of each
dd6b8e786e1fatagen2026-05-27 15:49 UTC445
                        // subscribed topic so the client can demonstrate
dd6b8e786e1fatagen2026-05-27 15:49 UTC446
                        // event handling.
dd6b8e786e1fatagen2026-05-27 15:49 UTC447
                        let body = serde_json::json!({ "subscribed": &topics });
dd6b8e786e1fatagen2026-05-27 15:49 UTC448
                        let resp = Response::ok(req.id, &body).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC449
                        codec
dd6b8e786e1fatagen2026-05-27 15:49 UTC450
                            .write(&mut writer, &ServerFrame::Response(resp.clone()))
dd6b8e786e1fatagen2026-05-27 15:49 UTC451
                            .unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC452
dd6b8e786e1fatagen2026-05-27 15:49 UTC453
                        for t in topics {
dd6b8e786e1fatagen2026-05-27 15:49 UTC454
                            let ev = Event::new(t, "tick", &serde_json::json!({})).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC455
                            codec.write(&mut writer, &ServerFrame::Event(ev)).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC456
                        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC457
                        continue;
dd6b8e786e1fatagen2026-05-27 15:49 UTC458
                    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC459
                    _ => Response::ok(req.id, &serde_json::Value::Null).unwrap(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC460
                };
dd6b8e786e1fatagen2026-05-27 15:49 UTC461
                codec
dd6b8e786e1fatagen2026-05-27 15:49 UTC462
                    .write(&mut writer, &ServerFrame::Response(resp))
dd6b8e786e1fatagen2026-05-27 15:49 UTC463
                    .unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC464
            }
dd6b8e786e1fatagen2026-05-27 15:49 UTC465
        });
dd6b8e786e1fatagen2026-05-27 15:49 UTC466
        (a, server_handle)
dd6b8e786e1fatagen2026-05-27 15:49 UTC467
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC468
dd6b8e786e1fatagen2026-05-27 15:49 UTC469
    fn client_on(stream: UnixStream) -> Client {
dd6b8e786e1fatagen2026-05-27 15:49 UTC470
        let reader_half = stream.try_clone().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC471
        let writer_half = stream;
dd6b8e786e1fatagen2026-05-27 15:49 UTC472
        let mut me = Client {
dd6b8e786e1fatagen2026-05-27 15:49 UTC473
            reader: BufReader::new(reader_half),
dd6b8e786e1fatagen2026-05-27 15:49 UTC474
            writer: BufWriter::new(writer_half),
dd6b8e786e1fatagen2026-05-27 15:49 UTC475
            codec: Codec::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC476
            next_id: 1,
dd6b8e786e1fatagen2026-05-27 15:49 UTC477
            pending_events: VecDeque::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC478
            hello: HelloData {
dd6b8e786e1fatagen2026-05-27 15:49 UTC479
                daemon: String::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC480
                version: String::new(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC481
                protocol: 0,
dd6b8e786e1fatagen2026-05-27 15:49 UTC482
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC483
            socket_path: PathBuf::from("<test>"),
dd6b8e786e1fatagen2026-05-27 15:49 UTC484
        };
dd6b8e786e1fatagen2026-05-27 15:49 UTC485
        me.handshake().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC486
        me
dd6b8e786e1fatagen2026-05-27 15:49 UTC487
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC488
dd6b8e786e1fatagen2026-05-27 15:49 UTC489
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC490
    fn handshake_then_status() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC491
        let (client_sock, _server) = spawn_test_server();
dd6b8e786e1fatagen2026-05-27 15:49 UTC492
        let mut client = client_on(client_sock);
dd6b8e786e1fatagen2026-05-27 15:49 UTC493
        assert_eq!(client.hello().daemon, "headroom");
dd6b8e786e1fatagen2026-05-27 15:49 UTC494
        assert_eq!(client.hello().protocol, headroom_ipc::PROTOCOL_VERSION);
dd6b8e786e1fatagen2026-05-27 15:49 UTC495
dd6b8e786e1fatagen2026-05-27 15:49 UTC496
        let status = client.status().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC497
        assert_eq!(status.profile, "default");
dd6b8e786e1fatagen2026-05-27 15:49 UTC498
        assert!(!status.bypass);
dd6b8e786e1fatagen2026-05-27 15:49 UTC499
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC500
dd6b8e786e1fatagen2026-05-27 15:49 UTC501
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC502
    fn profile_use_returns_name() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC503
        let (client_sock, _server) = spawn_test_server();
dd6b8e786e1fatagen2026-05-27 15:49 UTC504
        let mut client = client_on(client_sock);
dd6b8e786e1fatagen2026-05-27 15:49 UTC505
        let name = client.profile_use("night").unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC506
        assert_eq!(name, "night");
dd6b8e786e1fatagen2026-05-27 15:49 UTC507
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC508
dd6b8e786e1fatagen2026-05-27 15:49 UTC509
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC510
    fn subscribe_then_consume_event() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC511
        let (client_sock, _server) = spawn_test_server();
dd6b8e786e1fatagen2026-05-27 15:49 UTC512
        let mut client = client_on(client_sock);
dd6b8e786e1fatagen2026-05-27 15:49 UTC513
        let acked = client.subscribe(&[Topic::Meters]).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC514
        assert_eq!(acked, vec![Topic::Meters]);
dd6b8e786e1fatagen2026-05-27 15:49 UTC515
dd6b8e786e1fatagen2026-05-27 15:49 UTC516
        let ev = client.next_event().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC517
        assert_eq!(ev.topic, Topic::Meters);
dd6b8e786e1fatagen2026-05-27 15:49 UTC518
        assert_eq!(ev.event, "tick");
dd6b8e786e1fatagen2026-05-27 15:49 UTC519
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC520
dd6b8e786e1fatagen2026-05-27 15:49 UTC521
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC522
    fn events_interleaved_during_request_are_queued() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC523
        // The test server pushes events *after* the subscribe response,
dd6b8e786e1fatagen2026-05-27 15:49 UTC524
        // so let's check that requesting another op afterwards drains
dd6b8e786e1fatagen2026-05-27 15:49 UTC525
        // them through the queue.
dd6b8e786e1fatagen2026-05-27 15:49 UTC526
        let (client_sock, _server) = spawn_test_server();
dd6b8e786e1fatagen2026-05-27 15:49 UTC527
        let mut client = client_on(client_sock);
dd6b8e786e1fatagen2026-05-27 15:49 UTC528
        client.subscribe(&[Topic::Meters, Topic::Profile]).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC529
dd6b8e786e1fatagen2026-05-27 15:49 UTC530
        // Now issue another request. The server hasn't sent the events
dd6b8e786e1fatagen2026-05-27 15:49 UTC531
        // until we read more, but our client will keep reading.
dd6b8e786e1fatagen2026-05-27 15:49 UTC532
        let status = client.status().unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC533
        assert_eq!(status.profile, "default");
dd6b8e786e1fatagen2026-05-27 15:49 UTC534
dd6b8e786e1fatagen2026-05-27 15:49 UTC535
        // We may have buffered events from the prior subscribe and from
dd6b8e786e1fatagen2026-05-27 15:49 UTC536
        // any in flight; drain them.
dd6b8e786e1fatagen2026-05-27 15:49 UTC537
        let mut topics = Vec::new();
dd6b8e786e1fatagen2026-05-27 15:49 UTC538
        while let Some(ev) = client.pending_event() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC539
            topics.push(ev.topic);
dd6b8e786e1fatagen2026-05-27 15:49 UTC540
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC541
        // The events arrived between the subscribe-ack and the status
dd6b8e786e1fatagen2026-05-27 15:49 UTC542
        // response; both should be queued.
dd6b8e786e1fatagen2026-05-27 15:49 UTC543
        assert!(topics.contains(&Topic::Meters));
dd6b8e786e1fatagen2026-05-27 15:49 UTC544
        assert!(topics.contains(&Topic::Profile));
dd6b8e786e1fatagen2026-05-27 15:49 UTC545
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC546
}