Skip to main content

reovim_client_cli/
client.rs

1//! gRPC v2 client wrapper.
2//!
3//! Provides a unified client interface to all gRPC v2 services.
4
5use {
6    reovim_protocol::v2::{
7        // Debug service types (#468: CLI uses DebugService for client-targeting ops)
8        DebugCaptureRequest,
9        DebugCaptureResponse,
10        DebugGetCursorRequest,
11        DebugGetCursorResponse,
12        DebugGetExtensionStateRequest,
13        DebugGetExtensionStateResponse,
14        DebugGetModeRequest,
15        DebugGetModeResponse,
16        DebugListClientsRequest,
17        DebugListClientsResponse,
18        DebugListExtensionsRequest,
19        DebugListExtensionsResponse,
20        DebugSendKeysRequest,
21        DebugSendKeysResponse,
22        GetCursorRequest,
23        GetCursorResponse,
24        GetModeRequest,
25        GetModeResponse,
26        GetRawContentRequest,
27        GetRawContentResponse,
28        GetRegistersRequest,
29        GetRegistersResponse,
30        GetScreenContentRequest,
31        GetScreenContentResponse,
32        InfoRequest,
33        InfoResponse,
34        // Phase 15: Presence types
35        JoinRequest,
36        JoinResponse,
37        LeaveRequest,
38        LeaveResponse,
39        ListBuffersRequest,
40        ListBuffersResponse,
41        ListClientsRequest,
42        ListClientsResponse,
43        // Phase 17 (#481): Debug types
44        LogTailRequest,
45        LogTailResponse,
46        PingRequest,
47        PingResponse,
48        SendKeysRequest,
49        SendKeysResponse,
50        SetSyncModeRequest,
51        SetSyncModeResponse,
52        UpdatePresenceRequest,
53        UpdatePresenceResponse,
54        buffer_service_client::BufferServiceClient,
55        debug_service_client::DebugServiceClient,
56        input_service_client::InputServiceClient,
57        presence_service_client::PresenceServiceClient,
58        server_service_client::ServerServiceClient,
59        state_service_client::StateServiceClient,
60    },
61    tonic::{Request, transport::Channel},
62};
63
64/// Error type for gRPC client operations.
65#[derive(Debug)]
66pub enum GrpcClientError {
67    /// Failed to connect to the server.
68    ConnectionFailed(String),
69    /// gRPC call failed.
70    GrpcError(tonic::Status),
71    /// Invalid CLI argument combination.
72    InvalidArgument(String),
73    /// Web capture script failed.
74    CaptureError(String),
75}
76
77impl std::fmt::Display for GrpcClientError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::ConnectionFailed(msg) => write!(f, "Connection failed: {msg}"),
81            Self::GrpcError(status) => write!(f, "gRPC error: {status}"),
82            Self::InvalidArgument(msg) => write!(f, "Invalid argument: {msg}"),
83            Self::CaptureError(msg) => write!(f, "Capture error: {msg}"),
84        }
85    }
86}
87
88impl std::error::Error for GrpcClientError {}
89
90#[cfg_attr(coverage_nightly, coverage(off))]
91impl From<tonic::Status> for GrpcClientError {
92    fn from(status: tonic::Status) -> Self {
93        Self::GrpcError(status)
94    }
95}
96
97#[cfg_attr(coverage_nightly, coverage(off))]
98impl From<tonic::transport::Error> for GrpcClientError {
99    fn from(err: tonic::transport::Error) -> Self {
100        Self::ConnectionFailed(err.to_string())
101    }
102}
103
104/// gRPC v2 client for interacting with the reovim server.
105///
106/// Wraps all service clients (Input, State, Buffer, Server, Presence, Debug) and provides
107/// a unified interface.
108///
109/// # Phase #479: Client ID Management
110///
111/// The client auto-joins the presence session on first use and stores the assigned
112/// `client_id`. All subsequent calls use this ID for per-client state isolation.
113pub struct GrpcClient {
114    input: InputServiceClient<Channel>,
115    state: StateServiceClient<Channel>,
116    buffer: BufferServiceClient<Channel>,
117    server: ServerServiceClient<Channel>,
118    presence: PresenceServiceClient<Channel>,
119    debug: DebugServiceClient<Channel>,
120    /// Server address for error messages.
121    address: String,
122    /// Client ID assigned by server (None until joined).
123    client_id: Option<u64>,
124    /// Session token for token-based authentication (#483).
125    ///
126    /// Received from `Join()` response, sent as `x-reovim-token` metadata
127    /// header on every subsequent gRPC request.
128    session_token: Option<String>,
129}
130
131impl GrpcClient {
132    /// Connect to a gRPC server at the given address.
133    ///
134    /// # Arguments
135    ///
136    /// * `addr` - Server address in `host:port` format (e.g., "127.0.0.1:12540").
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the connection fails.
141    #[cfg_attr(coverage_nightly, coverage(off))]
142    pub async fn connect(addr: &str) -> Result<Self, GrpcClientError> {
143        // Build the endpoint URL
144        let url = format!("http://{addr}");
145        let channel = Channel::from_shared(url)
146            .map_err(|e| GrpcClientError::ConnectionFailed(e.to_string()))?
147            .connect()
148            .await?;
149
150        Ok(Self {
151            input: InputServiceClient::new(channel.clone()),
152            state: StateServiceClient::new(channel.clone()),
153            buffer: BufferServiceClient::new(channel.clone()),
154            server: ServerServiceClient::new(channel.clone()),
155            presence: PresenceServiceClient::new(channel.clone()),
156            debug: DebugServiceClient::new(channel),
157            address: addr.to_string(),
158            client_id: None,
159            session_token: None,
160        })
161    }
162
163    /// Wrap a proto message in a `tonic::Request` with session token metadata (#483).
164    ///
165    /// If a session token has been stored (from `Join()`), injects it as the
166    /// `x-reovim-token` gRPC metadata header. The server interceptor resolves
167    /// this token to the caller's `ClientId`.
168    #[cfg_attr(coverage_nightly, coverage(off))]
169    fn make_request<T>(&self, body: T) -> Request<T> {
170        let mut request = Request::new(body);
171        if let Some(ref token) = self.session_token {
172            request
173                .metadata_mut()
174                .insert("x-reovim-token", token.parse().expect("session token is valid ASCII"));
175        }
176        request
177    }
178
179    /// Ensure the client has joined the presence session.
180    ///
181    /// If not already joined, calls `presence_join()` to get a client ID.
182    /// Panics if joining fails - CLI cannot operate without a valid client ID.
183    ///
184    /// # Phase #479: Auto-join for per-client state
185    #[cfg_attr(coverage_nightly, coverage(off))]
186    async fn ensure_joined(&mut self) -> u64 {
187        if let Some(id) = self.client_id {
188            return id;
189        }
190
191        match self.presence_join("cli", "CLI").await {
192            Ok(resp) => {
193                self.client_id = Some(resp.client_id);
194                resp.client_id
195            }
196            Err(e) => panic!(
197                "FATAL: Failed to join presence session.\n\
198                 Server: {}\n\
199                 Error: {e}\n\
200                 Ensure server is running and accepts connections.",
201                self.address
202            ),
203        }
204    }
205
206    /// Handle gRPC errors with panic vs retry policy.
207    ///
208    /// # Phase #479: Panic on client bugs, panic on transient (for now)
209    ///
210    /// This function can be used by command handlers that want to panic on
211    /// errors rather than propagating them to the caller.
212    #[allow(dead_code)]
213    #[cfg_attr(coverage_nightly, coverage(off))]
214    fn handle_grpc_error(e: &tonic::Status, operation: &str) -> ! {
215        use tonic::Code;
216
217        match e.code() {
218            // PANIC - Client bug or misconfiguration
219            Code::NotFound
220            | Code::InvalidArgument
221            | Code::PermissionDenied
222            | Code::FailedPrecondition
223            | Code::Internal
224            | Code::Unimplemented => {
225                panic!(
226                    "FATAL: {operation} failed\n\
227                     Code: {:?}\n\
228                     Message: {}\n\
229                     This indicates a client bug or misconfiguration.",
230                    e.code(),
231                    e.message()
232                );
233            }
234            // RETRY - Transient issues (for now, panic - retry logic can be added later)
235            Code::Unavailable
236            | Code::ResourceExhausted
237            | Code::DeadlineExceeded
238            | Code::Aborted => {
239                panic!(
240                    "FATAL: {operation} failed (transient)\n\
241                     Code: {:?}\n\
242                     Message: {}\n\
243                     Server may be temporarily unavailable.",
244                    e.code(),
245                    e.message()
246                );
247            }
248            // Unknown - log and panic
249            _ => {
250                panic!(
251                    "FATAL: {operation} failed (unknown)\n\
252                     Code: {:?}\n\
253                     Message: {}",
254                    e.code(),
255                    e.message()
256                );
257            }
258        }
259    }
260
261    /// Send keys to the editor.
262    ///
263    /// # Arguments
264    ///
265    /// * `keys` - Keys in vim notation (e.g., `iHello<Esc>`).
266    ///
267    /// # Returns
268    ///
269    /// The response containing success status and key status.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the gRPC call fails.
274    ///
275    /// # Phase #479: Auto-join and panic on error
276    ///
277    /// The client auto-joins the presence session on first call.
278    /// Panics if joining fails or if the server returns a fatal error.
279    #[cfg_attr(coverage_nightly, coverage(off))]
280    pub async fn send_keys(&mut self, keys: &str) -> Result<SendKeysResponse, GrpcClientError> {
281        self.ensure_joined().await;
282        let request = self.make_request(SendKeysRequest {
283            keys: keys.to_string(),
284        });
285        let response = self.input.send_keys(request).await?;
286        Ok(response.into_inner())
287    }
288
289    /// Get the current editor mode.
290    ///
291    /// # Phase #479: Auto-join and per-client state
292    ///
293    /// The client auto-joins on first call and queries its per-client mode.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if the gRPC call fails.
298    #[cfg_attr(coverage_nightly, coverage(off))]
299    pub async fn get_mode(&mut self) -> Result<GetModeResponse, GrpcClientError> {
300        self.ensure_joined().await;
301        let request = self.make_request(GetModeRequest { client_id: 0 });
302        let response = self.state.get_mode(request).await?;
303        Ok(response.into_inner())
304    }
305
306    /// Get the mode for a specific client.
307    ///
308    /// # Per-client state (#471): Per-client mode isolation
309    ///
310    /// Returns the mode from the specified client's per-client mode stack.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if the gRPC call fails.
315    #[cfg_attr(coverage_nightly, coverage(off))]
316    pub async fn get_mode_for_client(
317        &mut self,
318        client_id: u64,
319    ) -> Result<GetModeResponse, GrpcClientError> {
320        let request = self.make_request(GetModeRequest { client_id });
321        let response = self.state.get_mode(request).await?;
322        Ok(response.into_inner())
323    }
324
325    /// Get the cursor position.
326    ///
327    /// # Phase #479: Auto-join and per-client state
328    ///
329    /// The client auto-joins on first call and queries its per-client cursor.
330    ///
331    /// # Errors
332    ///
333    /// Returns an error if the gRPC call fails.
334    #[cfg_attr(coverage_nightly, coverage(off))]
335    pub async fn get_cursor(&mut self) -> Result<GetCursorResponse, GrpcClientError> {
336        self.ensure_joined().await;
337        let request = self.make_request(GetCursorRequest {
338            window_id: None,
339            client_id: 0,
340        });
341        let response = self.state.get_cursor(request).await?;
342        Ok(response.into_inner())
343    }
344
345    /// Get the cursor position for a specific client.
346    ///
347    /// # Per-client state (#471): Per-client cursor isolation
348    ///
349    /// Returns the cursor from the specified client's per-client editing state.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if the gRPC call fails.
354    #[cfg_attr(coverage_nightly, coverage(off))]
355    pub async fn get_cursor_for_client(
356        &mut self,
357        client_id: u64,
358    ) -> Result<GetCursorResponse, GrpcClientError> {
359        let request = self.make_request(GetCursorRequest {
360            window_id: None,
361            client_id,
362        });
363        let response = self.state.get_cursor(request).await?;
364        Ok(response.into_inner())
365    }
366
367    /// List all open buffers.
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if the gRPC call fails.
372    #[cfg_attr(coverage_nightly, coverage(off))]
373    pub async fn list_buffers(&mut self) -> Result<ListBuffersResponse, GrpcClientError> {
374        let request = self.make_request(ListBuffersRequest {});
375        let response = self.buffer.list(request).await?;
376        Ok(response.into_inner())
377    }
378
379    /// Get raw buffer content.
380    ///
381    /// # Arguments
382    ///
383    /// * `buffer_id` - Optional buffer ID. Uses active buffer if None.
384    ///
385    /// # Errors
386    ///
387    /// Returns an error if the gRPC call fails.
388    #[cfg_attr(coverage_nightly, coverage(off))]
389    pub async fn get_buffer_content(
390        &mut self,
391        buffer_id: Option<u64>,
392    ) -> Result<GetRawContentResponse, GrpcClientError> {
393        let request = self.make_request(GetRawContentRequest {
394            buffer_id,
395            start_line: None,
396            end_line: None,
397        });
398        let response = self.buffer.get_raw_content(request).await?;
399        Ok(response.into_inner())
400    }
401
402    /// Ping the server (health check).
403    ///
404    /// # Errors
405    ///
406    /// Returns an error if the gRPC call fails.
407    #[cfg_attr(coverage_nightly, coverage(off))]
408    pub async fn ping(&mut self) -> Result<PingResponse, GrpcClientError> {
409        let request = self.make_request(PingRequest {});
410        let response = self.server.ping(request).await?;
411        Ok(response.into_inner())
412    }
413
414    /// Get server info.
415    ///
416    /// # Errors
417    ///
418    /// Returns an error if the gRPC call fails.
419    #[cfg_attr(coverage_nightly, coverage(off))]
420    pub async fn info(&mut self) -> Result<InfoResponse, GrpcClientError> {
421        let request = self.make_request(InfoRequest {});
422        let response = self.server.info(request).await?;
423        Ok(response.into_inner())
424    }
425
426    /// Get register contents.
427    ///
428    /// # Arguments
429    ///
430    /// * `names` - Optional register names to query. If empty, returns all non-empty registers.
431    ///
432    /// # Errors
433    ///
434    /// Returns an error if the gRPC call fails.
435    #[cfg_attr(coverage_nightly, coverage(off))]
436    pub async fn get_registers(
437        &mut self,
438        names: Vec<String>,
439    ) -> Result<GetRegistersResponse, GrpcClientError> {
440        let request = self.make_request(GetRegistersRequest {
441            names,
442            client_id: 0, // 0 = self (authenticated client)
443        });
444        let response = self.state.get_registers(request).await?;
445        Ok(response.into_inner())
446    }
447
448    /// Get screen content via TUI capture relay.
449    ///
450    /// Requests a screen capture from a specific TUI client via the server.
451    ///
452    /// # Arguments
453    ///
454    /// * `client_id` - Target client ID to capture from
455    /// * `format` - Capture format: `plain_text`, `raw_ansi`, or `cell_grid`
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if the gRPC call fails, no TUI is connected, or capture times out.
460    #[cfg_attr(coverage_nightly, coverage(off))]
461    pub async fn get_screen_content(
462        &mut self,
463        client_id: u64,
464        format: &str,
465    ) -> Result<GetScreenContentResponse, GrpcClientError> {
466        // client_id here is a TARGET (which TUI to capture), not caller identity
467        let request = self.make_request(GetScreenContentRequest {
468            client_id,
469            format: format.to_string(),
470        });
471        let response = self.state.get_screen_content(request).await?;
472        Ok(response.into_inner())
473    }
474
475    // =========================================================================
476    // Presence Service Methods (Phase 15)
477    // =========================================================================
478
479    /// Join the presence session.
480    ///
481    /// Registers this client with the session and receives an assigned client ID.
482    ///
483    /// # Arguments
484    ///
485    /// * `client_type` - Client type identifier ("cli", "tui", "web", "test").
486    /// * `display_name` - User-friendly display name.
487    ///
488    /// # Returns
489    ///
490    /// The response containing assigned client ID and list of connected peers.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if the gRPC call fails.
495    #[cfg_attr(coverage_nightly, coverage(off))]
496    pub async fn presence_join(
497        &mut self,
498        client_type: &str,
499        display_name: &str,
500    ) -> Result<JoinResponse, GrpcClientError> {
501        let request = JoinRequest {
502            client_type: client_type.to_string(),
503            display_name: display_name.to_string(),
504        };
505        let response = self.presence.join(request).await?.into_inner();
506        // Store client ID and session token for subsequent requests (#483)
507        self.client_id = Some(response.client_id);
508        if !response.session_token.is_empty() {
509            self.session_token = Some(response.session_token.clone());
510        }
511        Ok(response)
512    }
513
514    /// Leave the presence session.
515    ///
516    /// # Phase 5 (#483): Token identifies the client — no `client_id` parameter.
517    ///
518    /// # Errors
519    ///
520    /// Returns an error if the gRPC call fails.
521    #[cfg_attr(coverage_nightly, coverage(off))]
522    pub async fn presence_leave(&mut self) -> Result<LeaveResponse, GrpcClientError> {
523        let request = self.make_request(LeaveRequest {});
524        let response = self.presence.leave(request).await?;
525        Ok(response.into_inner())
526    }
527
528    /// List all connected clients.
529    ///
530    /// # Returns
531    ///
532    /// The response containing all connected clients.
533    ///
534    /// # Errors
535    ///
536    /// Returns an error if the gRPC call fails.
537    #[cfg_attr(coverage_nightly, coverage(off))]
538    pub async fn presence_list(&mut self) -> Result<ListClientsResponse, GrpcClientError> {
539        let request = self.make_request(ListClientsRequest {});
540        let response = self.presence.list_clients(request).await?;
541        Ok(response.into_inner())
542    }
543
544    /// Update this client's presence state.
545    ///
546    /// # Phase 5 (#483): Token identifies the client — no `client_id` parameter.
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if the gRPC call fails.
551    #[cfg_attr(coverage_nightly, coverage(off))]
552    pub async fn presence_update(
553        &mut self,
554        buffer_id: Option<u64>,
555        mode: Option<String>,
556    ) -> Result<UpdatePresenceResponse, GrpcClientError> {
557        let request = self.make_request(UpdatePresenceRequest {
558            buffer_id,
559            visible_lines: None,
560            mode,
561        });
562        let response = self.presence.update_presence(request).await?;
563        Ok(response.into_inner())
564    }
565
566    /// Set sync mode for this client.
567    ///
568    /// # Phase 5 (#483): Token identifies the client — no `client_id` parameter.
569    ///
570    /// # Errors
571    ///
572    /// Returns an error if the gRPC call fails.
573    #[cfg_attr(coverage_nightly, coverage(off))]
574    pub async fn presence_set_sync_mode(
575        &mut self,
576        sync_mode: i32,
577        follow_target: Option<u64>,
578    ) -> Result<SetSyncModeResponse, GrpcClientError> {
579        let request = self.make_request(SetSyncModeRequest {
580            mode: sync_mode,
581            follow_target,
582        });
583        let response = self.presence.set_sync_mode(request).await?;
584        Ok(response.into_inner())
585    }
586
587    // =========================================================================
588    // Debug Service Methods (Phase 17, #481)
589    // =========================================================================
590
591    /// Get recent log entries from the server ring buffer.
592    ///
593    /// # Arguments
594    ///
595    /// * `count` - Number of entries to retrieve (default: 50).
596    /// * `level` - Optional level filter (trace, debug, info, warn, error).
597    /// * `target` - Optional target module filter (contains match).
598    /// * `grep` - Optional message filter (case-insensitive contains).
599    ///
600    /// # Returns
601    ///
602    /// The response containing log entries.
603    ///
604    /// # Errors
605    ///
606    /// Returns an error if the gRPC call fails.
607    #[cfg_attr(coverage_nightly, coverage(off))]
608    pub async fn log_tail(
609        &mut self,
610        count: u32,
611        level: Option<String>,
612        target: Option<String>,
613        grep: Option<String>,
614    ) -> Result<LogTailResponse, GrpcClientError> {
615        let request = self.make_request(LogTailRequest {
616            count,
617            level,
618            target,
619            grep,
620        });
621        let response = self.debug.log_tail(request).await?;
622        Ok(response.into_inner())
623    }
624
625    // =========================================================================
626    // Debug Client-Targeting Methods (#468)
627    //
628    // CLI is stateless — no join, no token, no presence.
629    // These methods target specific connected clients (TUI/Web) by ID.
630    // =========================================================================
631
632    /// Send keys to a specific client's state via `DebugService`.
633    ///
634    /// No auth required — CLI targets the client by ID directly.
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if the gRPC call fails or target client doesn't exist.
639    #[cfg_attr(coverage_nightly, coverage(off))]
640    pub async fn debug_send_keys(
641        &mut self,
642        keys: &str,
643        target_client_id: u64,
644    ) -> Result<DebugSendKeysResponse, GrpcClientError> {
645        let request = Request::new(DebugSendKeysRequest {
646            keys: keys.to_string(),
647            target_client_id,
648        });
649        let response = self.debug.debug_send_keys(request).await?;
650        Ok(response.into_inner())
651    }
652
653    /// Capture a specific client's screen content via `DebugService`.
654    ///
655    /// No auth required — CLI targets the client by ID directly.
656    ///
657    /// # Errors
658    ///
659    /// Returns an error if the gRPC call fails, client doesn't exist, or capture times out.
660    #[cfg_attr(coverage_nightly, coverage(off))]
661    pub async fn debug_capture(
662        &mut self,
663        target_client_id: u64,
664        format: &str,
665    ) -> Result<DebugCaptureResponse, GrpcClientError> {
666        let request = Request::new(DebugCaptureRequest {
667            target_client_id,
668            format: format.to_string(),
669        });
670        let response = self.debug.debug_capture(request).await?;
671        Ok(response.into_inner())
672    }
673
674    /// Get a specific client's current editor mode via `DebugService`.
675    ///
676    /// No auth required — CLI targets the client by ID directly.
677    ///
678    /// # Errors
679    ///
680    /// Returns an error if the gRPC call fails or target client doesn't exist.
681    #[cfg_attr(coverage_nightly, coverage(off))]
682    pub async fn debug_get_mode(
683        &mut self,
684        target_client_id: u64,
685    ) -> Result<DebugGetModeResponse, GrpcClientError> {
686        let request = Request::new(DebugGetModeRequest { target_client_id });
687        let response = self.debug.debug_get_mode(request).await?;
688        Ok(response.into_inner())
689    }
690
691    /// Get a specific client's cursor position via `DebugService`.
692    ///
693    /// No auth required — CLI targets the client by ID directly.
694    ///
695    /// # Errors
696    ///
697    /// Returns an error if the gRPC call fails or target client doesn't exist.
698    #[cfg_attr(coverage_nightly, coverage(off))]
699    pub async fn debug_get_cursor(
700        &mut self,
701        target_client_id: u64,
702    ) -> Result<DebugGetCursorResponse, GrpcClientError> {
703        let request = Request::new(DebugGetCursorRequest { target_client_id });
704        let response = self.debug.debug_get_cursor(request).await?;
705        Ok(response.into_inner())
706    }
707
708    /// List connected clients via `DebugService`.
709    ///
710    /// No auth required — read-only debug query.
711    ///
712    /// # Errors
713    ///
714    /// Returns an error if the gRPC call fails.
715    #[cfg_attr(coverage_nightly, coverage(off))]
716    pub async fn debug_list_clients(
717        &mut self,
718    ) -> Result<DebugListClientsResponse, GrpcClientError> {
719        let request = Request::new(DebugListClientsRequest {});
720        let response = self.debug.debug_list_clients(request).await?;
721        Ok(response.into_inner())
722    }
723
724    /// Query extension state for a specific client via `DebugService`.
725    ///
726    /// No auth required — CLI targets the client by ID directly.
727    ///
728    /// # Errors
729    ///
730    /// Returns an error if the gRPC call fails or extension kind is unknown.
731    #[cfg_attr(coverage_nightly, coverage(off))]
732    pub async fn debug_get_extension_state(
733        &mut self,
734        kind: &str,
735        target_client_id: u64,
736    ) -> Result<DebugGetExtensionStateResponse, GrpcClientError> {
737        let request = Request::new(DebugGetExtensionStateRequest {
738            kind: kind.to_string(),
739            target_client_id,
740        });
741        let response = self.debug.debug_get_extension_state(request).await?;
742        Ok(response.into_inner())
743    }
744
745    /// List all registered extensions via `DebugService`.
746    ///
747    /// No auth required — read-only debug query.
748    ///
749    /// # Errors
750    ///
751    /// Returns an error if the gRPC call fails.
752    #[cfg_attr(coverage_nightly, coverage(off))]
753    pub async fn debug_list_extensions(
754        &mut self,
755    ) -> Result<DebugListExtensionsResponse, GrpcClientError> {
756        let request = Request::new(DebugListExtensionsRequest {});
757        let response = self.debug.debug_list_extensions(request).await?;
758        Ok(response.into_inner())
759    }
760}
761
762#[cfg(test)]
763#[path = "client_tests.rs"]
764mod tests;