1use std::collections::BTreeMap;
2use std::env;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use clap::ValueEnum;
7use color_eyre::eyre::{Result, bail};
8use serde::{Deserialize, Serialize};
9
10pub const DEFAULT_CONFIG_FILE: &str = "omnigraph.yaml";
11
12pub fn graph_resource_id_for_selection(
13 selected_graph: Option<&str>,
14 normalized_uri: &str,
15) -> String {
16 selected_graph.unwrap_or(normalized_uri).to_string()
17}
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct ProjectConfig {
21 pub name: Option<String>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TargetConfig {
26 pub uri: String,
27 pub bearer_token_env: Option<String>,
28 #[serde(default)]
33 pub policy: PolicySettings,
34 #[serde(default)]
41 pub queries: BTreeMap<String, QueryEntry>,
42}
43
44#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
45#[serde(rename_all = "snake_case")]
46pub enum ReadOutputFormat {
47 #[default]
48 Table,
49 Kv,
50 Csv,
51 Jsonl,
52 Json,
53}
54
55#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
56#[serde(rename_all = "snake_case")]
57pub enum TableCellLayout {
58 #[default]
59 Truncate,
60 Wrap,
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct CliDefaults {
65 #[serde(rename = "graph")]
66 pub graph: Option<String>,
67 pub branch: Option<String>,
68 pub output_format: Option<ReadOutputFormat>,
69 pub table_max_column_width: Option<usize>,
70 pub table_cell_layout: Option<TableCellLayout>,
71 pub actor: Option<String>,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct ServerDefaults {
81 #[serde(rename = "graph")]
82 pub graph: Option<String>,
83 pub bind: Option<String>,
84 #[serde(default)]
89 pub policy: PolicySettings,
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93pub struct AuthDefaults {
94 pub env_file: Option<String>,
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct QueryDefaults {
99 #[serde(default)]
100 pub roots: Vec<String>,
101}
102
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104pub struct PolicySettings {
105 pub file: Option<String>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct QueryEntry {
115 pub file: String,
119 #[serde(default)]
120 pub mcp: McpSettings,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct McpSettings {
134 #[serde(default = "mcp_expose_default")]
135 pub expose: bool,
136 pub tool_name: Option<String>,
137}
138
139fn mcp_expose_default() -> bool {
140 true
141}
142
143impl Default for McpSettings {
144 fn default() -> Self {
145 Self {
146 expose: mcp_expose_default(),
147 tool_name: None,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum AliasCommand {
155 #[serde(alias = "query")]
160 Read,
161 #[serde(alias = "mutate")]
165 Change,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct AliasConfig {
170 pub command: AliasCommand,
171 pub query: String,
172 pub name: Option<String>,
173 #[serde(default)]
174 pub args: Vec<String>,
175 #[serde(rename = "graph")]
176 pub graph: Option<String>,
177 pub branch: Option<String>,
178 pub format: Option<ReadOutputFormat>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct OmnigraphConfig {
183 #[serde(default)]
184 pub project: ProjectConfig,
185 #[serde(default, rename = "graphs")]
186 pub graphs: BTreeMap<String, TargetConfig>,
187 #[serde(default)]
188 pub server: ServerDefaults,
189 #[serde(default)]
190 pub auth: AuthDefaults,
191 #[serde(default)]
192 pub cli: CliDefaults,
193 #[serde(default)]
194 pub query: QueryDefaults,
195 #[serde(default)]
196 pub aliases: BTreeMap<String, AliasConfig>,
197 #[serde(default)]
198 pub policy: PolicySettings,
199 #[serde(default)]
204 pub queries: BTreeMap<String, QueryEntry>,
205 #[serde(skip)]
206 base_dir: PathBuf,
207}
208
209impl Default for OmnigraphConfig {
210 fn default() -> Self {
211 Self {
212 project: ProjectConfig::default(),
213 graphs: BTreeMap::new(),
214 server: ServerDefaults::default(),
215 auth: AuthDefaults::default(),
216 cli: CliDefaults::default(),
217 query: QueryDefaults::default(),
218 aliases: BTreeMap::new(),
219 policy: PolicySettings::default(),
220 queries: BTreeMap::new(),
221 base_dir: PathBuf::new(),
222 }
223 }
224}
225
226impl OmnigraphConfig {
227 pub fn base_dir(&self) -> &Path {
228 &self.base_dir
229 }
230
231 pub fn cli_branch(&self) -> &str {
232 self.cli.branch.as_deref().unwrap_or("main")
233 }
234
235 pub fn cli_output_format(&self) -> ReadOutputFormat {
236 self.cli.output_format.unwrap_or_default()
237 }
238
239 pub fn table_max_column_width(&self) -> usize {
240 self.cli.table_max_column_width.unwrap_or(80)
241 }
242
243 pub fn table_cell_layout(&self) -> TableCellLayout {
244 self.cli.table_cell_layout.unwrap_or_default()
245 }
246
247 pub fn cli_graph_name(&self) -> Option<&str> {
248 self.cli.graph.as_deref()
249 }
250
251 pub fn server_graph_name(&self) -> Option<&str> {
252 self.server.graph.as_deref()
253 }
254
255 pub fn server_bind(&self) -> &str {
256 self.server.bind.as_deref().unwrap_or("127.0.0.1:8080")
257 }
258
259 pub fn resolve_target_name<'a>(
260 &self,
261 explicit_uri: Option<&str>,
262 explicit_target: Option<&'a str>,
263 default_target: Option<&'a str>,
264 ) -> Option<&'a str> {
265 explicit_target.or_else(|| {
266 if explicit_uri.is_some() {
267 None
268 } else {
269 default_target
270 }
271 })
272 }
273
274 pub fn graph_bearer_token_env(
275 &self,
276 explicit_uri: Option<&str>,
277 explicit_target: Option<&str>,
278 default_target: Option<&str>,
279 ) -> Option<&str> {
280 let target_name =
281 self.resolve_target_name(explicit_uri, explicit_target, default_target)?;
282 self.graphs
283 .get(target_name)
284 .and_then(|target| target.bearer_token_env.as_deref())
285 }
286
287 pub fn resolve_auth_env_file(&self) -> Option<PathBuf> {
288 self.auth
289 .env_file
290 .as_deref()
291 .map(|path| self.resolve_config_path(path))
292 }
293
294 pub fn resolve_policy_file(&self) -> Option<PathBuf> {
295 self.policy
296 .file
297 .as_deref()
298 .map(|path| self.resolve_config_path(path))
299 }
300
301 pub fn resolve_target_policy_file(&self, target_name: &str) -> Option<PathBuf> {
305 let target = self.graphs.get(target_name)?;
306 target
307 .policy
308 .file
309 .as_deref()
310 .map(|path| self.resolve_config_path(path))
311 }
312
313 pub fn query_entries(&self) -> &BTreeMap<String, QueryEntry> {
315 &self.queries
316 }
317
318 pub fn target_query_entries(
321 &self,
322 target_name: &str,
323 ) -> Option<&BTreeMap<String, QueryEntry>> {
324 self.graphs.get(target_name).map(|target| &target.queries)
325 }
326
327 pub fn query_entries_for(&self, graph: Option<&str>) -> &BTreeMap<String, QueryEntry> {
335 match graph {
336 Some(name) if self.graphs.contains_key(name) => &self.graphs[name].queries,
337 _ => &self.queries,
338 }
339 }
340
341 pub fn resolve_graph_selection<'a>(&self, graph: Option<&'a str>) -> Result<Option<&'a str>> {
357 match graph {
358 Some(name) if self.graphs.contains_key(name) => {
359 self.ensure_top_level_blocks_honored(Some(name))?;
360 Ok(Some(name))
361 }
362 Some(name) => bail!("graph '{}' not found in {}", name, DEFAULT_CONFIG_FILE),
363 None => Ok(None),
364 }
365 }
366
367 pub fn resolve_policy_tooling_graph_selection(&self) -> Result<Option<&str>> {
368 self.resolve_graph_selection(self.cli_graph_name().or_else(|| self.server_graph_name()))
369 }
370
371 pub fn resolve_policy_file_for(&self, graph: Option<&str>) -> Option<PathBuf> {
379 match graph {
380 Some(name) if self.graphs.contains_key(name) => self.resolve_target_policy_file(name),
381 _ => self.resolve_policy_file(),
382 }
383 }
384
385 pub fn populated_top_level_blocks(&self) -> Vec<&'static str> {
391 let mut blocks = Vec::new();
392 if self.policy.file.is_some() {
393 blocks.push("policy.file");
394 }
395 if !self.queries.is_empty() {
396 blocks.push("queries");
397 }
398 blocks
399 }
400
401 pub fn ensure_top_level_blocks_honored(&self, selected: Option<&str>) -> Result<()> {
408 if let Some(name) = selected {
409 let unhonored = self.populated_top_level_blocks();
410 if !unhonored.is_empty() {
411 bail!(
412 "named graph '{name}' uses its own `graphs.{name}.…` block, but top-level {} \
413 {} set and would be ignored. Move it to `graphs.{name}` (e.g. \
414 `graphs.{name}.policy.file`, `graphs.{name}.queries`).",
415 unhonored.join(" and "),
416 if unhonored.len() == 1 { "is" } else { "are" },
417 );
418 }
419 }
420 Ok(())
421 }
422
423 pub fn resolve_query_file(&self, value: &str) -> PathBuf {
428 self.resolve_config_path(value)
429 }
430
431 pub fn resolve_server_policy_file(&self) -> Option<PathBuf> {
434 self.server
435 .policy
436 .file
437 .as_deref()
438 .map(|path| self.resolve_config_path(path))
439 }
440
441 pub fn resolve_uri_value(&self, value: &str) -> String {
445 self.resolve_config_uri(value)
446 }
447
448 pub fn resolve_policy_tests_file(&self) -> Option<PathBuf> {
449 let policy_file = self.resolve_policy_file()?;
450 Some(policy_file.with_file_name("policy.tests.yaml"))
451 }
452
453 pub fn alias(&self, name: &str) -> Result<&AliasConfig> {
454 self.aliases
455 .get(name)
456 .ok_or_else(|| color_eyre::eyre::eyre!("alias '{}' not found", name))
457 }
458
459 pub fn resolve_target_uri(
460 &self,
461 explicit_uri: Option<String>,
462 explicit_target: Option<&str>,
463 default_target: Option<&str>,
464 ) -> Result<String> {
465 if let Some(uri) = explicit_uri {
466 return Ok(uri);
467 }
468
469 let target_name = explicit_target.or(default_target).ok_or_else(|| {
470 color_eyre::eyre::eyre!("URI must be provided via <URI>, --target, or config")
471 })?;
472 let target = self.graphs.get(target_name).ok_or_else(|| {
473 color_eyre::eyre::eyre!(
474 "graph '{}' not found in {}",
475 target_name,
476 DEFAULT_CONFIG_FILE
477 )
478 })?;
479 Ok(self.resolve_config_uri(&target.uri))
480 }
481
482 pub fn resolve_query_path(&self, query: &Path) -> Result<PathBuf> {
483 if query.is_absolute() {
484 return Ok(query.to_path_buf());
485 }
486
487 let direct = self.base_dir.join(query);
488 if direct.exists() {
489 return Ok(direct);
490 }
491
492 for root in &self.query.roots {
493 let candidate = self.base_dir.join(root).join(query);
494 if candidate.exists() {
495 return Ok(candidate);
496 }
497 }
498
499 bail!("query file '{}' not found", query.display());
500 }
501
502 fn resolve_config_uri(&self, value: &str) -> String {
503 if value.contains("://") {
504 return value.to_string();
505 }
506
507 let path = Path::new(value);
508 if path.is_absolute() {
509 value.to_string()
510 } else {
511 self.base_dir.join(path).to_string_lossy().to_string()
512 }
513 }
514
515 fn resolve_config_path(&self, value: &str) -> PathBuf {
516 let path = Path::new(value);
517 if path.is_absolute() {
518 path.to_path_buf()
519 } else {
520 self.base_dir.join(path)
521 }
522 }
523}
524
525pub fn default_config_path() -> PathBuf {
526 PathBuf::from(DEFAULT_CONFIG_FILE)
527}
528
529pub fn load_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
530 load_config_in(&env::current_dir()?, config_path)
531}
532
533fn load_config_in(cwd: &Path, config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
534 let explicit_path = config_path.cloned();
535 let config_path = explicit_path.or_else(|| {
536 let default_path = cwd.join(DEFAULT_CONFIG_FILE);
537 default_path.exists().then_some(default_path)
538 });
539
540 let mut config = if let Some(path) = &config_path {
541 serde_yaml::from_str::<OmnigraphConfig>(&fs::read_to_string(path)?)?
542 } else {
543 OmnigraphConfig::default()
544 };
545
546 config.base_dir = if let Some(path) = config_path {
547 absolute_base_dir(cwd, &path)?
548 } else {
549 cwd.to_path_buf()
550 };
551
552 Ok(config)
553}
554
555fn absolute_base_dir(cwd: &Path, path: &Path) -> Result<PathBuf> {
556 let path = if path.is_absolute() {
557 path.to_path_buf()
558 } else {
559 cwd.join(path)
560 };
561 Ok(path
562 .parent()
563 .map(Path::to_path_buf)
564 .unwrap_or_else(|| cwd.to_path_buf()))
565}
566
567#[cfg(test)]
568mod tests {
569 use std::fs;
570 use std::path::{Path, PathBuf};
571
572 use tempfile::tempdir;
573
574 use super::{
575 ReadOutputFormat, TableCellLayout, graph_resource_id_for_selection, load_config_in,
576 };
577
578 #[test]
579 fn load_config_reads_yaml_defaults_from_current_dir() {
580 let temp = tempdir().unwrap();
581 fs::write(
582 temp.path().join("omnigraph.yaml"),
583 r#"
584graphs:
585 local:
586 uri: ./demo.omni
587 bearer_token_env: DEMO_TOKEN
588auth:
589 env_file: .env.omni
590cli:
591 graph: local
592 branch: main
593 output_format: kv
594 table_max_column_width: 40
595 table_cell_layout: wrap
596policy: {}
597"#,
598 )
599 .unwrap();
600
601 let config = load_config_in(temp.path(), None).unwrap();
602 assert_eq!(config.cli_graph_name(), Some("local"));
603 assert_eq!(config.cli_branch(), "main");
604 assert_eq!(config.cli_output_format(), ReadOutputFormat::Kv);
605 assert_eq!(config.table_max_column_width(), 40);
606 assert_eq!(config.table_cell_layout(), TableCellLayout::Wrap);
607 assert_eq!(
608 config.graph_bearer_token_env(None, None, config.cli_graph_name()),
609 Some("DEMO_TOKEN")
610 );
611 assert_eq!(
612 config.resolve_auth_env_file().unwrap(),
613 temp.path().join(".env.omni")
614 );
615 assert_eq!(
616 PathBuf::from(
617 config
618 .resolve_target_uri(None, None, config.cli_graph_name())
619 .unwrap()
620 ),
621 temp.path().join("./demo.omni")
622 );
623 }
624
625 #[test]
626 fn load_config_does_not_walk_parent_directories() {
627 let temp = tempdir().unwrap();
628 let child = temp.path().join("child");
629 fs::create_dir_all(&child).unwrap();
630 fs::write(
631 temp.path().join("omnigraph.yaml"),
632 "graphs:\n local:\n uri: ./demo.omni\n",
633 )
634 .unwrap();
635
636 let config = load_config_in(&child, None).unwrap();
637 assert!(config.graphs.is_empty());
638 }
639
640 #[test]
641 fn graph_resource_id_for_selection_uses_name_or_anonymous_uri() {
642 assert_eq!(
643 graph_resource_id_for_selection(Some("local"), "/tmp/graph.omni"),
644 "local"
645 );
646 assert_eq!(
647 graph_resource_id_for_selection(None, "/tmp/graph.omni"),
648 "/tmp/graph.omni"
649 );
650 }
651
652 #[test]
653 fn resolve_graph_selection_validates_membership_and_coherence() {
654 let temp = tempdir().unwrap();
655 fs::write(
656 temp.path().join("omnigraph.yaml"),
657 "graphs:\n local:\n uri: ./demo.omni\n",
658 )
659 .unwrap();
660 let config = load_config_in(temp.path(), None).unwrap();
661
662 assert_eq!(config.resolve_graph_selection(Some("local")).unwrap(), Some("local"));
664 assert_eq!(config.resolve_graph_selection(None).unwrap(), None);
666 let err = config.resolve_graph_selection(Some("ghost")).unwrap_err().to_string();
668 assert!(
669 err.contains("ghost") && err.contains("not found"),
670 "unknown graph must error naming it: {err}"
671 );
672
673 let temp2 = tempdir().unwrap();
678 fs::write(
679 temp2.path().join("omnigraph.yaml"),
680 "graphs:\n local:\n uri: ./demo.omni\npolicy:\n file: ./top.yaml\n",
681 )
682 .unwrap();
683 let incoherent = load_config_in(temp2.path(), None).unwrap();
684 let err = incoherent
685 .resolve_graph_selection(Some("local"))
686 .unwrap_err()
687 .to_string();
688 assert!(
689 err.contains("local") && err.contains("policy.file"),
690 "named graph + populated top-level block must be rejected, naming both: {err}"
691 );
692 assert_eq!(
693 incoherent.resolve_graph_selection(None).unwrap(),
694 None,
695 "anonymous selection still honors top-level"
696 );
697 }
698
699 #[test]
700 fn policy_tooling_graph_selection_prefers_cli_then_server_and_validates() {
701 let temp = tempdir().unwrap();
702 fs::write(
703 temp.path().join("omnigraph.yaml"),
704 "graphs:\n local:\n uri: ./local.omni\n prod:\n uri: ./prod.omni\n\
705 server:\n graph: local\ncli:\n graph: prod\n",
706 )
707 .unwrap();
708 let config = load_config_in(temp.path(), None).unwrap();
709 assert_eq!(
710 config.resolve_policy_tooling_graph_selection().unwrap(),
711 Some("prod")
712 );
713
714 let temp = tempdir().unwrap();
715 fs::write(
716 temp.path().join("omnigraph.yaml"),
717 "graphs:\n local:\n uri: ./local.omni\nserver:\n graph: local\n",
718 )
719 .unwrap();
720 let config = load_config_in(temp.path(), None).unwrap();
721 assert_eq!(
722 config.resolve_policy_tooling_graph_selection().unwrap(),
723 Some("local")
724 );
725
726 let temp = tempdir().unwrap();
727 fs::write(temp.path().join("omnigraph.yaml"), "policy: {}\n").unwrap();
728 let config = load_config_in(temp.path(), None).unwrap();
729 assert_eq!(config.resolve_policy_tooling_graph_selection().unwrap(), None);
730
731 let temp = tempdir().unwrap();
732 fs::write(
733 temp.path().join("omnigraph.yaml"),
734 "graphs:\n local:\n uri: ./local.omni\nserver:\n graph: ghost\n",
735 )
736 .unwrap();
737 let config = load_config_in(temp.path(), None).unwrap();
738 let err = config
739 .resolve_policy_tooling_graph_selection()
740 .unwrap_err()
741 .to_string();
742 assert!(
743 err.contains("ghost") && err.contains("not found"),
744 "unknown server.graph must use graph-selection validation: {err}"
745 );
746 }
747
748 #[test]
749 fn resolve_query_path_searches_config_roots() {
750 let temp = tempdir().unwrap();
751 fs::create_dir_all(temp.path().join("queries")).unwrap();
752 fs::write(
753 temp.path().join("omnigraph.yaml"),
754 "query:\n roots:\n - queries\npolicy: {}\n",
755 )
756 .unwrap();
757 fs::write(
758 temp.path().join("queries").join("test.gq"),
759 "query q { return {} }",
760 )
761 .unwrap();
762
763 let config = load_config_in(temp.path(), None).unwrap();
764 let resolved = config.resolve_query_path(Path::new("test.gq")).unwrap();
765 assert_eq!(resolved, temp.path().join("queries").join("test.gq"));
766 }
767
768 #[test]
769 fn resolve_query_path_prefers_config_base_dir_over_ambient_cwd() {
770 let workspace = tempdir().unwrap();
771 let config_dir = workspace.path().join("config");
772 let ambient_dir = workspace.path().join("ambient");
773 fs::create_dir_all(&config_dir).unwrap();
774 fs::create_dir_all(&ambient_dir).unwrap();
775 fs::write(config_dir.join("omnigraph.yaml"), "policy: {}\n").unwrap();
776 fs::write(config_dir.join("local.gq"), "query local { return {} }").unwrap();
777 fs::write(ambient_dir.join("local.gq"), "query ambient { return {} }").unwrap();
778
779 let config =
780 load_config_in(&ambient_dir, Some(&config_dir.join("omnigraph.yaml"))).unwrap();
781 let resolved = config.resolve_query_path(Path::new("local.gq")).unwrap();
782
783 assert_eq!(resolved, config_dir.join("local.gq"));
784 }
785
786 #[test]
787 fn queries_block_round_trips_inline_and_per_graph() {
788 let temp = tempdir().unwrap();
789 fs::write(
790 temp.path().join("omnigraph.yaml"),
791 r#"
792graphs:
793 prod:
794 uri: s3://bucket/prod
795 queries:
796 find_user:
797 file: ./queries/find_user.gq
798 mcp:
799 expose: true
800 tool_name: lookup_user
801 internal_audit:
802 file: ./queries/audit.gq
803queries:
804 single_mode_q:
805 file: ./q.gq
806"#,
807 )
808 .unwrap();
809
810 let config = load_config_in(temp.path(), None).unwrap();
811
812 let prod = config.target_query_entries("prod").unwrap();
814 assert_eq!(prod.len(), 2);
815 let find_user = &prod["find_user"];
816 assert_eq!(find_user.file, "./queries/find_user.gq");
817 assert!(find_user.mcp.expose);
818 assert_eq!(find_user.mcp.tool_name.as_deref(), Some("lookup_user"));
819 let audit = &prod["internal_audit"];
821 assert!(audit.mcp.expose);
822 assert!(audit.mcp.tool_name.is_none());
823
824 assert_eq!(config.query_entries().len(), 1);
826
827 assert_eq!(config.query_entries_for(Some("prod")).len(), 2);
832 assert_eq!(config.query_entries_for(None).len(), 1);
833 assert_eq!(config.query_entries_for(Some("nonexistent")).len(), 1);
834
835 assert_eq!(
837 config.resolve_query_file(&find_user.file),
838 temp.path().join("./queries/find_user.gq")
839 );
840 }
841
842 #[test]
843 fn resolve_policy_file_for_follows_identity() {
844 let temp = tempdir().unwrap();
845 fs::write(
846 temp.path().join("omnigraph.yaml"),
847 "policy:\n file: ./top.yaml\ngraphs:\n prod:\n uri: s3://b/prod\n \
848 policy:\n file: ./prod.yaml\n bare:\n uri: s3://b/bare\n",
849 )
850 .unwrap();
851 let config = load_config_in(temp.path(), None).unwrap();
852
853 assert!(
855 config
856 .resolve_policy_file_for(Some("prod"))
857 .unwrap()
858 .ends_with("prod.yaml")
859 );
860 assert!(config.resolve_policy_file_for(Some("bare")).is_none());
863 assert!(
865 config
866 .resolve_policy_file_for(None)
867 .unwrap()
868 .ends_with("top.yaml")
869 );
870 assert!(
871 config
872 .resolve_policy_file_for(Some("nope"))
873 .unwrap()
874 .ends_with("top.yaml")
875 );
876 }
877
878 #[test]
879 fn queries_block_absent_yields_empty_registry() {
880 let temp = tempdir().unwrap();
881 fs::write(
882 temp.path().join("omnigraph.yaml"),
883 "graphs:\n local:\n uri: ./demo.omni\n",
884 )
885 .unwrap();
886
887 let config = load_config_in(temp.path(), None).unwrap();
888 assert!(config.query_entries().is_empty());
890 assert!(
891 config
892 .target_query_entries("local")
893 .unwrap()
894 .is_empty()
895 );
896 }
897
898 #[test]
899 fn policy_block_accepts_non_empty_mapping() {
900 let temp = tempdir().unwrap();
901 fs::write(
902 temp.path().join("omnigraph.yaml"),
903 "policy:\n file: ./policy.yaml\n",
904 )
905 .unwrap();
906
907 let config = load_config_in(temp.path(), None).unwrap();
908 assert_eq!(
909 config.resolve_policy_file().unwrap(),
910 temp.path().join("policy.yaml")
911 );
912 }
913
914 #[test]
915 fn scoped_auth_env_ignores_default_target_when_uri_is_explicit() {
916 let temp = tempdir().unwrap();
917 fs::write(
918 temp.path().join("omnigraph.yaml"),
919 r#"
920graphs:
921 demo:
922 uri: https://example.com
923 bearer_token_env: DEMO_TOKEN
924cli:
925 graph: demo
926"#,
927 )
928 .unwrap();
929
930 let config = load_config_in(temp.path(), None).unwrap();
931 assert_eq!(
932 config.graph_bearer_token_env(
933 Some("https://override.example.com"),
934 None,
935 config.cli_graph_name()
936 ),
937 None
938 );
939 assert_eq!(
940 config.graph_bearer_token_env(
941 Some("https://override.example.com"),
942 Some("demo"),
943 config.cli_graph_name()
944 ),
945 Some("DEMO_TOKEN")
946 );
947 }
948}