Skip to main content

agy_bridge/
lib.rs

1#![doc = include_str!("../README.md")]
2//! agy-bridge: Standalone reusable `PyO3` bridge for the Google Antigravity SDK.
3
4// Allow `::agy_bridge::` paths (generated by the `#[llm_tool]` proc macro) to
5// resolve when compiling tests within this crate.
6extern crate self as agy_bridge;
7
8/// Agent lifecycle management: creation, chat, shutdown.
9pub mod agent;
10/// Configuration types for agents, models, capabilities, and MCP servers.
11pub mod config;
12
13/// Multimodal content types for chat input (text, image, document, audio, video).
14pub mod content;
15/// Error types for the bridge.
16pub mod error;
17/// Pre/post-turn and tool-call lifecycle hooks.
18pub mod hooks;
19/// Policy rules for tool-call filtering and workspace scoping.
20pub mod policies;
21/// Quota tracking and backoff state.
22pub mod quota;
23/// Python runtime bridge: command dispatch over a dedicated thread.
24pub mod runtime;
25/// Safety filter detection heuristics and confidence hierarchy.
26pub mod safety;
27/// Streaming response channels for text, thought, and tool-call events.
28pub mod streaming;
29/// Custom Rust tool dispatch and definition types.
30pub mod tools;
31/// Event-driven trigger definitions.
32pub mod triggers;
33/// Shared domain types (messages, steps, usage metadata).
34pub mod types;
35
36// ── Re-exports ──────────────────────────────────────────────────────────────
37// Flat re-exports of the most commonly used types so callers can write
38// `use agy_bridge::{AgentConfig, Error, ToolRegistry, Content};`
39// without diving into sub-modules.
40
41pub use config::{
42    AgentConfig, BuiltinTools, CapabilitiesConfig, GeminiConfig, LocalAgentConfig, McpServer,
43    McpSseServer, McpStdioServer, McpStreamableHttpServer, SystemInstructions,
44};
45pub use content::{Audio, Content, ContentPrimitive, Document, Image, Video};
46pub use error::Error;
47pub use hooks::{HookCallback, HookEntry, HookPoint, HookResult, HookSet, Hooks};
48/// Re-export the `#[llm_tool]` proc-macro so users only need `agy_bridge` in
49/// their dependency list.
50pub use llm_tool_macros::llm_tool;
51pub use policies::{AskUserHandler, PolicyDecision, PolicyRule, PolicySet};
52pub use runtime::RuntimeConfig;
53pub use safety::{Confidence, SafetyVerdict, detect_safety_interference};
54pub use streaming::{ChatResponseHandle, ChatResult, ResponseEvent, StreamChunk};
55pub use tools::{RustTool, ToolContext, ToolDefinition, ToolError, ToolOutput, ToolRegistry};
56pub use triggers::{TriggerConfig, TriggerEntry};
57pub use types::{ConversationMessage, MessageRole, Step, UsageMetadata};
58
59/// Convenience prelude — pull in everything you need with a single glob import.
60///
61/// ```
62/// use agy_bridge::prelude::*;
63/// ```
64///
65/// This re-exports the most commonly used types, traits, and macros from the
66/// crate so you can get started quickly without hunting for individual paths.
67pub mod prelude {
68    pub use llm_tool_macros::llm_tool;
69
70    pub use crate::{
71        Agent, AgyBridge,
72        config::{
73            AgentConfig, BuiltinTools, CapabilitiesConfig, GeminiConfig, LocalAgentConfig,
74            McpServer, McpSseServer, McpStdioServer, McpStreamableHttpServer, SystemInstructions,
75        },
76        content::{Audio, Content, ContentPrimitive, Document, Image, Video},
77        error::Error,
78        hooks::{HookPoint, HookResult, Hooks},
79        policies::{AskUserHandler, PolicyDecision, PolicyRule, PolicySet},
80        streaming::{ChatResponseHandle, ChatResult, ResponseEvent, StreamChunk},
81        tools::{RustTool, ToolContext, ToolDefinition, ToolError, ToolOutput, ToolRegistry},
82        triggers::{TriggerConfig, TriggerEntry},
83        types::{ConversationMessage, MessageRole, Step, UsageMetadata},
84    };
85}
86
87use std::sync::Arc;
88
89/// Load environment variables from a `.env` file into the process environment.
90///
91/// 1. Walks upward from `CARGO_MANIFEST_DIR` (if set) or the current working
92///    directory to find the nearest `.env` file.
93/// 2. Parses each `KEY=VALUE` line (skipping blanks and `#`-comments).
94/// 3. For every key that is **not** already present in the process
95///    environment, calls [`std::env::set_var`] to inject it.
96/// 4. Returns a [`HashMap`](std::collections::HashMap) of the newly-set
97///    key/value pairs (keys that were already set are omitted).
98///
99/// Results are cached via [`OnceLock`](std::sync::OnceLock) — the file is
100/// read and environment variables are set at most once. Subsequent calls
101/// return a clone of the cached map without re-reading the file or
102/// modifying the environment.
103///
104/// # Safety
105///
106/// This function calls [`std::env::set_var`], which is **not** thread-safe.
107/// It **must** be called during single-threaded startup, before any
108/// additional threads are spawned (including the Tokio runtime). Calling it
109/// after threads exist is undefined behaviour.
110///
111/// # Example
112///
113/// ```
114/// let new_vars = agy_bridge::load_dotenv();
115/// // OnceLock-cached: safe to call multiple times, only loads .env once.
116/// let _ = new_vars.len();
117/// ```
118pub fn load_dotenv() -> &'static std::collections::HashMap<String, String> {
119    use std::sync::OnceLock;
120
121    static CACHED: OnceLock<std::collections::HashMap<String, String>> = OnceLock::new();
122
123    CACHED.get_or_init(|| {
124        let start = std::env::var_os("CARGO_MANIFEST_DIR").map_or_else(
125            || {
126                std::env::current_dir().unwrap_or_else(|e| {
127                    tracing::debug!("load_dotenv: current_dir() failed: {e}, using fallback \".\"");
128                    std::path::PathBuf::from(".")
129                })
130            },
131            std::path::PathBuf::from,
132        );
133
134        let mut dir = start.as_path();
135        loop {
136            let candidate = dir.join(".env");
137            if candidate.is_file() {
138                let mut env_map = std::collections::HashMap::new();
139                match std::fs::read_to_string(&candidate) {
140                    Ok(contents) => {
141                        for line in contents.lines() {
142                            if let Some((k, v)) = parse_dotenv_line(line)
143                                && std::env::var_os(k).is_none()
144                            {
145                                // SAFETY: Called inside the OnceLock closure during
146                                // single-threaded initialization, before any threads
147                                // are spawned. set_var is not thread-safe, but here
148                                // we are the only thread.
149                                unsafe {
150                                    std::env::set_var(k, v);
151                                }
152                                env_map.insert(k.to_owned(), v.to_owned());
153                            }
154                        }
155                    }
156                    Err(e) => {
157                        tracing::warn!(error = %e, "Failed to read .env file at {}", candidate.display());
158                    }
159                }
160                return env_map;
161            }
162            match dir.parent() {
163                Some(parent) => dir = parent,
164                None => return std::collections::HashMap::new(),
165            }
166        }
167    })
168}
169
170/// Parse a single line from a `.env` file.
171///
172/// Returns `Some((key, value))` for valid `KEY=VALUE` lines, stripping
173/// surrounding whitespace and quotes (single or double) from the value.
174/// Returns `None` for blank lines, comments, or lines without `=`.
175///
176/// This is factored out of [`load_dotenv`] for testability.
177pub(crate) fn parse_dotenv_line(line: &str) -> Option<(&str, &str)> {
178    let line = line.trim();
179    if line.is_empty() || line.starts_with('#') {
180        return None;
181    }
182    let (k, v) = line.split_once('=')?;
183    let k = k.trim();
184    if k.is_empty() {
185        return None;
186    }
187    let v = v.trim();
188    // Strip surrounding quotes (single or double)
189    let v = v
190        .strip_prefix('"')
191        .and_then(|s| s.strip_suffix('"'))
192        .or_else(|| v.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
193        .unwrap_or(v);
194    Some((k, v))
195}
196
197/// Convenience alias for an agent backed by the bridge's runtime.
198///
199/// This hides the generic `Runtime` parameter so consumers never see the
200/// underlying Python bridge type.
201pub type Agent = agent::AgentHandle<runtime::PythonRuntime>;
202
203/// Primary entry point for the Antigravity bridge.
204///
205/// Wraps the runtime and provides a clean Rust API for creating agents.
206///
207/// # Example
208///
209/// ```rust
210/// # use agy_bridge::AgyBridge;
211/// # use agy_bridge::config::AgentConfig;
212/// # #[tokio::main]
213/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
214/// # agy_bridge::load_dotenv();
215/// // Zero-config:
216/// // let bridge = AgyBridge::builder().build()?;
217///
218/// // With custom timeouts:
219/// let bridge = AgyBridge::builder()
220///     .chat_timeout(std::time::Duration::from_secs(120))
221///     .build()?;
222///
223/// // Create an agent (simple):
224/// // let agent = bridge.agent(AgentConfig::default()).await?;
225/// # let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
226/// # let project_root = manifest_dir.parent().unwrap().parent().unwrap();
227/// # let agent = bridge.agent(
228/// #     AgentConfig::builder()
229/// #         .system_instructions("Reply with 'Hello!' and nothing else. Never use tools.")
230/// #         .capabilities(agy_bridge::config::CapabilitiesConfig::custom_tools_only())
231/// #         .workspaces(vec![project_root])
232/// #         .build()
233/// # ).await?;
234///
235/// // Create an agent with tools and hooks:
236/// // let agent = bridge.agent(config)
237/// //     .tools(registry)
238/// //     .hooks(hooks)
239/// //     .await?;
240///
241/// let answer = agent.chat("Hello!").await?.text().await?;
242/// # Ok(())
243/// # }
244/// ```
245pub struct AgyBridge {
246    runtime: Arc<runtime::PythonRuntime>,
247}
248
249/// Builder for constructing an [`AgyBridge`] instance.
250///
251/// Created via [`AgyBridge::builder()`]. All settings have sensible defaults;
252/// call [`.build()`](Self::build) to finalise.
253///
254/// # Example
255///
256/// ```
257/// # use agy_bridge::AgyBridge;
258/// let bridge = AgyBridge::builder()
259///     .chat_timeout(std::time::Duration::from_secs(120))
260///     .channel_capacity(128)
261///     .build()?;
262/// # Ok::<(), agy_bridge::error::Error>(())
263/// ```
264pub struct AgyBridgeBuilder {
265    config: runtime::RuntimeConfig,
266}
267
268impl AgyBridgeBuilder {
269    /// Set the mpsc channel buffer size for the command channel.
270    #[must_use]
271    pub fn channel_capacity(mut self, capacity: usize) -> Self {
272        self.config.channel_capacity = capacity;
273        self
274    }
275
276    /// Set the timeout for individual runtime operations.
277    #[must_use]
278    pub fn operation_timeout(mut self, timeout: std::time::Duration) -> Self {
279        self.config.operation_timeout = timeout;
280        self
281    }
282
283    /// Set the timeout for joining the Python thread on shutdown.
284    #[must_use]
285    pub fn shutdown_timeout(mut self, timeout: std::time::Duration) -> Self {
286        self.config.shutdown_timeout = timeout;
287        self
288    }
289
290    /// Set the timeout for a single `agent.chat()` round-trip.
291    #[must_use]
292    pub fn chat_timeout(mut self, timeout: std::time::Duration) -> Self {
293        self.config.chat_timeout = timeout;
294        self
295    }
296
297    /// Set the delay between successive chat commands to prevent burst requests.
298    #[must_use]
299    pub fn inter_agent_delay(mut self, delay: std::time::Duration) -> Self {
300        self.config.inter_agent_delay = delay;
301        self
302    }
303
304    /// Replace the entire runtime configuration at once.
305    ///
306    /// Useful when you already have a [`RuntimeConfig`] struct. Individual
307    /// setters called *after* this will override the corresponding fields.
308    #[must_use]
309    pub fn runtime_config(mut self, config: runtime::RuntimeConfig) -> Self {
310        self.config = config;
311        self
312    }
313
314    /// Build the [`AgyBridge`], starting the Python runtime.
315    ///
316    /// # Errors
317    ///
318    /// Returns [`error::Error`] if the Python runtime cannot be started
319    /// (e.g. missing Antigravity SDK installation).
320    pub fn build(self) -> Result<AgyBridge, error::Error> {
321        Ok(AgyBridge {
322            runtime: Arc::new(runtime::PythonRuntime::new(self.config)?),
323        })
324    }
325}
326
327impl AgyBridge {
328    /// Create a new builder for configuring and constructing an [`AgyBridge`].
329    ///
330    /// # Example
331    ///
332    /// ```
333    /// # use agy_bridge::AgyBridge;
334    /// let bridge = AgyBridge::builder().build()?;
335    /// # Ok::<(), agy_bridge::error::Error>(())
336    /// ```
337    #[must_use]
338    pub fn builder() -> AgyBridgeBuilder {
339        AgyBridgeBuilder {
340            config: runtime::RuntimeConfig::default(),
341        }
342    }
343
344    /// Begin building a new agent on this bridge.
345    ///
346    /// Returns an [`AgentBuilder`] that can be directly `.await`ed for the
347    /// simple case, or chained with [`.tools()`](AgentBuilder::tools) and
348    /// [`.hooks()`](AgentBuilder::hooks) before awaiting.
349    ///
350    /// # Examples
351    ///
352    /// ```rust
353    /// # use agy_bridge::{AgyBridge, config::AgentConfig};
354    /// # #[tokio::main]
355    /// # async fn main() -> Result<(), agy_bridge::error::Error> {
356    /// # agy_bridge::load_dotenv();
357    /// # let bridge = AgyBridge::builder().build()?;
358    /// // Simple — no tools or hooks:
359    /// let agent = bridge.agent(AgentConfig::default()).await?;
360    ///
361    /// // With tools:
362    /// // let agent = bridge.agent(config).tools(registry).await?;
363    ///
364    /// // With tools and hooks:
365    /// // let agent = bridge.agent(config).tools(registry).hooks(hooks).await?;
366    /// # Ok(())
367    /// # }
368    /// ```
369    #[must_use]
370    pub fn agent(&self, config: config::AgentConfig) -> AgentBuilder<'_> {
371        AgentBuilder {
372            bridge: self,
373            config,
374            registry: None,
375            hooks: None,
376            policy_handler: None,
377        }
378    }
379
380    /// Convenience shorthand for `self.agent(AgentConfig::default())`.
381    ///
382    /// Creates an agent builder with default configuration. Chain
383    /// [`.tools()`](AgentBuilder::tools) or [`.hooks()`](AgentBuilder::hooks)
384    /// before awaiting, or `.await` directly for a bare agent.
385    ///
386    /// # Examples
387    ///
388    /// ```rust
389    /// # use agy_bridge::AgyBridge;
390    /// # #[tokio::main]
391    /// # async fn main() -> Result<(), agy_bridge::error::Error> {
392    /// # agy_bridge::load_dotenv();
393    /// # let bridge = AgyBridge::builder().build()?;
394    /// let agent = bridge.default_agent().await?;
395    /// # Ok(())
396    /// # }
397    /// ```
398    #[must_use]
399    pub fn default_agent(&self) -> AgentBuilder<'_> {
400        self.agent(config::AgentConfig::default())
401    }
402}
403
404/// Builder for creating an [`Agent`] on an [`AgyBridge`].
405///
406/// Obtained from [`AgyBridge::agent()`]. Implements [`IntoFuture`] so you can
407/// `.await` it directly, or chain optional [`.tools()`](Self::tools) /
408/// [`.hooks()`](Self::hooks) calls before awaiting.
409pub struct AgentBuilder<'a> {
410    bridge: &'a AgyBridge,
411    config: config::AgentConfig,
412    registry: Option<tools::ToolRegistry>,
413    hooks: Option<hooks::Hooks>,
414    policy_handler: Option<Arc<dyn policies::AskUserHandler>>,
415}
416
417impl AgentBuilder<'_> {
418    /// Attach a [`ToolRegistry`] containing custom
419    /// Rust tools for the agent.
420    ///
421    /// The registry's tool definitions are automatically merged into the
422    /// agent configuration.
423    ///
424    /// # Errors (at build time)
425    ///
426    /// Returns [`error::Error::InvalidConfig`] if `config.tools` is
427    /// already non-empty — pass tools via the registry **or** via
428    /// `config.tools`, not both.
429    #[must_use]
430    pub fn tools(mut self, registry: tools::ToolRegistry) -> Self {
431        self.registry = Some(registry);
432        self
433    }
434
435    /// Attach [`Hooks`] for lifecycle event
436    /// callbacks (pre/post turn, tool-call gating, etc.).
437    #[must_use]
438    pub fn hooks(mut self, hooks: hooks::Hooks) -> Self {
439        self.hooks = Some(hooks);
440        self
441    }
442
443    /// Attach a custom [`AskUserHandler`] to manage interactive tool-call confirmations.
444    #[must_use]
445    pub fn policy_handler(mut self, handler: impl policies::AskUserHandler + 'static) -> Self {
446        self.policy_handler = Some(Arc::new(handler));
447        self
448    }
449
450    /// Set a pre-existing conversation ID to resume.
451    #[must_use]
452    pub fn conversation_id(mut self, id: impl Into<String>) -> Self {
453        self.config.conversation_id = Some(id.into());
454        self
455    }
456
457    /// Set the model backend (e.g. `"gemini-3.5-flash"`).
458    #[must_use]
459    pub fn model(mut self, model: impl Into<String>) -> Self {
460        self.config.model = model.into();
461        self
462    }
463
464    /// Set system instructions for the agent.
465    #[must_use]
466    pub fn system_instructions(
467        mut self,
468        instructions: impl Into<config::SystemInstructions>,
469    ) -> Self {
470        self.config.system_instructions = Some(instructions.into());
471        self
472    }
473
474    /// Append workspace directories the agent is allowed to access and modify.
475    #[must_use]
476    pub fn workspaces(
477        mut self,
478        workspaces: impl IntoIterator<Item = impl Into<std::path::PathBuf>>,
479    ) -> Self {
480        self.config
481            .workspaces
482            .extend(workspaces.into_iter().map(Into::into));
483        self
484    }
485
486    /// Append policy rules to govern tool execution.
487    #[must_use]
488    pub fn policies(
489        mut self,
490        policies: impl IntoIterator<Item = impl Into<policies::PolicyRule>>,
491    ) -> Self {
492        self.config
493            .policies
494            .extend(policies.into_iter().map(Into::into));
495        self
496    }
497
498    /// Append triggers that autonomously wake the agent.
499    #[must_use]
500    pub fn triggers(
501        mut self,
502        triggers: impl IntoIterator<Item = impl Into<triggers::TriggerEntry>>,
503    ) -> Self {
504        self.config
505            .triggers
506            .extend(triggers.into_iter().map(Into::into));
507        self
508    }
509
510    /// Append MCP servers for the agent.
511    #[must_use]
512    pub fn mcp_servers(
513        mut self,
514        servers: impl IntoIterator<Item = impl Into<config::McpServer>>,
515    ) -> Self {
516        self.config
517            .mcp_servers
518            .extend(servers.into_iter().map(Into::into));
519        self
520    }
521
522    /// Append paths for agent skills.
523    #[must_use]
524    pub fn skills(
525        mut self,
526        skills: impl IntoIterator<Item = impl Into<std::path::PathBuf>>,
527    ) -> Self {
528        self.config
529            .skills
530            .extend(skills.into_iter().map(Into::into));
531        self
532    }
533
534    /// Set the maximum number of quota retry attempts before giving up.
535    #[must_use]
536    pub fn max_quota_retries(mut self, retries: u32) -> Self {
537        self.config.max_quota_retries = Some(retries);
538        self
539    }
540
541    /// Validate configuration and create the agent.
542    ///
543    /// Prefer using `.await` directly on the builder (via [`IntoFuture`])
544    /// instead of calling this method explicitly.
545    ///
546    /// # Errors
547    ///
548    /// Returns [`error::Error::InvalidConfig`] if:
549    /// - `config.tools` is non-empty **and** a `ToolRegistry` was provided.
550    /// - The capabilities configuration is self-contradictory (e.g. both
551    ///   `enabled_tools` and `disabled_tools` specified).
552    ///
553    /// Returns other [`error::Error`] variants if agent creation fails.
554    pub async fn build(mut self) -> Result<Agent, error::Error> {
555        // Validate capabilities.
556        if let Some(ref caps) = self.config.capabilities {
557            caps.validate().map_err(|msg| error::Error::InvalidConfig {
558                message: msg.to_string(),
559            })?;
560        }
561
562        // Handle tool registry.
563        let arc_registry = if let Some(registry) = self.registry {
564            if !self.config.tools.is_empty() {
565                return Err(error::Error::InvalidConfig {
566                    message: "config.tools is non-empty and a ToolRegistry was also provided; \
567                              pass tools via the registry or via config.tools, not both"
568                        .to_string(),
569                });
570            }
571            self.config.tools = registry.definitions();
572            Some(Arc::new(registry))
573        } else {
574            None
575        };
576
577        // Handle hooks.
578        let arc_hooks = if let Some(hooks) = self.hooks {
579            if !self.config.hooks.is_empty() {
580                return Err(error::Error::InvalidConfig {
581                    message: "config.hooks is non-empty and a Hooks instance was also provided; \
582                              configure hooks via Hooks or config.hooks, not both"
583                        .to_string(),
584                });
585            }
586            self.config.hooks = hooks.entries();
587            Some(Arc::new(hooks))
588        } else {
589            None
590        };
591
592        // Handle policy handler.
593        let arc_policy = self.policy_handler;
594
595        agent::AgentHandle::new(
596            Arc::clone(&self.bridge.runtime),
597            self.config,
598            arc_registry,
599            arc_hooks,
600            arc_policy,
601        )
602        .await
603    }
604}
605
606impl<'a> std::future::IntoFuture for AgentBuilder<'a> {
607    type Output = Result<Agent, error::Error>;
608    type IntoFuture =
609        std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
610
611    fn into_future(self) -> Self::IntoFuture {
612        Box::pin(self.build())
613    }
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619
620    // ── parse_dotenv_line regression tests ────────────────────────────
621
622    #[test]
623    fn dotenv_strips_double_quotes() {
624        let (k, v) = parse_dotenv_line(r#"API_KEY="my-secret""#).unwrap();
625        assert_eq!(k, "API_KEY");
626        assert_eq!(v, "my-secret");
627    }
628
629    #[test]
630    fn dotenv_strips_single_quotes() {
631        let (k, v) = parse_dotenv_line("TOKEN='abc123'").unwrap();
632        assert_eq!(k, "TOKEN");
633        assert_eq!(v, "abc123");
634    }
635
636    #[test]
637    fn dotenv_unquoted_value_unchanged() {
638        let (k, v) = parse_dotenv_line("FOO=bar").unwrap();
639        assert_eq!(k, "FOO");
640        assert_eq!(v, "bar");
641    }
642
643    #[test]
644    fn dotenv_mismatched_quotes_preserved() {
645        // Opening double quote but closing single quote → not stripped.
646        let (k, v) = parse_dotenv_line(r#"KEY="value'"#).unwrap();
647        assert_eq!(k, "KEY");
648        assert_eq!(v, r#""value'"#);
649    }
650
651    #[test]
652    fn dotenv_empty_quoted_value() {
653        let (k, v) = parse_dotenv_line(r#"EMPTY="""#).unwrap();
654        assert_eq!(k, "EMPTY");
655        assert_eq!(v, "");
656    }
657
658    #[test]
659    fn dotenv_whitespace_around_key_value() {
660        let (k, v) = parse_dotenv_line("  MY_VAR  =  \"hello world\"  ").unwrap();
661        assert_eq!(k, "MY_VAR");
662        assert_eq!(v, "hello world");
663    }
664
665    #[test]
666    fn dotenv_comment_line_is_none() {
667        assert!(parse_dotenv_line("# this is a comment").is_none());
668    }
669
670    #[test]
671    fn dotenv_blank_line_is_none() {
672        assert!(parse_dotenv_line("   ").is_none());
673    }
674
675    #[test]
676    fn dotenv_empty_key_is_none() {
677        assert!(parse_dotenv_line("=value").is_none());
678    }
679
680    #[test]
681    fn dotenv_no_equals_is_none() {
682        assert!(parse_dotenv_line("JUSTKEY").is_none());
683    }
684
685    #[test]
686    fn dotenv_value_with_internal_equals() {
687        let (k, v) = parse_dotenv_line("DSN=postgres://host:5432/db?opt=1").unwrap();
688        assert_eq!(k, "DSN");
689        assert_eq!(v, "postgres://host:5432/db?opt=1");
690    }
691
692    #[test]
693    fn dotenv_value_with_embedded_quotes_not_stripped() {
694        // Quotes in the middle are not stripped — only surrounding ones.
695        let (k, v) = parse_dotenv_line(r#"MSG=say "hello""#).unwrap();
696        assert_eq!(k, "MSG");
697        assert_eq!(v, r#"say "hello""#);
698    }
699
700    #[test]
701    fn test_load_dotenv_returns_static_reference_identity() {
702        let map1 = load_dotenv();
703        let map2 = load_dotenv();
704        assert!(std::ptr::eq(map1, map2));
705    }
706}