Files
foxhunt/docs/plans/2026-03-02-fxt-broker-check-implementation.md
jgrusewski eefb191a62 docs: add implementation plan for fxt broker check command
7 tasks: deps → config → proto → broker.rs → main.rs wiring → lint → verify.
TDD approach with exact file paths and commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 13:28:51 +01:00

27 KiB

fxt broker check Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add fxt broker check command that validates IBKR connectivity via direct ibapi handshake and broker-gateway gRPC health check.

Architecture: New broker subcommand in the fxt CLI. Direct IB check uses ibapi crate (optional, feature-gated). gRPC check calls BrokerGatewayService::HealthCheck + GetSessionStatus. Config reads from ~/.foxhunt/config.toml [broker] section with env var and CLI flag overrides.

Tech Stack: Rust, clap 4 (derive), ibapi 1.2, tonic 0.14, tokio


Task 1: Add ibapi dependency and broker-check feature to fxt

Files:

  • Modify: bin/fxt/Cargo.toml

Step 1: Add ibapi optional dep and broker-check feature

In bin/fxt/Cargo.toml, add to [dependencies] section after the common.workspace = true line:

# Broker connectivity check (optional, for direct IB Gateway validation)
ibapi = { workspace = true, optional = true }

In [features] section, change to:

[features]
default = ["broker-check"]
# Test utilities feature for integration tests
test-utils = []
# Direct IBKR connectivity check via ibapi crate
broker-check = ["dep:ibapi"]

Step 2: Verify it compiles

Run: SQLX_OFFLINE=true cargo check -p fxt Expected: PASS (ibapi is optional, nothing uses it yet)

Step 3: Commit

git add bin/fxt/Cargo.toml
git commit -m "feat(fxt): add ibapi optional dep behind broker-check feature"

Task 2: Add BrokerConfig to TliConfig

Files:

  • Modify: bin/fxt/src/config.rs

Step 1: Write the failing test

Add to the existing mod tests block at the bottom of bin/fxt/src/config.rs:

#[test]
fn test_config_with_broker_section() {
    let toml_str = r#"
        api_gateway_url = "http://localhost:50051"

        [broker]
        gateway_url = "http://broker-gw:50060"

        [broker.ibkr]
        host = "10.0.0.5"
        port = 4002
        client_id = 42
    "#;
    let config: TliConfig = toml::from_str(toml_str).unwrap();
    assert_eq!(config.broker.gateway_url, "http://broker-gw:50060");
    assert_eq!(config.broker.ibkr.host, "10.0.0.5");
    assert_eq!(config.broker.ibkr.port, 4002);
    assert_eq!(config.broker.ibkr.client_id, 42);
}

#[test]
fn test_config_broker_defaults() {
    let config: TliConfig = toml::from_str("").unwrap();
    assert_eq!(config.broker.gateway_url, "http://localhost:50060");
    assert_eq!(config.broker.ibkr.host, "127.0.0.1");
    assert_eq!(config.broker.ibkr.port, 4002);
    assert_eq!(config.broker.ibkr.client_id, 99);
}

Step 2: Run test to verify it fails

Run: SQLX_OFFLINE=true cargo test -p fxt --lib config::tests::test_config_with_broker_section Expected: FAIL — TliConfig has no broker field

Step 3: Add BrokerConfig structs and wire into TliConfig

Add these structs above the TliConfig struct in bin/fxt/src/config.rs:

/// IBKR-specific connection settings
#[derive(Debug, Serialize, Deserialize)]
pub struct IbkrConfig {
    /// IB Gateway host
    #[serde(default = "default_ibkr_host")]
    pub host: String,
    /// IB Gateway port (4002 = paper, 4001 = live)
    #[serde(default = "default_ibkr_port")]
    pub port: u16,
    /// TWS client ID (unique per connection)
    #[serde(default = "default_ibkr_client_id")]
    pub client_id: i32,
}

fn default_ibkr_host() -> String {
    "127.0.0.1".to_owned()
}
fn default_ibkr_port() -> u16 {
    4002
}
fn default_ibkr_client_id() -> i32 {
    99
}

impl Default for IbkrConfig {
    fn default() -> Self {
        Self {
            host: default_ibkr_host(),
            port: default_ibkr_port(),
            client_id: default_ibkr_client_id(),
        }
    }
}

/// Broker connectivity settings
#[derive(Debug, Serialize, Deserialize)]
pub struct BrokerConfig {
    /// Broker Gateway gRPC URL
    #[serde(default = "default_broker_gateway_url")]
    pub gateway_url: String,
    /// IBKR-specific settings
    #[serde(default)]
    pub ibkr: IbkrConfig,
}

fn default_broker_gateway_url() -> String {
    "http://localhost:50060".to_owned()
}

impl Default for BrokerConfig {
    fn default() -> Self {
        Self {
            gateway_url: default_broker_gateway_url(),
            ibkr: IbkrConfig::default(),
        }
    }
}

Add a broker field to TliConfig:

/// Broker connectivity settings
#[serde(default)]
pub broker: BrokerConfig,

Update the Default impl for TliConfig to include broker: BrokerConfig::default().

Step 4: Run tests to verify they pass

Run: SQLX_OFFLINE=true cargo test -p fxt --lib config::tests Expected: ALL PASS

Step 5: Commit

git add bin/fxt/src/config.rs
git commit -m "feat(fxt): add BrokerConfig section to TliConfig"

Task 3: Copy broker_gateway.proto and compile in build.rs

Files:

  • Create: bin/fxt/proto/broker_gateway.proto (copy from services/broker_gateway_service/proto/broker_gateway.proto)
  • Modify: bin/fxt/build.rs
  • Modify: bin/fxt/src/lib.rs

Step 1: Copy the proto file

cp services/broker_gateway_service/proto/broker_gateway.proto bin/fxt/proto/broker_gateway.proto

Step 2: Add broker_gateway.proto to build.rs

In bin/fxt/build.rs, add "proto/broker_gateway.proto" to the compile_protos list and add a rerun-if-changed line:

.compile_protos(
    &[
        "proto/trading.proto",
        "proto/health.proto",
        "proto/ml.proto",
        "proto/config.proto",
        "proto/ml_training.proto",
        "proto/trading_agent.proto",
        "proto/broker_gateway.proto",
    ],
    &["proto"],
)?;

println!("cargo:rerun-if-changed=proto/broker_gateway.proto");

Step 3: Add broker_gateway proto module to lib.rs

In bin/fxt/src/lib.rs, inside the pub mod proto { ... } block, add:

/// Broker Gateway service protobuf definitions
///
/// Contains message types and service interfaces for the broker gateway,
/// including health checks, session status, and order routing.
#[allow(clippy::shadow_reuse, clippy::impl_trait_in_params, clippy::empty_structs_with_brackets)]
pub mod broker_gateway {
    tonic::include_proto!("broker_gateway");
}

Step 4: Verify it compiles

Run: SQLX_OFFLINE=true cargo check -p fxt Expected: PASS

Step 5: Commit

git add bin/fxt/proto/broker_gateway.proto bin/fxt/build.rs bin/fxt/src/lib.rs
git commit -m "feat(fxt): compile broker_gateway.proto for gRPC client stubs"

Task 4: Create broker.rs command module — config resolution + check runner

Files:

  • Create: bin/fxt/src/commands/broker.rs
  • Modify: bin/fxt/src/commands/mod.rs

Step 1: Write the failing test for config resolution

Create bin/fxt/src/commands/broker.rs with tests first:

//! Broker connectivity check command
//!
//! Validates IBKR TWS/Gateway connectivity via:
//! 1. Direct ibapi handshake (feature-gated behind `broker-check`)
//! 2. Broker Gateway gRPC health check

use anyhow::Result;
use clap::{Args, Subcommand};
use colored::Colorize;
use std::time::Instant;

use crate::config::TliConfig;

