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