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>
This commit is contained in:
jgrusewski
2026-03-02 13:28:51 +01:00
parent 50d1aff97c
commit eefb191a62

View File

@@ -0,0 +1,948 @@
# `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:
```toml
# Broker connectivity check (optional, for direct IB Gateway validation)
ibapi = { workspace = true, optional = true }
```
In `[features]` section, change to:
```toml
[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**
```bash
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`:
```rust
#[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`:
```rust
/// 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`:
```rust
/// 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**
```bash
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**
```bash
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:
```rust
.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:
```rust
/// 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**
```bash
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:
```rust
//! 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:
```rust
pub mod broker;
```
And add to the re-exports:
```rust
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**
```bash
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:
```rust
use fxt::commands::broker::{execute_broker_command, BrokerArgs};
```
Add to the `Commands` enum (after the `Trade` variant):
```rust
/// 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:
```rust
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:
```rust
#[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**
```bash
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:
```rust
#[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**
```bash
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)**
```bash
git add -A
git commit -m "feat(fxt): fxt broker check command complete"
```