/// Resolved broker check settings (CLI > env > config > defaults).
#[derive(Debug)]
pub struct ResolvedBrokerConfig {
    pub ibkr_host: String,
    pub ibkr_port: u16,
    pub ibkr_client_id: i32,
    pub gateway_url: String,
    pub skip_ibkr: bool,
    pub skip_gateway: bool,
}

/// Broker command arguments
#[derive(Args, Debug)]
pub struct BrokerArgs {
    #[command(subcommand)]
    command: BrokerCommand,
}

/// Broker subcommands
#[derive(Subcommand, Debug, Clone)]
pub enum BrokerCommand {
    /// Check broker connectivity (IB Gateway + Broker Gateway gRPC)
    #[clap(
        long_about = "Validate IBKR TWS/Gateway connectivity.\n\n\
        Runs two checks:\n\
        1. Direct IB Gateway: TCP connect + ibapi handshake + account summary\n\
        2. Broker Gateway gRPC: HealthCheck + GetSessionStatus RPCs\n\n\
        Config precedence: CLI flags > env vars > ~/.foxhunt/config.toml > defaults\n\n\
        Examples:\n\
          fxt broker check\n\
          fxt broker check --host 10.0.0.5 --port 4002\n\
          fxt broker check --skip-ibkr\n\
          fxt broker check --skip-gateway"
    )]
    Check(CheckArgs),
}

/// Arguments for `fxt broker check`
#[derive(Debug, Args, Clone)]
pub struct CheckArgs {
    /// IB Gateway host
    #[arg(long, env = "IBKR_HOST")]
    pub host: Option<String>,

    /// IB Gateway port (4002 = paper, 4001 = live)
    #[arg(long, env = "IBKR_PORT")]
    pub port: Option<u16>,

    /// TWS client ID (must be unique per connection)
    #[arg(long, env = "IBKR_CLIENT_ID")]
    pub client_id: Option<i32>,

    /// Broker Gateway gRPC URL
    #[arg(long, env = "BROKER_GATEWAY_URL")]
    pub gateway_url: Option<String>,

    /// Skip direct IB Gateway check
    #[arg(long, default_value_t = false)]
    pub skip_ibkr: bool,

    /// Skip Broker Gateway gRPC check
    #[arg(long, default_value_t = false)]
    pub skip_gateway: bool,
}

/// Resolve config: CLI flags > env vars (handled by clap) > config file > defaults.
pub fn resolve_config(args: &CheckArgs, config: &TliConfig) -> ResolvedBrokerConfig {
    ResolvedBrokerConfig {
        ibkr_host: args
            .host
            .clone()
            .unwrap_or_else(|| config.broker.ibkr.host.clone()),
        ibkr_port: args.port.unwrap_or(config.broker.ibkr.port),
        ibkr_client_id: args.client_id.unwrap_or(config.broker.ibkr.client_id),
        gateway_url: args
            .gateway_url
            .clone()
            .unwrap_or_else(|| config.broker.gateway_url.clone()),
        skip_ibkr: args.skip_ibkr,
        skip_gateway: args.skip_gateway,
    }
}

/// Single check result
#[derive(Debug)]
pub struct CheckResult {
    pub name: String,
    pub passed: bool,
    pub detail: String,
    pub duration_ms: u128,
}

/// Print a check result line
fn print_check(result: &CheckResult) {
    let status = if result.passed {
        "✓".green().bold()
    } else {
        "✗".red().bold()
    };
    let dots = ".".repeat(24_usize.saturating_sub(result.name.len()));
    println!(
        "  {} {} {}  {}",
        result.name, dots, status, result.detail
    );
}

// ── IB Gateway direct check ──────────────────────────────────────────

