mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-15 01:55:13 +02:00
feat(cli): the operator config surface — identity and output defaults (RFC-007 PR 1)
~/.omnigraph/config.yaml joins the resolution chains as the operator surface: operator.actor becomes the last hop of THE actor chain (--as > legacy cli.actor during the RFC-008 window > operator.actor > none, one implementation for direct-engine and cluster commands alike) and defaults.output joins the read-format cascade below every more-specific source. Discovery honors $OMNIGRAPH_HOME (tilde-expanded, #139 finding 9); an absent file is an empty layer; unknown keys WARN and load (a file written for later slices must not break this CLI); malformed YAML is a loud error. The module is CLI-only — the server never reads operator config (invariant 11 by construction). $OMNIGRAPH_CONFIG becomes a first-class stand-in for --config in load_config (flag > env > ./omnigraph.yaml), one meaning in both binaries. The test harness pins hermeticity: spawned binaries get a nonexistent OMNIGRAPH_HOME by default so no test ever reads the developer's real operator config. New coverage: loader unit tests, the env-precedence matrix on load_config_in, and spawned-binary e2es for the actor chain (operator wins with no flag/legacy key; legacy outranks it; --as wins) and the format cascade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
08ce8dc34d
commit
be4bd46212
7 changed files with 445 additions and 36 deletions
|
|
@ -526,12 +526,23 @@ pub fn default_config_path() -> PathBuf {
|
|||
PathBuf::from(DEFAULT_CONFIG_FILE)
|
||||
}
|
||||
|
||||
/// `OMNIGRAPH_CONFIG` env var: a first-class stand-in for `--config`, one
|
||||
/// name with one meaning in both binaries (the container entrypoint already
|
||||
/// uses it for the server; RFC-007 §D1 extends it to the CLI).
|
||||
pub const CONFIG_PATH_ENV: &str = "OMNIGRAPH_CONFIG";
|
||||
|
||||
pub fn load_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
||||
load_config_in(&env::current_dir()?, config_path)
|
||||
let env_path = env::var_os(CONFIG_PATH_ENV).map(PathBuf::from);
|
||||
load_config_in(&env::current_dir()?, config_path, env_path.as_ref())
|
||||
}
|
||||
|
||||
fn load_config_in(cwd: &Path, config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
||||
let explicit_path = config_path.cloned();
|
||||
fn load_config_in(
|
||||
cwd: &Path,
|
||||
config_path: Option<&PathBuf>,
|
||||
env_path: Option<&PathBuf>,
|
||||
) -> Result<OmnigraphConfig> {
|
||||
// Precedence: explicit --config flag > $OMNIGRAPH_CONFIG > ./omnigraph.yaml.
|
||||
let explicit_path = config_path.or(env_path).cloned();
|
||||
let config_path = explicit_path.or_else(|| {
|
||||
let default_path = cwd.join(DEFAULT_CONFIG_FILE);
|
||||
default_path.exists().then_some(default_path)
|
||||
|
|
@ -575,6 +586,28 @@ mod tests {
|
|||
ReadOutputFormat, TableCellLayout, graph_resource_id_for_selection, load_config_in,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn env_config_path_stands_in_for_the_flag_but_loses_to_it() {
|
||||
let temp = tempdir().unwrap();
|
||||
let flag_path = temp.path().join("flag.yaml");
|
||||
let env_path = temp.path().join("env.yaml");
|
||||
fs::write(&flag_path, "cli:\n actor: act-flag\n").unwrap();
|
||||
fs::write(&env_path, "cli:\n actor: act-env\n").unwrap();
|
||||
|
||||
// $OMNIGRAPH_CONFIG used when no flag…
|
||||
let config = load_config_in(temp.path(), None, Some(&env_path)).unwrap();
|
||||
assert_eq!(config.cli.actor.as_deref(), Some("act-env"));
|
||||
|
||||
// …loses to an explicit --config…
|
||||
let config = load_config_in(temp.path(), Some(&flag_path), Some(&env_path)).unwrap();
|
||||
assert_eq!(config.cli.actor.as_deref(), Some("act-flag"));
|
||||
|
||||
// …and beats the cwd default file.
|
||||
fs::write(temp.path().join("omnigraph.yaml"), "cli:\n actor: act-cwd\n").unwrap();
|
||||
let config = load_config_in(temp.path(), None, Some(&env_path)).unwrap();
|
||||
assert_eq!(config.cli.actor.as_deref(), Some("act-env"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_reads_yaml_defaults_from_current_dir() {
|
||||
let temp = tempdir().unwrap();
|
||||
|
|
@ -598,7 +631,7 @@ policy: {}
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(config.cli_graph_name(), Some("local"));
|
||||
assert_eq!(config.cli_branch(), "main");
|
||||
assert_eq!(config.cli_output_format(), ReadOutputFormat::Kv);
|
||||
|
|
@ -633,7 +666,7 @@ policy: {}
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(&child, None).unwrap();
|
||||
let config = load_config_in(&child, None, None).unwrap();
|
||||
assert!(config.graphs.is_empty());
|
||||
}
|
||||
|
||||
|
|
@ -657,7 +690,7 @@ policy: {}
|
|||
"graphs:\n local:\n uri: ./demo.omni\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
|
||||
// A known graph passes through unchanged.
|
||||
assert_eq!(config.resolve_graph_selection(Some("local")).unwrap(), Some("local"));
|
||||
|
|
@ -680,7 +713,7 @@ policy: {}
|
|||
"graphs:\n local:\n uri: ./demo.omni\npolicy:\n file: ./top.yaml\n",
|
||||
)
|
||||
.unwrap();
|
||||
let incoherent = load_config_in(temp2.path(), None).unwrap();
|
||||
let incoherent = load_config_in(temp2.path(), None, None).unwrap();
|
||||
let err = incoherent
|
||||
.resolve_graph_selection(Some("local"))
|
||||
.unwrap_err()
|
||||
|
|
@ -705,7 +738,7 @@ policy: {}
|
|||
server:\n graph: local\ncli:\n graph: prod\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
config.resolve_policy_tooling_graph_selection().unwrap(),
|
||||
Some("prod")
|
||||
|
|
@ -717,7 +750,7 @@ policy: {}
|
|||
"graphs:\n local:\n uri: ./local.omni\nserver:\n graph: local\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
config.resolve_policy_tooling_graph_selection().unwrap(),
|
||||
Some("local")
|
||||
|
|
@ -725,7 +758,7 @@ policy: {}
|
|||
|
||||
let temp = tempdir().unwrap();
|
||||
fs::write(temp.path().join("omnigraph.yaml"), "policy: {}\n").unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(config.resolve_policy_tooling_graph_selection().unwrap(), None);
|
||||
|
||||
let temp = tempdir().unwrap();
|
||||
|
|
@ -734,7 +767,7 @@ policy: {}
|
|||
"graphs:\n local:\n uri: ./local.omni\nserver:\n graph: ghost\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
let err = config
|
||||
.resolve_policy_tooling_graph_selection()
|
||||
.unwrap_err()
|
||||
|
|
@ -760,7 +793,7 @@ policy: {}
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
let resolved = config.resolve_query_path(Path::new("test.gq")).unwrap();
|
||||
assert_eq!(resolved, temp.path().join("queries").join("test.gq"));
|
||||
}
|
||||
|
|
@ -777,7 +810,7 @@ policy: {}
|
|||
fs::write(ambient_dir.join("local.gq"), "query ambient { return {} }").unwrap();
|
||||
|
||||
let config =
|
||||
load_config_in(&ambient_dir, Some(&config_dir.join("omnigraph.yaml"))).unwrap();
|
||||
load_config_in(&ambient_dir, Some(&config_dir.join("omnigraph.yaml")), None).unwrap();
|
||||
let resolved = config.resolve_query_path(Path::new("local.gq")).unwrap();
|
||||
|
||||
assert_eq!(resolved, config_dir.join("local.gq"));
|
||||
|
|
@ -807,7 +840,7 @@ queries:
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
|
||||
// Per-graph registry (multi-graph mode).
|
||||
let prod = config.target_query_entries("prod").unwrap();
|
||||
|
|
@ -848,7 +881,7 @@ queries:
|
|||
policy:\n file: ./prod.yaml\n bare:\n uri: s3://b/bare\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
|
||||
// Named graph with its own policy → per-graph (not top-level).
|
||||
assert!(
|
||||
|
|
@ -884,7 +917,7 @@ queries:
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
// Additive: no `queries:` anywhere → empty registries everywhere.
|
||||
assert!(config.query_entries().is_empty());
|
||||
assert!(
|
||||
|
|
@ -904,7 +937,7 @@ queries:
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
config.resolve_policy_file().unwrap(),
|
||||
temp.path().join("policy.yaml")
|
||||
|
|
@ -927,7 +960,7 @@ cli:
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let config = load_config_in(temp.path(), None).unwrap();
|
||||
let config = load_config_in(temp.path(), None, None).unwrap();
|
||||
assert_eq!(
|
||||
config.graph_bearer_token_env(
|
||||
Some("https://override.example.com"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue