trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! Shared test doubles for the runtime adapter unit tests.
//!
//! Why: the `claude_code`, `tcode`, and `mod` test blocks all need an in-memory
//! [`ManagedTmuxDriver`] that records the lines sent to it so adapter spawn paths
//! can be exercised without a real `tmux` binary. Keeping three byte-identical
//! copies of that fake (one per test module) was a maintenance hazard flagged in
//! the #1212 review (#1213); this module is the single source of truth.
//! What: defines [`FakeTmux`], a `Mutex`-guarded recorder implementing
//! [`ManagedTmuxDriver`]; every method is a no-op except `send_line`, which
//! appends `(session_name, text)`, and `send_line_to_pane`, which appends
//! `(session_name, pane_id, text)` (sibling-window hijack fix, follow-up to
//! #2456), so tests can assert exactly what was sent and whether it was
//! session-scoped or pane-scoped.
//! Test: consumed by the `tcode`/`claude_code`/`mod` test blocks; its own
//! behaviour is verified transitively through those tests (e.g.
//! `tcode_adapter_spawn_sends_run_task`).

use std::sync::{Arc, Mutex};

use crate::session_manager::{ManagedError, ManagedTmuxDriver};

/// In-memory [`ManagedTmuxDriver`] that records every `send_line` /
/// `send_line_to_pane` call.
///
/// Why: adapter tests must assert the exact shell line an adapter sends to the
/// pane without spawning `tmux`; recording sends in a `Mutex<Vec<…>>` makes that
/// assertion trivial and deterministic. Separating the two logs
/// (`sends` vs. `pane_sends`) lets a test assert not just WHAT was sent but
/// whether it targeted the session-scoped "active pane" or a SPECIFIC pane —
/// the exact distinction the sibling-window hijack fix depends on.
/// What: stores `Vec<(session_name, text)>` (`sends`) and
/// `Vec<(session_name, pane_id, text)>` (`pane_sends`) behind separate
/// `Mutex`es; all driver methods are inert except those two. Both are
/// `pub(crate)` so test blocks can lock and inspect them.
/// Test: exercised by every runtime adapter test that calls `spawn`/
/// `spawn_resume`.
pub(crate) struct FakeTmux {
    pub(crate) sends: Mutex<Vec<(String, String)>>,
    /// Records every `send_line_to_pane` call as `(session_name, pane_id,
    /// text)` (sibling-window hijack fix, follow-up to #2456).
    pub(crate) pane_sends: Mutex<Vec<(String, String, String)>>,
    /// Records every `set_environment` call as `(session, key, value)` (#2157
    /// item 1) so adapter tests can assert `TM_MANAGED_SESSION_ID` is durably
    /// published at spawn/resume, not just exported into the pane shell line.
    pub(crate) env_sets: Mutex<Vec<(String, String, String)>>,
    /// What this fake "shell" has PRINTED, which `capture` returns.
    ///
    /// #8233: the launcher now confirms the pane executes what it is typed
    /// before it types a launch. A double that never prints anything is an
    /// unresponsive shell, and every adapter test would start failing for a
    /// reason invented by the double. Answering the probe — and ONLY the probe,
    /// via `pane_handshake::probe_reply` — makes this fake a shell that works,
    /// which is what these tests have always assumed.
    pane_text: Mutex<String>,
}

impl FakeTmux {
    /// Construct an empty recorder wrapped in an `Arc`.
    ///
    /// Why: adapters take `Arc<dyn ManagedTmuxDriver + Send + Sync>`, so the fake
    /// is handed out pre-wrapped; returning `Arc<Self>` (not `Arc<dyn …>`) lets
    /// the caller keep a typed handle for `clone()` + later inspection.
    /// What: allocates a `FakeTmux` with empty send logs inside an `Arc`.
    /// Test: called at the top of every runtime adapter test.
    pub(crate) fn new() -> Arc<Self> {
        Arc::new(Self {
            sends: Mutex::new(Vec::new()),
            pane_sends: Mutex::new(Vec::new()),
            env_sets: Mutex::new(Vec::new()),
            pane_text: Mutex::new(String::new()),
        })
    }

    /// Answer a probe line as a working shell would, without recording it.
    ///
    /// What: `true` when `text` was a probe and has been answered — the caller
    /// then returns without logging, so every existing `sends`/`pane_sends`
    /// assertion still sees exactly the launch lines it always did.
    fn answer_probe(&self, text: &str) -> bool {
        let Some(out) = crate::runtime::pane_handshake::probe_reply(text) else {
            return false;
        };
        let mut pane = self.pane_text.lock().expect("FakeTmux pane text poisoned");
        pane.push_str(&out);
        pane.push('\n');
        true
    }
}

impl ManagedTmuxDriver for FakeTmux {
    fn create_session(&self, _name: &str, _workdir: &str) -> Result<(), ManagedError> {
        Ok(())
    }

    fn kill_session(&self, _name: &str) -> Result<(), ManagedError> {
        Ok(())
    }

    fn send_line(&self, name: &str, text: &str) -> Result<(), ManagedError> {
        if self.answer_probe(text) {
            return Ok(());
        }
        self.sends
            .lock()
            .expect("FakeTmux send log mutex poisoned")
            .push((name.to_owned(), text.to_owned()));
        Ok(())
    }

    /// Records `(name, pane_id, text)` instead of delegating to `send_line`
    /// (the trait default) — a test asserting `pane_sends` got the call and
    /// `sends` stayed empty is exactly what proves the adapter chose the
    /// pane-scoped path over the session-scoped one.
    fn send_line_to_pane(&self, name: &str, pane_id: &str, text: &str) -> Result<(), ManagedError> {
        if self.answer_probe(text) {
            return Ok(());
        }
        self.pane_sends
            .lock()
            .expect("FakeTmux pane-send log mutex poisoned")
            .push((name.to_owned(), pane_id.to_owned(), text.to_owned()));
        Ok(())
    }

    fn capture(&self, _name: &str, _lines: usize) -> Result<String, ManagedError> {
        Ok(self
            .pane_text
            .lock()
            .expect("FakeTmux pane text poisoned")
            .clone())
    }

    /// #8233: the pane-scoped read the handshake uses when a `pane_id` is
    /// known. Same text as [`Self::capture`] — this fake has one pane.
    fn capture_pane(
        &self,
        name: &str,
        _pane_id: &str,
        lines: usize,
    ) -> Result<String, ManagedError> {
        self.capture(name, lines)
    }

    fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
        Ok(Vec::new())
    }

    /// #5859: overrides the CHECKED probe so `session_exists` (which now
    /// delegates to it) keeps answering `false` rather than diverging.
    fn session_exists_checked(&self, _name: &str) -> Result<bool, ManagedError> {
        Ok(false)
    }

    fn set_environment(&self, name: &str, key: &str, value: &str) -> Result<(), ManagedError> {
        self.env_sets
            .lock()
            .expect("FakeTmux env-set log mutex poisoned")
            .push((name.to_owned(), key.to_owned(), value.to_owned()));
        Ok(())
    }
}