/// TCP connect probe (fail-fast before ibapi handshake).
async fn check_tcp_connect(host: &str, port: u16) -> CheckResult {
    let addr = format!("{host}:{port}");
    let start = Instant::now();
    match tokio::time::timeout(
        std::time::Duration::from_secs(5),
        tokio::net::TcpStream::connect(&addr),
    )
    .await
    {
        Ok(Ok(_)) => CheckResult {
            name: "TCP connect".to_owned(),
            passed: true,
            detail: format!("{addr} ({}ms)", start.elapsed().as_millis()),
            duration_ms: start.elapsed().as_millis(),
        },
        Ok(Err(e)) => CheckResult {
            name: "TCP connect".to_owned(),
            passed: false,
            detail: format!("{e} ({addr})"),
            duration_ms: start.elapsed().as_millis(),
        },
        Err(_) => CheckResult {
            name: "TCP connect".to_owned(),
            passed: false,
            detail: format!("Timeout after 5s ({addr})"),
            duration_ms: start.elapsed().as_millis(),
        },
    }
}

/// Full ibapi handshake (blocking, runs on spawn_blocking).
#[cfg(feature = "broker-check")]
async fn check_ibapi_handshake(host: &str, port: u16, client_id: i32) -> CheckResult {
    let addr = format!("{host}:{port}");
    let start = Instant::now();
    match tokio::task::spawn_blocking(move || ibapi::Client::connect(&addr, client_id)).await {
        Ok(Ok(client)) => {
            let server_version = client.server_version();
            // client is dropped here which disconnects
            CheckResult {
                name: "ibapi handshake".to_owned(),
                passed: true,
                detail: format!(
                    "client_id={client_id}, server v{server_version} ({}ms)",
                    start.elapsed().as_millis()
                ),
                duration_ms: start.elapsed().as_millis(),
            }
        }
        Ok(Err(e)) => CheckResult {
            name: "ibapi handshake".to_owned(),
            passed: false,
            detail: format!("{e}"),
            duration_ms: start.elapsed().as_millis(),
        },
        Err(e) => CheckResult {
            name: "ibapi handshake".to_owned(),
            passed: false,
            detail: format!("Task join error: {e}"),
            duration_ms: start.elapsed().as_millis(),
        },
    }
}

#[cfg(not(feature = "broker-check"))]
async fn check_ibapi_handshake(_host: &str, _port: u16, _client_id: i32) -> CheckResult {
    CheckResult {
        name: "ibapi handshake".to_owned(),
        passed: false,
        detail: "Skipped (compile with --features broker-check)".to_owned(),
        duration_ms: 0,
    }
}

// ── Broker Gateway gRPC check ────────────────────────────────────────

/// gRPC HealthCheck against broker_gateway_service.
async fn check_grpc_health(gateway_url: &str) -> CheckResult {
    use crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient;
    use crate::proto::broker_gateway::HealthCheckRequest;

    let start = Instant::now();
    let channel = match tonic::transport::Channel::from_shared(gateway_url.to_owned()) {
        Ok(endpoint) => {
            match tokio::time::timeout(std::time::Duration::from_secs(5), endpoint.connect()).await
            {
                Ok(Ok(ch)) => ch,
                Ok(Err(e)) => {
                    return CheckResult {
                        name: "Health check".to_owned(),
                        passed: false,
                        detail: format!("Connection failed: {e}"),
                        duration_ms: start.elapsed().as_millis(),
                    };
                }
                Err(_) => {
                    return CheckResult {
                        name: "Health check".to_owned(),
                        passed: false,
                        detail: format!("Timeout after 5s ({gateway_url})"),
                        duration_ms: start.elapsed().as_millis(),
                    };
                }
            }
        }
        Err(e) => {
            return CheckResult {
                name: "Health check".to_owned(),
                passed: false,
                detail: format!("Invalid URL: {e}"),
                duration_ms: start.elapsed().as_millis(),
            };
        }
    };

    let mut client = BrokerGatewayServiceClient::new(channel);
    match client.health_check(HealthCheckRequest {}).await {
        Ok(resp) => {
            let inner = resp.into_inner();
            CheckResult {
                name: "Health check".to_owned(),
                passed: inner.healthy,
                detail: format!(
                    "{} ({}ms)",
                    inner.message,
                    start.elapsed().as_millis()
                ),
                duration_ms: start.elapsed().as_millis(),
            }
        }
        Err(e) => CheckResult {
            name: "Health check".to_owned(),
            passed: false,
            detail: format!("{} ({})", e.code(), e.message()),
            duration_ms: start.elapsed().as_millis(),
        },
    }
}

