Validate the graph selection in queries list

Graph-existence validation was a side effect of URI resolution: every
URI-resolving command rejects an unknown --target via resolve_target_uri, but
queries list opens no URI, so query_entries_for(Some(unknown)) silently fell
back to the top-level registry and showed the wrong (or empty) catalog.

Make membership a property of the selection: add the fallible
resolve_graph_selection alongside the infallible query_entries_for (a known
name passes through, an unknown name errors with the same message as
resolve_target_uri, None stays anonymous), and validate the selection in
execute_queries_list. query_entries_for is unchanged — server boot's bare-URI
path still needs its None -> top-level arm.
This commit is contained in:
Ragnor Comerford 2026-06-01 11:22:41 +02:00
parent fb56a31c16
commit 98831d4fa9
No known key found for this signature in database
2 changed files with 43 additions and 2 deletions

View file

@ -1787,8 +1787,12 @@ fn execute_queries_list(
) -> Result<()> {
let config = load_cli_config(config_path)?;
// `list` takes no URI, so the selection is the target or the configured
// default graph (named → its per-graph block; else top-level).
let selected = target.as_deref().or_else(|| config.cli_graph_name());
// default graph (named → its per-graph block; else top-level). Validate
// membership explicitly — every URI-resolving command rejects an unknown
// name as a side effect of `resolve_target_uri`, but `list` opens no URI,
// so it would otherwise fall back to the top-level registry silently.
let selected =
config.resolve_graph_selection(target.as_deref().or_else(|| config.cli_graph_name()))?;
let registry = load_registry_or_report(&config, selected)?;
let output = QueriesListOutput {

View file

@ -331,6 +331,21 @@ impl OmnigraphConfig {
}
}
/// Validate a graph selection against `graphs:` — the fallible
/// counterpart to the infallible [`OmnigraphConfig::query_entries_for`].
/// A known name passes through; an unknown name errors (the **same**
/// message [`OmnigraphConfig::resolve_target_uri`] produces, so a command
/// that opens no URI rejects an unknown `--target` exactly like the
/// URI-resolving commands do); an anonymous selection (`None`) stays
/// anonymous, resolving to the top-level registry downstream.
pub fn resolve_graph_selection<'a>(&self, graph: Option<&'a str>) -> Result<Option<&'a str>> {
match graph {
Some(name) if self.graphs.contains_key(name) => Ok(Some(name)),
Some(name) => bail!("graph '{}' not found in {}", name, DEFAULT_CONFIG_FILE),
None => Ok(None),
}
}
/// The policy file that applies for a graph selection — the policy
/// sibling of [`OmnigraphConfig::query_entries_for`], so policy and
/// queries resolve by the same identity rule. A named graph in
@ -576,6 +591,28 @@ policy: {}
assert!(config.graphs.is_empty());
}
#[test]
fn resolve_graph_selection_validates_membership() {
let temp = tempdir().unwrap();
fs::write(
temp.path().join("omnigraph.yaml"),
"graphs:\n local:\n uri: ./demo.omni\n",
)
.unwrap();
let config = load_config_in(temp.path(), None).unwrap();
// A known graph passes through unchanged.
assert_eq!(config.resolve_graph_selection(Some("local")).unwrap(), Some("local"));
// An anonymous selection stays anonymous (→ top-level registry downstream).
assert_eq!(config.resolve_graph_selection(None).unwrap(), None);
// An unknown name errors, naming the graph (matching resolve_target_uri).
let err = config.resolve_graph_selection(Some("ghost")).unwrap_err().to_string();
assert!(
err.contains("ghost") && err.contains("not found"),
"unknown graph must error naming it: {err}"
);
}
#[test]
fn resolve_query_path_searches_config_roots() {
let temp = tempdir().unwrap();