mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-21 02:28:07 +02:00
* feat(MR-656): inline query strings in CLI and HTTP server
CLI:
- Add -e / --query-string <STRING> to omnigraph read and omnigraph change
- Exactly one of --query, --query-string, --alias is required (3-way XOR)
- Empty --query-string is rejected with a clear error
HTTP:
- New POST /query (read-only, clean field names: query/name/params/branch/snapshot)
- Mutations on /query are rejected with 400 -- use POST /change instead
- ChangeRequest fields polished: query (alias query_source), name (alias query_name)
- POST /read and POST /change remain byte-compatible for existing clients
Tests:
- cli.rs: -e happy-path on read/change, mutex error vs --query, empty -e rejected
- system_local.rs: inline -e read and -e change exercise the local flow
- system_remote.rs: inline -e read/change over HTTP plus direct /query 200/400
- server.rs: /query 200, /query 400 on mutation, /change legacy field alias
- openapi.rs: new /query path, QueryRequest schema, ChangeRequest field-name polish
Docs: cli.md (-e examples), cli-reference.md (read/change rows), server.md (/query)
Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
* feat(MR-656): rename read/change to query/mutate with deprecation signals
HTTP server:
- Add POST /mutate as canonical write endpoint (pairs with POST /query).
- Mark POST /read and POST /change as deprecated. Three-channel signal:
* OpenAPI: `deprecated: true` on the operation (every codegen flags
the generated SDK method).
* RFC 9745: response `Deprecation: true` header on every response.
* RFC 8288: response `Link: </successor>; rel="successor-version"`
pointing at /query and /mutate respectively.
- Share business logic across /mutate and /change via run_mutate(); the
/change wrapper is the only place that adds the deprecation headers.
- ChangeRequest field aliases (query_source/query_name) preserved.
- AliasCommand serde now accepts `query`/`mutate` alongside `read`/`change`.
CLI:
- Promote `omnigraph query` / `omnigraph mutate` to top-level canonical
subcommands (clap visible_alias keeps `omnigraph read` / `omnigraph
change` working forever).
- Promote `omnigraph lint` / `omnigraph check` to top-level (was nested
under `omnigraph query lint`, which is now a deprecated argv shim that
rewrites to the canonical form).
- Argv-level preprocessing prints a one-line deprecation warning to
stderr when any legacy spelling is used. Canonical names are silent.
Tests:
- Server: /mutate works, /change emits Deprecation+Link headers, /read
emits Deprecation+Link headers, /query carries no deprecation signal.
- OpenAPI: /read and /change flagged deprecated; /query and /mutate not.
- CLI: canonical `lint` matches deprecated `query lint` / `query check`
output; `read` / `change` print deprecation warnings.
Docs:
- cli.md: new canonical examples; "Deprecated names" migration table.
- cli-reference.md: top-level table updated; aliases.<name>.command
accepts both legacy and canonical spellings.
- server.md: endpoint inventory shows /query and /mutate as canonical
and /read and /change as deprecated; dedicated section explains the
three-channel deprecation signal.
- og-cheet-sheet.md: use new `omnigraph lint` / `omnigraph check`.
- openapi.json regenerated.
Migration is purely cosmetic — every deprecated form continues to work
indefinitely; only the spelling changes.
Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
* fix(MR-656): address Devin Review findings on /query and /change
Two issues raised by Devin Review on PR #110:
1. `POST /query` mutation-rejection error pointed at the deprecated
`/change` endpoint instead of the canonical `/mutate`. Fixed in
three places: the runtime error message in `server_query`, the
utoipa 400-response description, and the handler doc comment. The
`QueryRequest` schema docstrings in `api.rs` got the same update so
the openapi.json bodies match. Server and openapi tests updated.
2. `execute_change_remote` serialized `ChangeRequest` directly, which
emits the new canonical field names `query` / `name` on the wire.
`#[serde(alias = "query_source")]` only affects deserialization, so
a newer CLI talking to an older server would have its `/change`
POST body fail with "missing field: query_source". Fixed by
extracting a `legacy_change_request_body` helper that hand-rolls
the JSON with the legacy keys (`query_source` / `query_name`), the
same byte-stable contract `execute_read_remote` already uses
against `/read`. Added two unit tests on the helper to lock the
wire shape in.
Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
* docs(dev): RFC 001 — inline + stored queries, envelope, MCP
Tracked artifact consolidating the design across MR-656 (this branch),
MR-976 (Phase 1 envelope hardening parent, with MR-977/978/979/980
sub-issues), and MR-969 (stored queries + MCP).
Sections:
* Two paths, one engine — inline `/query` + `/mutate` (this PR) coexist
with stored `/queries/{name}` (MR-969). Same `run_query` / `run_mutate`
backend (the fold-in landed in the previous commit).
* Request envelope ("before") — Idempotency-Key, If-Match, X-Deadline,
X-Trace-Id, expect, dry_run, fields. Phase 1 ships the load-bearing
subset on `/mutate`.
* Response envelope ("after") — audit_id, snapshot_id, commit_id, stats,
warnings. Closes the provenance loop today's `ChangeOutput` leaves
open.
* `.gq` pragmas — `@description`, `@returns`, `@mcp`. Source-of-truth
for the stored-query agent contract; no separate YAML registry.
* Multi-graph MCP — per-graph `/graphs/{id}/mcp/tools` + `/mcp/invoke`.
Token binds to one graph by default; cross-graph agents loop.
* Cedar split — `read`/`change` for inline, `invoke_query` for stored.
Operators deny ad-hoc for agent groups while keeping curated tool
list open.
* Rejected alternatives — per-env override files, compiled bundles,
tool-name prefixing across graphs, body-field graph dispatch.
Index entry added under "Active Implementation Plans" so future agents
land on the RFC before touching queries / mutations / envelope code.
`scripts/check-agents-md.sh` clean (35 links, 34 docs).
* docs(server): clarify why run_query lacks AppState parameter
run_mutate takes state for workload admission; run_query doesn't because
reads aren't admission-gated today. Mark the asymmetry as intentional and
flag the two future events that would grow the signature: Phase 1's
`expect: { max_rows_scanned: N }` budget (MR-976) or per-actor admission
extending to stored-read invocations (MR-969). Prevents the natural
"make these symmetrical" follow-up.
* refactor(server): run_query / run_mutate take &ResolvedActor
Replace `Option<Extension<ResolvedActor>>` in the helpers with
`Option<&ResolvedActor>`. Saves MR-969's stored-query handler from
wrapping a bare actor in axum's `Extension(...)` before calling.
Handler signatures (`server_query`, `server_read`, `server_mutate`,
`server_change`) keep `Option<Extension<ResolvedActor>>` because that
is what axum injects, and unwrap at the call site with
`actor.as_ref().map(|Extension(actor)| actor)`.
Net: -13/+10 LOC, 89/0 server tests pass.
* docs(releases): v0.6.0 — describe inline + canonical-named queries (MR-656)
Extend the v0.6.0 release notes to cover the third piece of work landing
alongside the graph terminology rename and multi-graph server mode:
canonical-named `POST /query` and `POST /mutate` endpoints, the CLI's
new `-e/--query-string` flag, the top-level promotion of `lint` /
`check`, and the three-channel deprecation signal on `/read` and
`/change` (OpenAPI `deprecated: true` + RFC 9745 + RFC 8288).
Additions:
* Top blurb: "Two pieces" -> "Three pieces" with a bullet describing
the rename + inline flow.
* Breaking Changes: new "Query / mutation rename" subsection covering
the `ChangeRequest` field rename (with the back-compat serde aliases
and the CLI's `legacy_change_request_body` byte-stable wire helper)
and the `omnigraph query lint` -> `omnigraph lint` move.
* New: 5 bullets — the two endpoints, the CLI subcommands, the `-e`
flag, the deprecation signal channels, the widened `aliases.<name>.command`
vocabulary.
* User Impact: one bullet making explicit that the rename is cosmetic
on the client side and migration is voluntary.
* Documentation: pointers to the updated `server.md` / `cli.md` /
`cli-reference.md` and the new `docs/dev/rfc-001-queries-envelope-mcp.md`.
+15/-1 lines. `./scripts/check-agents-md.sh` clean.
* refactor(cli): demote `check` from visible_alias to deprecation shim
`omnigraph check` was a clap `visible_alias` on `lint`, advertised in
`--help` as an equivalent canonical name. Per MR-981 §6 (long-form
flags as canonical, short forms as visible aliases), visible aliases
on subcommand names hurt agent CX: agents emit either spelling
depending on training-data drift, and there's no length signal
pointing at the canonical name.
Changes:
* Remove `#[command(visible_alias = "check")]` from the `Lint` variant.
`omnigraph --help` now shows only `lint`.
* Add bare `check` to `rewrite_deprecated_argv` so `omnigraph check
<args>` still works — it rewrites to `omnigraph lint <args>` and
emits a one-line stderr deprecation warning, matching the existing
pattern for `read` / `change` / `query lint` / `query check`.
* Fix the nested `query check` shim to substitute `check` -> `lint` in
the rewritten argv (previously it relied on `check` being a
visible_alias to reach the `Lint` variant).
* New test `deprecated_check_top_level_rewrites_to_lint` covers: bare
`check` produces identical stdout to `lint`, emits the deprecation
warning, and `check` does NOT appear as an alias in `omnigraph
--help`.
* Release notes updated to reflect the deprecation-shim treatment and
cross-reference MR-981 §6 reasoning.
Cargo / Go users typing `check` still work indefinitely; one stderr
nudge per invocation teaches the canonical name. Agents see only
`lint` in `--help --json` so they emit one canonical form.
67/0 omnigraph-cli tests pass; 39 workspace test suites green.
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ragnor Comerford <ragnor.comerford@gmail.com>
Co-authored-by: Ragnor Comerford <hello@ragnor.co>
542 lines
16 KiB
Rust
542 lines
16 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use clap::ValueEnum;
|
|
use color_eyre::eyre::{Result, bail};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub const DEFAULT_CONFIG_FILE: &str = "omnigraph.yaml";
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct ProjectConfig {
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TargetConfig {
|
|
pub uri: String,
|
|
pub bearer_token_env: Option<String>,
|
|
/// Per-graph Cedar policy file (MR-668). In single-graph mode this
|
|
/// field is unused — the top-level `policy.file` applies. In
|
|
/// multi-graph mode, each `graphs.<id>.policy.file` governs that
|
|
/// graph's HTTP-layer Cedar enforcement.
|
|
#[serde(default)]
|
|
pub policy: PolicySettings,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ReadOutputFormat {
|
|
#[default]
|
|
Table,
|
|
Kv,
|
|
Csv,
|
|
Jsonl,
|
|
Json,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TableCellLayout {
|
|
#[default]
|
|
Truncate,
|
|
Wrap,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct CliDefaults {
|
|
#[serde(rename = "graph")]
|
|
pub graph: Option<String>,
|
|
pub branch: Option<String>,
|
|
pub output_format: Option<ReadOutputFormat>,
|
|
pub table_max_column_width: Option<usize>,
|
|
pub table_cell_layout: Option<TableCellLayout>,
|
|
/// Default actor identity for CLI direct-engine writes (MR-722).
|
|
/// Used when `policy.file` is configured and the operator hasn't
|
|
/// passed `--as <actor>` on the command line. With policy configured
|
|
/// and neither this nor `--as` set, the engine-layer footgun guard
|
|
/// fires (no silent bypass).
|
|
pub actor: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct ServerDefaults {
|
|
#[serde(rename = "graph")]
|
|
pub graph: Option<String>,
|
|
pub bind: Option<String>,
|
|
/// Server-level Cedar policy (MR-668). Governs management endpoints
|
|
/// — currently `GET /graphs`; future runtime add/remove endpoints
|
|
/// will plug in here too. In single-graph mode this is unused — the
|
|
/// top-level `policy.file` covers the single graph.
|
|
#[serde(default)]
|
|
pub policy: PolicySettings,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct AuthDefaults {
|
|
pub env_file: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct QueryDefaults {
|
|
#[serde(default)]
|
|
pub roots: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct PolicySettings {
|
|
pub file: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AliasCommand {
|
|
/// Read alias (canonical: `query`). The legacy spelling `read` is
|
|
/// kept as the variant name for back-compat with serialized configs
|
|
/// and external SDK callers; `query` is accepted on the wire via the
|
|
/// serde alias.
|
|
#[serde(alias = "query")]
|
|
Read,
|
|
/// Mutation alias (canonical: `mutate`). The legacy spelling `change`
|
|
/// is kept as the variant name for back-compat; `mutate` is accepted
|
|
/// on the wire via the serde alias.
|
|
#[serde(alias = "mutate")]
|
|
Change,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AliasConfig {
|
|
pub command: AliasCommand,
|
|
pub query: String,
|
|
pub name: Option<String>,
|
|
#[serde(default)]
|
|
pub args: Vec<String>,
|
|
#[serde(rename = "graph")]
|
|
pub graph: Option<String>,
|
|
pub branch: Option<String>,
|
|
pub format: Option<ReadOutputFormat>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OmnigraphConfig {
|
|
#[serde(default)]
|
|
pub project: ProjectConfig,
|
|
#[serde(default, rename = "graphs")]
|
|
pub graphs: BTreeMap<String, TargetConfig>,
|
|
#[serde(default)]
|
|
pub server: ServerDefaults,
|
|
#[serde(default)]
|
|
pub auth: AuthDefaults,
|
|
#[serde(default)]
|
|
pub cli: CliDefaults,
|
|
#[serde(default)]
|
|
pub query: QueryDefaults,
|
|
#[serde(default)]
|
|
pub aliases: BTreeMap<String, AliasConfig>,
|
|
#[serde(default)]
|
|
pub policy: PolicySettings,
|
|
#[serde(skip)]
|
|
base_dir: PathBuf,
|
|
}
|
|
|
|
impl Default for OmnigraphConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
project: ProjectConfig::default(),
|
|
graphs: BTreeMap::new(),
|
|
server: ServerDefaults::default(),
|
|
auth: AuthDefaults::default(),
|
|
cli: CliDefaults::default(),
|
|
query: QueryDefaults::default(),
|
|
aliases: BTreeMap::new(),
|
|
policy: PolicySettings::default(),
|
|
base_dir: PathBuf::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OmnigraphConfig {
|
|
pub fn base_dir(&self) -> &Path {
|
|
&self.base_dir
|
|
}
|
|
|
|
pub fn cli_branch(&self) -> &str {
|
|
self.cli.branch.as_deref().unwrap_or("main")
|
|
}
|
|
|
|
pub fn cli_output_format(&self) -> ReadOutputFormat {
|
|
self.cli.output_format.unwrap_or_default()
|
|
}
|
|
|
|
pub fn table_max_column_width(&self) -> usize {
|
|
self.cli.table_max_column_width.unwrap_or(80)
|
|
}
|
|
|
|
pub fn table_cell_layout(&self) -> TableCellLayout {
|
|
self.cli.table_cell_layout.unwrap_or_default()
|
|
}
|
|
|
|
pub fn cli_graph_name(&self) -> Option<&str> {
|
|
self.cli.graph.as_deref()
|
|
}
|
|
|
|
pub fn server_graph_name(&self) -> Option<&str> {
|
|
self.server.graph.as_deref()
|
|
}
|
|
|
|
pub fn server_bind(&self) -> &str {
|
|
self.server.bind.as_deref().unwrap_or("127.0.0.1:8080")
|
|
}
|
|
|
|
pub fn resolve_target_name<'a>(
|
|
&self,
|
|
explicit_uri: Option<&str>,
|
|
explicit_target: Option<&'a str>,
|
|
default_target: Option<&'a str>,
|
|
) -> Option<&'a str> {
|
|
explicit_target.or_else(|| {
|
|
if explicit_uri.is_some() {
|
|
None
|
|
} else {
|
|
default_target
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn graph_bearer_token_env(
|
|
&self,
|
|
explicit_uri: Option<&str>,
|
|
explicit_target: Option<&str>,
|
|
default_target: Option<&str>,
|
|
) -> Option<&str> {
|
|
let target_name =
|
|
self.resolve_target_name(explicit_uri, explicit_target, default_target)?;
|
|
self.graphs
|
|
.get(target_name)
|
|
.and_then(|target| target.bearer_token_env.as_deref())
|
|
}
|
|
|
|
pub fn resolve_auth_env_file(&self) -> Option<PathBuf> {
|
|
self.auth
|
|
.env_file
|
|
.as_deref()
|
|
.map(|path| self.resolve_config_path(path))
|
|
}
|
|
|
|
pub fn resolve_policy_file(&self) -> Option<PathBuf> {
|
|
self.policy
|
|
.file
|
|
.as_deref()
|
|
.map(|path| self.resolve_config_path(path))
|
|
}
|
|
|
|
/// Resolve the per-graph policy file path for the named target,
|
|
/// relative to the config file's `base_dir`. Returns `None` if the
|
|
/// target is unknown or no per-graph `policy.file` is set.
|
|
pub fn resolve_target_policy_file(&self, target_name: &str) -> Option<PathBuf> {
|
|
let target = self.graphs.get(target_name)?;
|
|
target
|
|
.policy
|
|
.file
|
|
.as_deref()
|
|
.map(|path| self.resolve_config_path(path))
|
|
}
|
|
|
|
/// Resolve the server-level policy file path (used by management
|
|
/// endpoints). Returns `None` if `server.policy.file` is not set.
|
|
pub fn resolve_server_policy_file(&self) -> Option<PathBuf> {
|
|
self.server
|
|
.policy
|
|
.file
|
|
.as_deref()
|
|
.map(|path| self.resolve_config_path(path))
|
|
}
|
|
|
|
/// Resolve a raw config-supplied URI (which may be relative) to its
|
|
/// absolute form. URIs containing `://` are passed through as-is;
|
|
/// relative paths are joined with the config file's `base_dir`.
|
|
pub fn resolve_uri_value(&self, value: &str) -> String {
|
|
self.resolve_config_uri(value)
|
|
}
|
|
|
|
pub fn resolve_policy_tests_file(&self) -> Option<PathBuf> {
|
|
let policy_file = self.resolve_policy_file()?;
|
|
Some(policy_file.with_file_name("policy.tests.yaml"))
|
|
}
|
|
|
|
pub fn alias(&self, name: &str) -> Result<&AliasConfig> {
|
|
self.aliases
|
|
.get(name)
|
|
.ok_or_else(|| color_eyre::eyre::eyre!("alias '{}' not found", name))
|
|
}
|
|
|
|
pub fn resolve_target_uri(
|
|
&self,
|
|
explicit_uri: Option<String>,
|
|
explicit_target: Option<&str>,
|
|
default_target: Option<&str>,
|
|
) -> Result<String> {
|
|
if let Some(uri) = explicit_uri {
|
|
return Ok(uri);
|
|
}
|
|
|
|
let target_name = explicit_target.or(default_target).ok_or_else(|| {
|
|
color_eyre::eyre::eyre!("URI must be provided via <URI>, --target, or config")
|
|
})?;
|
|
let target = self.graphs.get(target_name).ok_or_else(|| {
|
|
color_eyre::eyre::eyre!(
|
|
"graph '{}' not found in {}",
|
|
target_name,
|
|
DEFAULT_CONFIG_FILE
|
|
)
|
|
})?;
|
|
Ok(self.resolve_config_uri(&target.uri))
|
|
}
|
|
|
|
pub fn resolve_query_path(&self, query: &Path) -> Result<PathBuf> {
|
|
if query.is_absolute() {
|
|
return Ok(query.to_path_buf());
|
|
}
|
|
|
|
let direct = self.base_dir.join(query);
|
|
if direct.exists() {
|
|
return Ok(direct);
|
|
}
|
|
|
|
for root in &self.query.roots {
|
|
let candidate = self.base_dir.join(root).join(query);
|
|
if candidate.exists() {
|
|
return Ok(candidate);
|
|
}
|
|
}
|
|
|
|
bail!("query file '{}' not found", query.display());
|
|
}
|
|
|
|
fn resolve_config_uri(&self, value: &str) -> String {
|
|
if value.contains("://") {
|
|
return value.to_string();
|
|
}
|
|
|
|
let path = Path::new(value);
|
|
if path.is_absolute() {
|
|
value.to_string()
|
|
} else {
|
|
self.base_dir.join(path).to_string_lossy().to_string()
|
|
}
|
|
}
|
|
|
|
fn resolve_config_path(&self, value: &str) -> PathBuf {
|
|
let path = Path::new(value);
|
|
if path.is_absolute() {
|
|
path.to_path_buf()
|
|
} else {
|
|
self.base_dir.join(path)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn default_config_path() -> PathBuf {
|
|
PathBuf::from(DEFAULT_CONFIG_FILE)
|
|
}
|
|
|
|
pub fn load_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
|
load_config_in(&env::current_dir()?, config_path)
|
|
}
|
|
|
|
fn load_config_in(cwd: &Path, config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
|
let explicit_path = config_path.cloned();
|
|
let config_path = explicit_path.or_else(|| {
|
|
let default_path = cwd.join(DEFAULT_CONFIG_FILE);
|
|
default_path.exists().then_some(default_path)
|
|
});
|
|
|
|
let mut config = if let Some(path) = &config_path {
|
|
serde_yaml::from_str::<OmnigraphConfig>(&fs::read_to_string(path)?)?
|
|
} else {
|
|
OmnigraphConfig::default()
|
|
};
|
|
|
|
config.base_dir = if let Some(path) = config_path {
|
|
absolute_base_dir(cwd, &path)?
|
|
} else {
|
|
cwd.to_path_buf()
|
|
};
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
fn absolute_base_dir(cwd: &Path, path: &Path) -> Result<PathBuf> {
|
|
let path = if path.is_absolute() {
|
|
path.to_path_buf()
|
|
} else {
|
|
cwd.join(path)
|
|
};
|
|
Ok(path
|
|
.parent()
|
|
.map(Path::to_path_buf)
|
|
.unwrap_or_else(|| cwd.to_path_buf()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use tempfile::tempdir;
|
|
|
|
use super::{ReadOutputFormat, TableCellLayout, load_config_in};
|
|
|
|
#[test]
|
|
fn load_config_reads_yaml_defaults_from_current_dir() {
|
|
let temp = tempdir().unwrap();
|
|
fs::write(
|
|
temp.path().join("omnigraph.yaml"),
|
|
r#"
|
|
graphs:
|
|
local:
|
|
uri: ./demo.omni
|
|
bearer_token_env: DEMO_TOKEN
|
|
auth:
|
|
env_file: .env.omni
|
|
cli:
|
|
graph: local
|
|
branch: main
|
|
output_format: kv
|
|
table_max_column_width: 40
|
|
table_cell_layout: wrap
|
|
policy: {}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config_in(temp.path(), None).unwrap();
|
|
assert_eq!(config.cli_graph_name(), Some("local"));
|
|
assert_eq!(config.cli_branch(), "main");
|
|
assert_eq!(config.cli_output_format(), ReadOutputFormat::Kv);
|
|
assert_eq!(config.table_max_column_width(), 40);
|
|
assert_eq!(config.table_cell_layout(), TableCellLayout::Wrap);
|
|
assert_eq!(
|
|
config.graph_bearer_token_env(None, None, config.cli_graph_name()),
|
|
Some("DEMO_TOKEN")
|
|
);
|
|
assert_eq!(
|
|
config.resolve_auth_env_file().unwrap(),
|
|
temp.path().join(".env.omni")
|
|
);
|
|
assert_eq!(
|
|
PathBuf::from(
|
|
config
|
|
.resolve_target_uri(None, None, config.cli_graph_name())
|
|
.unwrap()
|
|
),
|
|
temp.path().join("./demo.omni")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn load_config_does_not_walk_parent_directories() {
|
|
let temp = tempdir().unwrap();
|
|
let child = temp.path().join("child");
|
|
fs::create_dir_all(&child).unwrap();
|
|
fs::write(
|
|
temp.path().join("omnigraph.yaml"),
|
|
"graphs:\n local:\n uri: ./demo.omni\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config_in(&child, None).unwrap();
|
|
assert!(config.graphs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_query_path_searches_config_roots() {
|
|
let temp = tempdir().unwrap();
|
|
fs::create_dir_all(temp.path().join("queries")).unwrap();
|
|
fs::write(
|
|
temp.path().join("omnigraph.yaml"),
|
|
"query:\n roots:\n - queries\npolicy: {}\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
temp.path().join("queries").join("test.gq"),
|
|
"query q { return {} }",
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config_in(temp.path(), None).unwrap();
|
|
let resolved = config.resolve_query_path(Path::new("test.gq")).unwrap();
|
|
assert_eq!(resolved, temp.path().join("queries").join("test.gq"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_query_path_prefers_config_base_dir_over_ambient_cwd() {
|
|
let workspace = tempdir().unwrap();
|
|
let config_dir = workspace.path().join("config");
|
|
let ambient_dir = workspace.path().join("ambient");
|
|
fs::create_dir_all(&config_dir).unwrap();
|
|
fs::create_dir_all(&ambient_dir).unwrap();
|
|
fs::write(config_dir.join("omnigraph.yaml"), "policy: {}\n").unwrap();
|
|
fs::write(config_dir.join("local.gq"), "query local { return {} }").unwrap();
|
|
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();
|
|
let resolved = config.resolve_query_path(Path::new("local.gq")).unwrap();
|
|
|
|
assert_eq!(resolved, config_dir.join("local.gq"));
|
|
}
|
|
|
|
#[test]
|
|
fn policy_block_accepts_non_empty_mapping() {
|
|
let temp = tempdir().unwrap();
|
|
fs::write(
|
|
temp.path().join("omnigraph.yaml"),
|
|
"policy:\n file: ./policy.yaml\n",
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config_in(temp.path(), None).unwrap();
|
|
assert_eq!(
|
|
config.resolve_policy_file().unwrap(),
|
|
temp.path().join("policy.yaml")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scoped_auth_env_ignores_default_target_when_uri_is_explicit() {
|
|
let temp = tempdir().unwrap();
|
|
fs::write(
|
|
temp.path().join("omnigraph.yaml"),
|
|
r#"
|
|
graphs:
|
|
demo:
|
|
uri: https://example.com
|
|
bearer_token_env: DEMO_TOKEN
|
|
cli:
|
|
graph: demo
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config_in(temp.path(), None).unwrap();
|
|
assert_eq!(
|
|
config.graph_bearer_token_env(
|
|
Some("https://override.example.com"),
|
|
None,
|
|
config.cli_graph_name()
|
|
),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
config.graph_bearer_token_env(
|
|
Some("https://override.example.com"),
|
|
Some("demo"),
|
|
config.cli_graph_name()
|
|
),
|
|
Some("DEMO_TOKEN")
|
|
);
|
|
}
|
|
}
|