/// gRPC GetSessionStatus against broker_gateway_service.
async fn check_grpc_session(gateway_url: &str) -> CheckResult {
    use crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient;
    use crate::proto::broker_gateway::GetSessionStatusRequest;

    let start = Instant::now();
    let channel = match tonic::transport::Channel::from_shared(gateway_url.to_owned()) {
        Ok(endpoint) => match endpoint.connect().await {
            Ok(ch) => ch,
            Err(e) => {
                return CheckResult {
                    name: "Session status".to_owned(),
                    passed: false,
                    detail: format!("Connection failed: {e}"),
                    duration_ms: start.elapsed().as_millis(),
                };
            }
        },
        Err(e) => {
            return CheckResult {
                name: "Session status".to_owned(),
                passed: false,
                detail: format!("Invalid URL: {e}"),
                duration_ms: start.elapsed().as_millis(),
            };
        }
    };

    let mut client = BrokerGatewayServiceClient::new(channel);
    match client
        .get_session_status(GetSessionStatusRequest { session_id: None })
        .await
    {
        Ok(resp) => {
            let inner = resp.into_inner();
            let state_name = match inner.state {
                0 => "DISCONNECTED",
                1 => "CONNECTED",
                2 => "LOGGING_IN",
                3 => "ACTIVE",
                4 => "LOGGING_OUT",
                _ => "UNKNOWN",
            };
            CheckResult {
                name: "Session status".to_owned(),
                passed: inner.state >= 1, // CONNECTED or better
                detail: format!(
                    "{state_name} (seq: {}/{}, RTT: {:.1}ms)",
                    inner.sender_seq_num, inner.target_seq_num, inner.heartbeat_rtt_ms
                ),
                duration_ms: start.elapsed().as_millis(),
            }
        }
        Err(e) => CheckResult {
            name: "Session status".to_owned(),
            passed: false,
            detail: format!("{} ({})", e.code(), e.message()),
            duration_ms: start.elapsed().as_millis(),
        },
    }
}

// ── Orchestrator ─────────────────────────────────────────────────────

/// Run all broker checks and return overall pass/fail.
pub async fn run_broker_check(resolved: &ResolvedBrokerConfig) -> Result<bool> {
    println!("{}", "Broker Connectivity Check".bold());
    println!("{}", "═".repeat(40));

    let mut all_passed = true;

    // ── IB Gateway (direct) ──
    if !resolved.skip_ibkr {
        println!("{}", "IB Gateway (direct)".bold());
        let tcp = check_tcp_connect(&resolved.ibkr_host, resolved.ibkr_port).await;
        print_check(&tcp);

        if tcp.passed {
            let handshake = check_ibapi_handshake(
                &resolved.ibkr_host,
                resolved.ibkr_port,
                resolved.ibkr_client_id,
            )
            .await;
            print_check(&handshake);
            if !handshake.passed {
                all_passed = false;
            }
        } else {
            all_passed = false;
        }
        println!();
    }

    // ── Broker Gateway (gRPC) ──
    if !resolved.skip_gateway {
        println!("{}", "Broker Gateway (gRPC)".bold());
        let health = check_grpc_health(&resolved.gateway_url).await;
        print_check(&health);

        if health.passed {
            let session = check_grpc_session(&resolved.gateway_url).await;
            print_check(&session);
            if !session.passed {
                all_passed = false;
            }
        } else {
            all_passed = false;
        }
        println!();
    }

    // ── Summary ──
    if all_passed {
        println!("{}", "Result: All checks passed ✓".green().bold());
    } else {
        println!("{}", "Result: One or more checks failed ✗".red().bold());
    }

    Ok(all_passed)
}

