Skip to main content

dakera_client/
admin.rs

1//! Admin operations for the Dakera client.
2//!
3//! Provides methods for cluster management, cache, configuration, quotas,
4//! slow queries, backups, and TTL management.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::types::{WarmCacheRequest, WarmCacheResponse};
12use crate::DakeraClient;
13
14// ============================================================================
15// Cluster Types
16// ============================================================================
17
18/// Ops stats response — Read-scoped; works with read-only API keys
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct OpsStats {
21    pub version: String,
22    pub total_vectors: u64,
23    pub namespace_count: u64,
24    pub uptime_seconds: u64,
25    pub timestamp: u64,
26    pub state: String,
27}
28
29/// Cluster status response
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ClusterStatus {
32    pub cluster_id: String,
33    pub state: String,
34    pub node_count: u32,
35    pub total_vectors: u64,
36    pub namespace_count: u64,
37    pub version: String,
38    pub timestamp: u64,
39    /// Redis connectivity status (OPS-3).
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub redis_healthy: Option<bool>,
42}
43
44/// Node information
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct NodeInfo {
47    pub node_id: String,
48    pub address: String,
49    pub role: String,
50    pub status: String,
51    pub version: String,
52    pub uptime_seconds: u64,
53    pub vector_count: u64,
54    pub memory_bytes: u64,
55    #[serde(default)]
56    pub cpu_percent: f32,
57    #[serde(default)]
58    pub memory_percent: f32,
59    pub last_heartbeat: u64,
60}
61
62/// Node list response
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct NodeListResponse {
65    pub nodes: Vec<NodeInfo>,
66    pub total: u32,
67}
68
69// ============================================================================
70// Namespace Admin Types
71// ============================================================================
72
73/// Index statistics
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct IndexStats {
76    pub index_type: String,
77    pub is_built: bool,
78    pub size_bytes: u64,
79    pub indexed_vectors: u64,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub last_rebuild: Option<u64>,
82}
83
84/// Detailed namespace statistics
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct NamespaceAdminInfo {
87    pub name: String,
88    pub vector_count: u64,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub dimension: Option<usize>,
91    pub index_type: String,
92    pub storage_bytes: u64,
93    pub document_count: u64,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub created_at: Option<u64>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub updated_at: Option<u64>,
98    pub index_stats: IndexStats,
99}
100
101/// Namespace list response
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct NamespaceListResponse {
104    pub namespaces: Vec<NamespaceAdminInfo>,
105    pub total: u64,
106    pub total_vectors: u64,
107}
108
109/// Optimize namespace request
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct OptimizeRequest {
112    #[serde(default)]
113    pub force: bool,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub target_index_type: Option<String>,
116}
117
118/// Optimize namespace response
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct OptimizeResponse {
121    pub success: bool,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub job_id: Option<String>,
124    pub message: String,
125}
126
127// ============================================================================
128// Index Admin Types
129// ============================================================================
130
131/// Index statistics for all namespaces
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct IndexStatsResponse {
134    pub namespaces: HashMap<String, IndexStats>,
135    pub total_indexed_vectors: u64,
136    pub total_size_bytes: u64,
137}
138
139/// Rebuild index request
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct RebuildIndexRequest {
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub namespace: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub index_type: Option<String>,
146    #[serde(default)]
147    pub force: bool,
148}
149
150/// Rebuild index response
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RebuildIndexResponse {
153    pub success: bool,
154    pub job_id: String,
155    pub message: String,
156}
157
158// ============================================================================
159// Cache Admin Types
160// ============================================================================
161
162/// Cache statistics
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct CacheStats {
165    pub enabled: bool,
166    pub cache_type: String,
167    pub entries: u64,
168    pub size_bytes: u64,
169    pub hits: u64,
170    pub misses: u64,
171    pub hit_rate: f64,
172    pub evictions: u64,
173}
174
175/// Clear cache request
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ClearCacheRequest {
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub namespace: Option<String>,
180}
181
182/// Clear cache response
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ClearCacheResponse {
185    pub success: bool,
186    pub entries_cleared: u64,
187    pub message: String,
188}
189
190// ============================================================================
191// Configuration Types
192// ============================================================================
193
194/// Runtime configuration
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct RuntimeConfig {
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub max_vectors_per_namespace: Option<u64>,
199    pub default_index_type: String,
200    pub cache_enabled: bool,
201    pub cache_max_size_bytes: u64,
202    pub rate_limit_enabled: bool,
203    pub rate_limit_rps: u32,
204    pub query_timeout_ms: u64,
205    /// Whether AutoPilot background tasks (dedup + consolidation) are enabled
206    #[serde(default = "default_true")]
207    pub autopilot_enabled: bool,
208    /// Cosine-similarity threshold for AutoPilot deduplication (0.0–1.0)
209    #[serde(default = "default_dedup_threshold")]
210    pub autopilot_dedup_threshold: f32,
211    /// How often AutoPilot deduplication runs (hours)
212    #[serde(default = "default_dedup_interval")]
213    pub autopilot_dedup_interval_hours: u64,
214    /// How often AutoPilot consolidation runs (hours)
215    #[serde(default = "default_consolidation_interval")]
216    pub autopilot_consolidation_interval_hours: u64,
217}
218
219fn default_true() -> bool {
220    true
221}
222fn default_dedup_threshold() -> f32 {
223    0.93
224}
225fn default_dedup_interval() -> u64 {
226    6
227}
228fn default_consolidation_interval() -> u64 {
229    12
230}
231
232/// Update configuration response
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct UpdateConfigResponse {
235    pub success: bool,
236    pub config: RuntimeConfig,
237    pub message: String,
238    #[serde(default, skip_serializing_if = "Vec::is_empty")]
239    pub warnings: Vec<String>,
240}
241
242// ============================================================================
243// Quota Types
244// ============================================================================
245
246/// Quota configuration
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct QuotaConfig {
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub max_vectors: Option<u64>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub max_storage_bytes: Option<u64>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub max_queries_per_minute: Option<u64>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub max_writes_per_minute: Option<u64>,
257}
258
259/// Quota usage
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct QuotaUsage {
262    #[serde(default)]
263    pub current_vectors: u64,
264    #[serde(default)]
265    pub current_storage_bytes: u64,
266    #[serde(default)]
267    pub queries_this_minute: u64,
268    #[serde(default)]
269    pub writes_this_minute: u64,
270}
271
272/// Quota status for a namespace
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct QuotaStatus {
275    pub namespace: String,
276    pub config: QuotaConfig,
277    pub usage: QuotaUsage,
278}
279
280/// Quota list response
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct QuotaListResponse {
283    pub quotas: Vec<QuotaStatus>,
284    pub total: u64,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub default_config: Option<QuotaConfig>,
287}
288
289// ============================================================================
290// Slow Query Types
291// ============================================================================
292
293/// Slow query entry
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct SlowQueryEntry {
296    pub id: String,
297    pub timestamp: u64,
298    pub namespace: String,
299    pub query_type: String,
300    pub duration_ms: f64,
301    #[serde(default)]
302    pub parameters: Option<serde_json::Value>,
303    #[serde(default)]
304    pub results_count: u64,
305    #[serde(default)]
306    pub vectors_scanned: u64,
307}
308
309/// Slow query list response
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct SlowQueryListResponse {
312    pub queries: Vec<SlowQueryEntry>,
313    pub total: u64,
314    pub threshold_ms: f64,
315}
316
317// ============================================================================
318// Backup Types
319// ============================================================================
320
321/// Backup information
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct BackupInfo {
324    pub backup_id: String,
325    pub name: String,
326    pub backup_type: String,
327    pub status: String,
328    pub namespaces: Vec<String>,
329    pub vector_count: u64,
330    pub size_bytes: u64,
331    pub created_at: u64,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub completed_at: Option<u64>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub duration_seconds: Option<u64>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub storage_path: Option<String>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub error: Option<String>,
340    pub encrypted: bool,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub compression: Option<String>,
343}
344
345/// List backups response
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct BackupListResponse {
348    pub backups: Vec<BackupInfo>,
349    pub total: u64,
350}
351
352/// Create backup request
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct CreateBackupRequest {
355    pub name: String,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub backup_type: Option<String>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub namespaces: Option<Vec<String>>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub encrypt: Option<bool>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub compression: Option<String>,
364}
365
366/// Create backup response
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct CreateBackupResponse {
369    pub backup: BackupInfo,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub estimated_completion: Option<u64>,
372}
373
374/// Restore backup request
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct RestoreBackupRequest {
377    pub backup_id: String,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub target_namespaces: Option<Vec<String>>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub overwrite: Option<bool>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub point_in_time: Option<u64>,
384}
385
386/// Restore backup response
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct RestoreBackupResponse {
389    pub restore_id: String,
390    pub status: String,
391    pub backup_id: String,
392    pub namespaces: Vec<String>,
393    pub started_at: u64,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub estimated_completion: Option<u64>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub progress_percent: Option<u8>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub vectors_restored: Option<u64>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub completed_at: Option<u64>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub duration_seconds: Option<u64>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub error: Option<String>,
406}
407
408// ============================================================================
409// AutoPilot Types (PILOT-1 / PILOT-2 / PILOT-3)
410// ============================================================================
411
412/// AutoPilot configuration
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct AutoPilotConfig {
415    pub enabled: bool,
416    pub dedup_threshold: f32,
417    pub dedup_interval_hours: u64,
418    pub consolidation_interval_hours: u64,
419}
420
421/// Result snapshot from a deduplication cycle
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct DedupResultSnapshot {
424    pub namespaces_processed: usize,
425    pub memories_scanned: usize,
426    pub duplicates_removed: usize,
427}
428
429/// Result snapshot from a consolidation cycle
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct ConsolidationResultSnapshot {
432    pub namespaces_processed: usize,
433    pub memories_scanned: usize,
434    pub clusters_merged: usize,
435    pub memories_consolidated: usize,
436}
437
438/// PILOT-1: AutoPilot status response
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct AutoPilotStatusResponse {
441    pub config: AutoPilotConfig,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub last_dedup_at: Option<u64>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub last_consolidation_at: Option<u64>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub last_dedup: Option<DedupResultSnapshot>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub last_consolidation: Option<ConsolidationResultSnapshot>,
450    pub total_dedup_removed: u64,
451    pub total_consolidated: u64,
452}
453
454/// PILOT-2: AutoPilot configuration update request (all fields optional)
455#[derive(Debug, Clone, Serialize, Deserialize, Default)]
456pub struct AutoPilotConfigRequest {
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub enabled: Option<bool>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub dedup_threshold: Option<f32>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub dedup_interval_hours: Option<u64>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub consolidation_interval_hours: Option<u64>,
465}
466
467/// PILOT-2: AutoPilot configuration update response
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct AutoPilotConfigResponse {
470    pub success: bool,
471    pub config: AutoPilotConfig,
472    pub message: String,
473}
474
475/// PILOT-3: Trigger action
476#[derive(Debug, Clone, Serialize, Deserialize)]
477#[serde(rename_all = "lowercase")]
478pub enum AutoPilotTriggerAction {
479    Dedup,
480    Consolidate,
481    All,
482}
483
484/// PILOT-3: Trigger request
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct AutoPilotTriggerRequest {
487    pub action: AutoPilotTriggerAction,
488}
489
490/// Dedup result returned by a manual trigger
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct AutoPilotDedupResult {
493    pub namespaces_processed: usize,
494    pub memories_scanned: usize,
495    pub duplicates_removed: usize,
496}
497
498/// Consolidation result returned by a manual trigger
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct AutoPilotConsolidationResult {
501    pub namespaces_processed: usize,
502    pub memories_scanned: usize,
503    pub clusters_merged: usize,
504    pub memories_consolidated: usize,
505}
506
507/// PILOT-3: Trigger response
508#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct AutoPilotTriggerResponse {
510    pub success: bool,
511    pub action: AutoPilotTriggerAction,
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub dedup: Option<AutoPilotDedupResult>,
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub consolidation: Option<AutoPilotConsolidationResult>,
516    pub message: String,
517}
518
519// ============================================================================
520// Decay Engine Types (DECAY-1 / DECAY-2)
521// ============================================================================
522
523/// DECAY-1: Current decay configuration
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct DecayConfigResponse {
526    /// Decay strategy: "exponential", "linear", or "step"
527    pub strategy: String,
528    /// Half-life in hours
529    pub half_life_hours: f64,
530    /// Minimum importance threshold; memories below are hard-deleted on next cycle
531    pub min_importance: f32,
532}
533
534/// DECAY-1: Runtime configuration update request (all fields optional)
535#[derive(Debug, Clone, Serialize, Deserialize, Default)]
536pub struct DecayConfigUpdateRequest {
537    /// Decay strategy: "exponential", "linear", or "step"
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub strategy: Option<String>,
540    /// Half-life in hours (must be > 0)
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub half_life_hours: Option<f64>,
543    /// Minimum importance threshold 0.0–1.0
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub min_importance: Option<f32>,
546}
547
548/// DECAY-1: Runtime configuration update response
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct DecayConfigUpdateResponse {
551    pub success: bool,
552    pub config: DecayConfigResponse,
553    pub message: String,
554}
555
556/// DECAY-2: Stats from a single decay cycle
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct LastDecayCycleStats {
559    pub namespaces_processed: usize,
560    pub memories_processed: usize,
561    pub memories_decayed: usize,
562    pub memories_deleted: usize,
563}
564
565/// DECAY-2: Decay activity counters and last-cycle snapshot
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct DecayStatsResponse {
568    /// Total memories whose importance was lowered by decay (all-time)
569    pub total_decayed: u64,
570    /// Total memories hard-deleted by decay or TTL expiry (all-time)
571    pub total_deleted: u64,
572    /// Unix timestamp of the last decay cycle (None if never run)
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub last_run_at: Option<u64>,
575    /// Number of decay cycles completed since startup
576    pub cycles_run: u64,
577    /// Stats from the most recent decay cycle (None if never run)
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub last_cycle: Option<LastDecayCycleStats>,
580}
581
582// ============================================================================
583// TTL Types
584// ============================================================================
585
586/// TTL cleanup request
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct TtlCleanupRequest {
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub namespace: Option<String>,
591}
592
593/// TTL cleanup response
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct TtlCleanupResponse {
596    pub success: bool,
597    pub vectors_removed: u64,
598    pub namespaces_cleaned: Vec<String>,
599    pub message: String,
600}
601
602/// TTL statistics for a namespace
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct TtlStats {
605    pub namespace: String,
606    pub vectors_with_ttl: u64,
607    pub expiring_within_hour: u64,
608    pub expiring_within_day: u64,
609    pub expired_pending_cleanup: u64,
610}
611
612/// TTL statistics response
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct TtlStatsResponse {
615    pub namespaces: Vec<TtlStats>,
616    pub total_with_ttl: u64,
617    pub total_expired: u64,
618}
619
620// ============================================================================
621// Admin Client Methods
622// ============================================================================
623
624impl DakeraClient {
625    // ====================================================================
626    // Cluster Management
627    // ====================================================================
628
629    /// Get server stats (version, total_vectors, namespace_count, uptime_seconds, timestamp).
630    ///
631    /// Requires Read scope — works with read-only API keys, unlike `cluster_status`.
632    pub async fn ops_stats(&self) -> Result<OpsStats> {
633        let url = format!("{}/v1/ops/stats", self.base_url);
634        let response = self.client.get(&url).send().await?;
635        self.handle_response(response).await
636    }
637
638    /// Get Prometheus metrics in text exposition format (INFRA-3).
639    ///
640    /// Requires Admin scope. Returns the raw Prometheus text exposition
641    /// format string suitable for scraping by a Prometheus server.
642    pub async fn ops_metrics(&self) -> Result<String> {
643        let url = format!("{}/v1/ops/metrics", self.base_url);
644        let response = self.client.get(&url).send().await?;
645        self.handle_text_response(response).await
646    }
647
648    /// Return all active DAKERA_* env vars (non-secret) from the running server (DAK-7477).
649    ///
650    /// Requires Admin scope. Returns a JSON object mapping DAKERA_* environment variable
651    /// names to their values, plus `_version` and optionally `_build_sha`. Secret-bearing
652    /// keys (TOKEN, KEY, SECRET, PASSWORD, CRED, URL, URI, DSN) are filtered server-side.
653    ///
654    /// Used by bench harnesses to verify the server is running with the exact
655    /// feature-flag configuration requested before scoring.
656    pub async fn debug_config(&self) -> Result<serde_json::Value> {
657        let url = format!("{}/debug/config", self.base_url);
658        let response = self.client.get(&url).send().await?;
659        self.handle_response(response).await
660    }
661
662    /// Get cluster status overview
663    pub async fn cluster_status(&self) -> Result<ClusterStatus> {
664        let url = format!("{}/v1/admin/cluster/status", self.base_url);
665        let response = self.client.get(&url).send().await?;
666        self.handle_response(response).await
667    }
668
669    /// List cluster nodes
670    pub async fn cluster_nodes(&self) -> Result<NodeListResponse> {
671        let url = format!("{}/v1/admin/cluster/nodes", self.base_url);
672        let response = self.client.get(&url).send().await?;
673        self.handle_response(response).await
674    }
675
676    // ====================================================================
677    // Namespace Administration
678    // ====================================================================
679
680    /// List all namespaces with detailed admin statistics
681    pub async fn list_namespaces_admin(&self) -> Result<NamespaceListResponse> {
682        let url = format!("{}/v1/admin/namespaces", self.base_url);
683        let response = self.client.get(&url).send().await?;
684        self.handle_response(response).await
685    }
686
687    /// Delete an entire namespace and all its data
688    pub async fn delete_namespace_admin(&self, namespace: &str) -> Result<serde_json::Value> {
689        let url = format!("{}/v1/admin/namespaces/{}", self.base_url, namespace);
690        let response = self.client.delete(&url).send().await?;
691        self.handle_response(response).await
692    }
693
694    /// Optimize a namespace
695    pub async fn optimize_namespace(
696        &self,
697        namespace: &str,
698        request: OptimizeRequest,
699    ) -> Result<OptimizeResponse> {
700        let url = format!(
701            "{}/v1/admin/namespaces/{}/optimize",
702            self.base_url, namespace
703        );
704        let response = self.client.post(&url).json(&request).send().await?;
705        self.handle_response(response).await
706    }
707
708    // ====================================================================
709    // Index Management
710    // ====================================================================
711
712    /// Get index statistics for all namespaces
713    pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
714        let url = format!("{}/v1/admin/indexes/stats", self.base_url);
715        let response = self.client.get(&url).send().await?;
716        self.handle_response(response).await
717    }
718
719    /// Rebuild indexes
720    pub async fn rebuild_indexes(
721        &self,
722        request: RebuildIndexRequest,
723    ) -> Result<RebuildIndexResponse> {
724        let url = format!("{}/v1/admin/indexes/rebuild", self.base_url);
725        let response = self.client.post(&url).json(&request).send().await?;
726        self.handle_response(response).await
727    }
728
729    // ====================================================================
730    // Cache Management
731    // ====================================================================
732
733    /// Get cache statistics
734    pub async fn cache_stats(&self) -> Result<CacheStats> {
735        let url = format!("{}/v1/admin/cache/stats", self.base_url);
736        let response = self.client.get(&url).send().await?;
737        self.handle_response(response).await
738    }
739
740    /// Clear cache, optionally for a specific namespace
741    pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
742        let url = format!("{}/v1/admin/cache/clear", self.base_url);
743        let request = ClearCacheRequest {
744            namespace: namespace.map(|s| s.to_string()),
745        };
746        let response = self.client.post(&url).json(&request).send().await?;
747        self.handle_response(response).await
748    }
749
750    /// Warm cache for a namespace via `POST /v1/admin/cache/warm`.
751    pub async fn cache_warm(&self, request: WarmCacheRequest) -> Result<WarmCacheResponse> {
752        let url = format!("{}/v1/admin/cache/warm", self.base_url);
753        let response = self.client.post(&url).json(&request).send().await?;
754        self.handle_response(response).await
755    }
756
757    // ====================================================================
758    // Configuration
759    // ====================================================================
760
761    /// Get runtime configuration
762    pub async fn get_config(&self) -> Result<RuntimeConfig> {
763        let url = format!("{}/v1/admin/config", self.base_url);
764        let response = self.client.get(&url).send().await?;
765        self.handle_response(response).await
766    }
767
768    /// Update runtime configuration
769    pub async fn update_config(
770        &self,
771        updates: HashMap<String, serde_json::Value>,
772    ) -> Result<UpdateConfigResponse> {
773        let url = format!("{}/v1/admin/config", self.base_url);
774        let response = self.client.put(&url).json(&updates).send().await?;
775        self.handle_response(response).await
776    }
777
778    // ====================================================================
779    // Quotas
780    // ====================================================================
781
782    /// List all namespace quotas
783    pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
784        let url = format!("{}/v1/admin/quotas", self.base_url);
785        let response = self.client.get(&url).send().await?;
786        self.handle_response(response).await
787    }
788
789    /// Get quota for a specific namespace
790    pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
791        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
792        let response = self.client.get(&url).send().await?;
793        self.handle_response(response).await
794    }
795
796    /// Set quota for a specific namespace
797    pub async fn set_quota(
798        &self,
799        namespace: &str,
800        config: QuotaConfig,
801    ) -> Result<serde_json::Value> {
802        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
803        let request = serde_json::json!({ "config": config });
804        let response = self.client.put(&url).json(&request).send().await?;
805        self.handle_response(response).await
806    }
807
808    /// Delete quota for a specific namespace
809    pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
810        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
811        let response = self.client.delete(&url).send().await?;
812        self.handle_response(response).await
813    }
814
815    /// Update quotas (alias for set_quota on default)
816    pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
817        let url = format!("{}/v1/admin/quotas/default", self.base_url);
818        let request = serde_json::json!({ "config": config });
819        let response = self.client.put(&url).json(&request).send().await?;
820        self.handle_response(response).await
821    }
822
823    // ====================================================================
824    // Slow Queries
825    // ====================================================================
826
827    /// List recent slow queries
828    pub async fn slow_queries(
829        &self,
830        limit: Option<usize>,
831        namespace: Option<&str>,
832        query_type: Option<&str>,
833    ) -> Result<SlowQueryListResponse> {
834        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
835        let mut params = Vec::new();
836        if let Some(l) = limit {
837            params.push(format!("limit={}", l));
838        }
839        if let Some(ns) = namespace {
840            params.push(format!("namespace={}", ns));
841        }
842        if let Some(qt) = query_type {
843            params.push(format!("query_type={}", qt));
844        }
845        if !params.is_empty() {
846            url.push('?');
847            url.push_str(&params.join("&"));
848        }
849        let response = self.client.get(&url).send().await?;
850        self.handle_response(response).await
851    }
852
853    /// Get slow query summary and patterns
854    pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
855        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
856        let response = self.client.get(&url).send().await?;
857        self.handle_response(response).await
858    }
859
860    /// Clear slow query log
861    pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
862        let url = format!("{}/v1/admin/slow-queries", self.base_url);
863        let response = self.client.delete(&url).send().await?;
864        self.handle_response(response).await
865    }
866
867    // ====================================================================
868    // Backups
869    // ====================================================================
870
871    /// Create a new backup
872    pub async fn create_backup(
873        &self,
874        request: CreateBackupRequest,
875    ) -> Result<CreateBackupResponse> {
876        let url = format!("{}/v1/admin/backups", self.base_url);
877        let response = self.client.post(&url).json(&request).send().await?;
878        self.handle_response(response).await
879    }
880
881    /// List all backups
882    pub async fn list_backups(&self) -> Result<BackupListResponse> {
883        let url = format!("{}/v1/admin/backups", self.base_url);
884        let response = self.client.get(&url).send().await?;
885        self.handle_response(response).await
886    }
887
888    /// Get backup details by ID
889    pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
890        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
891        let response = self.client.get(&url).send().await?;
892        self.handle_response(response).await
893    }
894
895    /// Restore from a backup
896    pub async fn restore_backup(
897        &self,
898        request: RestoreBackupRequest,
899    ) -> Result<RestoreBackupResponse> {
900        let url = format!("{}/v1/admin/backups/restore", self.base_url);
901        let response = self.client.post(&url).json(&request).send().await?;
902        self.handle_response(response).await
903    }
904
905    /// Delete a backup
906    pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
907        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
908        let response = self.client.delete(&url).send().await?;
909        self.handle_response(response).await
910    }
911
912    // ====================================================================
913    // TTL Management
914    // ====================================================================
915
916    /// Configure TTL for a namespace.
917    pub async fn configure_ttl(
918        &self,
919        namespace: &str,
920        ttl_seconds: u64,
921        strategy: Option<&str>,
922    ) -> Result<serde_json::Value> {
923        let url = format!("{}/v1/admin/namespaces/{}/ttl", self.base_url, namespace);
924        let mut body = serde_json::json!({ "ttl_seconds": ttl_seconds });
925        if let Some(s) = strategy {
926            body["strategy"] = serde_json::Value::String(s.to_string());
927        }
928        let response = self.client.post(&url).json(&body).send().await?;
929        self.handle_response(response).await
930    }
931
932    /// Run TTL cleanup on expired vectors
933    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
934        let url = format!("{}/v1/admin/ttl/cleanup", self.base_url);
935        let request = TtlCleanupRequest {
936            namespace: namespace.map(|s| s.to_string()),
937        };
938        let response = self.client.post(&url).json(&request).send().await?;
939        self.handle_response(response).await
940    }
941
942    /// Get TTL statistics
943    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
944        let url = format!("{}/v1/admin/ttl/stats", self.base_url);
945        let response = self.client.get(&url).send().await?;
946        self.handle_response(response).await
947    }
948
949    // ====================================================================
950    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
951    // ====================================================================
952
953    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
954    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
955        let url = format!("{}/v1/admin/autopilot/status", self.base_url);
956        let response = self.client.get(&url).send().await?;
957        self.handle_response(response).await
958    }
959
960    /// Update AutoPilot configuration at runtime (PILOT-2)
961    ///
962    /// All fields are optional — omit any field to keep its current value.
963    pub async fn autopilot_update_config(
964        &self,
965        request: AutoPilotConfigRequest,
966    ) -> Result<AutoPilotConfigResponse> {
967        let url = format!("{}/v1/admin/autopilot/config", self.base_url);
968        let response = self.client.put(&url).json(&request).send().await?;
969        self.handle_response(response).await
970    }
971
972    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
973    ///
974    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
975    /// The cycle runs synchronously and returns inline results.
976    pub async fn autopilot_trigger(
977        &self,
978        action: AutoPilotTriggerAction,
979    ) -> Result<AutoPilotTriggerResponse> {
980        let url = format!("{}/v1/admin/autopilot/trigger", self.base_url);
981        let request = AutoPilotTriggerRequest { action };
982        let response = self.client.post(&url).json(&request).send().await?;
983        self.handle_response(response).await
984    }
985
986    // ====================================================================
987    // Decay Engine Management (DECAY-1 / DECAY-2)
988    // ====================================================================
989
990    /// Get current decay engine configuration (DECAY-1).
991    ///
992    /// Returns the active strategy, half-life, and minimum importance threshold.
993    /// Requires Admin scope.
994    pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
995        let url = format!("{}/v1/admin/decay/config", self.base_url);
996        let response = self.client.get(&url).send().await?;
997        self.handle_response(response).await
998    }
999
1000    /// Update decay engine configuration at runtime (DECAY-1).
1001    ///
1002    /// Changes take effect on the next decay cycle — no restart required.
1003    /// All fields are optional; omit any to keep its current value.
1004    /// Requires Admin scope.
1005    pub async fn decay_update_config(
1006        &self,
1007        request: DecayConfigUpdateRequest,
1008    ) -> Result<DecayConfigUpdateResponse> {
1009        let url = format!("{}/v1/admin/decay/config", self.base_url);
1010        let response = self.client.put(&url).json(&request).send().await?;
1011        self.handle_response(response).await
1012    }
1013
1014    /// Get decay activity counters and last-cycle snapshot (DECAY-2).
1015    ///
1016    /// Returns cumulative totals (memories decayed/deleted, cycles run) and
1017    /// per-cycle statistics from the most recent run. Requires Admin scope.
1018    pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
1019        let url = format!("{}/v1/admin/decay/stats", self.base_url);
1020        let response = self.client.get(&url).send().await?;
1021        self.handle_response(response).await
1022    }
1023
1024    // ====================================================================
1025    // Product KPI Snapshot (OBS-2)
1026    // ====================================================================
1027
1028    /// Return a point-in-time product KPI snapshot (OBS-2).
1029    ///
1030    /// Calls `GET /v1/kpis`. Returns 8 operational metrics covering latency,
1031    /// error rate, and retention. Sub-millisecond — served from in-memory
1032    /// counters. Requires Admin scope.
1033    pub async fn get_kpis(&self) -> Result<KpiSnapshot> {
1034        let url = format!("{}/v1/kpis", self.base_url);
1035        let response = self.client.get(&url).send().await?;
1036        self.handle_response(response).await
1037    }
1038
1039    // ========================================================================
1040    // CE-54: Fulltext Reindex
1041    // ========================================================================
1042
1043    /// Backfill the BM25 fulltext index for memories stored before CE-12 auto-indexing (CE-54).
1044    ///
1045    /// Calls `POST /v1/admin/fulltext/reindex`. Requires Admin scope.
1046    ///
1047    /// Scans all memories in `namespace` (or every agent namespace when `None`) and adds
1048    /// any missing from the BM25 index. Safe to call multiple times — already-indexed
1049    /// memories are counted in `total_skipped` and not re-processed.
1050    pub async fn admin_fulltext_reindex(
1051        &self,
1052        namespace: Option<&str>,
1053    ) -> Result<FulltextReindexResponse> {
1054        let url = format!("{}/v1/admin/fulltext/reindex", self.base_url);
1055        let body = serde_json::json!({ "namespace": namespace });
1056        let response = self.client.post(&url).json(&body).send().await?;
1057        self.handle_response(response).await
1058    }
1059
1060    // =========================================================================
1061    // Cluster & Maintenance
1062    // =========================================================================
1063
1064    /// GET /v1/admin/cluster/replication — cluster replication status.
1065    pub async fn admin_cluster_replication(&self) -> Result<crate::types::ReplicationStatus> {
1066        let url = format!("{}/v1/admin/cluster/replication", self.base_url);
1067        let response = self.client.get(&url).send().await?;
1068        self.handle_response(response).await
1069    }
1070
1071    /// GET /v1/admin/cluster/shards — list shards.
1072    pub async fn admin_list_shards(&self) -> Result<crate::types::ShardListResponse> {
1073        let url = format!("{}/v1/admin/cluster/shards", self.base_url);
1074        let response = self.client.get(&url).send().await?;
1075        self.handle_response(response).await
1076    }
1077
1078    /// POST /v1/admin/cluster/shards/rebalance — rebalance shards.
1079    pub async fn admin_rebalance_shards(
1080        &self,
1081        request: crate::types::ShardRebalanceRequest,
1082    ) -> Result<crate::types::ShardRebalanceResponse> {
1083        let url = format!("{}/v1/admin/cluster/shards/rebalance", self.base_url);
1084        let response = self.client.post(&url).json(&request).send().await?;
1085        self.handle_response(response).await
1086    }
1087
1088    /// GET /v1/admin/cluster/maintenance — maintenance mode status.
1089    pub async fn admin_maintenance_status(&self) -> Result<crate::types::MaintenanceStatus> {
1090        let url = format!("{}/v1/admin/cluster/maintenance", self.base_url);
1091        let response = self.client.get(&url).send().await?;
1092        self.handle_response(response).await
1093    }
1094
1095    /// POST /v1/admin/cluster/maintenance/enable — enable maintenance mode.
1096    pub async fn admin_enable_maintenance(
1097        &self,
1098        request: crate::types::EnableMaintenanceRequest,
1099    ) -> Result<crate::types::MaintenanceStatus> {
1100        let url = format!("{}/v1/admin/cluster/maintenance/enable", self.base_url);
1101        let response = self.client.post(&url).json(&request).send().await?;
1102        self.handle_response(response).await
1103    }
1104
1105    /// POST /v1/admin/cluster/maintenance/disable — disable maintenance mode.
1106    pub async fn admin_disable_maintenance(
1107        &self,
1108        request: crate::types::DisableMaintenanceRequest,
1109    ) -> Result<crate::types::MaintenanceStatus> {
1110        let url = format!("{}/v1/admin/cluster/maintenance/disable", self.base_url);
1111        let response = self.client.post(&url).json(&request).send().await?;
1112        self.handle_response(response).await
1113    }
1114
1115    // =========================================================================
1116    // Quotas
1117    // =========================================================================
1118
1119    /// GET /v1/admin/quotas — list all namespace quotas.
1120    pub async fn admin_list_quotas(&self) -> Result<crate::types::QuotaListResponse> {
1121        let url = format!("{}/v1/admin/quotas", self.base_url);
1122        let response = self.client.get(&url).send().await?;
1123        self.handle_response(response).await
1124    }
1125
1126    /// GET /v1/admin/quotas/default — get default quota configuration.
1127    pub async fn admin_get_default_quota(&self) -> Result<crate::types::DefaultQuotaResponse> {
1128        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1129        let response = self.client.get(&url).send().await?;
1130        self.handle_response(response).await
1131    }
1132
1133    /// PUT /v1/admin/quotas/default — set default quota configuration.
1134    pub async fn admin_set_default_quota(
1135        &self,
1136        request: crate::types::SetDefaultQuotaRequest,
1137    ) -> Result<crate::types::SetQuotaResponse> {
1138        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1139        let response = self.client.put(&url).json(&request).send().await?;
1140        self.handle_response(response).await
1141    }
1142
1143    /// GET /v1/admin/quotas/{namespace} — get namespace quota.
1144    pub async fn admin_get_quota(&self, namespace: &str) -> Result<crate::types::QuotaStatus> {
1145        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1146        let response = self.client.get(&url).send().await?;
1147        self.handle_response(response).await
1148    }
1149
1150    /// PUT /v1/admin/quotas/{namespace} — set namespace quota.
1151    pub async fn admin_set_quota(
1152        &self,
1153        namespace: &str,
1154        request: crate::types::SetQuotaRequest,
1155    ) -> Result<crate::types::SetQuotaResponse> {
1156        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1157        let response = self.client.put(&url).json(&request).send().await?;
1158        self.handle_response(response).await
1159    }
1160
1161    /// DELETE /v1/admin/quotas/{namespace} — remove namespace quota.
1162    pub async fn admin_delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
1163        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1164        let response = self.client.delete(&url).send().await?;
1165        self.handle_response(response).await
1166    }
1167
1168    /// POST /v1/admin/quotas/{namespace}/check — check if operation would exceed quota.
1169    pub async fn admin_check_quota(
1170        &self,
1171        namespace: &str,
1172        request: crate::types::QuotaCheckRequest,
1173    ) -> Result<crate::types::QuotaCheckResult> {
1174        let url = format!("{}/v1/admin/quotas/{}/check", self.base_url, namespace);
1175        let response = self.client.post(&url).json(&request).send().await?;
1176        self.handle_response(response).await
1177    }
1178
1179    // =========================================================================
1180    // Slow Queries
1181    // =========================================================================
1182
1183    /// GET /v1/admin/slow-queries — list recent slow queries.
1184    pub async fn admin_list_slow_queries(
1185        &self,
1186        namespace: Option<&str>,
1187        query_type: Option<&str>,
1188        limit: Option<u32>,
1189    ) -> Result<Vec<serde_json::Value>> {
1190        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1191        let mut params = Vec::new();
1192        if let Some(ns) = namespace {
1193            params.push(format!("namespace={}", ns));
1194        }
1195        if let Some(qt) = query_type {
1196            params.push(format!("query_type={}", qt));
1197        }
1198        if let Some(l) = limit {
1199            params.push(format!("limit={}", l));
1200        }
1201        if !params.is_empty() {
1202            url.push('?');
1203            url.push_str(&params.join("&"));
1204        }
1205        let response = self.client.get(&url).send().await?;
1206        self.handle_response(response).await
1207    }
1208
1209    /// GET /v1/admin/slow-queries/summary — slow query summary.
1210    pub async fn admin_slow_query_summary(&self) -> Result<serde_json::Value> {
1211        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
1212        let response = self.client.get(&url).send().await?;
1213        self.handle_response(response).await
1214    }
1215
1216    /// DELETE /v1/admin/slow-queries — clear slow query log.
1217    pub async fn admin_clear_slow_queries(
1218        &self,
1219        namespace: Option<&str>,
1220    ) -> Result<serde_json::Value> {
1221        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1222        if let Some(ns) = namespace {
1223            url.push_str(&format!("?namespace={}", ns));
1224        }
1225        let response = self.client.delete(&url).send().await?;
1226        self.handle_response(response).await
1227    }
1228
1229    /// PATCH /v1/admin/slow-queries/config — update slow query configuration.
1230    pub async fn admin_update_slow_query_config(
1231        &self,
1232        config: serde_json::Value,
1233    ) -> Result<serde_json::Value> {
1234        let url = format!("{}/v1/admin/slow-queries/config", self.base_url);
1235        let response = self.client.patch(&url).json(&config).send().await?;
1236        self.handle_response(response).await
1237    }
1238
1239    // =========================================================================
1240    // Backups
1241    // =========================================================================
1242
1243    /// GET /v1/admin/backups — list all backups.
1244    pub async fn admin_list_backups(&self) -> Result<crate::types::BackupListResponse> {
1245        let url = format!("{}/v1/admin/backups", self.base_url);
1246        let response = self.client.get(&url).send().await?;
1247        self.handle_response(response).await
1248    }
1249
1250    /// POST /v1/admin/backups — create a new backup.
1251    pub async fn admin_create_backup(
1252        &self,
1253        request: crate::types::CreateBackupRequest,
1254    ) -> Result<crate::types::CreateBackupResponse> {
1255        let url = format!("{}/v1/admin/backups", self.base_url);
1256        let response = self.client.post(&url).json(&request).send().await?;
1257        self.handle_response(response).await
1258    }
1259
1260    /// GET /v1/admin/backups/{id} — get backup details.
1261    pub async fn admin_get_backup(&self, backup_id: &str) -> Result<crate::types::AdminBackupInfo> {
1262        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1263        let response = self.client.get(&url).send().await?;
1264        self.handle_response(response).await
1265    }
1266
1267    /// DELETE /v1/admin/backups/{id} — delete a backup.
1268    pub async fn admin_delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
1269        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1270        let response = self.client.delete(&url).send().await?;
1271        self.handle_response(response).await
1272    }
1273
1274    /// GET /v1/admin/backups/schedule — get backup schedule.
1275    pub async fn admin_get_backup_schedule(&self) -> Result<crate::types::BackupSchedule> {
1276        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1277        let response = self.client.get(&url).send().await?;
1278        self.handle_response(response).await
1279    }
1280
1281    /// POST /v1/admin/backups/schedule — update backup schedule.
1282    pub async fn admin_update_backup_schedule(
1283        &self,
1284        request: crate::types::UpdateBackupScheduleRequest,
1285    ) -> Result<crate::types::BackupSchedule> {
1286        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1287        let response = self.client.post(&url).json(&request).send().await?;
1288        self.handle_response(response).await
1289    }
1290
1291    /// POST /v1/admin/backups/restore — restore from backup.
1292    pub async fn admin_restore_backup(
1293        &self,
1294        request: crate::types::RestoreBackupRequest,
1295    ) -> Result<crate::types::RestoreBackupResponse> {
1296        let url = format!("{}/v1/admin/backups/restore", self.base_url);
1297        let response = self.client.post(&url).json(&request).send().await?;
1298        self.handle_response(response).await
1299    }
1300
1301    /// GET /v1/admin/backups/restore/{id} — restore operation status.
1302    pub async fn admin_get_restore_status(
1303        &self,
1304        restore_id: &str,
1305    ) -> Result<crate::types::RestoreBackupResponse> {
1306        let url = format!("{}/v1/admin/backups/restore/{}", self.base_url, restore_id);
1307        let response = self.client.get(&url).send().await?;
1308        self.handle_response(response).await
1309    }
1310
1311    // =========================================================================
1312    // Ops — Diagnostics & Jobs
1313    // =========================================================================
1314
1315    /// GET /ops/diagnostics — system diagnostics.
1316    pub async fn ops_diagnostics(&self) -> Result<serde_json::Value> {
1317        let url = format!("{}/ops/diagnostics", self.base_url);
1318        let response = self.client.get(&url).send().await?;
1319        self.handle_response(response).await
1320    }
1321
1322    /// GET /ops/jobs — list background jobs.
1323    pub async fn ops_list_jobs(&self) -> Result<Vec<crate::types::JobInfo>> {
1324        let url = format!("{}/ops/jobs", self.base_url);
1325        let response = self.client.get(&url).send().await?;
1326        self.handle_response(response).await
1327    }
1328
1329    /// GET /ops/jobs/{id} — get job status.
1330    pub async fn ops_get_job(&self, job_id: &str) -> Result<crate::types::JobInfo> {
1331        let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
1332        let response = self.client.get(&url).send().await?;
1333        self.handle_response(response).await
1334    }
1335
1336    /// POST /ops/compact — trigger compaction.
1337    pub async fn ops_compact(
1338        &self,
1339        request: crate::types::CompactionRequest,
1340    ) -> Result<crate::types::CompactionResponse> {
1341        let url = format!("{}/ops/compact", self.base_url);
1342        let response = self.client.post(&url).json(&request).send().await?;
1343        self.handle_response(response).await
1344    }
1345
1346    /// POST /ops/shutdown — request graceful shutdown.
1347    pub async fn ops_shutdown(&self) -> Result<serde_json::Value> {
1348        let url = format!("{}/ops/shutdown", self.base_url);
1349        let response = self.client.post(&url).send().await?;
1350        self.handle_response(response).await
1351    }
1352
1353    // =========================================================================
1354    // Backup Download / Upload
1355    // =========================================================================
1356
1357    /// Download a backup as gzipped bytes via `GET /v1/admin/backups/{id}/download`.
1358    pub async fn download_backup(&self, backup_id: &str) -> Result<Vec<u8>> {
1359        let url = format!("{}/v1/admin/backups/{}/download", self.base_url, backup_id);
1360        let response = self.client.get(&url).send().await?;
1361        if !response.status().is_success() {
1362            let status = response.status();
1363            let body = response.text().await.unwrap_or_default();
1364            return Err(crate::error::ClientError::Server {
1365                status: status.as_u16(),
1366                message: body,
1367                code: None,
1368            });
1369        }
1370        Ok(response.bytes().await?.to_vec())
1371    }
1372
1373    /// Upload a backup from gzipped bytes via `POST /v1/admin/backups/upload`.
1374    pub async fn upload_backup(&self, data: Vec<u8>) -> Result<crate::types::CreateBackupResponse> {
1375        let url = format!("{}/v1/admin/backups/upload", self.base_url);
1376        let response = self
1377            .client
1378            .post(&url)
1379            .header("Content-Type", "application/gzip")
1380            .body(data)
1381            .send()
1382            .await?;
1383        self.handle_response(response).await
1384    }
1385
1386    // =========================================================================
1387    // Storage Tier Overview
1388    // =========================================================================
1389
1390    /// Get storage tier overview via `GET /v1/admin/storage/tiers`.
1391    pub async fn storage_tier_overview(&self) -> Result<crate::types::StorageTierOverview> {
1392        let url = format!("{}/v1/admin/storage/tiers", self.base_url);
1393        let response = self.client.get(&url).send().await?;
1394        self.handle_response(response).await
1395    }
1396
1397    // =========================================================================
1398    // Background Activity
1399    // =========================================================================
1400
1401    /// Get background activity metrics via `GET /v1/admin/background-activity`.
1402    pub async fn background_activity(&self) -> Result<serde_json::Value> {
1403        let url = format!("{}/v1/admin/background-activity", self.base_url);
1404        let response = self.client.get(&url).send().await?;
1405        self.handle_response(response).await
1406    }
1407
1408    // =========================================================================
1409    // Memory Type Stats
1410    // =========================================================================
1411
1412    /// Get per-type memory statistics via `GET /v1/admin/memory-type-stats`.
1413    pub async fn memory_type_stats(&self) -> Result<crate::types::MemoryTypeStatsResponse> {
1414        let url = format!("{}/v1/admin/memory-type-stats", self.base_url);
1415        let response = self.client.get(&url).send().await?;
1416        self.handle_response(response).await
1417    }
1418
1419    // =========================================================================
1420    // Migrate Namespace Dimensions
1421    // =========================================================================
1422
1423    /// Migrate namespace embedding dimensions via `POST /v1/admin/namespaces/migrate-dimensions`.
1424    pub async fn migrate_namespace_dimensions(
1425        &self,
1426        request: crate::types::MigrateNamespaceDimensionsRequest,
1427    ) -> Result<crate::types::MigrateDimensionsResponse> {
1428        let url = format!("{}/v1/admin/namespaces/migrate-dimensions", self.base_url);
1429        let response = self.client.post(&url).json(&request).send().await?;
1430        self.handle_response(response).await
1431    }
1432
1433    // =========================================================================
1434    // ReembedJob Force-Drain (v0.11.82+)
1435    // =========================================================================
1436
1437    /// Synchronously drain all static vectors to full ONNX quality via
1438    /// `POST /v1/admin/reembed/drain` (v0.11.82+).
1439    ///
1440    /// Runs the re-embedding upgrade loop until zero `_embedding_kind=static`
1441    /// candidates remain across all namespaces, or `request.timeout_secs` elapses.
1442    /// Requires Admin scope. Useful as a pre-benchmark steady-state gate when
1443    /// `DAKERA_TIERED=1`.
1444    ///
1445    /// A [`DrainReembedResponse::remaining`] of `0` guarantees all vectors are at
1446    /// full ONNX quality.
1447    pub async fn drain_reembed(
1448        &self,
1449        request: crate::types::DrainReembedRequest,
1450    ) -> Result<crate::types::DrainReembedResponse> {
1451        let url = format!("{}/v1/admin/reembed/drain", self.base_url);
1452        let response = self.client.post(&url).json(&request).send().await?;
1453        self.handle_response(response).await
1454    }
1455
1456    /// Return the count of static vectors pending re-embedding via
1457    /// `GET /v1/admin/reembed/static-count` (v0.11.91+).
1458    ///
1459    /// Operators can poll this alongside [`drain_reembed`][Self::drain_reembed]
1460    /// to monitor drain progress. A [`StaticCountResponse::static_count`] of `0`
1461    /// means steady state — all vectors are at full ONNX quality.
1462    ///
1463    /// Requires Admin scope.
1464    pub async fn admin_reembed_static_count(&self) -> Result<crate::types::StaticCountResponse> {
1465        let url = format!("{}/v1/admin/reembed/static-count", self.base_url);
1466        let response = self.client.get(&url).send().await?;
1467        self.handle_response(response).await
1468    }
1469}
1470
1471// ============================================================================
1472// Product KPI Snapshot (OBS-2)
1473// ============================================================================
1474
1475/// Point-in-time product KPI snapshot returned by `GET /v1/kpis` (OBS-2).
1476///
1477/// All latency values are in milliseconds. Rate/percentage values are in the
1478/// range `0.0`–`100.0`. Integer counts are unsigned.
1479///
1480/// Requires Admin scope.
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1482pub struct KpiSnapshot {
1483    /// Median recall latency across all namespaces over the last minute (ms).
1484    pub recall_latency_p50_ms: f64,
1485    /// 99th-percentile recall latency across all namespaces over the last minute (ms).
1486    pub recall_latency_p99_ms: f64,
1487    /// Median store latency across all namespaces over the last minute (ms).
1488    pub store_latency_p50_ms: f64,
1489    /// 5xx error rate as a percentage of total API requests over the last minute.
1490    pub api_error_rate_5xx_pct: f64,
1491    /// Distinct agent identifiers that stored or recalled a memory in the last 24 hours.
1492    pub active_agents_count: u64,
1493    /// Total sessions created in the rolling 7-day window.
1494    pub session_count_week: u64,
1495    /// Current number of nodes in the cross-agent knowledge graph.
1496    pub cross_agent_network_node_count: u64,
1497    /// Percentage of memories created 7 days ago that are still active.
1498    pub memory_retention_7d_pct: f64,
1499}
1500
1501// ============================================================================
1502// CE-54: Fulltext Reindex (Admin)
1503// ============================================================================
1504
1505/// Per-namespace result from `POST /v1/admin/fulltext/reindex` (CE-54).
1506#[derive(Debug, Clone, Serialize, Deserialize)]
1507pub struct FulltextReindexNamespaceResult {
1508    /// Namespace that was scanned.
1509    pub namespace: String,
1510    /// Total vectors examined.
1511    pub vectors_scanned: usize,
1512    /// Memories newly added to the BM25 index.
1513    pub newly_indexed: usize,
1514    /// Memories already in the BM25 index (skipped).
1515    pub already_indexed: usize,
1516    /// Memories that could not be parsed.
1517    pub parse_failures: usize,
1518}
1519
1520/// Response from `POST /v1/admin/fulltext/reindex` (CE-54).
1521///
1522/// Returned by [`DakeraClient::admin_fulltext_reindex`].
1523#[derive(Debug, Clone, Serialize, Deserialize)]
1524pub struct FulltextReindexResponse {
1525    /// Number of namespaces scanned.
1526    pub namespaces_processed: usize,
1527    /// Total memories newly added to BM25 across all namespaces.
1528    pub total_indexed: usize,
1529    /// Total memories already in the BM25 index (skipped).
1530    pub total_skipped: usize,
1531    /// Per-namespace breakdown.
1532    pub details: Vec<FulltextReindexNamespaceResult>,
1533}
1534
1535// ============================================================================
1536// Tests
1537// ============================================================================
1538
1539#[cfg(test)]
1540mod tests {
1541    use super::*;
1542
1543    // -------------------------------------------------------------------------
1544    // OpsStats
1545    // -------------------------------------------------------------------------
1546
1547    #[test]
1548    fn test_ops_stats_deserializes() {
1549        let json = r#"{
1550            "version": "0.11.104",
1551            "total_vectors": 100000,
1552            "namespace_count": 5,
1553            "uptime_seconds": 86400,
1554            "timestamp": 1785000000,
1555            "state": "healthy"
1556        }"#;
1557        let stats: OpsStats = serde_json::from_str(json).unwrap();
1558        assert_eq!(stats.version, "0.11.104");
1559        assert_eq!(stats.total_vectors, 100000);
1560        assert_eq!(stats.state, "healthy");
1561    }
1562
1563    // -------------------------------------------------------------------------
1564    // ClusterStatus
1565    // -------------------------------------------------------------------------
1566
1567    #[test]
1568    fn test_cluster_status_deserializes_without_redis_healthy() {
1569        let json = r#"{
1570            "cluster_id": "cl-1",
1571            "state": "active",
1572            "node_count": 3,
1573            "total_vectors": 500000,
1574            "namespace_count": 10,
1575            "version": "0.11.104",
1576            "timestamp": 1785000000
1577        }"#;
1578        let cs: ClusterStatus = serde_json::from_str(json).unwrap();
1579        assert_eq!(cs.cluster_id, "cl-1");
1580        assert_eq!(cs.node_count, 3);
1581        assert!(cs.redis_healthy.is_none());
1582    }
1583
1584    #[test]
1585    fn test_cluster_status_deserializes_with_redis_healthy() {
1586        let json = r#"{
1587            "cluster_id": "cl-2",
1588            "state": "active",
1589            "node_count": 1,
1590            "total_vectors": 0,
1591            "namespace_count": 0,
1592            "version": "0.11.104",
1593            "timestamp": 1785000000,
1594            "redis_healthy": true
1595        }"#;
1596        let cs: ClusterStatus = serde_json::from_str(json).unwrap();
1597        assert_eq!(cs.redis_healthy, Some(true));
1598    }
1599
1600    // -------------------------------------------------------------------------
1601    // NodeInfo
1602    // -------------------------------------------------------------------------
1603
1604    #[test]
1605    fn test_node_info_deserializes_with_defaults() {
1606        let json = r#"{
1607            "node_id": "n1",
1608            "address": "10.0.0.1:8080",
1609            "role": "primary",
1610            "status": "online",
1611            "version": "0.11.104",
1612            "uptime_seconds": 3600,
1613            "vector_count": 10000,
1614            "memory_bytes": 1073741824,
1615            "last_heartbeat": 1785000000
1616        }"#;
1617        let node: NodeInfo = serde_json::from_str(json).unwrap();
1618        assert_eq!(node.node_id, "n1");
1619        assert!((node.cpu_percent - 0.0).abs() < 1e-6);
1620        assert!((node.memory_percent - 0.0).abs() < 1e-6);
1621    }
1622
1623    // -------------------------------------------------------------------------
1624    // NodeListResponse
1625    // -------------------------------------------------------------------------
1626
1627    #[test]
1628    fn test_node_list_response_deserializes_empty() {
1629        let json = r#"{"nodes": [], "total": 0}"#;
1630        let resp: NodeListResponse = serde_json::from_str(json).unwrap();
1631        assert_eq!(resp.total, 0);
1632        assert!(resp.nodes.is_empty());
1633    }
1634
1635    // -------------------------------------------------------------------------
1636    // IndexStats
1637    // -------------------------------------------------------------------------
1638
1639    #[test]
1640    fn test_index_stats_optional_last_rebuild_absent() {
1641        let json = r#"{
1642            "index_type": "hnsw",
1643            "is_built": true,
1644            "size_bytes": 2048,
1645            "indexed_vectors": 100
1646        }"#;
1647        let stats: IndexStats = serde_json::from_str(json).unwrap();
1648        assert!(stats.last_rebuild.is_none());
1649        assert!(stats.is_built);
1650    }
1651
1652    #[test]
1653    fn test_index_stats_with_last_rebuild() {
1654        let json = r#"{
1655            "index_type": "flat",
1656            "is_built": false,
1657            "size_bytes": 0,
1658            "indexed_vectors": 0,
1659            "last_rebuild": 1785000000
1660        }"#;
1661        let stats: IndexStats = serde_json::from_str(json).unwrap();
1662        assert_eq!(stats.last_rebuild, Some(1785000000));
1663    }
1664
1665    // -------------------------------------------------------------------------
1666    // OptimizeRequest serialization
1667    // -------------------------------------------------------------------------
1668
1669    #[test]
1670    fn test_optimize_request_default_force_false() {
1671        let req = OptimizeRequest {
1672            force: false,
1673            target_index_type: None,
1674        };
1675        let json = serde_json::to_string(&req).unwrap();
1676        assert!(json.contains("\"force\":false"));
1677        assert!(!json.contains("target_index_type"));
1678    }
1679
1680    #[test]
1681    fn test_optimize_request_with_target_index_type() {
1682        let req = OptimizeRequest {
1683            force: true,
1684            target_index_type: Some("hnsw".to_string()),
1685        };
1686        let json = serde_json::to_string(&req).unwrap();
1687        assert!(json.contains("\"force\":true"));
1688        assert!(json.contains("\"target_index_type\":\"hnsw\""));
1689    }
1690
1691    // -------------------------------------------------------------------------
1692    // OptimizeResponse
1693    // -------------------------------------------------------------------------
1694
1695    #[test]
1696    fn test_optimize_response_deserializes_with_job_id() {
1697        let json = r#"{"success": true, "job_id": "job-abc", "message": "started"}"#;
1698        let resp: OptimizeResponse = serde_json::from_str(json).unwrap();
1699        assert!(resp.success);
1700        assert_eq!(resp.job_id.as_deref(), Some("job-abc"));
1701    }
1702
1703    #[test]
1704    fn test_optimize_response_deserializes_without_job_id() {
1705        let json = r#"{"success": true, "message": "done"}"#;
1706        let resp: OptimizeResponse = serde_json::from_str(json).unwrap();
1707        assert!(resp.job_id.is_none());
1708    }
1709
1710    // -------------------------------------------------------------------------
1711    // RebuildIndexRequest serialization
1712    // -------------------------------------------------------------------------
1713
1714    #[test]
1715    fn test_rebuild_index_request_all_optional_omitted() {
1716        let req = RebuildIndexRequest {
1717            namespace: None,
1718            index_type: None,
1719            force: false,
1720        };
1721        let json = serde_json::to_string(&req).unwrap();
1722        assert!(!json.contains("namespace"));
1723        assert!(!json.contains("index_type"));
1724        assert!(json.contains("\"force\":false"));
1725    }
1726
1727    #[test]
1728    fn test_rebuild_index_request_with_namespace() {
1729        let req = RebuildIndexRequest {
1730            namespace: Some("prod".to_string()),
1731            index_type: Some("hnsw".to_string()),
1732            force: true,
1733        };
1734        let json = serde_json::to_string(&req).unwrap();
1735        assert!(json.contains("\"namespace\":\"prod\""));
1736        assert!(json.contains("\"index_type\":\"hnsw\""));
1737        assert!(json.contains("\"force\":true"));
1738    }
1739
1740    // -------------------------------------------------------------------------
1741    // CacheStats
1742    // -------------------------------------------------------------------------
1743
1744    #[test]
1745    fn test_cache_stats_deserializes() {
1746        let json = r#"{
1747            "enabled": true,
1748            "cache_type": "redis",
1749            "entries": 1000,
1750            "size_bytes": 524288,
1751            "hits": 8000,
1752            "misses": 2000,
1753            "hit_rate": 0.8,
1754            "evictions": 50
1755        }"#;
1756        let stats: CacheStats = serde_json::from_str(json).unwrap();
1757        assert!(stats.enabled);
1758        assert_eq!(stats.cache_type, "redis");
1759        assert!((stats.hit_rate - 0.8).abs() < 1e-9);
1760    }
1761
1762    // -------------------------------------------------------------------------
1763    // ClearCacheRequest serialization
1764    // -------------------------------------------------------------------------
1765
1766    #[test]
1767    fn test_clear_cache_request_without_namespace_omits_field() {
1768        let req = ClearCacheRequest { namespace: None };
1769        let json = serde_json::to_string(&req).unwrap();
1770        assert!(!json.contains("namespace"));
1771    }
1772
1773    #[test]
1774    fn test_clear_cache_request_with_namespace() {
1775        let req = ClearCacheRequest {
1776            namespace: Some("prod-ns".to_string()),
1777        };
1778        let json = serde_json::to_string(&req).unwrap();
1779        assert!(json.contains("\"namespace\":\"prod-ns\""));
1780    }
1781
1782    // -------------------------------------------------------------------------
1783    // ClearCacheResponse
1784    // -------------------------------------------------------------------------
1785
1786    #[test]
1787    fn test_clear_cache_response_deserializes() {
1788        let json = r#"{"success": true, "entries_cleared": 42, "message": "ok"}"#;
1789        let resp: ClearCacheResponse = serde_json::from_str(json).unwrap();
1790        assert!(resp.success);
1791        assert_eq!(resp.entries_cleared, 42);
1792    }
1793
1794    // -------------------------------------------------------------------------
1795    // RuntimeConfig defaults
1796    // -------------------------------------------------------------------------
1797
1798    #[test]
1799    fn test_runtime_config_autopilot_defaults() {
1800        let json = r#"{
1801            "default_index_type": "hnsw",
1802            "cache_enabled": true,
1803            "cache_max_size_bytes": 1073741824,
1804            "rate_limit_enabled": false,
1805            "rate_limit_rps": 1000,
1806            "query_timeout_ms": 5000
1807        }"#;
1808        let cfg: RuntimeConfig = serde_json::from_str(json).unwrap();
1809        // autopilot_enabled defaults to true
1810        assert!(cfg.autopilot_enabled);
1811        // autopilot_dedup_threshold defaults to 0.93
1812        assert!((cfg.autopilot_dedup_threshold - 0.93).abs() < 1e-4);
1813        // autopilot_dedup_interval_hours defaults to 6
1814        assert_eq!(cfg.autopilot_dedup_interval_hours, 6);
1815        // autopilot_consolidation_interval_hours defaults to 12
1816        assert_eq!(cfg.autopilot_consolidation_interval_hours, 12);
1817    }
1818
1819    // -------------------------------------------------------------------------
1820    // UpdateConfigResponse
1821    // -------------------------------------------------------------------------
1822
1823    #[test]
1824    fn test_update_config_response_warnings_default_empty() {
1825        let json = r#"{
1826            "success": true,
1827            "config": {
1828                "default_index_type": "hnsw",
1829                "cache_enabled": false,
1830                "cache_max_size_bytes": 0,
1831                "rate_limit_enabled": false,
1832                "rate_limit_rps": 0,
1833                "query_timeout_ms": 5000
1834            },
1835            "message": "updated"
1836        }"#;
1837        let resp: UpdateConfigResponse = serde_json::from_str(json).unwrap();
1838        assert!(resp.success);
1839        assert!(resp.warnings.is_empty());
1840    }
1841
1842    // -------------------------------------------------------------------------
1843    // QuotaConfig serialization
1844    // -------------------------------------------------------------------------
1845
1846    #[test]
1847    fn test_quota_config_all_none_serializes_empty() {
1848        let cfg = QuotaConfig {
1849            max_vectors: None,
1850            max_storage_bytes: None,
1851            max_queries_per_minute: None,
1852            max_writes_per_minute: None,
1853        };
1854        let json = serde_json::to_string(&cfg).unwrap();
1855        assert_eq!(json, "{}");
1856    }
1857
1858    #[test]
1859    fn test_quota_config_with_max_vectors() {
1860        let cfg = QuotaConfig {
1861            max_vectors: Some(1_000_000),
1862            max_storage_bytes: None,
1863            max_queries_per_minute: None,
1864            max_writes_per_minute: None,
1865        };
1866        let json = serde_json::to_string(&cfg).unwrap();
1867        assert!(json.contains("\"max_vectors\":1000000"));
1868        assert!(!json.contains("max_storage"));
1869    }
1870
1871    // -------------------------------------------------------------------------
1872    // QuotaUsage defaults
1873    // -------------------------------------------------------------------------
1874
1875    #[test]
1876    fn test_quota_usage_all_fields_default_zero() {
1877        let json = r#"{}"#;
1878        let usage: QuotaUsage = serde_json::from_str(json).unwrap();
1879        assert_eq!(usage.current_vectors, 0);
1880        assert_eq!(usage.current_storage_bytes, 0);
1881        assert_eq!(usage.queries_this_minute, 0);
1882        assert_eq!(usage.writes_this_minute, 0);
1883    }
1884
1885    // -------------------------------------------------------------------------
1886    // QuotaListResponse
1887    // -------------------------------------------------------------------------
1888
1889    #[test]
1890    fn test_quota_list_response_optional_default_config() {
1891        let json = r#"{"quotas": [], "total": 0}"#;
1892        let resp: QuotaListResponse = serde_json::from_str(json).unwrap();
1893        assert!(resp.default_config.is_none());
1894        assert_eq!(resp.total, 0);
1895    }
1896
1897    // -------------------------------------------------------------------------
1898    // SlowQueryListResponse
1899    // -------------------------------------------------------------------------
1900
1901    #[test]
1902    fn test_slow_query_list_response_deserializes() {
1903        let json = r#"{"queries": [], "total": 0, "threshold_ms": 100.0}"#;
1904        let resp: SlowQueryListResponse = serde_json::from_str(json).unwrap();
1905        assert!((resp.threshold_ms - 100.0).abs() < 1e-9);
1906        assert!(resp.queries.is_empty());
1907    }
1908
1909    // -------------------------------------------------------------------------
1910    // BackupInfo
1911    // -------------------------------------------------------------------------
1912
1913    #[test]
1914    fn test_backup_info_deserializes_minimal() {
1915        let json = r#"{
1916            "backup_id": "bkp-1",
1917            "name": "daily",
1918            "backup_type": "full",
1919            "status": "completed",
1920            "namespaces": ["ns-1"],
1921            "vector_count": 5000,
1922            "size_bytes": 1048576,
1923            "created_at": 1785000000,
1924            "encrypted": true
1925        }"#;
1926        let bkp: BackupInfo = serde_json::from_str(json).unwrap();
1927        assert_eq!(bkp.backup_id, "bkp-1");
1928        assert!(bkp.encrypted);
1929        assert!(bkp.completed_at.is_none());
1930        assert!(bkp.error.is_none());
1931        assert!(bkp.compression.is_none());
1932    }
1933
1934    // -------------------------------------------------------------------------
1935    // CreateBackupRequest serialization
1936    // -------------------------------------------------------------------------
1937
1938    #[test]
1939    fn test_create_backup_request_minimal_omits_optional() {
1940        let req = CreateBackupRequest {
1941            name: "snapshot".to_string(),
1942            backup_type: None,
1943            namespaces: None,
1944            encrypt: None,
1945            compression: None,
1946        };
1947        let json = serde_json::to_string(&req).unwrap();
1948        assert!(json.contains("\"name\":\"snapshot\""));
1949        assert!(!json.contains("backup_type"));
1950        assert!(!json.contains("namespaces"));
1951        assert!(!json.contains("encrypt"));
1952        assert!(!json.contains("compression"));
1953    }
1954
1955    #[test]
1956    fn test_create_backup_request_with_namespaces() {
1957        let req = CreateBackupRequest {
1958            name: "ns-backup".to_string(),
1959            backup_type: Some("incremental".to_string()),
1960            namespaces: Some(vec!["ns-a".to_string(), "ns-b".to_string()]),
1961            encrypt: Some(true),
1962            compression: None,
1963        };
1964        let json = serde_json::to_string(&req).unwrap();
1965        assert!(json.contains("\"backup_type\":\"incremental\""));
1966        assert!(json.contains("\"ns-a\""));
1967        assert!(json.contains("\"encrypt\":true"));
1968    }
1969
1970    // -------------------------------------------------------------------------
1971    // NamespaceListResponse
1972    // -------------------------------------------------------------------------
1973
1974    #[test]
1975    fn test_namespace_list_response_deserializes() {
1976        let json = r#"{"namespaces": [], "total": 0, "total_vectors": 0}"#;
1977        let resp: NamespaceListResponse = serde_json::from_str(json).unwrap();
1978        assert_eq!(resp.total, 0);
1979        assert_eq!(resp.total_vectors, 0);
1980    }
1981
1982    // -------------------------------------------------------------------------
1983    // IndexStatsResponse with HashMap
1984    // -------------------------------------------------------------------------
1985
1986    #[test]
1987    fn test_index_stats_response_deserializes_with_hashmap() {
1988        let json = r#"{
1989            "namespaces": {
1990                "default": {
1991                    "index_type": "hnsw",
1992                    "is_built": true,
1993                    "size_bytes": 4096,
1994                    "indexed_vectors": 200
1995                }
1996            },
1997            "total_indexed_vectors": 200,
1998            "total_size_bytes": 4096
1999        }"#;
2000        let resp: IndexStatsResponse = serde_json::from_str(json).unwrap();
2001        assert_eq!(resp.total_indexed_vectors, 200);
2002        assert!(resp.namespaces.contains_key("default"));
2003    }
2004}