/// Execute broker command (public interface for main.rs).
pub async fn execute_broker_command(args: BrokerArgs, config: &TliConfig) -> Result<bool> {
    match args.command {
        BrokerCommand::Check(check_args) => {
            let resolved = resolve_config(&check_args, config);
            run_broker_check(&resolved).await
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use crate::config::TliConfig;

    #[test]
    fn test_resolve_config_uses_defaults() {
        let args = CheckArgs {
            host: None,
            port: None,
            client_id: None,
            gateway_url: None,
            skip_ibkr: false,
            skip_gateway: false,
        };
        let config = TliConfig::default();
        let resolved = resolve_config(&args, &config);

        assert_eq!(resolved.ibkr_host, "127.0.0.1");
        assert_eq!(resolved.ibkr_port, 4002);
        assert_eq!(resolved.ibkr_client_id, 99);
        assert_eq!(resolved.gateway_url, "http://localhost:50060");
        assert!(!resolved.skip_ibkr);
        assert!(!resolved.skip_gateway);
    }

    #[test]
    fn test_resolve_config_cli_overrides() {
        let args = CheckArgs {
            host: Some("10.0.0.5".to_owned()),
            port: Some(4001),
            client_id: Some(7),
            gateway_url: Some("http://custom:9090".to_owned()),
            skip_ibkr: true,
            skip_gateway: false,
        };
        let config = TliConfig::default();
        let resolved = resolve_config(&args, &config);

        assert_eq!(resolved.ibkr_host, "10.0.0.5");
        assert_eq!(resolved.ibkr_port, 4001);
        assert_eq!(resolved.ibkr_client_id, 7);
        assert_eq!(resolved.gateway_url, "http://custom:9090");
        assert!(resolved.skip_ibkr);
    }

    #[test]
    fn test_resolve_config_file_overrides_defaults() {
        let args = CheckArgs {
            host: None,
            port: None,
            client_id: None,
            gateway_url: None,
            skip_ibkr: false,
            skip_gateway: false,
        };
        let config: TliConfig = toml::from_str(
            r#"
            [broker]
            gateway_url = "http://broker-gw:50060"
            [broker.ibkr]
            host = "192.168.1.100"
            port = 4001
            client_id = 55
            "#,
        )
        .unwrap();
        let resolved = resolve_config(&args, &config);

        assert_eq!(resolved.ibkr_host, "192.168.1.100");
        assert_eq!(resolved.ibkr_port, 4001);
        assert_eq!(resolved.ibkr_client_id, 55);
        assert_eq!(resolved.gateway_url, "http://broker-gw:50060");
    }

    #[tokio::test]
    async fn test_tcp_connect_refused() {
        // Connect to a port that's almost certainly not listening
        let result = check_tcp_connect("127.0.0.1", 19999).await;
        assert!(!result.passed);
        assert_eq!(result.name, "TCP connect");
    }

    #[tokio::test]
    async fn test_grpc_health_unreachable() {
        let result = check_grpc_health("http://127.0.0.1:19998").await;
        assert!(!result.passed);
        assert_eq!(result.name, "Health check");
    }
}

Step 2: Wire broker module into commands/mod.rs

In bin/fxt/src/commands/mod.rs, add:

pub mod broker;

And add to the re-exports:

pub use broker::{execute_broker_command, BrokerArgs};

Step 3: Verify tests pass

Run: SQLX_OFFLINE=true cargo test -p fxt --lib commands::broker::tests Expected: ALL PASS (5 tests: 3 config resolution + 2 connectivity failure)

Step 4: Commit

git add bin/fxt/src/commands/broker.rs bin/fxt/src/commands/mod.rs
git commit -m "feat(fxt): add broker check command with config resolution and connectivity checks"

Task 5: Wire Broker subcommand into main.rs

Files:

  • Modify: bin/fxt/src/main.rs

Step 1: Add Broker variant to Commands enum

In bin/fxt/src/main.rs, add the import:

use fxt::commands::broker::{execute_broker_command, BrokerArgs};

Add to the Commands enum (after the Trade variant):

/// Broker connectivity check
#[clap(
    long_about = "Validate broker connectivity.\n\n\
    Checks:\n\
    1. Direct IB Gateway: TCP + ibapi handshake\n\
    2. Broker Gateway gRPC: HealthCheck + SessionStatus\n\n\
    Examples:\n\
      fxt broker check\n\
      fxt broker check --host 10.0.0.5 --port 4002\n\
      fxt broker check --skip-ibkr"
)]
Broker {
    #[command(flatten)]
    broker_args: BrokerArgs,
},

Step 2: Add match arm in main()

In the match cli.command { ... } block in main(), add:

Commands::Broker { broker_args } => {
    let passed = execute_broker_command(broker_args, &config).await?;
    if !passed {
        std::process::exit(1);
    }
    Ok(())
}

Note: Broker does NOT require JWT auth — it's an infrastructure check.

Step 3: Pass config to execute_broker_command

The config variable is already loaded at the top of main(). Pass it directly.

Step 4: Verify it compiles and CLI help works

Run: SQLX_OFFLINE=true cargo build -p fxt Run: ./target/debug/fxt broker check --help Expected: Shows help with --host, --port, --client-id, --gateway-url, --skip-ibkr, --skip-gateway

Step 5: Add CLI parsing test

In the mod tests block at the bottom of main.rs, add:

#[test]
fn test_cli_parsing_broker_check() {
    let cli = Cli::parse_from(&["fxt", "broker", "check"]);
    match cli.command {
        Commands::Broker { .. } => {},
        _ => panic!("Expected Broker command"),
    }
}

#[test]
fn test_cli_parsing_broker_check_with_flags() {
    let cli = Cli::parse_from(&[
        "fxt",
        "broker",
        "check",
        "--host",
        "10.0.0.5",
        "--port",
        "4001",
        "--skip-gateway",
    ]);
    match cli.command {
        Commands::Broker { .. } => {},
        _ => panic!("Expected Broker command"),
    }
}

Step 6: Run all fxt tests

Run: SQLX_OFFLINE=true cargo test -p fxt --lib Expected: ALL PASS

Step 7: Commit

git add bin/fxt/src/main.rs
git commit -m "feat(fxt): wire broker check subcommand into CLI"

Task 6: Suppress unused_crate_dependencies for ibapi

Files:

  • Modify: bin/fxt/src/main.rs
  • Modify: bin/fxt/src/lib.rs

Since ibapi is used only inside cfg(feature) blocks in broker.rs, Rust may warn about unused crate dependencies in the top-level main.rs or lib.rs.

Step 1: Add ibapi suppression

In bin/fxt/src/main.rs, in the section with all the use X as _; lines, add:

#[cfg(feature = "broker-check")]
use ibapi as _;

Step 2: Verify clean compile

Run: SQLX_OFFLINE=true cargo check -p fxt 2>&1 | grep -i warning Expected: No ibapi-related warnings

Step 3: Run clippy

Run: SQLX_OFFLINE=true cargo clippy -p fxt -- -D warnings Expected: PASS (0 errors, 0 warnings)

Step 4: Commit

git add bin/fxt/src/main.rs bin/fxt/src/lib.rs
git commit -m "chore(fxt): suppress unused_crate_dependencies for ibapi"

Task 7: Full integration test and final verification

Files:

  • (none new — verification only)

Step 1: Run full workspace check

Run: SQLX_OFFLINE=true cargo check --workspace Expected: PASS

Step 2: Run all fxt tests

Run: SQLX_OFFLINE=true cargo test -p fxt --lib Expected: ALL PASS

Step 3: Run clippy on fxt

Run: SQLX_OFFLINE=true cargo clippy -p fxt -- -D warnings Expected: 0 errors, 0 warnings

Step 4: Smoke test the binary

Run: SQLX_OFFLINE=true cargo run -p fxt -- broker check --skip-ibkr --skip-gateway Expected: Prints header, skips both checks, exits 0

Run: SQLX_OFFLINE=true cargo run -p fxt -- broker check --skip-gateway Expected: Attempts TCP connect to 127.0.0.1:4002, likely fails (no IB Gateway running locally), exits 1

Step 5: Final commit (if any fixups needed)

git add -A
git commit -m "feat(fxt): fxt broker check command complete"